本文是我學習機器學習兩個月以來的一個小結,主要涉及Logistic回歸、Softmax回歸、神經(jīng)網(wǎng)絡和CNN四種算法,包括了簡單的原理介紹以及在TensorFlow中的實現(xiàn)。
MNIST是什么
MNIST是一組經(jīng)過預處理的手寫數(shù)字圖片數(shù)據(jù)集,它為機器學習的初學者提供了一個練手的機會,可以在真實的數(shù)據(jù)上用學到的算法來解決問題。由于很多的機器學習教程都以MNIST作為入門項目,因此它也被稱作是機器學習領域的“hello world”。
MNIST中每個樣本都是一張長28、寬28的灰度圖片,其中包含一個0-9的數(shù)字。我們需要做的,就是根據(jù)訓練數(shù)據(jù)建立一個模型用來識別輸入圖片中的數(shù)字。這是典型的分類問題,每個樣本的輸入是784維向量:一張圖片有28*28=784個像素點,每個點用一個浮點數(shù)表示其亮度;輸出是10維向量,十個分量分別表示輸入圖中數(shù)字是0~9的可能性,其中可能性最大的,就是算法預測的結果。
準備工作
本文的代碼采用Python和TensorFlow編寫,所以需要一個Python開發(fā)環(huán)境,2.7或者3.0都可以。很多開源軟件和庫對Windows的支持都不是很好,建議使用Linux或者Mac OS X,可以避免很多不必要的麻煩。推薦使用pip安裝TensorFlow:
pip install tensorflow
如果有一塊支持CUDA的顯卡,就可以安裝TensorFlow的GPU版本。使用GPU計算會大大縮短模型的訓練時間:
pip install tensorflow-gpu
安裝問題可以參考TensorFlow的官方文檔或者pip的主頁,這里不再贅述。
框架
在開始具體的算法之前,我們先搭建一個通用的框架??蚣芤瓿梢恍┎煌惴ǘ夹枰龅墓ぷ鳎热缂虞d數(shù)據(jù)集、定義和訓練模型,驗證模型準確率等等。這樣后面實現(xiàn)具體算法的時候就只需要關注跟算法相關的代碼。下面是框架的代碼,具體的解釋已經(jīng)放在注釋里了,[...]的部分就是在各種算法中需要實現(xiàn)的部分。
# encoding: utf-8
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
# 模型參數(shù),需要聲明為tensorflow變量(tf.Variable)
[...]
# 預測函數(shù),根據(jù)輸入和模型參數(shù)計算輸出結果。這個函數(shù)定義了算法模型,
# 不同算法的區(qū)別主要就在這里
def inference(x):
[...]
# 損失函數(shù)(cost function),不同算法會使用不同的損失函數(shù),但在這篇
# 文章里都是調(diào)用tf提供的庫函數(shù),因而區(qū)別不大
def loss(x, y):
[...]
# 訓練數(shù)據(jù),這里使用TensorFlow的占位符機制,其作用類似于函數(shù)的形參
X = tf.placeholder(tf.float32, [None, 784])
y_ = tf.placeholder(tf.float32, [None, 10])
z = inference(X)
total_loss = loss(X, y_)
# 學習速率,取值過大可能導致算法不能收斂。不同算法可能需要使用的不同值
learning_rate = 0.5
# 使用梯度下降算法尋找損失函數(shù)的極小值
train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(total_loss)
# 驗證預測的準確率
correct_prediction = tf.equal(tf.argmax(z, 1), tf.argmax(y_, 1))
evaluate = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# 讀取數(shù)據(jù)集,這里tf已經(jīng)封裝好了
mnist = input_data.read_data_sets("./data", one_hot=True)
# 把loss作為scalar summary寫到tf的日志,這樣就可以通過tensorboard
# 查看損失函數(shù)的變化情況,進行算法調(diào)試
writer = tf.summary.FileWriter("./log", graph=tf.get_default_graph())
loss_summary = tf.summary.scalar(b'Loss', total_loss)
with tf.Session() as sess:
# 初始化TensorFlow變量,也就是模型參數(shù)
sess.run(tf.global_variables_initializer())
# 訓練模型
training_steps = 10000
batch_size = 100
for step in range(training_steps):
batch_xs, batch_ys = mnist.train.next_batch(batch_size)
placeholder_dict = {X: batch_xs, y_: batch_ys}
sess.run(train_op, feed_dict=placeholder_dict)
summary = sess.run(loss_summary, feed_dict=placeholder_dict)
writer.add_summary(summary, global_step=step)
#在測試集上驗證模型準確率
print sess.run(evaluate,
feed_dict={X: mnist.test.images,
y_: mnist.test.labels})
機器學習的過程,就是用模型對訓練數(shù)據(jù)進行擬合的過程。這里有兩個核心,其一是“模型”。一個機器學習模型應該包括兩個部分:從輸入到輸出的計算過程,也就是框架里的inference()函數(shù);以及計算模型擬合程度的損失函數(shù),也就是loss()函數(shù)。本文中的幾種算法,還有其他更復雜的機器學習算法,都是一些經(jīng)過驗證具有實用價值的模型。機器學習算法的第二個關鍵是“擬合”,也就是在給定的模型和訓練數(shù)據(jù)下,尋找損失函數(shù)的極小值。拿人類類比一下,“模型”決定了我們?nèi)绾卫靡延械慕?jīng)驗做出決策,而“擬合”決定了我們?nèi)绾胃鶕?jù)決策的結果學習新的經(jīng)驗。不同機器學習算法的模型千差萬別,但是擬合的過程都是類似的。在這篇文章里,我們用到的是最基礎的批量梯度下降算法(Batch Gradient Descent),TensorFlow已經(jīng)幫我們實現(xiàn)了該算法,因此我們要做的就是定義好模型,提供模型參數(shù)和inference()和loss()兩個函數(shù),然后使用GradientDescentOptimizer就可以完成擬合的過程。TensorFlow還提供了其他最優(yōu)化算法,可以參考這里。
框架代碼里用到了TensorFlow的summary,其作用是把變量的值記錄在日志里,這樣就可以通過日志跟蹤某個變量在模型運算過程中的變化情況,比如這里用來跟蹤損失函數(shù)。程序運行結束之后,可以用TensorBoard查看:
tensorboard --logdir=./log/
其中./log就是FileWriter()中指定的日志目錄。執(zhí)行命令之后按照提示在瀏覽器中打開http://127.0.0.1:6006, 在“SCALARS”標簽頁中就能看到loss變量的變化曲線。跟蹤損失函數(shù)的值在調(diào)試機器學習代碼時是很有用的。在模型和代碼都正確的情況下,損失函數(shù)應該是逐漸減小的,否則就是代碼有問題,可能是模型問題,代碼實現(xiàn)不對,或者學習速率(learning_rate)過大導致?lián)p失函數(shù)無法收斂。
算法1:Logistic回歸
Logistic回歸是一種簡單的二元分類算法,其核心是sigmoid函數(shù),其函數(shù)定義如下:

