前言
本文章的參考卷積神經(jīng)網(wǎng)絡(luò)應(yīng)用于人臉識別,通過Tensorflow改寫的代碼。也通過自己的想法改動了一些代碼。本文算是一個小小的demo吧,因為之前都是基礎(chǔ)篇。而這個算是基于之前的基礎(chǔ)學(xué)習(xí)做的demo。雖然本demo比較簡單,但我覺得可以給大家一些啟示。
源碼地址:https://github.com/Salon-sai/learning-tensorflow/tree/master/lesson3
數(shù)據(jù)材料
這是一個小型的人臉數(shù)據(jù)庫,一共有40個人,每個人有10張照片作為樣本數(shù)據(jù)。這些圖片都是黑白照片,意味著這些圖片都只有灰度0-255,沒有rgb三通道。于是我們需要對這張大圖片切分成一個個的小臉。整張圖片大小是1190 × 942,一共有20 × 20張照片。那么每張照片的大小就是(1190 / 20)× (942 / 20)= 57 × 47 (大約,以為每張圖片之間存在間距)。

設(shè)計思路
那么我們得到這些樣本圖片之后,再將圖片進(jìn)行分類,每一個人一個類別,每個類別10張圖片樣本。我就取每一類的前八張作為訓(xùn)練樣本,每類第九張作為校驗樣本,每類第十張圖片作為測試樣本。每張圖片的label使用one-hot向量表示。
至于模型設(shè)計上面,選取了CNN卷積神經(jīng)網(wǎng)絡(luò)作為設(shè)計模型,一共有兩層卷積,一層全連接層,最后一層用softmax作為分類。然后用softmax的預(yù)測結(jié)果和真實的labels做Cross-Entropy,這樣就得到了Loss Function了。
代碼展示(部分)和說明
獲取dataset
def load_data(dataset_path):
img = Image.open(dataset_path)
# 定義一個20 × 20的訓(xùn)練樣本,一共有40個人,每個人都10張樣本照片
img_ndarray = np.asarray(img, dtype='float64') / 64
# 記錄臉數(shù)據(jù)矩陣,57 * 47為每張臉的像素矩陣
faces = np.empty((400, 57 * 47))
for row in range(20):
for column in range(20):
faces[20 * row + column] = np.ndarray.flatten(
img_ndarray[row * 57: (row + 1) * 57, column * 47 : (column + 1) * 47]
)
label = np.zeros((400, 40))
for i in range(40):
label[i * 10: (i + 1) * 10, i] = 1
# 將數(shù)據(jù)分成訓(xùn)練集,驗證集,測試集
train_data = np.empty((320, 57 * 47))
train_label = np.zeros((320, 40))
vaild_data = np.empty((40, 57 * 47))
vaild_label = np.zeros((40, 40))
test_data = np.empty((40, 57 * 47))
test_label = np.zeros((40, 40))
for i in range(40):
train_data[i * 8: i * 8 + 8] = faces[i * 10: i * 10 + 8]
train_label[i * 8: i * 8 + 8] = label[i * 10: i * 10 + 8]
vaild_data[i] = faces[i * 10 + 8]
vaild_label[i] = label[i * 10 + 8]
test_data[i] = faces[i * 10 + 9]
test_label[i] = label[i * 10 + 9]
return [
(train_data, train_label),
(vaild_data, vaild_label),
(test_data, test_label)
]
跟之前說的思路一樣,讀取圖片然后按照每張照片的大小57 × 47去劃分照片,最后得到一個faces的數(shù)據(jù)矩陣,然后再給每張照片賦予相應(yīng)的label。最后劃分?jǐn)?shù)據(jù)成為訓(xùn)練數(shù)據(jù)集,校驗數(shù)據(jù)集,測試數(shù)據(jù)集。
模型網(wǎng)絡(luò)
1. 卷積層:
def convolutional_layer(data, kernel_size, bias_size, pooling_size):
kernel = tf.get_variable("conv", kernel_size, initializer=tf.random_normal_initializer())
bias = tf.get_variable('bias', bias_size, initializer=tf.random_normal_initializer())
conv = tf.nn.conv2d(data, kernel, strides=[1, 1, 1, 1], padding='SAME')
linear_output = tf.nn.relu(tf.add(conv, bias))
pooling = tf.nn.max_pool(linear_output, ksize=pooling_size, strides=pooling_size, padding="SAME")
return pooling
我定義了一個卷積層的函數(shù)用于,每在不同的scope下調(diào)用一次就生成相應(yīng)的卷積層及其參數(shù)。哈哈,是不是覺得這招很熟悉很好用呢?kernel,和bias分別是卷積核和偏移量(卷積就是對局部進(jìn)行Linear操作)。最后就是max_pool層(取得核中的最大值代碼這個核里面的元素,因為值越大顏色越深,那就越能代表他的特征。也可以理解成一個去噪的操作吧),做完pool層后,我們算是對圖片進(jìn)行一個(卷積+pooling)處理。
2. 全連接層和分類層的Linear部分:
def linear_layer(data, weights_size, biases_size):
weights = tf.get_variable("weigths", weights_size, initializer=tf.random_normal_initializer())
biases = tf.get_variable("biases", biases_size, initializer=tf.random_normal_initializer())
return tf.add(tf.matmul(data, weights), biases)
相信大家都知道這是一個很有簡單的線性操作,我在這里就不多說了。
3. 整個網(wǎng)絡(luò):
def convolutional_neural_network(data):
# 根據(jù)類別個數(shù)定義最后輸出層的神經(jīng)元
n_ouput_layer = 40
kernel_shape1=[5, 5, 1, 32]
kernel_shape2=[5, 5, 32, 64]
full_conn_w_shape = [15 * 12 * 64, 1024]
out_w_shape = [1024, n_ouput_layer]
bias_shape1=[32]
bias_shape2=[64]
full_conn_b_shape = [1024]
out_b_shape = [n_ouput_layer]
data = tf.reshape(data, [-1, 57, 47, 1])
# 經(jīng)過第一層卷積神經(jīng)網(wǎng)絡(luò)后,得到的張量shape為:[batch, 29, 24, 32]
with tf.variable_scope("conv_layer1") as layer1:
layer1_output = convolutional_layer(
data=data,
kernel_size=kernel_shape1,
bias_size=bias_shape1,
pooling_size=[1, 2, 2, 1]
)
# 經(jīng)過第二層卷積神經(jīng)網(wǎng)絡(luò)后,得到的張量shape為:[batch, 15, 12, 64]
with tf.variable_scope("conv_layer2") as layer2:
layer2_output = convolutional_layer(
data=layer1_output,
kernel_size=kernel_shape2,
bias_size=bias_shape2,
pooling_size=[1, 2, 2, 1]
)
with tf.variable_scope("full_connection") as full_layer3:
# 講卷積層張量數(shù)據(jù)拉成2-D張量只有有一列的列向量
layer2_output_flatten = tf.contrib.layers.flatten(layer2_output)
layer3_output = tf.nn.relu(
linear_layer(
data=layer2_output_flatten,
weights_size=full_conn_w_shape,
biases_size=full_conn_b_shape
)
)
# layer3_output = tf.nn.dropout(layer3_output, 0.8)
with tf.variable_scope("output") as output_layer4:
output = linear_layer(
data=layer3_output,
weights_size=out_w_shape,
biases_size=out_b_shape
)
return output;
一圖勝千言:

