1、LeNet
LeNet卷積神經(jīng)網(wǎng)絡(luò)是LeCun于1998年提出,是卷積神經(jīng)網(wǎng)絡(luò)的開篇之作。通過共享卷積核減少了網(wǎng)絡(luò)的參數(shù)。

在統(tǒng)計(jì)卷積神經(jīng)網(wǎng)絡(luò)層數(shù)時(shí),一般只統(tǒng)計(jì)卷積計(jì)算層和全連接計(jì)算層,其余操作可以認(rèn)為是卷積計(jì)算層的附屬。LeNet一共有5層網(wǎng)絡(luò):C1卷積層,C3卷積層,C5、F6、Output三層全連接層。
-
C1卷積:
- C:6個(gè)5*5的卷積核,步長(zhǎng)為1,不使用全零填充
- B:不使用批標(biāo)準(zhǔn)化(LeNet提出時(shí)還沒有BN操作)
- A: sigmoid激活函數(shù)
- P: 最大值池化,2*2池化核,步長(zhǎng)為2,不使用全零填充
- D: 沒有舍棄
-
C3卷積:
- C:16個(gè)5*5的卷積核,步長(zhǎng)為1,不使用全零填充
- B:不使用批標(biāo)準(zhǔn)化(LeNet提出時(shí)還沒有BN操作)
- A: sigmoid激活函數(shù)
- P: 最大值池化,2*2池化核,步長(zhǎng)為2,不使用全零填充
- D: 沒有舍棄
- C5全連接:120個(gè)神經(jīng)元,sigmoid激活函數(shù)
- F6全連接:84個(gè)神經(jīng)元,sigmoid激活函數(shù)
-
Output:10個(gè)神經(jīng)元,softmax激活函數(shù)
運(yùn)行代碼如下(幾乎model不一樣):
import tensorflow as tf
from tensorflow.keras import Model
from tensorflow.keras.layers import Conv2D, BatchNormalization, Activation, MaxPool2D, Dropout, Flatten, Dense
import matplotlib.pyplot as plt
import os
# 加載數(shù)據(jù)集
cifar10 = tf.keras.datasets.cifar10
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
class LeNet(Model):
def __init__(self):
super(LeNet, self).__init__()
self.c1 = Conv2D(filters=6, kernel_size=(5, 5), activation="sigmoid")
self.p1 = MaxPool2D(pool_size=(2, 2), strides=2)
self.c2 = Conv2D(filters=16, kernel_size=(5, 5), activation="sigmoid")
self.p2 = MaxPool2D(pool_size=(2, 2), strides=2)
self.flatten = Flatten()
self.f1 = Dense(120, activation="sigmoid")
self.f2 = Dense(84, activation="sigmoid")
self.f3 = Dense(10, activation="softmax")
def call(self, x):
x = self.c1(x)
x = self.p1(x)
x = self.c2(x)
x = self.p2(x)
x = self.flatten(x)
x = self.f1(x)
x = self.f2(x)
y = self.f3(x)
return y
# model = BaseLine()
model = LeNet()
# 配置訓(xùn)練方法
model.compile(
optimizer="adam",
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
metrics=["sparse_categorical_accuracy"]
)
# 斷點(diǎn)續(xù)訓(xùn),讀取模型
# checkpoint_save_path = "cifar10/BaseLine.ckpt"
checkpoint_save_path = "cifar10/LeNet.ckpt"
if os.path.exists(checkpoint_save_path + ".index"):
print("*******load the model******")
model.load_weights(checkpoint_save_path)
cp_callback = tf.keras.callbacks.ModelCheckpoint(
filepath=checkpoint_save_path,
save_weights_only=True,
save_best_only=True
)
# 訓(xùn)練模型
history = model.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test),
validation_freq=1, callbacks=[cp_callback])
# 打印網(wǎng)絡(luò)結(jié)構(gòu)和參數(shù)
model.summary()
# 寫入?yún)?shù)
with open("cifar10_lenet_weights.txt", "w") as f:
for v in model.trainable_variables:
f.write(str(v.name) + "\n")
f.write(str(v.shape) + "\n")
f.write(str(v.numpy()) + "\n")
# 顯示訓(xùn)練和預(yù)測(cè)的acc、loss曲線
acc = history.history["sparse_categorical_accuracy"]
val_acc = history.history["val_sparse_categorical_accuracy"]
loss = history.history["loss"]
val_loss = history.history["val_loss"]
plt.subplot(1, 2, 1)
plt.plot(acc, label="train acc")
plt.plot(val_acc, label="validation acc")
plt.title("train & validation acc")
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(loss, label="train loss")
plt.plot(val_loss, label="validation loss")
plt.title("train & validation loss")
plt.legend()
plt.show()
繪圖結(jié)果如下:

