CNN卷積神經網絡
本章提供一個對CNN卷積網絡的快速實現
全連接網絡 VS 卷積網絡
全連接神經網絡之所以不太適合圖像識別任務,主要有以下幾個方面的問題:
- 參數數量太多 考慮一個輸入10001000像素的圖片(一百萬像素,現在已經不能算大圖了),輸入層有10001000=100萬節點。假設第一個隱藏層有100個節點(這個數量并不多),那么僅這一層就有(1000*1000+1)*100=1億參數,這實在是太多了!我們看到圖像只擴大一點,參數數量就會多很多,因此它的擴展性很差。
- 沒有利用像素之間的位置信息 對于圖像識別任務來說,每個像素和其周圍像素的聯系是比較緊密的,和離得很遠的像素的聯系可能就很小了。如果一個神經元和上一層所有神經元相連,那么就相當于對于一個像素來說,把圖像的所有像素都等同看待,這不符合前面的假設。當我們完成每個連接權重的學習之后,最終可能會發現,有大量的權重,它們的值都是很小的(也就是這些連接其實無關緊要)。努力學習大量并不重要的權重,這樣的學習必將是非常低效的。
- 網絡層數限制 我們知道網絡層數越多其表達能力越強,但是通過梯度下降方法訓練深度全連接神經網絡很困難,因為全連接神經網絡的梯度很難傳遞超過3層。因此,我們不可能得到一個很深的全連接神經網絡,也就限制了它的能力。
那么,卷積神經網絡又是怎樣解決這個問題的呢?主要有三個思路:
- 局部連接 這個是最容易想到的,每個神經元不再和上一層的所有神經元相連,而只和一小部分神經元相連。這樣就減少了很多參數。
- 權值共享 一組連接可以共享同一個權重,而不是每個連接有一個不同的權重,這樣又減少了很多參數。
- 下采樣 可以使用Pooling來減少每層的樣本數,進一步減少參數數量,同時還可以提升模型的魯棒性。
對于圖像識別任務來說,卷積神經網絡通過盡可能保留重要的參數,去掉大量不重要的參數,來達到更好的學習效果。
卷積結構
卷積層
卷積層可以產生一組平行的特征圖(feature map),它通過在輸入圖像上滑動不同的卷積核并執行一定的運算而組成。此外,在每一個滑動的位置上,卷積核與輸入圖像之間會執行一個元素對應乘積并求和的運算以將感受視野內的信息投影到特征圖中的一個元素。這一滑動的過程可稱為步幅 Z_s,步幅 Z_s 是控制輸出特征圖尺寸的一個因素。卷積核的尺寸要比輸入圖像小得多,且重疊或平行地作用于輸入圖像中,一張特征圖中的所有元素都是通過一個卷積核計算得出的,也即一張特征圖共享了相同的權重和偏置項。
池化層
池化(Pooling)是卷積神經網絡中另一個重要的概念,它實際上是一種非線性形式的降采樣。有多種不同形式的非線性池化函數,而其中“最大池化(Max pooling)”是最為常見的。它是將輸入的圖像劃分為若干個矩形區域,對每個子區域輸出最大值。
一個特征的精確位置遠不及它相對于其他特征的粗略位置重要。池化層會不斷地減小數據的空間大小,因此參數的數量和計算量也會下降,這在一定程度上也控制了過擬合。通常來說,CNN的網絡結構中的卷積層之間都會周期性地插入池化層。池化操作提供了另一種形式的平移不變性。因為卷積核是一種特征發現器,我們通過卷積層可以很容易地發現圖像中的各種邊緣。但是卷積層發現的特征往往過于精確,我們即使高速連拍拍攝一個物體,照片中的物體的邊緣像素位置也不大可能完全一致,通過池化層我們可以降低卷積層對邊緣的敏感性。
全連接層
最后,在經過幾個卷積和最大池化層之后,神經網絡中的高級推理通過完全連接層來完成。就和常規的非卷積人工神經網絡中一樣,完全連接層中的神經元與前一層中的所有激活都有聯系。因此,它們的激活可以作為仿射變換來計算,也就是先乘以一個矩陣然后加上一個偏差(bias)偏移量(向量加上一個固定的或者學習來的偏差量)。
卷積神經網絡(LeNet)
模型實現
LeNet是最早發布的卷積神經網絡之一,因其在計算機視覺任務中的高效性能而受到廣泛關注。
用Pytorch框架實現此類模型非常簡單。我們只需要實例化一個Sequential
塊并將需要的層連接在一起。
import torch
from torch import nn
from d2l import torch as d2lnet = nn.Sequential(nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),nn.AvgPool2d(kernel_size=2, stride=2),nn.Flatten(),nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),nn.Linear(120, 84), nn.Sigmoid(),nn.Linear(84, 10))
將一個大小為28×28的單通道(黑白)圖像通過LeNet。通過在每一層打印輸出的形狀,我們可以檢查模型
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:X = layer(X)print(layer.__class__.__name__,'output shape: \t',X.shape)
Conv2d output shape: torch.Size([1, 6, 28, 28])
Sigmoid output shape: torch.Size([1, 6, 28, 28])
AvgPool2d output shape: torch.Size([1, 6, 14, 14])
Conv2d output shape: torch.Size([1, 16, 10, 10])
Sigmoid output shape: torch.Size([1, 16, 10, 10])
AvgPool2d output shape: torch.Size([1, 16, 5, 5])
Flatten output shape: torch.Size([1, 400])
Linear output shape: torch.Size([1, 120])
Sigmoid output shape: torch.Size([1, 120])
Linear output shape: torch.Size([1, 84])
Sigmoid output shape: torch.Size([1, 84])
Linear output shape: torch.Size([1, 10])
模型訓練
現在我們已經實現了LeNet,讓我們看看LeNet在Fashion-MNIST數據集上的表現。
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
由于完整的數據集位于內存中,因此在模型使用GPU計算數據集之前,我們需要將其復制到顯存中。
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save"""使用GPU計算模型在數據集上的精度"""if isinstance(net, nn.Module):net.eval() # 設置為評估模式if not device:device = next(iter(net.parameters())).device# 正確預測的數量,總預測的數量metric = d2l.Accumulator(2)with torch.no_grad():for X, y in data_iter:if isinstance(X, list):# BERT微調所需的(之后將介紹)X = [x.to(device) for x in X]else:X = X.to(device)y = y.to(device)metric.add(d2l.accuracy(net(X), y), y.numel())return metric[0] / metric[1]
與全連接層一樣,我們使用交叉熵損失函數和小批量隨機梯度下降。
#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):"""用GPU訓練模型"""def init_weights(m):if type(m) == nn.Linear or type(m) == nn.Conv2d:nn.init.xavier_uniform_(m.weight)net.apply(init_weights)print('training on', device)net.to(device)optimizer = torch.optim.SGD(net.parameters(), lr=lr)loss = nn.CrossEntropyLoss()animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],legend=['train loss', 'train acc', 'test acc'])timer, num_batches = d2l.Timer(), len(train_iter)for epoch in range(num_epochs):# 訓練損失之和,訓練準確率之和,樣本數metric = d2l.Accumulator(3)net.train()for i, (X, y) in enumerate(train_iter):timer.start()optimizer.zero_grad()X, y = X.to(device), y.to(device)y_hat = net(X)l = loss(y_hat, y)l.backward()optimizer.step()with torch.no_grad():metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])timer.stop()train_l = metric[0] / metric[2]train_acc = metric[1] / metric[2]if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:animator.add(epoch + (i + 1) / num_batches,(train_l, train_acc, None))test_acc = evaluate_accuracy_gpu(net, test_iter)animator.add(epoch + 1, (None, None, test_acc))print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, 'f'test acc {test_acc:.3f}')print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec 'f'on {str(device)}')
訓練和評估LeNet-5模型。
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.469, train acc 0.823, test acc 0.779
55296.6 examples/sec on cuda:0