訓(xùn)練過程:
在訓(xùn)練中我使用softmax_cross_entropy_with_logits作為Loss function,AdamOptimizer作為優(yōu)化算法(學(xué)習(xí)率為0.01,由于我的本本已經(jīng)是6年前的產(chǎn)物,而且沒有GPU,唯有調(diào)大點,讓他快點收斂算了[哭崩的臉.jpg])。
predict = convolutional_neural_network(X)
cost_func = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=predict, labels=Y))
optimizer = tf.train.AdamOptimizer(1e-2).minimize(cost_func)
# 用于保存訓(xùn)練的最佳模型
saver = tf.train.Saver()
model_dir = './model'
model_path = model_dir + '/best.ckpt'
with tf.Session() as session:
# 若不存在模型數(shù)據(jù),需要訓(xùn)練模型參數(shù)
if not os.path.exists(model_path + ".index"):
session.run(tf.global_variables_initializer())
best_loss = float('Inf')
for epoch in range(20):
epoch_loss = 0
for i in range(np.shape(train_set_x)[0] / batch_size):
x = train_set_x[i * batch_size: (i + 1) * batch_size]
y = train_set_y[i * batch_size: (i + 1) * batch_size]
_, cost = session.run([optimizer, cost_func], feed_dict={X: x, Y: y})
epoch_loss += cost
print(epoch, ' : ', epoch_loss)
if best_loss > epoch_loss:
best_loss = epoch_loss
if not os.path.exists(model_dir):
os.mkdir(model_dir)
print("create the directory: %s" % model_dir)
save_path = saver.save(session, model_path)
print("Model saved in file: %s" % save_path)
再次強調(diào),我的本本很舊了,而且鄙人又是學(xué)生。因此我把epoch設(shè)置為20。還好loss function應(yīng)該是一個convex function,所以也在20個epoch里到達(dá)min值0。(假如有錢就施舍一下給小弟弟吧,我又窮又想要GPU,難道我去拍AV當(dāng)男優(yōu)賺錢嗎?還是當(dāng)鴨好呢?)。最后就是有個Saver用于保存模型,畢竟他讓我本本元氣打傷,不可以讓訓(xùn)練的好的模型就此消失與內(nèi)存中。我都是保留最優(yōu)(loss function最?。┑哪P?。
校驗與測試
# 恢復(fù)數(shù)據(jù)并校驗和測試
saver.restore(session, model_path)
correct = tf.equal(tf.argmax(predict,1), tf.argmax(Y,1))
valid_accuracy = tf.reduce_mean(tf.cast(correct,'float'))
print('valid set accuracy: ', valid_accuracy.eval({X: vaild_set_x, Y: valid_set_y}))
test_pred = tf.argmax(predict, 1).eval({X: test_set_x})
test_true = np.argmax(test_set_y, 1)
test_correct = correct.eval({X: test_set_x, Y: test_set_y})
incorrect_index = [i for i in range(np.shape(test_correct)[0]) if not test_correct[i]]
for i in incorrect_index:
print('picture person is %i, but mis-predicted as person %i'
%(test_true[i], test_pred[i]))
plot(incorrect_index, "olivettifaces.gif")
最后就是恢復(fù)模型并且計算出校驗集的準(zhǔn)確率以及測試數(shù)據(jù)哪些出現(xiàn)錯誤,并標(biāo)注出來。
畫出在測試集中錯誤的數(shù)據(jù)
def plot(error_index, dataset_path):
img = mpimg.imread(dataset_path)
plt.imshow(img)
currentAxis = plt.gca()
for index in error_index:
row = index // 2
column = index % 2
currentAxis.add_patch(
patches.Rectangle(
xy=(
47 * 9 if column == 0 else 47 * 19,
row * 57
),
width=47,
height=57,
linewidth=1,
edgecolor='r',
facecolor='none'
)
)
plt.savefig("result.png")
plt.show()

我沒騙大家,我只是用cpu運算,我真的很窮(做鴨還是當(dāng)男優(yōu)呢?)。
最后是測試集合,當(dāng)中有五個標(biāo)記錯誤,在console也看到。這張圖片是標(biāo)注了那些被分類錯誤的。

總結(jié)
這是一個實戰(zhàn)篇,也算是給大家介紹一些關(guān)于Tensorflow卷積和pool的使用,但我覺得這個難度一般般吧。因為我覺得這些數(shù)據(jù)比較小,而且模型都比較簡單,大家應(yīng)該可以掌握。
不知道你有沒有這樣的感受,在剛剛?cè)腴T機器學(xué)習(xí)的時候,我們一般都是從MNIST、CIFAR-10這一類知名公開數(shù)據(jù)集開始快速上手,復(fù)現(xiàn)別人的結(jié)果,但總覺得過于簡單,給人的感覺太不真實。因為這些數(shù)據(jù)太“完美”了(干凈的輸入,均衡的類別,分布基本一致的測試集,還有大量現(xiàn)成的參考模型),要成為真正的數(shù)據(jù)科學(xué)家,光在這些數(shù)據(jù)集上跑模型卻是遠(yuǎn)遠(yuǎn)不夠的。而現(xiàn)實中你幾乎不可能遇到這樣的數(shù)據(jù)(現(xiàn)實數(shù)據(jù)往往有著殘缺的輸入,類別嚴(yán)重不均衡,分布不一致甚至隨時變動的測試集,幾乎沒有可以參考的論文),這往往讓剛進(jìn)入工作的同學(xué)手忙腳亂,無所適從。
引用來源與知乎分分鐘帶你殺入Kaggle Top 1%
正是如此,我們需要做不同實戰(zhàn),無論論文里面的idea還是比賽,我們都應(yīng)該盡力去實現(xiàn)它,不要說我懂得這個模型就啥都不做。希望大家保持一種謙卑的學(xué)習(xí)態(tài)度認(rèn)真努力吧。
好了,我這邊快下雨了。趕緊去飯?zhí)贸渣c飯先,不然就餓死于宿舍。就沒辦法做鴨做男優(yōu)拍AV賺錢換電腦,加GPU了。