2、AlexNet
AlexNet網(wǎng)絡(luò)誕生于2012年,當(dāng)年ImageNet競(jìng)賽的冠軍,top5錯(cuò)誤率為16.4%。AlexNet使用relu激活函數(shù),提升了訓(xùn)練速度;使用dropout,緩解了過擬合。

AlexNet使用了八層網(wǎng)絡(luò)結(jié)構(gòu):
- 第一層卷積:
- C: 96個(gè)3*3卷積核,步長(zhǎng)為1,不使用全零填充
- B: 局部相應(yīng)標(biāo)準(zhǔn)化(LRN,現(xiàn)在使用較少,改用BN)
- A: relu激活函數(shù)
- P: 最大值池化,3*3池化核,步長(zhǎng)2
- D:不舍棄
- 第二層卷積:
- C: 256個(gè)3*3卷積核,步長(zhǎng)為1,不使用全零填充
- B: 局部相應(yīng)標(biāo)準(zhǔn)化(LRN,現(xiàn)在使用較少,改用BN)
- A: relu激活函數(shù)
- P: 最大值池化,3*3池化核,步長(zhǎng)2
- D:不舍棄
- 第三層卷積:
- C: 384個(gè)3*3卷積核,步長(zhǎng)為1,使用全零填充
- B: 不標(biāo)準(zhǔn)化
- A: relu激活函數(shù)
- P: 不池化
- D:不舍棄
- 第四層卷積:
- C: 384個(gè)3*3卷積核,步長(zhǎng)為1,使用全零填充
- B: 不標(biāo)準(zhǔn)化
- A: relu激活函數(shù)
- P: 不池化
- D:不舍棄
- 第五層卷積:
- C: 256個(gè)3*3卷積核,步長(zhǎng)為1,使用全零填充
- B: 不標(biāo)準(zhǔn)化
- A: relu激活函數(shù)
- P: 最大值池化,池化核3*3,步長(zhǎng)2
- D:不舍棄
- 第六層全連接:2048個(gè)神經(jīng)元,relu激活函數(shù),0.5的舍棄
- 第七層全連接:2048個(gè)神經(jīng)元,relu激活函數(shù),0.5的舍棄
- 第八層全連接:10個(gè)神經(jīng)元,softmax激活函數(shù)
相關(guān)代碼如下(只貼變更部分):
class AlexNet(Model):
def __init__(self):
super(AlexNet, self).__init__()
self.c1 = Conv2D(filters=96, kernel_size=(3, 3))
self.b1 = BatchNormalization()
self.a1 = Activation("relu")
self.p1 = MaxPool2D(pool_size=(3, 3), strides=2)
self.c2 = Conv2D(filters=256, kernel_size=(3, 3))
self.b2 = BatchNormalization()
self.a2 = Activation("relu")
self.p2 = MaxPool2D(pool_size=(3, 3), strides=2)
self.c3 = Conv2D(filters=384, kernel_size=(3, 3), padding="same",
activation="relu")
self.c4 = Conv2D(filters=384, kernel_size=(3, 3), padding="same",
activation="relu")
self.c5 = Conv2D(filters=256, kernel_size=(3, 3), padding="same",
activation="relu")
self.p3 = MaxPool2D(pool_size=(3, 3), strides=2)
self.flatten = Flatten()
self.f1 = Dense(2048, activation="relu")
self.d1 = Dropout(0.5)
self.f2 = Dense(2048, activation="relu")
self.d2 = Dropout(0.5)
self.f3 = Dense(10, activation="softmax")
def call(self, x):
x = self.c1(x)
x = self.b1(x)
x = self.a1(x)
x = self.p1(x)
x = self.c2(x)
x = self.b2(x)
x = self.a2(x)
x = self.p2(x)
x = self.c3(x)
x = self.c4(x)
x = self.c5(x)
x = self.p3(x)
x = self.flatten(x)
x = self.f1(x)
x = self.d1(x)
x = self.f2(x)
x = self.d2(x)
y = self.f3(x)
return y
ps:AlexNet速度明顯慢了很多。LeNet大概喝口水就跑完了,這個(gè)AlexNet跑了有50min左右(畢竟神經(jīng)元個(gè)數(shù)明顯增多了嘛)·····最后結(jié)果有九百多萬個(gè)參數(shù)····
pps: 我把GPU相關(guān)軟件裝好了———原來8G的CPU跑50分鐘,現(xiàn)在2G的GPU跑了大概5分鐘-----55555----GPU萬歲
3、VGGNet
VGGNet是2014年ImageNet競(jìng)賽的亞軍,top5錯(cuò)誤率減小到了7.3%。VGGNet使用小尺寸卷積核,在減少參數(shù)的同時(shí),提高了識(shí)別準(zhǔn)確率。VGGNet的網(wǎng)絡(luò)結(jié)構(gòu)規(guī)整,非常適合硬件加速。
VGGNet有16層網(wǎng)絡(luò)結(jié)構(gòu):

