1. 程式人生 > >1.MNIST手寫體識別Softmax&&CNN兩種實現

1.MNIST手寫體識別Softmax&&CNN兩種實現

一、MNIST資料集簡介

       MNIST資料集是由一些手寫數字的圖片和相應的標籤組成,標籤共有十類,分別對應0~9,下面給出資料集的具體示例:

  1. 原始資料集包含以下四個檔案,在tensorflow中,通過input_data模組,呼叫read_data_sets()可以獲取到,但是這個方法需要翻牆,沒法翻牆的同學,請點選這裡下載,下載完之後,放在python檔案同級目錄的MNIST_data資料夾下面:

  2. 在原始的資料集中,每一張圖片由一個784維的向量表示,我們reshape成(28,28)的形狀之後,原始資料變成這樣子:

  3. 在上面的基礎之下,我們再呼叫scipy.misc.toimage()方法,然後進行save()操作(注意,這種方法依賴於pillow模組,所以呼叫時先在控制檯執行 pip install pillow

    ),或者直接呼叫scipy.misc.imsave()方法,最終生成結果如下:

  4. 這一部分的程式碼儲存為save_pic.py,具體程式碼如下:

#coding:utf-8

from tensorflow.examples.tutorials.mnist import input_data
import scipy.misc
import os

# 讀取MNIST資料集
mnist = input_data.read_data_sets("MNIST_data/",one_hot = True)

# 將原始圖片儲存在MNIST_data/raw/資料夾下面,如果沒有這個資料夾,會自動建立
save_dir = 'MNIST_data/raw/' if os.path.exists(save_dir) is False: os.makedirs(save_dir) # 儲存前20張圖片 for i in range(20): image_array = mnist.train.images[i,:] # image_array 就是一個(1,784)的向量 image_array = image_array.reshape(28,28) # 儲存檔案的格式為: # mnist_train_0.jpg 、mnist_train_1.jpg ... file_name = save_dir+'mnist_train_%d.jpg'
%i # 將image_array儲存為圖片,通過scipy.misc.toimage方法,再呼叫save方法儲存 # scipy.misc.toimage(image_array,cmin=0.0,cmax=1.0).save(file_name) # 方法1 scipy.misc.imsave(file_name, image_array) # 方法2

二、Softmax迴歸識別MNIST

1. Softmax函式的定義:
       Softmax函式的功能是將各個類別的”得分“轉化成合理的概率值,要求是每個類別的概率值必須屬於(0,1),並且所有類別的概率值之和為1.
       舉一個簡單的例子:

2. Softmax在tensorflow中的應用:
       x是單個樣本的特徵,在MNIST資料集中,x是一個784維的向量;W,b是Softmax模型的引數,W是一個(784,10)的矩陣,b是一個10維的向量;
       Softmax模型的第一步就是計算樣本對應每一個類別的Logit;第二步就是將Logit經Softmax函式轉成概率值;第三步就是比較每個類別的概率值,將樣本劃分為概率值最大的那一類。
        (1) Logit = WT*x + b
        (2) y = softmax(Logit)
3. Softmax實現分類的程式碼儲存為softmax_regression.py,具體如下,裡面有詳細註釋:

#coding:utf-8

from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf 

# 讀取MNIST資料集
mnist = input_data.read_data_sets("MNIST_data/",one_hot = True)

# 接下來提到的placeholder和Variable,它們都代表著“Tensor”;
# Tensorflow中的Tensor並不是具體的數值,它代表著一些我們希望Tensorflow系統進行計算的“節點”

# 建立x,x是一個佔位符(placeholder),代表待識別的圖片
x = tf.placeholder(tf.float32, [None, 784]) # None為預設值,表示傳入圖片樣本的數量不固定

# W是Softmax模型的引數,將一個784維輸入轉換為10維的輸出
# 在tensorflow中,變數的引數用tf.Variable表示
W = tf.Variable(tf.zeros([784, 10]))  # 初始化一個(784,10)的零矩陣
# b是Softmax模型的另一個引數,稱為偏置項(bias)
b = tf.Variable(tf.zeros([10])) # 初始化一個(10,)的向量

# y表示模型的輸出; tf.matmul 矩陣乘法:矩陣a乘以矩陣b,生成a * b
# y是一個(N,10)的矩陣,N由訓練的樣本數決定,每一行代表了該樣本屬於每一類的概率
y = tf.nn.softmax(tf.matmul(x, W) + b) 