Sigmoid函數(shù)接受一個實數(shù)作為輸入?yún)?shù),輸出為(0, 1)區(qū)間內(nèi)的一個數(shù)值。這個輸出值可以被當作某件事發(fā)生的概率。具體到分類問題,我們可以在輸出值大于等于0.5時預測該樣本為正例,否則預測為反例。0.5這個閾值并非固定的,可以根據(jù)實際情況進行調(diào)整,選擇預測效果最佳的。對于MNIST問題,要使用Logistic回歸進行識別,首先要把輸入的784維特征向量轉(zhuǎn)化為一個標量,這可以通過線性函數(shù)來實現(xiàn):

或者寫成向量形式:

接下來,只要把線性函數(shù)的輸出作為sigmoid函數(shù)的輸入,就可以得到一個概率值。由于sigmoid函數(shù)本身是沒有可變參數(shù)的,因此模型的輸出主要取決與第一步的線性函數(shù)的參數(shù)W和b。Logistic回歸的模型有了,但是并沒有解決數(shù)字識別的問題。Logistic回歸是二元分類算法,只能回答“是”和“否”的問題,而MNIST問題有10種不同的可能。一種直接的思路是:訓練10個不同的分類器,分別對應10個數(shù)字,這種方法叫做“one-vs-all”。圖1就是用這種方法構建的手寫數(shù)字識別器示意圖。這個圖后面還會提到。