相關(guān)代碼如下(沒信心跑這個(gè)模型):
class VGGNet(Model):
def __init__(self):
super(VGGNet, self).__init__()
self.c1 = Conv2D(filters=64, kernel_size=(3, 3), padding='same')
self.b1 = BatchNormalization()
self.a1 = Activation('relu')
self.c2 = Conv2D(filters=64, kernel_size=(3, 3), padding='same', )
self.b2 = BatchNormalization()
self.a2 = Activation('relu')
self.p1 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
self.d1 = Dropout(0.2)
self.c3 = Conv2D(filters=128, kernel_size=(3, 3), padding='same')
self.b3 = BatchNormalization()
self.a3 = Activation('relu')
self.c4 = Conv2D(filters=128, kernel_size=(3, 3), padding='same')
self.b4 = BatchNormalization()
self.a4 = Activation('relu')
self.p2 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
self.d2 = Dropout(0.2)
self.c5 = Conv2D(filters=256, kernel_size=(3, 3), padding='same')
self.b5 = BatchNormalization()
self.a5 = Activation('relu')
self.c6 = Conv2D(filters=256, kernel_size=(3, 3), padding='same')
self.b6 = BatchNormalization()
self.a6 = Activation('relu')
self.c7 = Conv2D(filters=256, kernel_size=(3, 3), padding='same')
self.b7 = BatchNormalization()
self.a7 = Activation('relu')
self.p3 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
self.d3 = Dropout(0.2)
self.c8 = Conv2D(filters=512, kernel_size=(3, 3), padding='same')
self.b8 = BatchNormalization()
self.a8 = Activation('relu')
self.c9 = Conv2D(filters=512, kernel_size=(3, 3), padding='same')
self.b9 = BatchNormalization()
self.a9 = Activation('relu')
self.c10 = Conv2D(filters=512, kernel_size=(3, 3), padding='same')
self.b10 = BatchNormalization()
self.a10 = Activation('relu')
self.p4 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
self.d4 = Dropout(0.2)
self.c11 = Conv2D(filters=512, kernel_size=(3, 3), padding='same')
self.b11 = BatchNormalization()
self.a11 = Activation('relu')
self.c12 = Conv2D(filters=512, kernel_size=(3, 3), padding='same')
self.b12 = BatchNormalization()
self.a12 = Activation('relu')
self.c13 = Conv2D(filters=512, kernel_size=(3, 3), padding='same')
self.b13 = BatchNormalization()
self.a13 = Activation('relu')
self.p5 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
self.d5 = Dropout(0.2)
self.flatten = Flatten()
self.f1 = Dense(512, activation='relu')
self.d6 = Dropout(0.2)
self.f2 = Dense(512, activation='relu')
self.d7 = Dropout(0.2)
self.f3 = Dense(10, activation='softmax')
def call(self, x):
x = self.c1(x)
x = self.b1(x)
x = self.a1(x)
x = self.c2(x)
x = self.b2(x)
x = self.a2(x)
x = self.p1(x)
x = self.d1(x)
x = self.c3(x)
x = self.b3(x)
x = self.a3(x)
x = self.c4(x)
x = self.b4(x)
x = self.a4(x)
x = self.p2(x)
x = self.d2(x)
x = self.c5(x)
x = self.b5(x)
x = self.a5(x)
x = self.c6(x)
x = self.b6(x)
x = self.a6(x)
x = self.c7(x)
x = self.b7(x)
x = self.a7(x)
x = self.p3(x)
x = self.d3(x)
x = self.c8(x)
x = self.b8(x)
x = self.a8(x)
x = self.c9(x)
x = self.b9(x)
x = self.a9(x)
x = self.c10(x)
x = self.b10(x)
x = self.a10(x)
x = self.p4(x)
x = self.d4(x)
x = self.c11(x)
x = self.b11(x)
x = self.a11(x)
x = self.c12(x)
x = self.b12(x)
x = self.a12(x)
x = self.c13(x)
x = self.b13(x)
x = self.a13(x)
x = self.p5(x)
x = self.d5(x)
x = self.flatten(x)
x = self.f1(x)
x = self.d6(x)
x = self.f2(x)
x = self.d7(x)
y = self.f3(x)
return y
4、InceptionNet
InceptionNet誕生于2014年,是當(dāng)年ImageNet競(jìng)賽的冠軍,top5錯(cuò)誤率為6.67%。InceptionNet引入了Inception結(jié)構(gòu)塊,在同一網(wǎng)絡(luò)內(nèi)使用了不同尺寸的卷積核,提升了模型的感知力;使用了批標(biāo)準(zhǔn)化,緩解了梯度消失。
InceptionNet的核心是它的基本單元Inception結(jié)構(gòu)塊,無論是GoogleNet,也就是Inception v1,還是InceptionNet的后續(xù)版本,比如v2、v3、v4版本,都是基于Inception結(jié)構(gòu)塊搭建的網(wǎng)絡(luò)。Inception結(jié)構(gòu)塊在同一層網(wǎng)絡(luò)中使用了多個(gè)尺寸的卷積核,可以提取不同尺寸的特征。通過11的卷積核,作用到輸入特征圖的每個(gè)像素點(diǎn);通過設(shè)定少于輸入特征圖深度的11卷積核個(gè)數(shù),減少了輸出特征圖深度,起到了降維的作用,減少了參數(shù)量和計(jì)算量。

