本文是我學習機器學習兩個月以來的一個小結,主要涉及Logistic回歸、Softmax回歸、神經網絡和CNN四種算法,包括了簡單的原理介紹以及在TensorFlow中的實現。
MNIST是什么
MNIST是一組經過預處理的手寫數字圖片數據集,它為機器學習的初學者提供了一個練手的機會,可以在真實的數據上用學到的算法來解決問題。由于很多的機器學習教程都以MNIST作為入門項目,因此它也被稱作是機器學習領域的“hello world”。
MNIST中每個樣本都是一張長28、寬28的灰度圖片,其中包含一個0-9的數字。我們需要做的,就是根據訓練數據建立一個模型用來識別輸入圖片中的數字。這是典型的分類問題,每個樣本的輸入是784維向量:一張圖片有28*28=784個像素點,每個點用一個浮點數表示其亮度;輸出是10維向量,十個分量分別表示輸入圖中數字是0~9的可能性,其中可能性最大的,就是算法預測的結果。
準備工作
本文的代碼采用Python和TensorFlow編寫,所以需要一個Python開發環境,2.7或者3.0都可以。很多開源軟件和庫對Windows的支持都不是很好,建議使用Linux或者Mac OS X,可以避免很多不必要的麻煩。推薦使用pip安裝TensorFlow:
pip install tensorflow
如果有一塊支持CUDA的顯卡,就可以安裝TensorFlow的GPU版本。使用GPU計算會大大縮短模型的訓練時間:
pip install tensorflow-gpu
安裝問題可以參考TensorFlow的官方文檔或者pip的主頁,這里不再贅述。
框架
在開始具體的算法之前,我們先搭建一個通用的框架。框架要完成一些不同算法都需要做的工作,比如加載數據集、定義和訓練模型,驗證模型準確率等等。這樣后面實現具體算法的時候就只需要關注跟算法相關的代碼。下面是框架的代碼,具體的解釋已經放在注釋里了,[...]的部分就是在各種算法中需要實現的部分。
# encoding: utf-8
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
# 模型參數,需要聲明為tensorflow變量(tf.Variable)
[...]
# 預測函數,根據輸入和模型參數計算輸出結果。這個函數定義了算法模型,
# 不同算法的區別主要就在這里
def inference(x):
[...]
# 損失函數(cost function),不同算法會使用不同的損失函數,但在這篇
# 文章里都是調用tf提供的庫函數,因而區別不大
def loss(x, y):
[...]
# 訓練數據,這里使用TensorFlow的占位符機制,其作用類似于函數的形參
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
# 使用梯度下降算法尋找損失函數的極小值
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))
# 讀取數據集,這里tf已經封裝好了
mnist = input_data.read_data_sets("./data", one_hot=True)
# 把loss作為scalar summary寫到tf的日志,這樣就可以通過tensorboard
# 查看損失函數的變化情況,進行算法調試
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變量,也就是模型參數
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})
機器學習的過程,就是用模型對訓練數據進行擬合的過程。這里有兩個核心,其一是“模型”。一個機器學習模型應該包括兩個部分:從輸入到輸出的計算過程,也就是框架里的inference()函數;以及計算模型擬合程度的損失函數,也就是loss()函數。本文中的幾種算法,還有其他更復雜的機器學習算法,都是一些經過驗證具有實用價值的模型。機器學習算法的第二個關鍵是“擬合”,也就是在給定的模型和訓練數據下,尋找損失函數的極小值。拿人類類比一下,“模型”決定了我們如何利用已有的經驗做出決策,而“擬合”決定了我們如何根據決策的結果學習新的經驗。不同機器學習算法的模型千差萬別,但是擬合的過程都是類似的。在這篇文章里,我們用到的是最基礎的批量梯度下降算法(Batch Gradient Descent),TensorFlow已經幫我們實現了該算法,因此我們要做的就是定義好模型,提供模型參數和inference()和loss()兩個函數,然后使用GradientDescentOptimizer就可以完成擬合的過程。TensorFlow還提供了其他最優化算法,可以參考這里。
框架代碼里用到了TensorFlow的summary,其作用是把變量的值記錄在日志里,這樣就可以通過日志跟蹤某個變量在模型運算過程中的變化情況,比如這里用來跟蹤損失函數。程序運行結束之后,可以用TensorBoard查看:
tensorboard --logdir=./log/
其中./log就是FileWriter()中指定的日志目錄。執行命令之后按照提示在瀏覽器中打開http://127.0.0.1:6006, 在“SCALARS”標簽頁中就能看到loss變量的變化曲線。跟蹤損失函數的值在調試機器學習代碼時是很有用的。在模型和代碼都正確的情況下,損失函數應該是逐漸減小的,否則就是代碼有問題,可能是模型問題,代碼實現不對,或者學習速率(learning_rate)過大導致損失函數無法收斂。
算法1:Logistic回歸
Logistic回歸是一種簡單的二元分類算法,其核心是sigmoid函數,其函數定義如下:
Sigmoid函數接受一個實數作為輸入參數,輸出為(0, 1)區間內的一個數值。這個輸出值可以被當作某件事發生的概率。具體到分類問題,我們可以在輸出值大于等于0.5時預測該樣本為正例,否則預測為反例。0.5這個閾值并非固定的,可以根據實際情況進行調整,選擇預測效果最佳的。對于MNIST問題,要使用Logistic回歸進行識別,首先要把輸入的784維特征向量轉化為一個標量,這可以通過線性函數來實現:
或者寫成向量形式:
接下來,只要把線性函數的輸出作為sigmoid函數的輸入,就可以得到一個概率值。由于sigmoid函數本身是沒有可變參數的,因此模型的輸出主要取決與第一步的線性函數的參數W和b。Logistic回歸的模型有了,但是并沒有解決數字識別的問題。Logistic回歸是二元分類算法,只能回答“是”和“否”的問題,而MNIST問題有10種不同的可能。一種直接的思路是:訓練10個不同的分類器,分別對應10個數字,這種方法叫做“one-vs-all”。圖1就是用這種方法構建的手寫數字識別器示意圖。這個圖后面還會提到。
Logistic回歸算法對應的代碼如下:
# 模型參數,一個分類器的W是784維向量,10個分類器就是784*10的矩陣
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
# 預測函數
def inference(x):
z = tf.matmul(X, W) + b
# 返回預測結果,這里沒有計算sigmoid的原因是:
# 1. 損失函數需要用到z
# 2. sigmoid函數是單調遞增的,因此這里z越大,sigmoid的輸出也越大。
# 所以只根據z就可以確定預測結果,不必再計算sigmoid。
return z
# 損失函數,這里使用交叉熵(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”方法解決多標簽分類問題,但是這個結果還是有一點不符合常識:假定輸入圖片是一個數字的情況下,那么它必定是0-9這10個數字中的一個,算法輸出的10個概率之和應該正好為1。上一節的方法并不能保證這一點,而本節使用Softmax回歸算法可以。
Softmax回歸的核心是Softmax函數。該函數接受n個實數輸入,并輸出n個[0, 1]區間內的數值,第i個輸出的值為:
Softmax函數的n個輸出之和為1,正好可以用來建立多標簽分類的模型。要使用Softmax函數來建立MNIST的分類模型,首先要把輸入的784維特征向量轉換成10個特征值,這個工作我們在上一節就已經完成了。這里我們使用相同的方法:10個線性函數。接下來只要把10個特征值輸入Softmax函數即可。這個模型跟“one-vs-all”的Logistic回歸模型如出一轍,唯一的區別是sigmoid函數換成了softmax函數。反映在代碼實現上,只是在計算損失函數時用softmax_cross_entropy_with_logits()替換了sigmoid_cross_entropy_with_logits()。
# 模型參數
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
# 預測函數
def inference(x):
# 這里也只計算了線性函數的輸出而沒有計算softmax,
# 原因跟logistic回歸是一樣的
z = tf.matmul(X, W) + b
return z
# 損失函數
def loss(x, y):
z = inference(x)
return tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(
logits=z, labels=y))
算法3:神經網絡
神經網絡是通過模擬人的神經系統來實現機器學習的一類算法,也是深度學習的基礎。神經網絡中的每個單元(也稱作“神經元”)接受n個實數輸入,加權求和之后,再經過一個激活函數計算得到一個輸出。多個神經元并聯形成一個層,多個層串聯就形成了一個神經網絡。
看過神經網絡的描述,是不是覺得有點眼熟?回顧一下前面的Logistic回歸算法,Logistic分類器是不是跟這里的神經元差不多?如果使用sigmoid函數作為激活函數,一個神經元就是一個logistic分類器。事實上,sigmoid函數也確實是常用的激活函數之一,也是我們這里要使用的激活函數。其他常用激活函數還有tanh和ReLU。再看圖1中用“one-vs-all”方式構建的多標簽分類器,也基本符合神經網絡的定義。不過它還不能算是一個合格的神經網絡。一個神經網絡至少應該有三層:輸入層、隱藏層和輸出層。圖1只有輸入層和輸出層,缺少一個隱藏層。圖2就是本節要使用的神經網絡,也是最簡單的三層神經網絡。
雖然只是多了一個隱藏層而已,但是在很多應用場景中效果已經相當不錯。比如CMU在上世紀80年代開發的ALVINN系統,只用了最簡單的三層神經網絡就實現了自動駕駛,效果可以看這里。不能上YouTube的可以看參考資料1,里面也有這段演示。不過神經網絡的缺點也很明顯,就是計算量大。神經網絡的損失函數是所有層的損失函數之和,而訓練過程也要更新所有層的權值。訓練神經網絡的算法稱作反向傳播算法(BP,Backpropagation)。簡單來說,BP就是先計算輸出層的梯度,然后逐層反推,計算出所有隱藏層的梯度,然后根據這些梯度去更新權值。網絡層次越多,每層單元數量越多,計算量也就越大。如果不是GPU計算的出現大大縮短了神經網絡的訓練時間,深度學習現在也不會這么火爆。TensorFlow以及其他主流機器學習框架中都已經實現了BP算法,因此不需要我們關注這些細節。關于BP算法的推導和實現,有興趣可以去看參考資料1。本人水平有限,這里就不獻丑了。
下面是用神經網絡來解決MNIST問題的代碼:
# 模型參數
num_of_hidden_units = 256 # 隱藏層單元數
# 隱藏層,使用sigmoid激活函數
# 權值不能初始化為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]))
# 預測函數,算法的核心
def inference(x):
# 隱藏層
z1 = tf.matmul(X, W1) + b1
a1 = tf.sigmoid(z1)
# 輸出層,softmax函數不用計算
z2 = tf.matmul(a1, W2) + b2
return z2
# 損失函數(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:卷積神經網絡
卷積神經網絡(Convolutional Neural Network, CNN)是出現比較早也比較成熟的深度學習算法之一,主要應用與圖像識別問題,例如本文的MNIST。簡單來說,CNN就是具有至少一個卷積層的神經網絡。這里的卷積,是指離散卷積,其定義如下:
其中f是輸入圖像,g稱作核函數(kernal function)。卷積層的作用是對圖像進行特征提取,不同的核函數可以提取出圖像不同方面的特征。用傳統的機器學習方法進行圖像識別,需要使用專門的圖像處理算法預先將原始圖像轉換成某種特征向量,才能進行模型訓練。提取特征的質量直接關系到最后的結果。用CNN就沒有這些麻煩,可以直接用原始圖像訓練模型,這也是CNN(以及其他深度學習算法)受熱捧的原因之一。
數學不好,對卷積運算就不再糾纏了,TensorFlow中有現成的函數conv2d(),知道怎么調用就可以了。在本節的實現中,W_conv就是核函數,[5, 5, 1, 64]表示這個核函數可以對長為5、寬為5、通道數為1的圖像進行運算,輸出5*5*64的張量。通道數是指每個像素點用多少個數值表示。MNIST使用的是灰度圖像,每個像素點只需要一個數值,因此這里通道數為1。如果是RGB圖像,就需要三個數值,通道數就是3。
函數conv2d()就是把輸入圖像分成若干個5*5的小塊,逐個與核函數進行運算,然后輸出結果。參數padding表示進行卷積運算時對原始圖像的分塊方式,SAME表示輸出的尺寸與輸入圖像的長寬保持一致,因此在本例中conv2d()的輸出就是28*\28*64。按照這種分塊方式,輸入圖像的邊緣區域就沒有足夠多的像素進行卷積運算,算法會用0填充(也就是padding)之后再進行卷積。參數padding的另一種取值是VALID,表示不對原始圖像進行填充,卷積運算只會覆蓋到輸入圖像的有效像素。如果使用VALID方式,這里的輸出就變成了24*\24*64,隱藏層對應的參數也要相應的進行修改,有興趣可以試一下。
卷積層同樣需要一個激活函數,這里使用前面提到過的ReLU函數,定義如下:
卷積運算通常會增加每個樣本數據點的數量,比如這里就把28*28*1的輸入圖像變成了28*28*64個特征值。這大大增加了模型的運算量。因此,卷積層之后通常緊隨一個pooling層,用來進行下降抽樣(down sampling),加快運算速度。它把輸入圖像分割成指定大小的矩形區域,輸出每個區域內點的最大值(max-pooling)或平均值(mean-pooling)。本文的代碼中使用TensorFlow提供的max_pool()函數。參數ksize表示抽樣的尺寸,四個數值分別表示batch、height、width、channels,[1, 4, 4, 1]的含義就是每次對1個輸入樣本中4*4區域的1個通道進行抽樣。參數strides表示多次抽樣之間的間隔距離,strides和ksize的情況下,多次抽樣之間就不會有交叉。因此,這里做的就是對卷積輸出中每個不相交的4*4區域的每個通道分別取最大值,得到的輸出為7*7*64的張量。參數padding的含義與conv2d()相同。
到這里,用卷積運算進行特征提取已經完成了。對于更復雜的應用,可能有多個卷積層,這里就不討論了。在本文的實現中,pooling層之后就是和第3節中一樣的全連接隱藏層,不過這里使用ReLU替代sigmoid作為激活函數。最后再使用一個第2節中的softmax層作為輸出層。代碼如下:
# 卷積層,W_conv即核函數
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
# 損失函數
def loss(x, y):
z = inference(x)
return tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(
labels=y, logits=z, name='xentropy'))
把這段代碼放在框架里直接執行的話,會發現結果很差。原因是學習速率過大,導致損失函數不能收斂。只要把框架里learning_rate變量的值改為1e-3即可。
參考資料
- TensorFlow官方教程。學習開源項目,官方教程和文檔永遠是排在第一位的參考資料。尤其是Google這種大公司的開源項目,官方資料還是很靠譜的。
- 吳恩達(Andrew Ng)在Coursera上的機器學習在線課程,應該是最流行的機器學習教程了。內容深入淺出,非常適合入門。雖然剛開始不是很習慣吳恩達的口語發音,但是全套課程看下來,還是很喜歡這位老師的。課程中的實驗使用的是Matlab/Octave,如果沒有基礎可能會感覺有點兒吃力,好在需要自己寫的代碼并不是很多,有時間還是建議都做一下,對理解算法很有幫助。我就是連蒙帶猜加搜索,一點點啃下來的。
- Standford的Deep Learning Toturials。這個也是吳恩達那批人搞的,在2的基礎上增加了深度學習的內容,內容上具有連貫性。學習過2之后,再通過這個教程進入深度學習,是很不錯的選擇。
- 《Tensorflow for Machine Intelligence》。既有TensorFlow的教程,也有關于機器學習和深度學習算法的講解,對于初學者是很不錯的入門參考。書中的實驗代碼都可以從Github上下載。需要注意的是,因為這本書出版得比較早,里面關于TensorFlow的部分內容已經過時,有些代碼可能需要修改才能運行。