Logistic回歸算法對應的代碼如下:
# 模型參數(shù),一個分類器的W是784維向量,10個分類器就是784*10的矩陣
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
# 預測函數(shù)
def inference(x):
z = tf.matmul(X, W) + b
# 返回預測結果,這里沒有計算sigmoid的原因是:
# 1. 損失函數(shù)需要用到z
# 2. sigmoid函數(shù)是單調(diào)遞增的,因此這里z越大,sigmoid的輸出也越大。
# 所以只根據(jù)z就可以確定預測結果,不必再計算sigmoid。
return z
# 損失函數(shù),這里使用交叉熵(cross entropy)
def loss(x, y):
z = inference(x)
return tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(
logits=z, labels=y))
算法2:Softmax回歸
盡管Logistic回歸可以通過“one-vs-all”方法解決多標簽分類問題,但是這個結果還是有一點不符合常識:假定輸入圖片是一個數(shù)字的情況下,那么它必定是0-9這10個數(shù)字中的一個,算法輸出的10個概率之和應該正好為1。上一節(jié)的方法并不能保證這一點,而本節(jié)使用Softmax回歸算法可以。
Softmax回歸的核心是Softmax函數(shù)。該函數(shù)接受n個實數(shù)輸入,并輸出n個[0, 1]區(qū)間內(nèi)的數(shù)值,第i個輸出的值為:

Softmax函數(shù)的n個輸出之和為1,正好可以用來建立多標簽分類的模型。要使用Softmax函數(shù)來建立MNIST的分類模型,首先要把輸入的784維特征向量轉(zhuǎn)換成10個特征值,這個工作我們在上一節(jié)就已經(jīng)完成了。這里我們使用相同的方法:10個線性函數(shù)。接下來只要把10個特征值輸入Softmax函數(shù)即可。這個模型跟“one-vs-all”的Logistic回歸模型如出一轍,唯一的區(qū)別是sigmoid函數(shù)換成了softmax函數(shù)。反映在代碼實現(xiàn)上,只是在計算損失函數(shù)時用softmax_cross_entropy_with_logits()替換了sigmoid_cross_entropy_with_logits()。
# 模型參數(shù)
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
# 預測函數(shù)
def inference(x):
# 這里也只計算了線性函數(shù)的輸出而沒有計算softmax,
# 原因跟logistic回歸是一樣的
z = tf.matmul(X, W) + b
return z
# 損失函數(shù)
def loss(x, y):
z = inference(x)
return tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(
logits=z, labels=y))
算法3:神經(jīng)網(wǎng)絡
神經(jīng)網(wǎng)絡是通過模擬人的神經(jīng)系統(tǒng)來實現(xiàn)機器學習的一類算法,也是深度學習的基礎。神經(jīng)網(wǎng)絡中的每個單元(也稱作“神經(jīng)元”)接受n個實數(shù)輸入,加權求和之后,再經(jīng)過一個激活函數(shù)計算得到一個輸出。多個神經(jīng)元并聯(lián)形成一個層,多個層串聯(lián)就形成了一個神經(jīng)網(wǎng)絡。
看過神經(jīng)網(wǎng)絡的描述,是不是覺得有點眼熟?回顧一下前面的Logistic回歸算法,Logistic分類器是不是跟這里的神經(jīng)元差不多?如果使用sigmoid函數(shù)作為激活函數(shù),一個神經(jīng)元就是一個logistic分類器。事實上,sigmoid函數(shù)也確實是常用的激活函數(shù)之一,也是我們這里要使用的激活函數(shù)。其他常用激活函數(shù)還有tanh和ReLU。再看圖1中用“one-vs-all”方式構建的多標簽分類器,也基本符合神經(jīng)網(wǎng)絡的定義。不過它還不能算是一個合格的神經(jīng)網(wǎng)絡。一個神經(jīng)網(wǎng)絡至少應該有三層:輸入層、隱藏層和輸出層。圖1只有輸入層和輸出層,缺少一個隱藏層。圖2就是本節(jié)要使用的神經(jīng)網(wǎng)絡,也是最簡單的三層神經(jīng)網(wǎng)絡。