Inception結(jié)構(gòu)塊包含四個(gè)分支:
- 經(jīng)過1 * 1卷積核輸出到卷積連接器(最左列)
- 經(jīng)過1 * 1卷積核配合3*3卷積核輸出到卷積連接器(第二列)
- 經(jīng)過1 * 1卷積核配合5*5卷積核輸出到卷積連接器(第三列)
- 經(jīng)過3 * 3最大池化核配合1*1卷積核輸出到卷積連接器(第四列)
送到卷積連接器的特征尺寸相同,卷積連接器會(huì)把收到的這四路特征數(shù)據(jù)按深度方向拼接,形成Inception結(jié)構(gòu)塊的輸出。
代碼如下:
# 由于Inception結(jié)構(gòu)塊中的卷積均采用了CBA結(jié)構(gòu),先卷積,再BN,再采用relu激活函數(shù),
# 所以為了代碼復(fù)用,定義一個(gè)新的類ConvBNrelu
class ConvBNRelu(Model):
def __init__(self, ch, kernelsz=3, strides=1, padding="same"):
super(ConvBNRelu, self).__init__()
self.model = tf.keras.models.Sequential([
Conv2D(ch, kernel_size=kernelsz, strides=strides, padding=padding),
BatchNormalization(),
Activation("relu")
])
def call(self, x):
x = self.model(x)
return x
class InceptionBlk(Model):
def __init__(self, ch, strides=1):
super(InceptionBlk, self).__init__()
self.ch = ch
self.strides = strides
# 第一個(gè)分支
self.c1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
# 第二個(gè)分支
self.c2_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
self.c2_2 = ConvBNRelu(ch, kernelsz=3, strides=1)
# 第三個(gè)分支
self.c3_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
self.c3_2 = ConvBNRelu(ch, kernelsz=5, strides=1)
# 第四個(gè)分支
self.p4_1 = MaxPool2D(3, strides=1, padding="same")
self.c4_2 = ConvBNRelu(ch, kernelsz=1, strides=strides)
def call(self, x):
"""分別經(jīng)歷四個(gè)分支的傳播"""
x1 = self.c1(x)
x2_1 = self.c2_1(x)
x2_2 = self.c2_2(x2_1)
x3_1 = self.c3_1(x)
x3_2 = self.c3_2(x3_1)
x4_1 = self.p4_1(x)
x4_2 = self.c4_2(x4_1)
# 將四個(gè)分支的輸出堆疊在一起,并指定堆疊的維度是沿深度方向
x = tf.concat([x1, x2_2, x3_2, x4_2], axis=3)
return x
有了Inception結(jié)構(gòu)塊后,就可以搭建出一個(gè)精簡(jiǎn)版本的InceptionNet了。