# y_ 是真實的影象標籤,也用佔位符表示
# 一個10維的向量,one-hot(只有一項元素為1,其餘元素為0)表示
y_ = tf.placeholder(tf.float32, [None, 10]) 

# 至此,我們得到了兩個重要的Tensor:y和y_
# 於是我們可以構造損失函式(交叉熵損失)
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y))) # tf.reduce_mean求平均值

# 下一步是要讓損失減到最小,用梯度下降法
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

# 在優化損失前,必須建立一個會話(Session),並在會話中對變數進行初始化操作
# 這裡提到的會話,就可以看成是對上面那些tensor進行計算的上下文(context) 容器? 變數的值就是儲存在session,placeholder的值不會被儲存。
sess = tf.InteractiveSession()
tf.global_variables_initializer().run() # 初始化所有變數,分配記憶體

print('start training...')
# 進行1000步梯度下降 每次訓練提取100個數據
for _ in range(1000):
    # 在mnist.train中取100個訓練資料
    # batch_xs是形狀為(100,784)的訓練資料, batch_ys是形狀為(100,10)的實際標籤
    # next_batch函式返回 self._images[start:end], self._labels[start:end]
    batch_xs, batch_ys = mnist.train.next_batch(100)
    #在session中執行train_step,執行時傳入佔位符的值
    sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

# 正確的預測結果
correct_prediction = tf.equal(tf.argmax(y,1),tf.arg_max(y_,1)) # tf.argmax(y,1) 取出陣列中最大值的下標
# 計算預測準確率
accuracy = tf.reduce_mean(tf.cast(correct_prediction,tf.float32)) # tf.cast把陣列中的True轉為1,False轉為0
# 獲取最終模型的準確率
print(sess.run(accuracy,feed_dict={x:mnist.test.images,y_:mnist.test.labels}))

4. 程式執行結果:

三、兩層卷積神經網路識別MNIST

1. 本次CNN模型的訓練過程:
(1) 第一層卷積層:
       用32個大小為5×5的卷積核對原始影象進行卷積(卷積計算的方式是輸入矩陣與卷積核矩陣對應元素相乘,然後累加求和,作為卷積後的新元素值),卷積完成之後,構成一個新的矩陣;然後再對新的矩陣用2*2的池化核進行池化操作(取最大值法或者平均數法等),得到一個新矩陣;

(2) 第二層卷積層:
       用64個大小為5×5的卷積核對原始影象進行卷積,構成一個新的矩陣,然後再對新的矩陣用2*2的池化核進行池化操作,得到一個新矩陣;
(3) 第一層全連線:
     前面兩層卷積之後,將原來的28×28的矩陣,轉換成了一個三維的矩陣表示(7×7×64),我們首先將其展開成一維向量的形式,然後將其作為一層神經網路的輸入,輸出為1024維向量;
(4) 第二層全連線:
        因為我們最終要得到的是樣本在10種類別下的得分值(Logit),所以第二層全連線的功能是將由1024維向量表示的樣本作為神經網路的輸入,輸出結果為一個10維向量,每一個元素代表該樣本在每一個類別下的Logit;
(5) 損失函式優化:
       還是根據交叉熵來定義損失函式,由上面得到的十維向量,可以經softmax函式轉成概率值,然後用梯度下降演算法,得到最佳的引數W,b,模型訓練完畢。
2. 卷積核與池化核的padding屬性
       卷積和池化過程中都有padding的屬性,padding引數值為”VALID”或者”SAME”,不同的引數,最終生成的新矩陣形狀不一定相同。padding在兩個過程中的作用是一樣的,下面以池化過程為例,從程式碼的角度來進行講解,這裡引用了這篇部落格

# 建立一個常量x,是一個2*3的矩陣
x = tf.constant([[1., 2., 3.],
                 [4., 5., 6.]])
# 將x轉化成池化函式tf.nn.max_pool的引數
# [1,2,3,1]分別表示:圖片數為1,圖片高為2,寬為3,通道數為1
x = tf.reshape(x, [1, 2, 3, 1]) 

