基於深度學習和遷移學習的識花實踐(轉)
深度學習是人工智能領域近年來最火熱的話題之一,但是對於個人來說,以往想要玩轉深度學習除了要具備高超的編程技巧,還需要有海量的數據和強勁的硬件。不過 TensorFlow 和 Keras 等框架的出現大大降低了編程的復雜度,而遷移學習的思想也允許我們利用現有的模型加上少量數據和訓練時間,取得不俗的效果。
這篇文章將示范如何利用遷移學習訓練一個能從圖片中分類不同種類的花的模型,它在五種花中能達到 80% 以上的准確度(比瞎蒙高了 60% 哦),而且只需要普通的家用電腦就可以完成訓練過程。
什么是遷移學習
人類的思維可以將一個領域學習到的知識和經驗,應用到其他相似的領域中去。所以當面臨新的情景時,如果該情景與之前的經驗越相似,那么人就能越快掌握該領域的知識。而傳統的機器學習方法則會把不同的任務看成是完全獨立的,比如一個識別貓的模型,如果訓練集中的圖片都是白天的,那么訓練出來的模型對於識別夜晚的貓這個任務就可能表現得非常差。遷移學習便是受此啟發,試圖將模型從源任務上訓練到的知識遷移到目標任務的應用上。
舉例說,源任務可以是識別圖片中車輛,而目標任務可以是識別卡車,識別轎車,識別公交車等。合理的使用遷移學習可以避免針對每個目標任務單獨訓練模型,從而極大的節約了計算資源。
此外,遷移學習並不是一種特定的機器學習模型,它更像是一種優化技巧。通常來說,機器學習任務要求測試集和訓練集有相同的概率分布,然而在一些情況下往往會缺乏足夠大的有針對性的數據集來滿足一個特定的訓練任務。遷移學習提出我們可以在一個通用的大數據集上進行一定量的訓練后,再用針對性的小數據集進一步強化訓練。
接下來的例子中將示范如何將一個圖像識別的深度卷積網絡,VGG,遷移到識別花朵類型的新任務上,在原先的任務中,VGG 只能識別花,但是遷移學習可以讓模型不但能識別花,還能識別花的具體品種。
VGG 介紹
VGG 是視覺領域競賽 ILSVRC 在 2014 年的獲勝模型,以 7.3% 的錯誤率在 ImageNet 數據集上大幅刷新了前一年 11.7% 的世界紀錄。VGG16 基本上繼承了 AlexNet 深的思想,並且發揚光大,做到了更深。AlexNet 只用到了 8 層網絡,而 VGG 的兩個版本分別是 16 層網絡版和 19 層網絡版。在接下來的遷移學習實踐中,我們會采用稍微簡單的一些的 VGG16,他和 VGG19 有幾乎完全一樣的准確度,但是運算起來更快一些。
VGG 的結構圖如下:
VGG 的輸入數據格式是 244 * 224 * 3 的像素數據,經過一系列的卷積神經網絡和池化網絡處理之后,輸出的是一個 4096 維的特征數據,然后再通過 3 層全連接的神經網絡處理,最終由 softmax 規范化得到分類結果。
VGG16 模型可以通過這里下載(密碼 78g9),模型是一個. npy 文件,本質上是一個巨大的 numpy 對象,包含了 VGG16 模型中的所有參數,該文件大約有 500M,所以可見如果是從頭訓練這樣一個模型是非常耗時的,借助於遷移學習的思想,我們可以直接在這個模型的基礎上進行訓練。
識花數據集
我們要使用的花數據集可以在這里下載。
該數據集有包含如下數據:
花的種類 | 圖片數量(張) |
---|---|
daisy | 633 |
dandelion | 898 |
roses | 641 |
sunflowers | 699 |
tulips | 799 |
遷移學習實踐
有了預備知識之后,我們可以開始搭建屬於自己的識花網絡了。
首先我們會將所有的圖片交給 VGG16,利用 VGG16 的深度網絡結構中的五輪卷積網絡層和池化層,對每張圖片得到一個 4096 維的特征向量,然后我們直接用這個特征向量替代原來的圖片,再加若干層全連接的神經網絡,對花朵數據集進行訓練。
因此本質上,我們是將 VGG16 作為一個圖片特征提取器,然后在此基礎上再進行一次普通的神經網絡學習,這樣就將原先的 244 * 224 * 3 維度的數據轉化為了 4096 維的,而每一維度的信息量大大提高,從而大大降低了計算資源的消耗,實現了把學習物體識別中得到的知識應用到特殊的花朵分類問題上。
文件結構
為了更加方便的使用 VGG 網絡,我們可以直接使用 tensorflow 提供的 VGG 加載模塊,該模塊可以在這里下載。
首先保證代碼或者 jupyter notebook 運行的工作目錄下有 flowerphotos,tensorflowvgg 這兩個文件夾,分別是花朵數據集和 tensorflowvgg,然后將之前下載的 VGG16 拷貝到 tensorflowvgg 文件夾中。
├── transfer_learning.py(運行代碼)
├── flower_phtots
│ ├── daisy
│ ├── dandelion
│ ├── roses
│ └── ...
└── tensorflow_vgg
├── vgg16.py
├── vgg16.npy
└── ...
然后導入需要用的 python 模塊
import os import numpy as np import tensorflow as tf from tensorflow_vgg import vgg16 from tensorflow_vgg import utils
加載識花數據集
接下來我們將 flower_photos 文件夾中的花朵圖片都載入到進來,並且用圖片所在的子文件夾作為標簽值。
data_dir = 'flower_photos/'
contents = os.listdir(data_dir)
classes = [each for each in contents if os.path.isdir(data_dir + each)]
利用 VGG16 計算得到特征值
# 首先設置計算batch的值,如果運算平台的內存越大,這個值可以設置得越高 batch_size = 10 # 用codes_list來存儲特征值 codes_list = [] # 用labels來存儲花的類別 labels = [] # batch數組用來臨時存儲圖片數據 batch = [] codes = None with tf.Session() as sess: # 構建VGG16模型對象 vgg = vgg16.Vgg16() input_ = tf.placeholder(tf.float32, [None, 224, 224, 3]) with tf.name_scope("content_vgg"): # 載入VGG16模型 vgg.build(input_) # 對每個不同種類的花分別用VGG16計算特征值 for each in classes: print("Starting {} images".format(each)) class_path = data_dir + each files = os.listdir(class_path) for ii, file in enumerate(files, 1): # 載入圖片並放入batch數組中 img = utils.load_image(os.path.join(class_path, file)) batch.append(img.reshape((1, 224, 224, 3))) labels.append(each) # 如果圖片數量到了batch_size則開始具體的運算 if ii % batch_size == 0 or ii == len(files): images = np.concatenate(batch) feed_dict = {input_: images} # 計算特征值 codes_batch = sess.run(vgg.relu6, feed_dict=feed_dict) # 將結果放入到codes數組中 if codes is None: codes = codes_batch else: codes = np.concatenate((codes, codes_batch)) # 清空數組准備下一個batch的計算 batch = [] print('{} images processed'.format(ii))
這樣我們就可以得到一個 codes 數組,和一個 labels 數組,分別存儲了所有花朵的特征值和類別。
可以用如下的代碼將這兩個數組保存到硬盤上:
with open('codes', 'w') as f: codes.tofile(f) import csv with open('labels', 'w') as f: writer = csv.writer(f, delimiter='\n') writer.writerow(labels)
准備訓練集,驗證集和測試集
一次嚴謹的模型訓練一定是要包含驗證和測試這兩個部分的。首先我把 labels 數組中的分類標簽用 One Hot Encode 的方式替換。
from sklearn.preprocessing import LabelBinarizer lb = LabelBinarizer() lb.fit(labels) labels_vecs = lb.transform(labels)
接下來就是抽取數據,因為不同類型的花的數據數量並不是完全一樣的,而且 labels 數組中的數據也還沒有被打亂,所以最合適的方法是使用 StratifiedShuffleSplit 方法來進行分層隨機划分。假設我們使用訓練集:驗證集:測試集 = 8:1:1,那么代碼如下:
from sklearn.model_selection import StratifiedShuffleSplit ss = StratifiedShuffleSplit(n_splits=1, test_size=0.2) train_idx, val_idx = next(ss.split(codes, labels)) half_val_len = int(len(val_idx)/2) val_idx, test_idx = val_idx[:half_val_len], val_idx[half_val_len:] train_x, train_y = codes[train_idx], labels_vecs[train_idx] val_x, val_y = codes[val_idx], labels_vecs[val_idx] test_x, test_y = codes[test_idx], labels_vecs[test_idx] print("Train shapes (x, y):", train_x.shape, train_y.shape) print("Validation shapes (x, y):", val_x.shape, val_y.shape) print("Test shapes (x, y):", test_x.shape, test_y.shape)
這時如果我們輸出數據的維度,應該會得到如下結果:
Train shapes (x, y): (2936, 4096) (2936, 5)
Validation shapes (x, y): (367, 4096) (367, 5)
Test shapes (x, y): (367, 4096) (367, 5)
訓練網絡
分好了數據集之后,就可以開始對數據集進行訓練了,假設我們使用一個 256 維的全連接層,一個 5 維的全連接層(因為我們要分類五種不同類的花朵),和一個 softmax 層。當然,這里的網絡結構可以任意修改,你可以不斷嘗試其他的結構以找到合適的結構。
# 輸入數據的維度 inputs_ = tf.placeholder(tf.float32, shape=[None, codes.shape[1]]) # 標簽數據的維度 labels_ = tf.placeholder(tf.int64, shape=[None, labels_vecs.shape[1]]) # 加入一個256維的全連接的層 fc = tf.contrib.layers.fully_connected(inputs_, 256) # 加入一個5維的全連接層 logits = tf.contrib.layers.fully_connected(fc, labels_vecs.shape[1], activation_fn=None) # 計算cross entropy值 cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=labels_, logits=logits) # 計算損失函數 cost = tf.reduce_mean(cross_entropy) # 采用用得最廣泛的AdamOptimizer優化器 optimizer = tf.train.AdamOptimizer().minimize(cost) # 得到最后的預測分布 predicted = tf.nn.softmax(logits) # 計算准確度 correct_pred = tf.equal(tf.argmax(predicted, 1), tf.argmax(labels_, 1)) accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
為了方便把數據分成一個個 batch 以降低內存的使用,還可以再用一個函數專門用來生成 batch。
def get_batches(x, y, n_batches=10): """ 這是一個生成器函數,按照n_batches的大小將數據划分了小塊 """ batch_size = len(x)//n_batches for ii in range(0, n_batches*batch_size, batch_size): # 如果不是最后一個batch,那么這個batch中應該有batch_size個數據 if ii != (n_batches-1)*batch_size: X, Y = x[ii: ii+batch_size], y[ii: ii+batch_size] # 否則的話,那剩余的不夠batch_size的數據都湊入到一個batch中 else: X, Y = x[ii:], y[ii:] # 生成器語法,返回X和Y yield X, Y
現在可以運行訓練了,
# 運行多少輪次 epochs = 20 # 統計訓練效果的頻率 iteration = 0 # 保存模型的保存器 saver = tf.train.Saver() with tf.Session() as sess: sess.run(tf.global_variables_initializer()) for e in range(epochs): for x, y in get_batches(train_x, train_y): feed = {inputs_: x, labels_: y} # 訓練模型 loss, _ = sess.run([cost, optimizer], feed_dict=feed) print("Epoch: {}/{}".format(e+1, epochs), "Iteration: {}".format(iteration), "Training loss: {:.5f}".format(loss)) iteration += 1 if iteration % 5 == 0: feed = {inputs_: val_x, labels_: val_y} val_acc = sess.run(accuracy, feed_dict=feed) # 輸出用驗證機驗證訓練進度 print("Epoch: {}/{}".format(e, epochs), "Iteration: {}".format(iteration), "Validation Acc: {:.4f}".format(val_acc)) # 保存模型 saver.save(sess, "checkpoints/flowers.ckpt")
測試網絡
接下來就是用測試集來測試模型效果
with tf.Session() as sess: saver.restore(sess, tf.train.latest_checkpoint('checkpoints')) feed = {inputs_: test_x, labels_: test_y} test_acc = sess.run(accuracy, feed_dict=feed) print("Test accuracy: {:.4f}".format(test_acc))
最終我在自己電腦上得到了 88.83% 的准確度,你可以繼續調整 batch 的大小,或者模型的結構以得到一個更好的結果。
對這張有一個七星瓢蟲的蒲公英圖
模型給出的預測值如下
可以看出模型的效果還是相當穩定的,而且整個過程中我們的計算時間不過超過 30 分鍾,這就是遷移學習的魅力。
P.S
當然,其他的深度學習框架也可以很方便的實現遷移學習,比如這里的 Keras 代碼用大約 20 行實現了一個 VGG 遷移識別狗的品種的分類器。
參考資料
- A Survey on Transfer Learning[http://ieeexplore.ieee.org/document/5288526/]
- Tensorflow 的識花遷移學習 https://www.tensorflow.org/tutorials/image_retraining
- VGG 網絡 https://arxiv.org/pdf/1409.1556.pdf
- 卷積神經網絡的可視化 http://www.matthewzeiler.com/pubs/arxive2013/eccv2014.pdf
- Udaicty 深度學習 https://cn.udacity.com/course/deep-learning-nanodegree-foundation–nd101