代碼如下:
class Inception10(Model):
def __init__(self, num_blocks, num_classes, init_ch=16, **kwargs):
"""
:param num_blocks: block數(shù)量
:param num_classes: n分類
:param init_ch: 默認(rèn)輸出深度16
:param kwargs:
"""
super(Inception10, self).__init__(**kwargs)
self.in_channels = init_ch
self.out_channels = init_ch
self.num_blocks = num_blocks
self.init_ch = init_ch
self.c1 = ConvBNRelu(init_ch) # 其余參數(shù)已默認(rèn)
# 4個(gè)Inception結(jié)構(gòu)塊順序相連,每?jī)蓚€(gè)結(jié)構(gòu)塊組成一個(gè)block。
# 每個(gè)block中第一個(gè)結(jié)構(gòu)塊步長(zhǎng)為2,第二個(gè)步長(zhǎng)為1。這使得
# 第一個(gè)結(jié)構(gòu)塊輸出特征圖尺寸減半。因此把輸出特征圖深度加深,盡可能
# 保證特征抽取中信息的承載量一致
self.blocks = tf.keras.models.Sequential()
for block_id in range(num_blocks):
for layer_id in range(2):
if layer_id == 0:
block = InceptionBlk(self.out_channels, strides=2)
else:
block = InceptionBlk(self.out_channels, strides=1)
self.blocks.add(block)
# block_0通道數(shù)為16,經(jīng)過4個(gè)分支,輸出深度為4*16=64;由于*=2了,所以
# block_1通道數(shù)為32,同樣經(jīng)過4個(gè)分支,輸出深度為4*32=128;
self.out_channels *= 2
self.p1 = tf.keras.layers.GlobalAveragePooling2D() # 將128通道的數(shù)據(jù)送入平均池化
self.f1 = Dense(num_classes, activation="softmax") # 送入10分類的全連接
def call(self, x):
x = self.c1(x)
x = self.blocks(x)
x = self.p1(x)
y = self.f1(x)
return y
model = Inception10(num_blocks=2, num_classes=10)
5、ResNet
ResNet誕生于2015年,是當(dāng)年ImageNet競(jìng)賽的冠軍,Top5錯(cuò)誤率為3.57%。ResNet提出了層間殘差跳連,引入了前方信息,緩解梯度消失,使神經(jīng)網(wǎng)絡(luò)層數(shù)增加成為可能。