雖然只是多了一個隱藏層而已,但是在很多應用場景中效果已經(jīng)相當不錯。比如CMU在上世紀80年代開發(fā)的ALVINN系統(tǒng),只用了最簡單的三層神經(jīng)網(wǎng)絡就實現(xiàn)了自動駕駛,效果可以看這里。不能上YouTube的可以看參考資料1,里面也有這段演示。不過神經(jīng)網(wǎng)絡的缺點也很明顯,就是計算量大。神經(jīng)網(wǎng)絡的損失函數(shù)是所有層的損失函數(shù)之和,而訓練過程也要更新所有層的權值。訓練神經(jīng)網(wǎng)絡的算法稱作反向傳播算法(BP,Backpropagation)。簡單來說,BP就是先計算輸出層的梯度,然后逐層反推,計算出所有隱藏層的梯度,然后根據(jù)這些梯度去更新權值。網(wǎng)絡層次越多,每層單元數(shù)量越多,計算量也就越大。如果不是GPU計算的出現(xiàn)大大縮短了神經(jīng)網(wǎng)絡的訓練時間,深度學習現(xiàn)在也不會這么火爆。TensorFlow以及其他主流機器學習框架中都已經(jīng)實現(xiàn)了BP算法,因此不需要我們關注這些細節(jié)。關于BP算法的推導和實現(xiàn),有興趣可以去看參考資料1。本人水平有限,這里就不獻丑了。
下面是用神經(jīng)網(wǎng)絡來解決MNIST問題的代碼:
# 模型參數(shù)
num_of_hidden_units = 256 # 隱藏層單元數(shù)
# 隱藏層,使用sigmoid激活函數(shù)
# 權值不能初始化為0,否則訓練過程中權值的所有分量都會一直保持相同的值
W1 = tf.Variable(tf.truncated_normal([784, num_of_hidden_units]))
b1 = tf.Variable(tf.zeros([num_of_hidden_units]))
# softmax輸出層
# 權值不能初始化為0,否則訓練過程中權值的所有分量都會一直保持相同的值
W2 = tf.Variable(tf.truncated_normal([num_of_hidden_units, 10]))
b2 = tf.Variable(tf.zeros([10]))
# 預測函數(shù),算法的核心
def inference(x):
# 隱藏層
z1 = tf.matmul(X, W1) + b1
a1 = tf.sigmoid(z1)
# 輸出層,softmax函數(shù)不用計算
z2 = tf.matmul(a1, W2) + b2
return z2
# 損失函數(shù)(cost function)
def loss(x, y):
z = inference(x)
return tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(
logits=z, labels=y))
算法4:卷積神經(jīng)網(wǎng)絡
卷積神經(jīng)網(wǎng)絡(Convolutional Neural Network, CNN)是出現(xiàn)比較早也比較成熟的深度學習算法之一,主要應用與圖像識別問題,例如本文的MNIST。簡單來說,CNN就是具有至少一個卷積層的神經(jīng)網(wǎng)絡。這里的卷積,是指離散卷積,其定義如下:

其中f是輸入圖像,g稱作核函數(shù)(kernal function)。卷積層的作用是對圖像進行特征提取,不同的核函數(shù)可以提取出圖像不同方面的特征。用傳統(tǒng)的機器學習方法進行圖像識別,需要使用專門的圖像處理算法預先將原始圖像轉(zhuǎn)換成某種特征向量,才能進行模型訓練。提取特征的質(zhì)量直接關系到最后的結果。用CNN就沒有這些麻煩,可以直接用原始圖像訓練模型,這也是CNN(以及其他深度學習算法)受熱捧的原因之一。
數(shù)學不好,對卷積運算就不再糾纏了,TensorFlow中有現(xiàn)成的函數(shù)conv2d(),知道怎么調(diào)用就可以了。在本節(jié)的實現(xiàn)中,W_conv就是核函數(shù),[5, 5, 1, 64]表示這個核函數(shù)可以對長為5、寬為5、通道數(shù)為1的圖像進行運算,輸出5*5*64的張量。通道數(shù)是指每個像素點用多少個數(shù)值表示。MNIST使用的是灰度圖像,每個像素點只需要一個數(shù)值,因此這里通道數(shù)為1。如果是RGB圖像,就需要三個數(shù)值,通道數(shù)就是3。
函數(shù)conv2d()就是把輸入圖像分成若干個5*5的小塊,逐個與核函數(shù)進行運算,然后輸出結果。參數(shù)padding表示進行卷積運算時對原始圖像的分塊方式,SAME表示輸出的尺寸與輸入圖像的長寬保持一致,因此在本例中conv2d()的輸出就是28*\28*64。按照這種分塊方式,輸入圖像的邊緣區(qū)域就沒有足夠多的像素進行卷積運算,算法會用0填充(也就是padding)之后再進行卷積。參數(shù)padding的另一種取值是VALID,表示不對原始圖像進行填充,卷積運算只會覆蓋到輸入圖像的有效像素。如果使用VALID方式,這里的輸出就變成了24*\24*64,隱藏層對應的參數(shù)也要相應的進行修改,有興趣可以試一下。
卷積層同樣需要一個激活函數(shù),這里使用前面提到過的ReLU函數(shù),定義如下:

