寫在前面:大家好!我是【AI 菌】,一枚愛彈吉他的程序員。我
熱愛AI、熱愛分享、熱愛開源
! 這博客是我對學習的一點總結與記錄。如果您也對深度學習、機器視覺、算法、Python、C++
感興趣,可以關注我的動態,我們一起學習,一起進步~
我的博客地址爲:【AI 菌】的博客
我的Github項目地址是:【AI 菌】的Github
本教程會持續更新,如果對您有幫助的話,歡迎star收藏~
前言:
本專欄將分享我從零開始搭建神經網絡的學習過程,注重理論與實戰相結合,力爭打造最易上手的小白教程。在這過程中,我將使用谷歌TensorFlow2.0框架逐一復現經典的卷積神經網絡:LeNet、AlexNet、VGG系列、GooLeNet、ResNet 系列、DenseNet 系列,以及現在比較流行的:RCNN系列、SSD、YOLO系列等。
這一次我將復現非常經典的GooLeNet卷積神經網絡。首先在理論部分,我會依據論文對GooLeNet進行一個簡要的講解。然後在實戰部分,我會對自定義數據集進行加載、搭建GooLeNet網絡、迭代訓練,最終完成圖片分類和識別任務。
系列教程:
深度學習環境搭建:Anaconda3+tensorflow2.0+PyCharm
TF2.0深度學習實戰(二):用compile()和fit()快速搭建卷積神經網絡
TF2.0深度學習實戰(三):搭建LeNet-5卷積神經網絡
TF2.0深度學習實戰(四):搭建AlexNet卷積神經網絡
資源傳送門:
論文地址:《Going deeper with convolutions》
論文翻譯:GoogLeNet 原文翻譯:《Going deeper with convolutions》
github項目地址:【AI 菌】的Github
數據集下載:花分類數據集, 提取碼:9ao5
文章目錄
一、淺談GoogLeNet
1.1 GoogLeNet 簡介
GoogLeNet卷積神經網絡出自於《Going deeper with convolutions》這篇論文,是由谷歌公司Christian Szegedy、Yangqing Jia等人聯合發表。其研究成果在2014年 ILSVRC 挑戰賽 ImageNet 分類任務
上獲得冠軍
,而當時的亞軍就是上一篇文中講到的VGG系列。
很有意思的是,GoogLeNet名字是由Google的前綴Goog
與LeNet
的組合而來,這其實是對Yann LeCuns開拓性的LeNet-5網絡
的致敬。
GoogLeNet卷積神經網絡的最大貢獻在於,提出了非常經典的Inception模塊。該網絡結構的最大特點是網絡內部計算資源的利用率很高
。因此該設計允許在保持計算資源預算不變的情況下增加網絡的深度和寬度,使得GoogLeNet網絡層數達到了更深的22層
,但是網絡參數僅爲AlexNet的1/12
。
1.2 GoogLeNet 的創新點
- 引入了非常經典的
Inception模塊
。 - 採用了
模塊化設計
的思想。通過大量堆疊Inception模塊,形成了更深更復雜的網絡結構。 - 採用了
大量的1×1卷積核
。主要是用作降維
以消除計算瓶頸。 - 在網絡中間層設計了
兩個輔助分類器
。 - 採用
平均池化層
替代了原來的全連接層,使得模型參數大大減少。
1.3 GoogLeNet 網絡結構
GoogLeNet 網絡結構較爲複雜,我將分以下四個部分講解:
- Inception模塊
- 1x1卷積核降維
- 輔助分類器
- GoogLeNet整體結構
(1) Inception模塊
在論文中,重點研究了名爲Inception的高效的用於計算機視覺的深度神經網絡結構,該結構的名稱源自Lin等提出的論文《Network in Network》。在這種方法中,“深層”一詞有兩種不同的含義:首先,在某種意義上,我們以“Inception模塊”的形式引入了新的組織層次,在更直接的意義上是網絡深度的增加。Inception模塊結構如下:
圖(a)是原始的Inception模塊結構。圖(b)是改進後的Inception模塊結構。
圖(b)在(a)的基礎上,在3×3和5×5的卷積核之前,添加1×1的卷積核來縮減計算量,實質上是一個降維的過程
。
實驗中最後採用的是圖(b)所示的加入降維模塊
的Inception模塊,因此這裏以(b)爲例進行講解:
Inception模塊的輸入,通過4 個分支網絡得到 4 個網絡輸出,在通道軸上面進行拼接,形成 Inception 模塊的輸出。這四個分支網絡
分別是:
- 1x1 卷積層,步長爲1,padding=‘same’
- 1x1 卷積層,再通過一個 3x3 卷積層,步長爲1,padding=‘same’
- 1x1 卷積層,再通過一個 5x5 卷積層,步長爲1,padding=‘same’
- 3x3 最大池化城,再通過 1x1 卷積層,步長爲1,padding=‘same’
Inception 模塊的優點
:
- 在每個3×3和5×5的卷積核之前,添加了1×1的卷積核進行降維,大大減少了參數量。
- 融合不同尺度的特徵信息。
- 並行結構,結構稀疏,局部最優。
(2) 1x1卷積核降維
前面Inception模塊中提到,使用1x1卷積核來進行降維。所謂降維,就是通過降低卷積核的數量,從而大大減少模型參數。那下面我們就來演示一下,1x1卷積核具體是如何進行降維的。
如下圖所示,對於同樣的通道數爲512的輸入特徵圖,分別使用1x1卷積核和不使用1x1卷積核進行了一組對比實驗
。
不使用
1x1卷積核降維。由卷積規則(卷積核通道數=輸入特徵圖通道數)可知,5x5卷積核的通道數(維度)爲512。因此64個5x5卷積核的參數一共是:5x5x512x64=819200使用
1x1卷積核進行降維。若採用24個1x1卷積核進行降維,由卷積規則可知,中間層特徵圖的維度爲24。因此,當再用5x5卷積覈對中間層特徵圖進行卷積時,維度就降爲了24,也就是說這時候64個5x5卷積核的通道數降爲了24。因此總的參數量是:1x1x512x64+5x5x24x64=50688
對比可知:沒有進行降維的模型參數有819200個,經過1x1卷積核降維之後的模型參數僅有50688個,參數量減少了16倍多
!
對卷積規則還不太熟悉的盆友請戳這裏:深度學習筆記(一):卷積層+激活函數+池化層+全連接層
(3) 輔助分類器
考慮到網絡的深度較大,以有效方式將梯度傳播回所有層的能力有限,可能會產生梯度彌散
現象。因此在網絡中間層設計了兩個輔助分類器
,希望以此激勵網絡在較低層進行分類,從而增加了被傳播回的梯度信號,避免出現梯度彌散。
在訓練過程中
,它們的損失將以折扣權重添加到網絡的總損失中(輔助分類器的損失加權爲0.3)。 在測試過程中
,這些輔助網絡將被丟棄。其網絡結構如下:
設計了完全相同的兩個輔助分類器,這些分類器採用較小的卷積網絡的形式,位於Inception(4a)和(4d)模塊的輸出之上。
如上圖所示,具體結構與參數
如下:
- 第一層是一個平均池化下采樣層,池化核大小爲5x5,步長爲3
- 第二層是卷積層,卷積核大小爲1x1,步長爲1,卷積核個數是128
- 第三層是全連接層,節點個數是1024
- 第四層是全連接層,節點個數是1000,進行1000分類。
- 最後經過Softmax激活函數,將模型輸出轉化爲預測的類別概率輸出
(4) GoogLeNet整體結構
下表是原論文中給出的參數列表,描述了GoogLeNet每個卷積層的卷積核個數、卷積核大小等信息。
- 在該網絡中,輸入大小爲224×224的RGB顏色通道圖片。
- 所有卷積層,包括Inception模塊內部的那些卷積,均使用ReLU激活函數。
- 對於我們搭建的Inception模塊,需要關注的是#1x1, #3x3reduce, #3x3, #5x5reduce, #5x5, poolproj這六列,分別對應着
Inception模塊內置卷積層
所使用的卷積核個數。具體對應關係可參見下圖:
GoogLeNet的完整結構圖如下,由於原圖放大後很長,爲了排版更緊湊美觀,這裏只插入了論文原圖。想看大圖的盆友,可以戳這裏,在文章中有:GoogLeNet 原文翻譯:《Going deeper with convolutions》
1.4 GoogLeNet 的性能
在2014年 ILSVRC 挑戰賽 ImageNet 分類任務
上獲得冠軍
,測試結果如下表:
GoogLeNet最終在驗證和測試數據集上均獲得6.67%
的Top-5錯誤率
,排名第一。 與2012年的SuperVision方法相比,相對減少了56.5%
,與上一年的最佳方法(Clarifai)相比,減少了約40%
。
Top-5錯誤率
,是將真實類別與排名前5個的預測類進行比較:如果真實類別位於前五名預測類之中,則無論其排名如何,圖像被視爲正確分類 。 該挑戰賽使用Top-5錯誤率進行排名。
當然,GoogLeNet的檢測效果也很不錯,這裏暫時只對分類效果進行評價。
二、TensorFlow2.0搭建GoogLeNet實戰
2.1 數據集準備
(1) 數據集簡介
這次我採用的是花分類數據集
,該數據集一共有5個類別
,分別是:daisy、dandelion、roses、sunflowers、tulips,一共有3670張圖片。按9:1劃分數據集,其中訓練集train
中有3306張、驗證集val
中有364張圖片。
數據集下載地址:花分類數據集, 提取碼:9ao5
大家下載完,將文件解壓後直接放在工程根目錄下,就像我這樣:
(2) 加載數據集
這裏我採用的方式是,使用keras底層模塊圖像生成器
對數據集進行加載和預處理。需要說明一點的是:原來的類別標籤是daisy、dandelion、roses、sunflowers、tulips,不能直接喂入神經網絡,要將其轉化爲數字標籤
。並將創建好的數字標籤字典寫入了class_indices.json文件。
主要代碼如下:
# 定義訓練集圖像生成器,並對圖像進行預處理
train_image_generator = ImageDataGenerator(preprocessing_function=pre_function,
horizontal_flip=True) # 水平翻轉
# 使用圖像生成器從文件夾train_dir中讀取樣本,默認對標籤進行了one-hot編碼
train_data_gen = train_image_generator.flow_from_directory(directory=train_dir,
batch_size=batch_size,
shuffle=True,
target_size=(im_height, im_width),
class_mode='categorical') # 分類方式
total_train = train_data_gen.n # 訓練集樣本數
class_indices = train_data_gen.class_indices # 數字編碼標籤字典:{類別名稱:索引}
inverse_dict = dict((val, key) for key, val in class_indices.items()) # 轉換字典中鍵與值的位置
json_str = json.dumps(inverse_dict, indent=4) # 將轉換後的字典寫入文件class_indices.json
with open('class_indices.json', 'w') as json_file:
json_file.write(json_str)
2.2 網絡搭建
由於GooLeNet網絡結構較爲複雜,這裏我按論文中的各個主要結構:Inception模塊、輔助分類器、完整結構
,將它們用函數或類的形式進行封裝。主要代碼如下:
(1) Inception模塊
class Inception(layers.Layer):
# ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj分別對應Inception中各個卷積核的個數,**kwargs可變長度字典變量,存層名稱
def __init__(self, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj, **kwargs):
super(Inception, self).__init__(**kwargs)
# 分支1
self.branch1 = layers.Conv2D(ch1x1, kernel_size=1, activation="relu")
# 分支2
self.branch2 = Sequential([
layers.Conv2D(ch3x3red, kernel_size=1, activation="relu"),
layers.Conv2D(ch3x3, kernel_size=3, padding="SAME", activation="relu")])
# 分支3
self.branch3 = Sequential([
layers.Conv2D(ch5x5red, kernel_size=1, activation="relu"),
layers.Conv2D(ch5x5, kernel_size=5, padding="SAME", activation="relu")])
# 分支4
self.branch4 = Sequential([
layers.MaxPool2D(pool_size=3, strides=1, padding="SAME"),
layers.Conv2D(pool_proj, kernel_size=1, activation="relu")])
def call(self, inputs, **kwargs):
branch1 = self.branch1(inputs)
branch2 = self.branch2(inputs)
branch3 = self.branch3(inputs)
branch4 = self.branch4(inputs)
# 將4個分支輸出按通道連接
outputs = layers.concatenate([branch1, branch2, branch3, branch4])
return outputs
(2) 輔助分類器InceptionAux
class InceptionAux(layers.Layer):
# num_classes表示輸出分類節點數,**kwargs存放每層名稱
def __init__(self, num_classes, **kwargs):
super(InceptionAux, self).__init__(**kwargs)
self.averagePool = layers.AvgPool2D(pool_size=5, strides=3) # 平均池化
self.conv = layers.Conv2D(128, kernel_size=1, activation="relu")
self.fc1 = layers.Dense(1024, activation="relu") # 全連接層1
self.fc2 = layers.Dense(num_classes) # 全連接層2
self.softmax = layers.Softmax() # softmax激活函數
def call(self, inputs, **kwargs):
x = self.averagePool(inputs)
x = self.conv(x)
x = layers.Flatten()(x) # 拉直
x = layers.Dropout(rate=0.5)(x)
x = self.fc1(x)
x = layers.Dropout(rate=0.5)(x)
x = self.fc2(x)
x = self.softmax(x)
return x
(3) GooLeNet整體結構
def GoogLeNet(im_height=224, im_width=224, class_num=1000, aux_logits=False):
# 輸入224*224的3通道彩色圖片
input_image = layers.Input(shape=(im_height, im_width, 3), dtype="float32")
x = layers.Conv2D(64, kernel_size=7, strides=2, padding="SAME", activation="relu", name="conv2d_1")(input_image)
x = layers.MaxPool2D(pool_size=3, strides=2, padding="SAME", name="maxpool_1")(x)
x = layers.Conv2D(64, kernel_size=1, activation="relu", name="conv2d_2")(x)
x = layers.Conv2D(192, kernel_size=3, padding="SAME", activation="relu", name="conv2d_3")(x)
x = layers.MaxPool2D(pool_size=3, strides=2, padding="SAME", name="maxpool_2")(x)
# Inception模塊
x = Inception(64, 96, 128, 16, 32, 32, name="inception_3a")(x)
x = Inception(128, 128, 192, 32, 96, 64, name="inception_3b")(x)
x = layers.MaxPool2D(pool_size=3, strides=2, padding="SAME", name="maxpool_3")(x)
# Inception模塊
x = Inception(192, 96, 208, 16, 48, 64, name="inception_4a")(x)
# 判斷是否使用輔助分類器1。訓練時使用,測試時去掉。
if aux_logits:
aux1 = InceptionAux(class_num, name="aux_1")(x)
# Inception模塊
x = Inception(160, 112, 224, 24, 64, 64, name="inception_4b")(x)
x = Inception(128, 128, 256, 24, 64, 64, name="inception_4c")(x)
x = Inception(112, 144, 288, 32, 64, 64, name="inception_4d")(x)
# 判斷是否使用輔助分類器2。訓練時使用,測試時去掉。
if aux_logits:
aux2 = InceptionAux(class_num, name="aux_2")(x)
# Inception模塊
x = Inception(256, 160, 320, 32, 128, 128, name="inception_4e")(x)
x = layers.MaxPool2D(pool_size=3, strides=2, padding="SAME", name="maxpool_4")(x)
# Inception模塊
x = Inception(256, 160, 320, 32, 128, 128, name="inception_5a")(x)
x = Inception(384, 192, 384, 48, 128, 128, name="inception_5b")(x)
# 平均池化層
x = layers.AvgPool2D(pool_size=7, strides=1, name="avgpool_1")(x)
# 拉直
x = layers.Flatten(name="output_flatten")(x)
x = layers.Dropout(rate=0.4, name="output_dropout")(x)
x = layers.Dense(class_num, name="output_dense")(x)
aux3 = layers.Softmax(name="aux_3")(x)
# 判斷是否使用輔助分類器
if aux_logits:
model = models.Model(inputs=input_image, outputs=[aux1, aux2, aux3])
else:
model = models.Model(inputs=input_image, outputs=aux3)
return model
2.3 模型的裝配與訓練
部分代碼如下:
# 使用keras底層api進行網絡訓練。
loss_object = tf.keras.losses.CategoricalCrossentropy(from_logits=False) # 定義損失函數(這種方式需要one-hot編碼)
optimizer = tf.keras.optimizers.Adam(learning_rate=0.0003) # 優化器
train_loss = tf.keras.metrics.Mean(name='train_loss')
train_accuracy = tf.keras.metrics.CategoricalAccuracy(name='train_accuracy') # 定義平均準確率
test_loss = tf.keras.metrics.Mean(name='test_loss')
test_accuracy = tf.keras.metrics.CategoricalAccuracy(name='test_accuracy')
2.4 訓練集/驗證集上測試結果
由於不在實驗室,我用的是筆記本進行訓練的。大概跑1個epoch耗時15分鐘
,我跑了19個epochs,花了將近5個小時,訓練集上準確度達到83.9%,驗證集上準確率達到了83.7%。當時因爲用電腦有事,我提前終止了訓練
。大家有時間,可以多訓練下,精確度可以達到更高
。下面貼出我的部分訓練結果
:
圖中,打印出了每訓練完一個epoch後的loss值、訓練集分類準確度、測試集分類準確度。可見,網絡的損失值loss在不斷減小
,數據集上的準確度都在穩步上升
,因此模型此時是收斂的
,繼續訓練可以得到更好的分類準確度
。
2.5 加載模型,對單張圖片預測
在工程根目錄
下,放入一張類別爲daisy的圖片,將其命名爲daisy_test.jpg。我們讀入這張圖片,加載剛纔已經訓練好的模型,對圖片進行預測
。
預測代碼如下:
# 讀入圖片
img = Image.open("E:/DeepLearning/GoogLeNet/daisy_test.jpg")
# resize成224x224的格式
img = img.resize((im_width, im_height))
plt.imshow(img)
# 對原圖標準化處理
img = ((np.array(img) / 255.) - 0.5) / 0.5
# Add the image to a batch where it's the only member.
img = (np.expand_dims(img, 0))
# 讀class_indict文件
try:
json_file = open('./class_indices.json', 'r')
class_indict = json.load(json_file)
except Exception as e:
print(e)
exit(-1)
model = GoogLeNet(class_num=5, aux_logits=False) # 重新構建網絡
model.summary()
model.load_weights("./save_weights/myGoogLenet.h5", by_name=True) # 加載模型參數
result = model.predict(img)
predict_class = np.argmax(result)
print('預測出的類別是:', class_indict[str(predict_class)]) # 打印顯示出預測類別
plt.show()
輸入的圖片daisy_test.jpg屬於daisy類,圖片如下:
預測結果如下:
可見,預測結果與原圖daisy_test.jpg的標籤一致,預測成功!
寫到這裏文章就要結束了。電腦前的你是不是也想試一試呢?
爲了助你能快速搭建好網絡
,這裏奉上我的Github項目地址:【AI 菌】的Github
如果你想更深入理解GoogLeNet,建議戳戳這裏:論文翻譯:GoogLeNet 原文翻譯:《Going deeper with convolutions》
最後就要和大家說再見啦!如果這篇文章對您有幫助的話,請點個贊支持一下唄,謝謝!