# 池化操作
# 引數介紹
# x:輸入圖片,依然是[batch, height, width, channels]這樣的shape
# ksize:池化視窗的大小,四維向量,一般是[1, height, width, 1]
# strides:視窗在每一個維度上滑動的步長,一般也是[1, stride,stride, 1]
# padding:決定池化的方式,只能是“SAME”或者“VALID”其中之一
# 返回一個Tensor,型別不變,shape仍然是[batch, height, width, channels]
valid_pad = tf.nn.max_pool(x, [1, 2, 2, 1], [1, 2, 2, 1], padding='VALID')
same_pad = tf.nn.max_pool(x, [1, 2, 2, 1], [1, 2, 2, 1], padding='SAME')

print(valid_pad.get_shape())
print(same_pad.get_shape())

       最後輸出的結果為:

(1, 1, 1, 1) 
(1, 1, 2, 1)

       可以看出“SAME”的填充方式是比“VALID”的填充方式多了一列;
       我們來看看變數x是一個2x3的矩陣,max pooling視窗為2x2,兩個維度(向下移動、向右移動)的strides=2;

  1. 第一次由於視窗可以覆蓋(橙色區域做 max pooling),沒什麼問題,如下:
  2. 接下來就是“SAME”和“VALID”的區別所在:由於步長為 2,當向右滑動兩步之後,“VALID”方式發現餘下的視窗不到 2x2 所以直接將第三列捨棄,而“SAME”方式並不會把多出的一列丟棄,但是隻有一列了不夠 2x2 怎麼辦?填充!

  3. 我們設計網路結構時需要設定輸入輸出的shape,原始碼nn_ops.py中的convolution函式和pool函式給出的計算公式如下:

If padding == "SAME":
      output_spatial_shape[i] = ceil(input_spatial_shape[i] / strides[i])

    If padding == "VALID":
      output_spatial_shape[i] =
        ceil((input_spatial_shape[i] -
              (spatial_filter_shape[i]-1) * dilation_rate[i])
              / strides[i]).

       dilation_rate為一個可選的引數,預設為1,這裡我們可以先不管它。
       整理一下,對於“VALID”,輸出的形狀計算如下:
       new_height or new weight = ⌈(W - F +1) / S⌉
       整理一下,對於“SAME”,輸出的形狀計算如下:
       new_height or new weight = ⌈W / S⌉
       其中,W為輸入矩陣的高或寬,F為池化視窗的高或寬,S為height方向或者weight方向移動的步長;

通過這一部分對於卷積和池化過程中對於padding的理解,我們也就明白了,這個案例中,為什
麼VALID和SAME得到兩種不同形狀的矩陣了;此外,我們從下面的程式碼中,也就明白了,兩次卷
積層,得到的矩陣形狀,一個是(14,14),一個是(7,7)是怎麼計算來的了。

4. 兩層卷積神經網路實現分類的程式碼儲存為cnn.py,具體如下,裡面有詳細註釋:

#coding:utf-8

from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf 

# 讀取MNIST資料集
mnist = input_data.read_data_sets("MNIST_data/",one_hot = True)

# 讀入資料,兩個佔位符,用於表示 mnist.train.images,和 mnist.train.labels
x = tf.placeholder(tf.float32,[None,784])
y_ = tf.placeholder(tf.float32,[None,10])  

# 由於使用的是卷積神經網路對影象進行分類,故需要使用28*28的圖片表示;
# [-1,28,28,1]中的-1表示第一維的大小根據x自動確定
x_image = tf.reshape(x,[-1,28,28,1]) # 把輸入shape轉換成了4D tensor,第二與第三維度對應的是照片的高度與寬度,最後一個維度是顏色通道數

# 我們對訓練影象進行卷積計算

# 定義卷積核的權重引數矩陣,代表kernel
def weight_variable(shape):
    # tf.truncated_normal(shape, mean, stddev) :shape表示生成張量的維度,mean是均值,stddev是標準差。
    # 這個函式產生截斷正太分佈,均值和標準差自己設定,截斷式正態分佈生成的資料在(μ-2σ,μ+2σ)
    initial = tf.truncated_normal(shape,stddev=0.1) # 預設mean=0.0,stddev=1.0
    return tf.Variable(initial)

# 定義卷積核的偏置引數矩陣,代表bias
def bias_variable(shape):
    initial = tf.constant(0.1,shape=shape) # 建立一個常數張量,用0.1來填充
    return tf.Variable(initial)

