5.6 深度卷積神經網絡(AlexNet)
在LeNet提出后的將近20年里,神經網絡一度被其他機器學習方法超越,如支持向量機。雖然LeNet可以在早期的小數據集上取得好的成績,但是在更大的真實數據集上的表現并不盡如人意。一方面,神經網絡計算復雜。雖然20世紀90年代也有過一些針對神經網絡的加速硬件,但并沒有像之后GPU那樣大量普及。因此,訓練一個多通道、多層和有大量參數的卷積神經網絡在當年很難完成。另一方面,當年研究者還沒有大量深入研究參數初始化和非凸優化算法等諸多領域,導致復雜的神經網絡的訓練通常較困難。
我們在上一節看到,神經網絡可以直接基于圖像的原始像素進行分類。這種稱為端到端(end-to-end)的方法節省了很多中間步驟。然而,在很長一段時間里更流行的是研究者通過勤勞與智慧所設計并生成的手工特征。這類圖像分類研究的主要流程是:
- 獲取圖像數據集;
- 使用已有的特征提取函數生成圖像的特征;
- 使用機器學習模型對圖像的特征分類。
當時認為的機器學習部分僅限最后這一步。如果那時候跟機器學習研究者交談,他們會認為機器學習既重要又優美。優雅的定理證明了許多分類器的性質。機器學習領域生機勃勃、嚴謹而且極其有用。然而,如果跟計算機視覺研究者交談,則是另外一幅景象。他們會告訴你圖像識別里“不可告人”的現實是:計算機視覺流程中真正重要的是數據和特征。也就是說,使用較干凈的數據集和較有效的特征甚至比機器學習模型的選擇對圖像分類結果的影響更大。
5.6.1 學習特征表示
既然特征如此重要,它該如何表示呢?
我們已經提到,在相當長的時間里,特征都是基于各式各樣手工設計的函數從數據中提取的。事實上,不少研究者通過提出新的特征提取函數不斷改進圖像分類結果。這一度為計算機視覺的發展做出了重要貢獻。
然而,另一些研究者則持異議。他們認為特征本身也應該由學習得來。他們還相信,為了表征足夠復雜的輸入,特征本身應該分級表示。持這一想法的研究者相信,多層神經網絡可能可以學得數據的多級表征,并逐級表示越來越抽象的概念或模式。以圖像分類為例,并回憶5.1節(二維卷積層)中物體邊緣檢測的例子。在多層神經網絡中,圖像的第一級的表示可以是在特定的位置和?度是否出現邊緣;而第二級的表示說不定能夠將這些邊緣組合出有趣的模式,如花紋;在第三級的表示中,也許上一級的花紋能進一步匯合成對應物體特定部位的模式。這樣逐級表示下去,最終,模型能夠較容易根據最后一級的表示完成分類任務。需要強調的是,輸入的逐級表示由多層模型中的參數決定,而這些參數都是學出來的。
盡管一直有一群執著的研究者不斷鉆研,試圖學習視覺數據的逐級表征,然而很長一段時間里這些野心都未能實現。這其中有諸多因素值得我們一一分析。
5.6.1.1 缺失要素一:數據
包含許多特征的深度模型需要大量的有標簽的數據才能表現得比其他經典方法更好。限于早期計算機有限的存儲和90年代有限的研究預算,大部分研究只基于小的公開數據集。例如,不少研究論文基于加州大學歐文分校(UCI)提供的若干個公開數據集,其中許多數據集只有幾百至幾千張圖像。這一狀況在2010年前后興起的大數據浪潮中得到改善。特別是,2009年誕生的ImageNet數據集包含了1,000大類物體,每類有多達數千張不同的圖像。這一規模是當時其他公開數據集無法與之相提并論的。ImageNet數據集同時推動計算機視覺和機器學習研究進入新的階段,使此前的傳統方法不再有優勢。
5.6.1.2 缺失要素二:硬件
深度學習對計算資源要求很高。早期的硬件計算能力有限,這使訓練較復雜的神經網絡變得很困難。然而,通用GPU的到來改變了這一格局。很久以來,GPU都是為圖像處理和計算機游戲設計的,尤其是針對大吞吐量的矩陣和向量乘法從而服務于基本的圖形變換。值得慶幸的是,這其中的數學表達與深度網絡中的卷積層的表達類似。通用GPU這個概念在2001年開始興起,涌現出諸如OpenCL和CUDA之類的編程框架。這使得GPU也在2010年前后開始被機器學習社區使用。
5.6.2 AlexNet
2012年,AlexNet橫空出世。這個模型的名字來源于論文第一作者的姓名Alex Krizhevsky [1]。AlexNet使用了8層卷積神經網絡,并以很大的優勢贏得了ImageNet 2012圖像識別挑戰賽。它首次證明了學習到的特征可以超越手工設計的特征,從而一舉打破計算機視覺研究的前狀。
AlexNet網絡結構
AlexNet與LeNet的設計理念非常相似,但也有顯著的區別。
第一,與相對較小的LeNet相比,AlexNet包含8層變換,其中有5層卷積和2層全連接隱藏層,以及1個全連接輸出層。下面我們來詳細描述這些層的設計。
AlexNet第一層中的卷積窗口形狀是11×11。因為ImageNet中絕大多數圖像的高和寬均比MNIST圖像的高和寬大10倍以上,ImageNet圖像的物體占用更多的像素,所以需要更大的卷積窗口來捕獲物體。第二層中的卷積窗口形狀減小到5×5,之后全采用3×3。此外,第一、第二和第五個卷積層之后都使用了窗口形狀為3×33×3、步幅為2的最大池化層。而且,AlexNet使用的卷積通道數也大于LeNet中的卷積通道數數十倍。
緊接著最后一個卷積層的是兩個輸出個數為4096的全連接層。這兩個巨大的全連接層帶來將近1 GB的模型參數。由于早期顯存的限制,最早的AlexNet使用雙數據流的設計使一個GPU只需要處理一半模型。幸運的是,顯存在過去幾年得到了長足的發展,因此通常我們不再需要這樣的特別設計了。
第二,AlexNet將sigmoid激活函數改成了更加簡單的ReLU激活函數。一方面,ReLU激活函數的計算更簡單,例如它并沒有sigmoid激活函數中的求冪運算。另一方面,ReLU激活函數在不同的參數初始化方法下使模型更容易訓練。這是由于當sigmoid激活函數輸出極接近0或1時,這些區域的梯度幾乎為0,從而造成反向傳播無法繼續更新部分模型參數;而ReLU激活函數在正區間的梯度恒為1。因此,若模型參數初始化不當,sigmoid函數可能在正區間得到幾乎為0的梯度,從而令模型無法得到有效訓練。
第三,AlexNet通過丟棄法(參見3.13節)來控制全連接層的模型復雜度。而LeNet并沒有使用丟棄法。
第四,AlexNet引入了大量的圖像增廣,如翻轉、裁剪和顏色變化,從而進一步擴大數據集來緩解過擬合。我們將在后面的9.1節(圖像增廣)詳細介紹這種方法。
下面我們實現稍微簡化過的AlexNet。
import time
import torch
from torch import nn, optim
import torchvision
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
class AlexNet(nn.Module):
def __init__(self):
super(AlexNet, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding
nn.ReLU(),
nn.MaxPool2d(3, 2), # kernel_size, stride
# 減小卷積窗口,使用填充為2來使得輸入與輸出的高和寬一致,且增大輸出通道數
nn.Conv2d(96, 256, 5, 1, 2),
nn.ReLU(),
nn.MaxPool2d(3, 2),
# 連續3個卷積層,且使用更小的卷積窗口。除了最后的卷積層外,進一步增大了輸出通道數。
# 前兩個卷積層后不使用池化層來減小輸入的高和寬
nn.Conv2d(256, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 256, 3, 1, 1),
nn.ReLU(),
nn.MaxPool2d(3, 2)
)
# 這里全連接層的輸出個數比LeNet中的大數倍。使用丟棄層來緩解過擬合
self.fc = nn.Sequential(
nn.Linear(256*5*5, 4096),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Dropout(0.5),
# 輸出層。由于這里使用Fashion-MNIST,所以用類別數為10,而非論文中的1000
nn.Linear(4096, 10),
)
def forward(self, img):
feature = self.conv(img)
output = self.fc(feature.view(img.shape[0], -1))
return output
打印看看網絡結構。
net = AlexNet()
print(net)
輸出:
AlexNet(
(conv): Sequential(
(0): Conv2d(1, 96, kernel_size=(11, 11), stride=(4, 4))
(1): ReLU()
(2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(3): Conv2d(96, 256, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(4): ReLU()
(5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(6): Conv2d(256, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(7): ReLU()
(8): Conv2d(384, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(9): ReLU()
(10): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU()
(12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(fc): Sequential(
(0): Linear(in_features=6400, out_features=4096, bias=True)
(1): ReLU()
(2): Dropout(p=0.5)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU()
(5): Dropout(p=0.5)
(6): Linear(in_features=4096, out_features=10, bias=True)
)
)
5.6.3 讀取數據
雖然論文中AlexNet使用ImageNet數據集,但因為ImageNet數據集訓練時間較長,我們仍用前面的Fashion-MNIST數據集來演示AlexNet。讀取數據的時候我們額外做了一步將圖像高和寬擴大到AlexNet使用的圖像高和寬224。這個可以通過torchvision.transforms.Resize
實例來實現。也就是說,我們在ToTensor
實例前使用Resize
實例,然后使用Compose
實例來將這兩個變換串聯以方便調用。
# 本函數已保存在d2lzh_pytorch包中方便以后使用
def load_data_fashion_mnist(batch_size, resize=None, root='~/Datasets/FashionMNIST'):
"""Download the fashion mnist dataset and then load into memory."""
trans = []
if resize:
trans.append(torchvision.transforms.Resize(size=resize))
trans.append(torchvision.transforms.ToTensor())
transform = torchvision.transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=True, transform=transform)
mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=True, transform=transform)
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=4)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=4)
return train_iter, test_iter
batch_size = 128
# 如出現“out of memory”的報錯信息,可減小batch_size或resize
train_iter, test_iter = load_data_fashion_mnist(batch_size, resize=224)
5.6.4 訓練
這時候我們可以開始訓練AlexNet了。相對于LeNet,由于圖片尺寸變大了而且模型變大了,所以需要更大的顯存,也需要更長的訓練時間了。
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)
輸出:
training on cuda
epoch 1, loss 0.0047, train acc 0.770, test acc 0.865, time 128.3 sec
epoch 2, loss 0.0025, train acc 0.879, test acc 0.889, time 128.8 sec
epoch 3, loss 0.0022, train acc 0.898, test acc 0.901, time 130.4 sec
epoch 4, loss 0.0019, train acc 0.908, test acc 0.900, time 131.4 sec
epoch 5, loss 0.0018, train acc 0.913, test acc 0.902, time 129.9 sec
小結
- AlexNet跟LeNet結構類似,但使用了更多的卷積層和更大的參數空間來擬合大規模數據集ImageNet。它是淺層神經網絡和深度神經網絡的分界線。
- 雖然看上去AlexNet的實現比LeNet的實現也就多了幾行代碼而已,但這個觀念上的轉變和真正優秀實驗結果的產生令學術界付出了很多年。
參考文獻
[1] Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). Imagenet classification with deep convolutional neural networks. In Advances in neural information processing systems (pp. 1097-1105).
注:除代碼外本節與原書此節基本相同,原書傳送門