CIFAR-10與FC
數據集cifar-10包括約70000張圖片,每張圖片爲32X32X3的格式,總共分爲10各類別的數據。
實現對於cifar數據集的分類,僅僅利用fc層是不現實的,以一個500個神經元的隱含層爲例,共需要32X32X3X500約150萬個參數,但是如此簡單的結構肯定是無法達到所需的識別能力,所以總體需要的參數量可能是上億甚至更多。
過多的參數往往會帶來兩個問題:第一是計算量過大,可能會超過計算機的承載能力,並帶來極長的訓練時間;第二是過多參數會引起過擬合。
爲解決上述問題,將採用卷積神經網絡進行cifar-10的識別。
CNN
卷積神經網絡,其整體結構與之前所述的單層神經網絡大體一致,都有輸入層、fc層以及輸出層,不同的是在cnn中又加入了卷積層、降採樣層以及隨機失活層等。下文對於額外的層進行進一步的描述。
1.卷積層
卷積在信號處理中,是用於增強信號與消除噪聲;在對於圖像的處理上,一個卷積相當於一個模板,對圖像處理進行滑動與點積的計算,最終得到一個特徵圖(feature map);卷積的模板性體現在其本質在於對圖像進行某一種特徵的提取,圖像對於此特徵的近似程度就是最終卷積的值,也就是特徵圖。
卷積層最大的兩個優點就是局部連接與參數共享。
局部連接是對應着fc層的全連接而言的,在卷積層處理數據時,不會對圖像的每一個點進行處理,而是以一定的大小區塊劃分整圖,處理每一區塊得到的卷積結果即爲最終的特徵圖;局部連接劃分的區塊取決於卷積核大小以及步長與是否補零,而對於一個卷積核或者神經元來說,其參數量取決於卷積核自身的尺寸而與輸入的數據尺寸無關。
參數共享,就是指一個卷積核或者說神經元,對一張圖像做滑動卷積時,其內部的值是不變的;這在物理意義上表示爲一個卷積核就是一個模板,處理圖像時就是利用這一模板提取相似性結果,所以模板固定也就是參數固定。
2.降採樣層
降採樣即實現數據的壓縮,進而減少參數量,降低計算量,防止過擬合;一般採用池化的方式進行降採樣,但是也有網絡使用步長大於一的卷積層實現降採樣,所以注意池化只是降採樣的一種形式而非唯一形式。
池化就是對於卷積得到的feature map進行尺寸縮減,主要分爲最大池化與平均池化,分別可以達到抽取主要特徵與抽取平均特徵的目的,池化就是要儘量保存原有特徵,並達到降採樣的效果。
3.隨機失活層
即dropout層,主要是在非輸出層的fc層之後使用,目的也是爲了提高模型的泛化能力,防止過擬合;其思想是將每個神經元當作一個開關,開關的開與關具有一定的隨機性;利用這一機制,在每次訓練時只有部分神經元打開,參與訓練,得到一組權重參數,或者說是當前開關形式下的模型,最終將所有的模型組合,形成一個最終的模型,即爲訓練的結果;利用上述訓練過程,每次不完全訓練,參數量較小,並且最終融合的模型相當於多角度描述特徵,所以有更好的泛化能力。
TF中的卷積相關函數
1.二維卷積
tf.nn.conv2d(input,filter,strides,padding,name=None)
# input 是輸入數據,其格式是[batch, width, height, channel]這樣的四維格式,並且batch常寫作-1
要求輸入數據應爲浮點類型,即tf.float32或tf.float64
# filter 是卷積層,格式爲 [filter_width, filter_height,channel]的三維格式,其中前兩項表示本層每 個神經元的尺寸,最後的參數表示本層有多少個神經元,一個神經元得到一個特徵圖,最終得到的特徵圖數就是本層的神經元數目,所以這個參數也叫做輸出維度。
# stride 是步長,即一個卷積核載圖像上做滑動點積時,每次滑動的步長;其格式爲[1,2,2,1]類型,即4維格式,每一個參數表示在不同維度上的步長,且一般首末兩個參數爲1.
# padding 即補零,有VAlID與SAME兩個可選參數,表示是否進行補零來保證卷積前後尺寸是否變化
2.池化
tf.nn.average_pool(value,ksize,stride,padding,name=None)
tf.nn.max_pooling(同上)
# value 即卷積之後的值,所以格式也是[batch,width,height,channel]
# ksize 即池化窗口大小,因爲不會在batch與channel上做池化,所以其格式爲[1,2,2,1]的4維格式但是首末值爲1.
#stride padding的意義與二維卷積一致
TF載入CIFAR-10數據集
import urllib.request
import os
import tarfile
import pickle as p
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import OneHotEncoder
import tensorflow as tf
from time import time
url = 'http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz' # 下載鏈接
filepath = 'data/cifar-10-python.tar.gz' # 下載好的數據集應在的路徑
# 下載數據集
if not os.path.isfile(filepath):
result = urllib.request.urlretrieve(url, filepath)
print('download: ', result)
else:
print('Data file already exist')
# 解壓數據集
if not os.path.exists("data/cifar-10-batches-py"):
tfile = tarfile.open('data/cifar-10-python.tar.gz', 'r:gz')
result = tfile.extractall('data/')
print('Extracted to ./data/cifar-10-batches-py/')
else:
print('Directory already exist')
# 批次讀取數據 的函數
def load_CIFAR_batch(filename):
with open(filename,'rb') as f:
data_dict = p.load(f,encoding='bytes')
images = data_dict[b'data']
labels = data_dict[b'labels']
images = images.reshape(10000, 3, 32, 32)
images = images.transpose(0, 2, 3, 1)
labels = np.array(labels)
return images, labels
# 完整讀取數據的函數,通過多次調用批次讀取數據的函數實現
def load_CIFAR_data(data_dir):
images_train =[]
labels_train = []
for i in range(5):
f = os.path.join(data_dir, 'data_batch_%d'%(i+1))
print('loading', f)
image_batch, label_batch = load_CIFAR_batch(f)
images_train.append(image_batch)
labels_train.append(label_batch)
Xtrain = np.concatenate(images_train)
Ytrian = np.concatenate(labels_train)
del image_batch, label_batch
Xtest, Ytest = load_CIFAR_batch(os.path.join(data_dir, 'test_batch'))
return Xtrain, Ytrian, Xtest, Ytest
data_dir = 'data/cifar-10-batches-py' # 解壓抽取數據集後的路徑
Xtrain, Ytrain, Xtest, Ytest = load_CIFAR_data(data_dir) # 獲取訓練集測試集相關數據與標籤
對數據進行預處理
# 對數據進行預處理
# 對圖像進行數值標準化,注意標準化之前要先類型轉化爲浮點型
Xtrain_normalize = Xtrain.astype('float32')/255.0
Xtest_normalize = Xtest.astype('float32')/255.0
# 標籤進行獨熱編碼轉換
encoder = OneHotEncoder(sparse=False)
yy = [[0], [1], [2], [3], [4], [5], [6], [7], [8], [9]]
encoder.fit(yy)
Ytrain_reshape = Ytrain.reshape(-1, 1)
Ytrain_onehot = encoder.transform(Ytrain_reshape) # 50000,1->50000,10
Ytest_reshape = Ytest.reshape(-1, 1)
Ytest_onehot = encoder.transform(Ytest_reshape)
# 使用獨熱編碼是要用於對預測結果和標籤值的類歐氏距離,來表徵預測的偏差程度
定義網絡構造輔助函數
# 定義 權重 構造函數 使用截斷隨機初始化
# 輸入爲權重的尺寸,即卷積核的尺寸與通道數
def weight(shape):
return tf.Variable(tf.truncated_normal(shape, stddev=0.1), name="W")
# 定義 偏置 構造函數
# 偏置的輸入是一個以一維數組,大小等於卷積核數目
def bias(shape):
return tf.Variable(tf.constant(0.1, shape=shape), name="b")
# 定義 2維卷積 構造函數,卷積前後尺寸不變
# 輸入爲圖像或特徵圖 與 權重
# 輸出 再加上偏置 就得到卷積結果
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
# 定義 最大池化 構造函數,池化不改變通道數,但是將圖像的長寬各減一半
# 輸入爲卷積結果
def max_pool_2X2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME
構造網絡
# 定義網絡結構
# 使用了tf的命名空間
# 輸入層
with tf.name_scope("input_layer"):
# cifar數據集的圖像格式爲32X32X3,批次爲待定參數,用於批次讀取數據
x = tf.placeholder(tf.float32, [None, 32, 32, 3], name="x")
# 第一層
with tf.name_scope("conv_1"):
w1 = weight([3, 3, 3, 32]) # 尺寸爲3X3的卷積核,輸入通道是3,輸出是32
b1 = bias([32]) # 維度與輸出通道數目一致即可
conv_1 = conv2d(x, w1)+b1 # 進行卷積計算並加上偏置
conv_1 = tf.nn.relu(conv_1) # 對卷積結果進行激活 輸出爲 batch,32,32,32
# print(conv_1.shape)
# 第一池化層
with tf.name_scope("pool1"):
pool1 = max_pool_2X2(conv_1) # 對第一層激活結果池化,不改變通道數,
# 改變尺寸爲 batch,16,16,32
# print(pool1.shape)
# 第二層
with tf.name_scope("conv_2"):
w2 = weight([3, 3, 32, 64]) # 3X3的卷積核,輸入通道是32,輸出是64
b2 = bias([64]) # 維度與輸出通道一致即可
conv_2 = conv2d(pool1, w2)+b2 # 進行卷積計算,注意是對第一池化層數據進行卷積
conv_2 = tf.nn.relu(conv_2) # 對卷積結果進行激活 輸出爲 batch,16,16,64
# print(conv_2.shape)
# 第二池化層
with tf.name_scope("pool2"):
pool2 = max_pool_2X2(conv_2) # 對第二層激活結果池化,不改變通道數,
# 改變尺寸爲 batch,8,8,64
# 全連接層 # 注意卷積層向全連接層連接時要先做扁平化
with tf.name_scope("fc"):
flat = tf.reshape(pool2, [-1, 4096]) # pool2結果是四維的,在傳輸給全連接層時,要先做扁平化
w3 = weight([4096, 128]) # 輸入維度爲4096,輸出維度爲128,或者說有128個神經元在本fc層
b3 = bias([128]) # 偏置與輸出維度一致
# 4096 = 8X8X64,即將pool2得到的feature map數據,合併爲一個維度
h = tf.nn.relu(tf.matmul(flat, w3)+b3) # fc層使用矩陣乘法,並使用relu激活
h_dropout = tf.nn.dropout(h, keep_prob=0.8) # 引入隨機失活,防止過擬合
# 輸出層
with tf.name_scope("output"):
w4 = weight([128, 10]) # 輸出維度即爲分類數目
b4 = bias([10]) # 偏置與輸出維度一致
# 輸出層使用矩陣乘法,並使用softmax激活
preb = tf.nn.softmax(tf.matmul(h_dropout, w4)+b4) # -1,10
超參與優化器、損失函數的設定
# 構建優化器與損失函數
with tf.name_scope("optimizer"):
y = tf.placeholder(tf.float32, [None, 10], name="label")
loss_function = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=preb, labels=y))
optimizer = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(loss_function)
# 構建準確率,實際上是對於一個批次下所有準確度的均值
with tf.name_scope("evaluation"):
correct_prediction = tf.equal(tf.argmax(preb, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# 設置超參
train_epochs = 2500 # 總的訓練輪數
batch_size = 50 # 但批次訓練數目
total_batch = int(len(Xtrain)/batch_size) # 一輪訓練多少單批次
epoch_list = [] # 論數集合
accuracy_list = [] # 準確度集合
loss_list = [] # 損失值集合
epoch = tf.Variable(0, name="epoch", trainable=False) # 定義一個不可訓練的變量,用於統計訓練輪數,以及後續斷點續訓
start_time = time() # 計時開始
啓動會話設置斷點續訓
# 啓動會話
sess = tf.Session()
sess.run(tf.global_variables_initializer())
# 設置日誌目錄
ckpt_dir = 'CIFAR_log/'
if not os.path.exists(ckpt_dir):
os.makedirs(ckpt_dir) # 沒有這個目錄,將自動創建
# 生成存儲對象,用於後續存儲模型
saver = tf.train.Saver(max_to_keep=1)
# 構造斷點訓練
ckpt = tf.train.latest_checkpoint(ckpt_dir) # 嘗試從日誌目錄讀取存儲的模型
if ckpt!= None:
saver.restore(sess, ckpt)
print("載入成功")
else:
print("載入失敗,開始訓練")
開訓練
# 迭代訓練
# 首先定義批量獲取訓練數據的函數,返回值是經過歸一化的圖像與獨熱後的標籤
def get_train_batch(number, batch_size):
return Xtrain_normalize[number*batch_size:(number+1)*batch_size], Ytrain_onehot[number*batch_size:(number+1)*batch_size]
# 逐個輪數,逐個批次進行數據訓練
for ep in range(start,train_epochs):
for i in range(total_batch):
batch_x, batch_y = get_train_batch(i, batch_size)
sess.run(optimizer, feed_dict={x: batch_x, y: batch_y})
if i % 100 == 0:
print("Step{}".format(i), "finished")
# 每輪訓練完成,統計損失與準確率
loss, acc = sess.run([loss_function, accuracy], feed_dict={x: batch_x, y: batch_y})
epoch_list.append(ep+1) # 統計輪數
loss_list.append(loss) # 統計損失
accuracy_list.append(acc) # 統計準確率
print("Train epoch: ", "%2d" % (sess.run(epoch)+1), "Loss: ","{:.6f}".format(loss),"Accuracy: ", acc)
saver.save(sess, ckpt_dir+"CIFAR_cnn_model.ckpt", global_step=ep+1) # 每輪保存模型
sess.run(epoch.assign(ep+1)) # 更新輪數計數
duration = time()-start_time
print("Train duration: ", duration) 統計時長