# 卷積操作
# 引數介紹
# x:input,需要做卷積操作的輸入影象,shape為[訓練時1個batch的圖片數量,圖片高度,圖片寬度,圖片通道數]
# W:filter,也就是卷積核,shape為[卷積核的高度,卷積核的寬度,影象通道數,卷積核的數量]
# strides:卷積時在影象每一維的步長,一維向量,長度為4
# padding:決定卷積的方式,只能是“SAME”或者“VALID”其中之一 ,計算方式見ppt
def conv2d(x,W):
    return tf.nn.conv2d(x,W,strides=[1,1,1,1],padding='SAME')

# 池化操作
# 引數介紹
# x:feature map,依然是[batch, height, width, channels]這樣的shape
# ksize:池化視窗的大小,四維向量,一般是[1, height, width, 1],因為我們不想在batch和channels上做池化,所以這兩個維度設為了1
# strides:視窗在每一個維度上滑動的步長,一般也是[1, stride,stride, 1]
# padding:決定池化的方式,只能是“SAME”或者“VALID”其中之一
# 返回一個Tensor,型別不變,shape仍然是[batch, height, width, channels]這種形式
def max_pool_2x2(x):
    return tf.nn.max_pool(x,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')

# 第一層卷積
W_conv1 = weight_variable([5,5,1,32])  # 卷積核大小為(5,5),通道數為1,32個
b_conv1 = bias_variable([32]) # bias 32維向量
h_conv1 = tf.nn.relu(conv2d(x_image,W_conv1)+b_conv1) # relu啟用函式(負數變為0,正數不變化)
h_pool1 = max_pool_2x2(h_conv1) # 最大值池化法 

# 第二層卷積
W_conv2 = weight_variable([5,5,32,64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1,W_conv2)+b_conv2)
h_pool2 = max_pool_2x2(h_conv2) # 最終shape為(None,7,7,64)

# 兩層卷積之後是全連線層,輸出為1024維向量
W_fc1 = weight_variable([7*7*64,1024]) # 將7*7*64轉為1024維
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2,[-1,7*7*64]) # (None,7,7,64)轉為(-1,7*7*64)
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat,W_fc1)+b_fc1)
# 使用Dropout,keep_prob是一個佔位符,訓練時為0.5,測試時為1
keep_prob = tf.placeholder(tf.float32)

# 加入Dropout,是為了防止神經網路過擬合的一種手段,在每一次訓練中,以一定概率去掉神經網路中的某些連線
# 不是永久去除連線,0.5表示每一個連線有50%的機率被保留下來,測試時保留所有連線
h_fc1_drop = tf.nn.dropout(h_fc1,keep_prob) 

# 最後,再加入一層全連線,把上一步得到的h_fc1_frop轉換為10個類別
W_fc2 = weight_variable([1024,10])
b_fc2 = weight_variable([10])
y_conv = tf.matmul(h_fc1_drop,W_fc2)+b_fc2 # y_conv相當於Softmax模型中的 Logit

# 直接利用Logit定義交叉熵損失 tf.nn.softmax_cross_entropy_with_logits
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_,logits = y_conv))
# 定義train_step
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy) # 0.0001為學習率

# 定義測試的準確率
correct_prediction = tf.equal(tf.argmax(y_conv,1),tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction,tf.float32))

# 訓練過程
sess = tf.InteractiveSession()
tf.global_variables_initializer().run() # 初始化所有變數,分配記憶體

for i in range(20000):
    batch = mnist.train.next_batch(50)
    # 每一百步報告一次在驗證集上的準確率
    if i % 100 == 0:
        train_accuracy = accuracy.eval(feed_dict={x:batch[0],y_:batch[1],keep_prob:1.0})
        print("step %d,training accuracy %g"%(i,train_accuracy))
    train_step.run(feed_dict={x:batch[0],y_:batch[1],keep_prob:0.5})

# 訓練結束報告在測試集上的準確率
print("test accuracy %g"% accuracy.eval(feed_dict={x:mnist.test.images,y_:mnist.test.labels,keep_prob:1.0}))

5. 程式碼輸出結果:

  • 第一次卷積過後的shape:

  • 第二次卷積過後的shape:

  • 程式的執行過程: