使用MNIST数据集对0到9之间的数字进行手写数字识别是神经网络的一个典型入门教程。
该技术在现实场景中是颇有用的,好比能够把该技术用来扫描银行转账单或支票,其中账号和须要转帐的金额能够被识别处理并写在明肯定义的方框中。
在本教程中,咱们将介绍如何使用Julia编程语言和名为Flux的机器学习库来实现这一技术。
为何使用Flux和Julia?
本教程为何想使用Flux(https://fluxml.ai/) 和Julia(https://julialang.org/) ,而不是像Torch、PyTorch、Keras或TensorFlow 2.0这样的知名框架呢?
一个很好的缘由是由于Flux更易于学习,并且它提供更好的性能和拥有有更大的潜力,另一个缘由是,Flux在仍然是一个小库的状况下实现了不少功能。Flux库很是小,由于它所作的大部分工做都是由Julia编程语言自己提供的。
例如,若是你查看Gorgonia ML库(https://github.com/gorgonia/gorgonia) 中的Go编程语言,你将看到,它明确地展现了其余机器学习库如何构建一个须要执行和区分的表达式图。在Flux中,这个图就是Julia自己。Julia与LISP很是类似,由于Julia代码能够很容易地表示为数据结构,能够对其进行修改和计算。
机器学习概论
若是你是机器学习的新手,你能够跟着本教程来学习,但并非全部的东西对你来讲都是有价值的。你也能够看看我之前关于Medium的一些文章,它们可能会解释你一些新手的疑惑:git
using Flux, Flux.Data.MNIST, Statistics using Flux: onehotbatch, onecold, crossentropy, throttle using Base.Iterators: repeated # Load training data. 28x28 grayscale images of digits imgs = MNIST.images() # Reorder the layout of the data for the ANN imagestrip(image::Matrix{<:Gray}) = Float32.(reshape(image, :)) X = hcat(imagestrip.(imgs)...) # Target output. What digit each image represents. labels = MNIST.labels() Y = onehotbatch(labels, 0:9) # Defining the model (a neural network) m = Chain( Dense(28*28, 32, relu), Dense(32, 10), softmax) loss(x, y) = crossentropy(m(x), y) dataset = repeated((X, Y), 200) opt = ADAM() evalcb = () -> @show(loss(X, Y)) # Perform training on data Flux.train!(loss, params(m), dataset, opt, cb = throttle(evalcb, 10))
探索输入数据
数据预处理一般是数据科学中最大的工做之一。一般状况下,数据的组织或格式化方式与将其输入算法所需的方式不一样。
咱们首先将MNIST数据集加载为60000个28x28像素的灰度图像:github
imgs = MNIST.images()
如今,若是你这样处理数据,你可能不知道输出的数据是怎么样子的,但使用Julia研究,咱们只需检查一下:算法
julia> size(imgs) (60000,)
输出说明了imgs是一个包含60000个元素的一维数组。但这些元素是什么?编程
julia> eltype(imgs) Array{Gray{FixedPointNumbers.Normed{UInt8,8}},2}
你可能看不懂,但我能够简单地告诉你这是什么:数组
julia> eltype(imgs) <: Matrix{T} where T <: Gray true
这告诉咱们imgs中的每一个元素都是某种值矩阵,这些值属于某种类型T,它是Gray类型的子类型。什么是Gray类型?
咱们能够在Julia在线文档中查找:网络
help?> Gray Gray is a grayscale object. You can extract its value with gray(c).
若是咱们想知道这些灰度值矩阵的维数,则能够:数据结构
julia> size(imgs[1]) (28, 28) julia> size(imgs[2]) (28, 28)
这告诉咱们它们的尺寸为28x28像素。咱们能够经过简单地绘制其中的一些图来进一步验证这一点。Julia的Plots库使你能够绘制函数和图像。框架
julia> using Plots julia> plot(imgs[2])
得出了下面的图像,显然看起来像一个数字:机器学习
可是,你可能会发现了解更多的数据看起来是更有用。咱们能够很容易地一块儿绘制几个图像:编程语言
imgplots = plot.(imgs[1:9]) plot(imgplots...)
如今咱们知道了数据是什么样的了。
准备输入数据
然而,咱们不能像这样将数据输入到咱们的神经网络(ANN),由于每一个神经网络输入必须是列向量,而不是矩阵。
这是由于神经网络指望一个矩阵做为输入,矩阵中的每一列都是输入。ANN所看到的三乘十矩阵对应于十个不一样的输入,其中每一个输入包含三个不一样的值或者更具体地说是三个不一样的特征,所以,咱们将28x28灰度图像转换为28x28=784的长像素带。
其次,咱们的神经网络并不知道什么是灰度值,它是对浮点数据进行操做的,因此咱们必须同时转换数据的维度和元素类型。
数组中的列和行数称为其形状。不少人提到了张量,虽然它并不彻底精确,但它是一个涵盖了标量、向量、矩阵、立方体或任何等级的数组(基本上是数组的全部维度)的概念。
在Julia中,咱们可使用reshape函数来改变数组的形状。下面是一些你如何使用它的例子。
这将建立一个包含四个元素的列向量A:
julia> A = collect(1:4) 4-element Array{Int64,1}: 1 2 3 4
经过reshape咱们把它变成一个二乘二的矩阵B:
julia> B = reshape(A, (2, 2)) 2×2 Array{Int64,2}: 1 3 2 4
矩阵能够再次转换为列向量:
julia> reshape(B, 4) 4-element Array{Int64,1}: 1 2 3 4
找出一个列向量到底有多少个元素是不切实际的,你可让Julia只经过写来计算合适的长度。
julia> reshape(B, :) 4-element Array{Int64,1}: 1 2 3 4
有了这些信息,应该更容易看到imagestrip函数的实际功能了,它将28x28的灰度矩阵转换为784个32位浮点值的列向量。
imagestrip(image::Matrix{<:Gray}) = Float32.(reshape(image, :))
该.符号用于将函数应用于数组的每一个元素,所以Float32.(xs)与map(Float32, xs)是相同的。
接下来,咱们将imagestrip函数应用于6万张灰度图像中的每一张,生成784x6000个输入矩阵X。
X = hcat(imagestrip.(imgs)...)
这是如何运做的?能够想象为imagestrip.(imgs)将图像转换为单个输入值的数组,例如[X₁, X₂, X₃, ..., Xₙ],其中n = 60,000,每一个Xᵢ都是784个浮点值。
使用splat运算符...,咱们将其转换为全部这些列向量的水平链接,以产生模型输入。
X = hcat(X₁, X₂, X₃, ..., Xₙ)
若是要验证尺寸,则能够运行size(X)。接下来,咱们加载标签。
labels = MNIST.labels()
标签是咱们称之为监督学习中观察的"答案"部分。在咱们的任务中,标签是从0到9的数字。手绘数字的每个图像都应归类为十个不一样的数字之一,例如,若是这是一个包含不一样花卉品种的花瓣长度和花瓣宽度的虹膜数据集,那么该品种的名称就是标签。
Xᵢ表明咱们全部的特征向量,用机器学习的术语来讲,每一个像素的灰度值都是一个特征。
你能够将标签与咱们绘制的图像进行比较。
imgplots = plot.(imgs[1:9]) plot(imgplots...) labels[1:9]
独热编码
每一个图像一个标签,则有60000个标签,然而神经网络不能直接输出标签。例如,若是你正试图对猫和狗的图像进行分类,那么一个网络不能输出字符串“dog”或“cat”,由于它是使用浮点值的。
若是标签是一个不必定有用的数字,例如若是输出是一系列邮政编码,那么将3000的邮政编码视为1500的邮政编码的两倍是没有意义的,一样,当使用神经网络从图像中预测数字时,4的大小是2的两倍并不重要,数字也多是字母,所以它们的值不重要。
咱们在机器学习中处理这个问题的方法是使用所谓的独热编码,这意味着,若是咱们有标签A、B和C,而且咱们想用独热编码来表示它们,那么A是[一、0、0],B是[0、一、0],C是[0、0、1]。
这看起来很浪费空间,但在Julia one hot数组内部,它只跟踪元素的索引,并不保存全部的零。
下面是一些正在使用的编码示例:
julia> Flux.onehot('B', ['A', 'B', 'C']) 3-element Flux.OneHotVector: 0 1 0 julia> Flux.onehot("foo", ["foo", "bar", "baz"]) 3-element Flux.OneHotVector: 1 0 0
可是,咱们不会使用onehot函数,由于咱们正在建立一批独热编码标签,咱们将把60000张图片做为一个批次来处理。
机器学习的批次指的是在咱们模型(神经网络)的权值或参数更新以前必须完成的最小样本数量。
Y = onehotbatch(labels, 0:9)
这将建立目标输出。在理想状况下,模型(X)==Y,但在现实中,即便通过模型的训练,也会有一些误差。
咱们已经讨论完数据准备,如今让咱们用人工神经网络来构造咱们的模型。
构造神经网络模型
模型是真实世界的简化表示,就像咱们能够创建简化的物理模型同样,咱们也能够用数学或代码来建立物理世界的模型,现实中存在许多这样的数学模型。
例如,统计模型可使用统计数据来模拟人们一天中是如何到达商店的。通常来讲,人们会以一种遵循特定几率分布的方式到达。
在咱们的例子中,咱们试图用神经网络来模拟现实世界中的一些东西,固然,这只是对现实世界的一种近似。
当咱们创建一个神经网络时,咱们有不少能够玩的东西。网络是由多个层链接而成的,每一层一般都有一个激活函数。
创建一个神经网络的挑战是选择合适的层和激活函数,并决定每层应该有多少个节点。
咱们的模型很是简单,定义以下:
m = Chain( Dense(28^2, 32, relu), Dense(32, 10), softmax)
这是一个三层的神经网络。Chain用于将各个层链接在一块儿。第一层Dense(28^2, 32, relu)有784(28x28)个输入节点,对应于每一个图像中的像素数。
它使用校订线性单元(ReLU)函数做为激活函数。在经典的神经网络文献中,一般会介绍sigmoid和tanh。relu等激活函数,这些激活函数在大多数状况下都工做得很好,包括图像的分类。
下一层是咱们的隐藏层,它接受32个输入,由于前一层有32个输出,隐藏节点的数量没有明确的对错选择。
但输出的数量根据不一样任务是不同的,由于咱们但愿每一个数字有一个输出,这也就是“独热编码”发挥做用的地方。
Softmax函数
最后一层,是softmax函数,它之前一层的输出的矩阵做为输入,并沿着每一列进行归一化。
标准化将60000列中的每一列转换为几率分布。那究竟是什么意思?
几率是0到1之间的值,0表示事件永远不会发生,1是确定会发生。
与min-max归一化同样,softmax将全部输入归一化为0到1之间的值,可是与min max不一样的是它会确保全部值的和为一。这须要一些例子来讲明。
假设我建立了10个从1到10的随机值,咱们能够听任意范围和任意数量的值。
julia> ys = rand(1:10, 10) 10-element Array{Int64,1}: 9 6 10 5 10 2 6 6 7 9
如今让咱们使用不一样的归一化函数归一化这个数组,咱们将使用来自LinearAlgebra模块的normalize,由于它与Julia捆绑在一块儿。
但首先使用softmax:
julia> softmax(ys) 10-element Array{Float64,1}: 0.12919082661651196 0.006432032517257137 0.3511770763952676 0.002366212528045101 0.3511770763952676 0.00011780678490667763 0.006432032517257137 0.006432032517257137 0.017484077111717768 0.12919082661651196
如你所见,全部值都在0到1之间。如今看一下若是咱们把它们加起来会发生什么:
julia> sum(softmax(ys)) 0.9999999999999999
它们基本上变成了1。如今将其与normalize的功能进行对比:
julia> using LinearAlgebra julia> normalize(ys) 10-element Array{Float64,1}: 0.38446094597254243 0.25630729731502827 0.4271788288583805 0.21358941442919024 0.4271788288583805 0.0854357657716761 0.25630729731502827 0.25630729731502827 0.2990251802008663 0.38446094597254243 julia> sum(normalize(ys)) 2.9902518020086633 julia> norm(normalize(ys)) 1.0 julia> norm(softmax(ys)) 0.52959100847191
若是对用normalize归一化的值求和,它们只会获得一些随机值,然而若是咱们把结果反馈给norm,咱们获得的结果正好是1.0。
不一样之处在于,normalize将向量中的值进行了归一化,以便它们能够表示单位向量,即长度正好为一的向量。norm给出向量的大小。
相比之下,softmax不会将这些值视为向量,而是将其视为几率分布,每一个元素表示输入图像为该数字的几率。
假设咱们有A,B和C的图像做为输入,若是你从softmax获得一个输出值是[0.1,0.7,0.2],那么输入图像有10%的可能性是A的图形,有70%的可能性是B的图形,最后有20%的可能性是C的图形。
这就是为何咱们但愿softmax做为最后一层的缘由。用神经网络不能绝对肯定输入图像是什么,可是咱们能够给出一个几率分布,它表示更有多是哪一个数字。
定义损失函数
当训练咱们的神经网络(模型)给出准确的预测时,咱们须要定义人工神经网络(ANN)的评估指标。
为此,咱们使用所谓的损失函数。损失函数有不少名字,20年前当我被教授神经网络时,咱们曾称之为偏差函数,也有人称之为成本函数。
然而,归根结底,这是一种表达咱们的预测与现实相比有多正确的方式。
loss(x, y) = crossentropy(m(x), y)
训练神经网络其实是最小化这个函数的输出,因此这是一个优化问题。训练是一个反复调整模型中参数(权重)的过程,直到损失函数的输出变低,或者换句话说,直到咱们的预测偏差变低。
均方偏差函数(MSE)是计算预测错误程度的经典方法,这就意味着取差的平方,然而,MSE更适合于线性回归(将一条或多条直线拟合到某些观测值)。
在这种状况下,咱们改用交叉熵函数。当你的最后一层是softmax,进行分类而不是线性回归时,这是我比较推荐的选择。
指定Epoch
在机器学习术语中,Epoch是训练算法进行一次完整的迭代,换句话说:一个Epoch处理一个批次并更新权重
所以,若是咱们使用10个Epoch来进行训练,那么模型的参数/权重将更新/调整10次。
为了获得200个Epoch,咱们使用repeat重复咱们的批处理200次。它实际上不会重复咱们的数据200次,它只是用迭代器建立了这样的错觉。
dataset = repeated((X, Y), 200)
在数据集中,咱们获得的数组以下:
dataset = [(X1, Y1), (X2, Y2), ..., (X200, Y200)]
优化器
最多见和最著名的训练神经网络策略是梯度降低算法,这是由Julia中的Descent类型提供的。
然而,在咱们的例子中,当咱们处理大量带有至关数量噪声的数据时,建议改用ADAM优化器,这就是所谓的随机优化。
opt = ADAM()
进行训练
咱们终于能够进行训练了,但咱们但愿在训练进行的过程当中获得一些反馈。咱们定义了一个回调函数,在每次迭代(epoch)时,它将输出loss函数的值,从而显示错误。咱们但愿每次迭代时都能看到这个错误。
evalcb = () -> @show(loss(X, Y))
观察错误发展的一个有用的地方是,你能够看到是否有振荡。人工神经网络过快地朝着最低值过渡,会致使它朝相反的方向移动,若是速度太快,则会向相反的方向超调,振荡会变得更加重烈,直到偏差变为无穷大。
这是一个切换优化算法或下降学习率的提示。
无论怎样,这就是你训练的方式。注意,回调是可选的:
Flux.train!(loss, params(m), dataset, opt, cb = throttle(evalcb, 10))
评价模型预测精度
通过训练后,咱们能够测试模型在预测方面的表现。
咱们定义了这样一个函数:
accuracy(x, y) = mean(onecold((m(x))) .== onecold(y))
而后咱们用输入数据和标签做为输入参数来调用它:
@show accuracy(X, Y)
至于什么是onecold?在某种程度上,它与onehot实现的效果是相反的。
咱们的输出m(X)都是几率分布,而咱们的目标Y都是独热向量。
它们不能直接比较,因此咱们须要使用onecold来作一个转换。给定几率分布,它选择最可能的候选:
julia> onecold([0.1, 0.7, 0.2]) 2 julia> onecold([0.9, 0.05, 0.05]) 1
所以,使用onecold(m(X))咱们能够获得预测的标签,这能够与实际的标签onecold(y)进行比较。
用测试数据验证模型
到目前为止,咱们只根据咱们使用的训练数据来验证了咱们的模型,然而,若是该模型不适用于新的数据,它将是彻底无用的。
所以,在训练网络时,咱们一般将数据分为训练数据和测试数据。测试数据不是训练的一部分,只有在训练完成后才能进行测试。
tX = hcat(float.(reshape.(MNIST.images(:test), :))...) tY = onehotbatch(MNIST.labels(:test), 0:9) @show accuracy(tX, tY)
最后
我但愿这能帮助你理解创建神经网络的过程。
太多的教程倾向于跳过向初学者解释的内容,从而全部的新概念都会很快变得使人困惑。我但愿这为初学者在进一步探索机器学习以前提供了一个起点,特别是基于Julia的机器学习,由于我认为Julia有着光明的将来。
参考连接:https://medium.com/better-programming/handwriting-recognition-using-an-artificial-neural-network-78060d2a7963