使用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