通過上圖可見,在探索卷積實(shí)現(xiàn)特征提取的道路上,通過加深網(wǎng)絡(luò)層數(shù),可以取得越來越好的效果。但是單純的堆積網(wǎng)絡(luò)模型的層數(shù)會(huì)使模型退化,以至于后面的特征丟失了前面特征的原本模樣。于是,ResNet的作者用了一根跳連線,將前面的特征直接接到了后面,使得輸出Fx包含了堆疊卷積的非線性輸出F(x)和跳過這兩層堆疊卷積直接連接過來的恒等映射x,讓它們對(duì)應(yīng)元素相加。這一操作,有效緩解了神經(jīng)網(wǎng)絡(luò)模型堆疊導(dǎo)致的退化,使得神經(jīng)網(wǎng)絡(luò)可以向著更深層級(jí)發(fā)展。

ResNet塊中有兩種情況:
- 實(shí)線表示,兩層堆疊卷積沒有改變特征圖的維度
-
虛線表示,兩層堆疊卷積改變了特征圖的維度,需要借助1*1的卷積來調(diào)整x的維度,使W(x)與F(x)的維度一致
res塊2.png
ResNet塊有兩種形式:
- 一種在堆疊前后維度相同
-
另一種在堆疊卷積前后維度不同
ResNetBlock.png
封裝的ResNet塊代碼如下:
class ResnetBlock(Model):
def __init__(self, filters, strides=1, residual_path=False):
super(ResnetBlock, self).__init__()
self.filters = filters
self.strides = strides
self.residual_path = residual_path
self.c1 = Conv2D(filters, (3, 3), strides=strides, padding="same", use_bias=False)
self.b1 = BatchNormalization()
self.a1 = Activation("relu")
self.c2 = Conv2D(filters, (3, 3), strides==1, padding="same"., use_bias=False)
self.b2 = BatchNormalization()
if residual_path: # 堆疊卷積層前后維度不同為True
self.down_c1 = Conv2D(filters, (1, 1), strides=strides, padding="same", use_bias=False)
self.down_b1 = BatchNormalization()
self.a2 = Activation("relu")
def call(self, inputs):
residual = inputs
x = self.c1(inputs)
x = self.b1(x)
x = self.a1(x)
x = self.c2(x)
y = self.b2(x)
if self.residual_path: # 堆疊卷積層前后維度不同為True
residual = self.down_c1(inputs)
residual = self.down_b1(residual)
out = self.a2(y + residual) # 堆疊卷積和跳連卷積相加
return out
根據(jù)ResNet塊,可以搭建一個(gè)ResNet18的神經(jīng)網(wǎng)絡(luò)

代碼如下:
class ResNet18(Model):
def __init__(self, block_list, inital_filters=64):
"""
:param block_list: 每個(gè)block有幾個(gè)卷積層
:param inital_filters:
"""
super(ResNet18, self).__init__()
self.num_blocks = len(block_list) # 共有幾個(gè)block
self.block_list = block_list
self.out_filters = inital_filters
self.c1 = Conv2D(self.out_filters, (3, 3), strides=1, padding="same",
use_bias=False, kernel_initializer="he_normal")
self.b1 = BatchNormalization()
self.a1 = Activation("relu")
self.blocks = tf.keras.models.Sequential()
# 構(gòu)建ResNet網(wǎng)絡(luò)結(jié)構(gòu): 每一個(gè)ResNet塊有兩層卷積,
for block_id in range(len(block_list)):
for layer_id in range(block_list[block_id]):
if block_id != 0 and layer_id == 0: # 對(duì)除第一個(gè)block以外的每個(gè)block的輸入進(jìn)行下采樣
block = ResnetBlock(self.out_filters, strides=2, residual_path=True)
else:
block = ResnetBlock(self.out_filters, residual_path=False)
self.blocks.add(block)
self.out_filters *= 2 # 下一個(gè)block的卷積核數(shù)是上一個(gè)block的2倍
self.p1 = tf.keras.layers.GlobalAveragePooling2D()
self.f1 = Dense(10)
def call(self, inputs):
x = self.c1(inputs)
x = self.b1(x)
x = self.a1(x)
x = self.blocks(x)
x = self.p1(x)
y = self.f1(x)
return y
model = ResNet18([2, 2, 2, 2])

