寫在前面:大家好!我是【AI 菌】,一枚愛彈吉他的程序員。我
熱愛AI、熱愛分享、熱愛開源
! 這博客是我對學習的一點總結與記錄。如果您也對深度學習、機器視覺、算法、Python、C++
感興趣,可以關注我的動態,我們一起學習,一起進步~
我的博客地址爲:【AI 菌】的博客
我的Github項目地址是:【AI 菌】的Github
本教程會持續更新,如果對您有幫助的話,歡迎star收藏~
文章目錄
前言:
歡迎大家來到《TF2.0深度學習實戰》第四講,這一次我將復現經典的卷積神經網絡AlexNet,然後對自定義數據集進行迭代訓練,完成圖片分類任務。之後我將使用TensorFlow2.0框架逐一復現經典的卷積神經網絡:VGG系列、GooLeNet、ResNet 系列、DenseNet 系列,以及現在比較流行的:RCNN系列、YOLO系列等。
本博客將持續更新,歡迎關注。本着學習的心。希望和大家相互交流,一起進步~
學習記錄:
深度學習環境搭建:Anaconda3+tensorflow2.0+PyCharm
TF2.0深度學習實戰(二):用compile()和fit()快速搭建MINIST分類器
TF2.0深度學習實戰(三):LeNet-5搭建MINIST分類器
TF2.0深度學習實戰(六):搭建GoogLeNet卷積神經網絡
數據集的下載地址:寶可夢數據集,提取碼:9n21
一、詳解AlexNet
1.1 AlexNet 簡介
在2012年,多倫多大學的Alex Krizhevsky、Hinton等人提出了8層的深度神經網絡模型AlexNet,並且因此而獲得了ILSVRC12 挑戰賽 ImageNet 數據集分類任務的冠軍。爲了紀念Alex Krizhevsky所做的突出貢獻,所以將該網絡結構命名爲AlexNet。
Hinton當時是多倫多大學的教授,現在是公認的人工智能領域三巨頭之一。Deep Learning的概念就是由他提出來的。而Alex Krizhevsky是他當時的學生。
AlexNet 模型的優越性能啓發了業界朝着更深層的網絡模型方向研究。自 AlexNet 模型提出後,各種各樣的算法模型相繼被髮表,其中有 VGG 系列,GooLeNet,ResNet 系列,DenseNet 系列等等
1.2 AlexNet網絡結構
整體結構上,AlexNet包含8層;前5層是卷積層,剩下的3層是全連接層。全連接層的輸出是1000維的,最後通過softmax的得到各個類別的概率,實現了1000分類。整體結構如下:
由於當時計算機硬件性能有限,使得AlexNet採用了兩塊GTX580 3GB GPU進行分佈式訓練。但是現在的計算機水平完全可以考慮在單臺電腦上跑。後面的實戰過程中,我就將它簡化爲了單cpu/gpu版。
如果對卷積網絡基本結構、輸出圖像大小的推導過程等不太瞭解,建議先戳戳:神經網絡搭建:卷積層+激活函數+池化層+全連接層
具體每層網絡結構如下:
1.3 AlexNet的創新之處
(1)層數達到了較深的8層,這在當時已經是個突破了
(2)採用了 ReLU 激活函數。成功地解決了以往使用Sigmoid函數而產生的梯度彌散問題;並且使得網絡訓練的速度得到了一定的提升。
(3)引入了Dropout,提高了模型的泛化能力,防止了過擬合現象的發生。在AlexNet中主要是最後幾個全連接層使用了Dropout。
(4)多GPU訓練。受限於當時的計算機水平,使用多GPU,可以滿足大規模數據集和模型的訓練。
- 深入瞭解ReLU激活函數,可以參考這篇:深度學習筆記:激活函數全家桶
- 深入瞭解Dropout,可戳戳:防止過擬合(二):Dropout
1.4 AlexNet的性能
在LSVRC-2012挑戰賽的ImageNet數據集上進行1000分類。測試結果如下:
在 AlexNet 出現之前的網絡模型都是淺層的神經網絡,Top-5 錯誤率均在 25%以上,AlexNet 8層的深層神經網絡將 Top-5 錯誤率降低至 15.3%,比第二名的26.2%低很多,性能提升巨大。
注:top-5錯誤率是指測試圖像的正確標籤不在模型預測的五個最可能的結果之中。
LSVRC:全稱ImageNet Large Scale Visual Recognition Challenge
二、搭建AlexNet進行圖片分類
2.1 自定義數據集加載
數據集介紹
本次實驗採用的是寶可夢數據集,該數據集共收集了皮卡丘(Pikachu)、超夢(Mewtwo)、傑尼龜(Squirtle)、小火龍(Charmander)和妙蛙種子(Bulbasaur)共 5 種精靈生物(看過神奇寶貝的盆友肯定知道~),一共是1122張圖片。
每種精靈的信息如下表:
自定義數據集加載過程一共分爲三步:
(1)創建圖片路徑和標籤,寫入csv文件,然後再從csv文件讀取存入字符串數組。
def load_csv(root, filename, name2label):
# root:數據集根目錄
# filename:csv文件名
# name2label:類別名編碼表
if not os.path.exists(os.path.join(root, filename)): # 如果不存在csv,則創建一個
images = [] # 初始化存放圖片路徑的字符串數組
for name in name2label.keys(): # 遍歷所有子目錄,獲得所有圖片的路徑
# glob文件名匹配模式,不用遍歷整個目錄判斷而獲得文件夾下所有同類文件
# 只考慮後綴爲png,jpg,jpeg的圖片,比如:pokemon\\mewtwo\\00001.png
images += glob.glob(os.path.join(root, name, '*.png'))
images += glob.glob(os.path.join(root, name, '*.jpg'))
images += glob.glob(os.path.join(root, name, '*.jpeg'))
print(len(images), images) # 打印出images的長度和所有圖片路徑名
random.shuffle(images) # 隨機打亂存放順序
# 創建csv文件,並且寫入圖片路徑和標籤信息
with open(os.path.join(root, filename), mode='w', newline='') as f:
writer = csv.writer(f)
for img in images: # 遍歷images中存放的每一個圖片的路徑,如pokemon\\mewtwo\\00001.png
name = img.split(os.sep)[-2] # 用\\分隔,取倒數第二項作爲類名
label = name2label[name] # 找到類名鍵對應的值,作爲標籤
writer.writerow([img, label]) # 寫入csv文件,以逗號隔開,如:pokemon\\mewtwo\\00001.png, 2
print('written into csv file:', filename)
# 讀csv文件
images, labels = [], [] # 創建兩個空數組,用來存放圖片路徑和標籤
with open(os.path.join(root, filename)) as f:
reader = csv.reader(f)
for row in reader: # 逐行遍歷csv文件
img, label = row # 每行信息包括圖片路徑和標籤
label = int(label) # 強制類型轉換爲整型
images.append(img) # 插入到images數組的後面
labels.append(label)
assert len(images) == len(labels) # 斷言,判斷images和labels的長度是否相同
return images, labels
(2)創建數字編碼表,並劃分數據集
創建數字編碼表是爲了:將類別標籤轉化爲數字標籤。實現方法就是:將5個類別和數字1到5存入字典中,類別作爲鍵key,數字編碼作爲值value。一個類對應一個值{鍵:值},是一一對應的關係。
def load_pokemon(root, mode='train'):
# 創建數字編碼表
name2label = {} # 創建一個空字典{key:value},用來存放類別名和對應的標籤
for name in sorted(os.listdir(os.path.join(root))): # 遍歷根目錄下的子目錄,並排序
if not os.path.isdir(os.path.join(root, name)): # 如果不是文件夾,則跳過
continue
name2label[name] = len(name2label.keys()) # 給每個類別編碼一個數字
images, labels = load_csv(root, 'images.csv', name2label) # 讀取csv文件中已經寫好的圖片路徑,和對應的標籤
# 將數據集按6:2:2的比例分成訓練集、驗證集、測試集
if mode == 'train': # 60%
images = images[:int(0.6 * len(images))]
labels = labels[:int(0.6 * len(labels))]
elif mode == 'val': # 20% = 60%->80%
images = images[int(0.6 * len(images)):int(0.8 * len(images))]
labels = labels[int(0.6 * len(labels)):int(0.8 * len(labels))]
else: # 20% = 80%->100%
images = images[int(0.8 * len(images)):]
labels = labels[int(0.8 * len(labels)):]
return images, labels, name2label
(3)創建數據集對象。讀入圖片,對原圖進行預處理,然後轉化爲張量。
def preprocess(image_path, label):
# x: 圖片的路徑,y:圖片的數字編碼
x = tf.io.read_file(image_path) # 讀入圖片
x = tf.image.decode_jpeg(x, channels=3) # 將原圖解碼爲通道數爲3的三維矩陣
x = tf.image.resize(x, [244, 244])
# 數據增強
# x = tf.image.random_flip_up_down(x) # 上下翻轉
# x = tf.image.random_flip_left_right(x) # 左右鏡像
x = tf.image.random_crop(x, [224, 224, 3]) # 裁剪
x = tf.cast(x, dtype=tf.float32) / 255. # 歸一化
x = normalize(x)
y = tf.convert_to_tensor(label) # 轉換爲張量
return x, y
# 1.加載自定義數據集
images, labels, table = load_pokemon('pokemon', 'train')
print('images', len(images), images)
print('labels', len(labels), labels)
print(table)
db = tf.data.Dataset.from_tensor_slices((images, labels)) # 創建數據集對象
db = db.shuffle(1000).map(preprocess).batch(32).repeat(20) # 設置批量訓練的batch爲32,要將訓練集重複訓練20遍
2.2 網絡結構搭建
爲了使得網絡能在單cpu/gpu上訓練,在保持原來的整體結構上做了如下幾點微調:
(1)保持原有結構不變,使用但cpu/gpu進行訓練
(2)由於原網絡結構是進行1000分類,而我這裏是進行5分類。所以將全連接層的節點個數相應的減少了。把原來的節點數2048、2048、1000分別改爲了:1024、128、5。
# 2.網絡搭建
network = Sequential([
# 第一層
layers.Conv2D(48, kernel_size=11, strides=4, padding=[[0, 0], [2, 2], [2, 2], [0, 0]], activation='relu'), # 55*55*48
layers.MaxPooling2D(pool_size=3, strides=2), # 27*27*48
# 第二層
layers.Conv2D(128, kernel_size=5, strides=1, padding=[[0, 0], [2, 2], [2, 2], [0, 0]], activation='relu'), # 27*27*128
layers.MaxPooling2D(pool_size=3, strides=2), # 13*13*128
# 第三層
layers.Conv2D(192, kernel_size=3, strides=1, padding=[[0, 0], [1, 1], [1, 1], [0, 0]], activation='relu'), # 13*13*192
# 第四層
layers.Conv2D(192, kernel_size=3, strides=1, padding=[[0, 0], [1, 1], [1, 1], [0, 0]], activation='relu'), # 13*13*192
# 第五層
layers.Conv2D(128, kernel_size=3, strides=1, padding=[[0, 0], [1, 1], [1, 1], [0, 0]], activation='relu'), # 13*13*128
layers.MaxPooling2D(pool_size=3, strides=2), # 6*6*128
layers.Flatten(), # 6*6*128=4608
# 第六層
layers.Dense(1024, activation='relu'),
layers.Dropout(rate=0.5),
# 第七層
layers.Dense(128, activation='relu'),
layers.Dropout(rate=0.5),
# 第八層(輸出層)
layers.Dense(5)
])
network.build(input_shape=(32, 224, 224, 3)) # 設置輸入格式
network.summary()
2.3 迭代訓練
依據論文中的方法,我採用的是隨機梯度下降算法進行迭代訓練。由於我使用的是cpu進行訓練,因此一次只送入32組數據進行迭代,一共對整個數據集訓練20個epochs,每20輪打印出一次測試精確度。(如果使用GPU進行訓練的話,可以相應的將batch數調大)
# 3.模型訓練(計算梯度,迭代更新網絡參數)
optimizer = optimizers.SGD(lr=0.01) # 聲明採用批量隨機梯度下降方法,學習率=0.01
acc_meter = metrics.Accuracy()
x_step = []
y_accuracy = []
for step, (x, y) in enumerate(db): # 一次輸入batch組數據進行訓練
with tf.GradientTape() as tape: # 構建梯度記錄環境
x = tf.reshape(x, (-1, 224, 224, 3)) # 輸入[b, 224, 224, 3]
out = network(x) # 輸出[b, 5]
y_onehot = tf.one_hot(y, depth=5) # one-hot編碼
loss = tf.square(out - y_onehot)
loss = tf.reduce_sum(loss)/32 # 定義均方差損失函數,注意此處的32對應爲batch的大小
grads = tape.gradient(loss, network.trainable_variables) # 計算網絡中各個參數的梯度
optimizer.apply_gradients(zip(grads, network.trainable_variables)) # 更新網絡參數
acc_meter.update_state(tf.argmax(out, axis=1), y) # 比較預測值與標籤,並計算精確度
if step % 10 == 0: # 每200個step,打印一次結果
print('Step', step, ': Loss is: ', float(loss), ' Accuracy: ', acc_meter.result().numpy())
x_step.append(step)
y_accuracy.append(acc_meter.result().numpy())
acc_meter.reset_states()
2.4 可視化
每20輪採集一次精確度數據,最後通過可視化顯示出來。
# 4.可視化
plt.plot(x_step, y_accuracy, label="training")
plt.xlabel("step")
plt.ylabel("accuracy")
plt.title("accuracy of training")
plt.legend()
plt.show()
三、完整代碼與測試結果
此數據集較小,因此使用單cpu即可完成訓練,下圖是我訓練20個epos的過程中,測試精確度的變化。整個訓練過程大概不到10分鐘,測試精確度就達到了97%。當然,想得到更高的分類精度,可以多訓練幾個epos。
測試結果:
完整代碼如下:
import tensorflow as tf # 導入TF庫
from tensorflow.keras import layers, optimizers, datasets, Sequential, metrics # 導入TF子庫
import os, glob
import random, csv
import matplotlib.pyplot as plt
# 創建圖片路徑和標籤,並寫入csv文件
def load_csv(root, filename, name2label):
# root:數據集根目錄
# filename:csv文件名
# name2label:類別名編碼表
if not os.path.exists(os.path.join(root, filename)): # 如果不存在csv,則創建一個
images = [] # 初始化存放圖片路徑的字符串數組
for name in name2label.keys(): # 遍歷所有子目錄,獲得所有圖片的路徑
# glob文件名匹配模式,不用遍歷整個目錄判斷而獲得文件夾下所有同類文件
# 只考慮後綴爲png,jpg,jpeg的圖片,比如:pokemon\\mewtwo\\00001.png
images += glob.glob(os.path.join(root, name, '*.png'))
images += glob.glob(os.path.join(root, name, '*.jpg'))
images += glob.glob(os.path.join(root, name, '*.jpeg'))
print(len(images), images) # 打印出images的長度和所有圖片路徑名
random.shuffle(images) # 隨機打亂存放順序
# 創建csv文件,並且寫入圖片路徑和標籤信息
with open(os.path.join(root, filename), mode='w', newline='') as f:
writer = csv.writer(f)
for img in images: # 遍歷images中存放的每一個圖片的路徑,如pokemon\\mewtwo\\00001.png
name = img.split(os.sep)[-2] # 用\\分隔,取倒數第二項作爲類名
label = name2label[name] # 找到類名鍵對應的值,作爲標籤
writer.writerow([img, label]) # 寫入csv文件,以逗號隔開,如:pokemon\\mewtwo\\00001.png, 2
print('written into csv file:', filename)
# 讀csv文件
images, labels = [], [] # 創建兩個空數組,用來存放圖片路徑和標籤
with open(os.path.join(root, filename)) as f:
reader = csv.reader(f)
for row in reader: # 逐行遍歷csv文件
img, label = row # 每行信息包括圖片路徑和標籤
label = int(label) # 強制類型轉換爲整型
images.append(img) # 插入到images數組的後面
labels.append(label)
assert len(images) == len(labels) # 斷言,判斷images和labels的長度是否相同
return images, labels
# 首先遍歷pokemon根目錄下的所有子目錄。對每個子目錄,用類別名作爲編碼表的key,編碼表的長度作爲類別的標籤,存進name2label字典對象
def load_pokemon(root, mode='train'):
# 創建數字編碼表
name2label = {} # 創建一個空字典{key:value},用來存放類別名和對應的標籤
for name in sorted(os.listdir(os.path.join(root))): # 遍歷根目錄下的子目錄,並排序
if not os.path.isdir(os.path.join(root, name)): # 如果不是文件夾,則跳過
continue
name2label[name] = len(name2label.keys()) # 給每個類別編碼一個數字
images, labels = load_csv(root, 'images.csv', name2label) # 讀取csv文件中已經寫好的圖片路徑,和對應的標籤
# 將數據集按6:2:2的比例分成訓練集、驗證集、測試集
if mode == 'train': # 60%
images = images[:int(0.6 * len(images))]
labels = labels[:int(0.6 * len(labels))]
elif mode == 'val': # 20% = 60%->80%
images = images[int(0.6 * len(images)):int(0.8 * len(images))]
labels = labels[int(0.6 * len(labels)):int(0.8 * len(labels))]
else: # 20% = 80%->100%
images = images[int(0.8 * len(images)):]
labels = labels[int(0.8 * len(labels)):]
return images, labels, name2label
img_mean = tf.constant([0.485, 0.456, 0.406])
img_std = tf.constant([0.229, 0.224, 0.225])
def normalize(x, mean=img_mean, std=img_std):
x = (x - mean)/std
return x
# def denormalize(x, mean=img_mean, std=img_std):
# x = x * std + mean
# return x
def preprocess(image_path, label):
# x: 圖片的路徑,y:圖片的數字編碼
x = tf.io.read_file(image_path) # 讀入圖片
x = tf.image.decode_jpeg(x, channels=3) # 將原圖解碼爲通道數爲3的三維矩陣
x = tf.image.resize(x, [244, 244])
# 數據增強
# x = tf.image.random_flip_up_down(x) # 上下翻轉
# x = tf.image.random_flip_left_right(x) # 左右鏡像
x = tf.image.random_crop(x, [224, 224, 3]) # 裁剪
x = tf.cast(x, dtype=tf.float32) / 255. # 歸一化
x = normalize(x)
y = tf.convert_to_tensor(label) # 轉換爲張量
return x, y
# 1.加載自定義數據集
images, labels, table = load_pokemon('pokemon', 'train')
print('images', len(images), images)
print('labels', len(labels), labels)
print(table)
db = tf.data.Dataset.from_tensor_slices((images, labels)) # images: string path, labels: number
db = db.shuffle(1000).map(preprocess).batch(32).repeat(20)
# 2.網絡搭建
network = Sequential([
# 第一層
layers.Conv2D(48, kernel_size=11, strides=4, padding=[[0, 0], [2, 2], [2, 2], [0, 0]], activation='relu'), # 55*55*48
layers.MaxPooling2D(pool_size=3, strides=2), # 27*27*48
# 第二層
layers.Conv2D(128, kernel_size=5, strides=1, padding=[[0, 0], [2, 2], [2, 2], [0, 0]], activation='relu'), # 27*27*128
layers.MaxPooling2D(pool_size=3, strides=2), # 13*13*128
# 第三層
layers.Conv2D(192, kernel_size=3, strides=1, padding=[[0, 0], [1, 1], [1, 1], [0, 0]], activation='relu'), # 13*13*192
# 第四層
layers.Conv2D(192, kernel_size=3, strides=1, padding=[[0, 0], [1, 1], [1, 1], [0, 0]], activation='relu'), # 13*13*192
# 第五層
layers.Conv2D(128, kernel_size=3, strides=1, padding=[[0, 0], [1, 1], [1, 1], [0, 0]], activation='relu'), # 13*13*128
layers.MaxPooling2D(pool_size=3, strides=2), # 6*6*128
layers.Flatten(), # 6*6*128=4608
# 第六層
layers.Dense(1024, activation='relu'),
layers.Dropout(rate=0.5),
# 第七層
layers.Dense(128, activation='relu'),
layers.Dropout(rate=0.5),
# 第八層(輸出層)
layers.Dense(5)
])
network.build(input_shape=(32, 224, 224, 3)) # 設置輸入格式
network.summary()
# 3.模型訓練(計算梯度,迭代更新網絡參數)
optimizer = optimizers.SGD(lr=0.01) # 聲明採用批量隨機梯度下降方法,學習率=0.01
acc_meter = metrics.Accuracy()
x_step = []
y_accuracy = []
for step, (x, y) in enumerate(db): # 一次輸入batch組數據進行訓練
with tf.GradientTape() as tape: # 構建梯度記錄環境
x = tf.reshape(x, (-1, 224, 224, 3)) # 將輸入拉直,[b,28,28]->[b,784]
out = network(x) # 輸出[b, 10]
y_onehot = tf.one_hot(y, depth=5) # one-hot編碼
loss = tf.square(out - y_onehot)
loss = tf.reduce_sum(loss)/32 # 定義均方差損失函數,注意此處的32對應爲batch的大小
grads = tape.gradient(loss, network.trainable_variables) # 計算網絡中各個參數的梯度
optimizer.apply_gradients(zip(grads, network.trainable_variables)) # 更新網絡參數
acc_meter.update_state(tf.argmax(out, axis=1), y) # 比較預測值與標籤,並計算精確度
if step % 10 == 0: # 每200個step,打印一次結果
print('Step', step, ': Loss is: ', float(loss), ' Accuracy: ', acc_meter.result().numpy())
x_step.append(step)
y_accuracy.append(acc_meter.result().numpy())
acc_meter.reset_states()
# 4.可視化
plt.plot(x_step, y_accuracy, label="training")
plt.xlabel("step")
plt.ylabel("accuracy")
plt.title("accuracy of training")
plt.legend()
plt.show()