卷積運算通常會增加每個樣本數(shù)據(jù)點的數(shù)量,比如這里就把28*28*1的輸入圖像變成了28*28*64個特征值。這大大增加了模型的運算量。因此,卷積層之后通常緊隨一個pooling層,用來進行下降抽樣(down sampling),加快運算速度。它把輸入圖像分割成指定大小的矩形區(qū)域,輸出每個區(qū)域內(nèi)點的最大值(max-pooling)或平均值(mean-pooling)。本文的代碼中使用TensorFlow提供的max_pool()函數(shù)。參數(shù)ksize表示抽樣的尺寸,四個數(shù)值分別表示batch、height、width、channels,[1, 4, 4, 1]的含義就是每次對1個輸入樣本中4*4區(qū)域的1個通道進行抽樣。參數(shù)strides表示多次抽樣之間的間隔距離,strides和ksize的情況下,多次抽樣之間就不會有交叉。因此,這里做的就是對卷積輸出中每個不相交的4*4區(qū)域的每個通道分別取最大值,得到的輸出為7*7*64的張量。參數(shù)padding的含義與conv2d()相同。
到這里,用卷積運算進行特征提取已經(jīng)完成了。對于更復雜的應用,可能有多個卷積層,這里就不討論了。在本文的實現(xiàn)中,pooling層之后就是和第3節(jié)中一樣的全連接隱藏層,不過這里使用ReLU替代sigmoid作為激活函數(shù)。最后再使用一個第2節(jié)中的softmax層作為輸出層。代碼如下:
# 卷積層,W_conv即核函數(shù)
W_conv = tf.Variable(tf.truncated_normal([5, 5, 1, 64]))
b_conv = tf.Variable(tf.zeros([64]))
# 全連接隱藏層
W_hidden = tf.Variable(tf.truncated_normal([7 * 7 * 64, 1024]))
b_hidden = tf.Variable(tf.zeros([1024]))
# 輸出層
W_output = tf.Variable(tf.truncated_normal([1024, 10]))
b_output = tf.Variable(tf.zeros([10]))
# feedforward
def inference(x):
# 首先要把
x_img = tf.reshape(x, [-1, 28, 28, 1])
# 卷積運算
convedActivations = tf.nn.relu(
tf.nn.conv2d(
x_img, W_conv, strides=[1, 1, 1, 1], padding='SAME') + b_conv)
# pooling
pooledActivations = tf.nn.max_pool(
convedActivations,
ksize=[1, 4, 4, 1],
strides=[1, 4, 4, 1],
padding='SAME')
# 隱藏層
pooledActivationsFlat = tf.reshape(pooledActivations, [-1, 7 * 7 * 64])
hiddenActivations = tf.nn.relu(tf.matmul(pooledActivationsFlat, W_hidden) + b_hidden)
# 輸出層
logits = tf.matmul(hiddenActivations, W_output) + b_output
return logits
# 損失函數(shù)
def loss(x, y):
z = inference(x)
return tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(
labels=y, logits=z, name='xentropy'))
把這段代碼放在框架里直接執(zhí)行的話,會發(fā)現(xiàn)結果很差。原因是學習速率過大,導致?lián)p失函數(shù)不能收斂。只要把框架里learning_rate變量的值改為1e-3即可。
參考資料
- TensorFlow官方教程。學習開源項目,官方教程和文檔永遠是排在第一位的參考資料。尤其是Google這種大公司的開源項目,官方資料還是很靠譜的。
- 吳恩達(Andrew Ng)在Coursera上的機器學習在線課程,應該是最流行的機器學習教程了。內(nèi)容深入淺出,非常適合入門。雖然剛開始不是很習慣吳恩達的口語發(fā)音,但是全套課程看下來,還是很喜歡這位老師的。課程中的實驗使用的是Matlab/Octave,如果沒有基礎可能會感覺有點兒吃力,好在需要自己寫的代碼并不是很多,有時間還是建議都做一下,對理解算法很有幫助。我就是連蒙帶猜加搜索,一點點啃下來的。
- Standford的Deep Learning Toturials。這個也是吳恩達那批人搞的,在2的基礎上增加了深度學習的內(nèi)容,內(nèi)容上具有連貫性。學習過2之后,再通過這個教程進入深度學習,是很不錯的選擇。
- 《Tensorflow for Machine Intelligence》。既有TensorFlow的教程,也有關于機器學習和深度學習算法的講解,對于初學者是很不錯的入門參考。書中的實驗代碼都可以從Github上下載。需要注意的是,因為這本書出版得比較早,里面關于TensorFlow的部分內(nèi)容已經(jīng)過時,有些代碼可能需要修改才能運行。