卷積神經(jīng)網(wǎng)絡(luò)

本文主要介紹一些卷積層和池化層,并解釋填充、步幅、輸入通道和輸出通道的含義。
以及LeNet的一些應用

二維互相關(guān)運算

二維互相關(guān)(cross-correlation)運算的輸入是一個二維輸入數(shù)組和一個二維核(kernel)數(shù)組,輸出也是一個二維數(shù)組,其中核數(shù)組通常稱為卷積核或過濾器(filter)。卷積核的尺寸通常小于輸入數(shù)組,卷積核在輸入數(shù)組上滑動,在每個位置上,卷積核與該位置處的輸入子數(shù)組按元素相乘并求和,得到輸出數(shù)組中相應位置的元素。圖1展示了一個互相關(guān)運算的例子,陰影部分分別是輸入的第一個計算區(qū)域、核數(shù)組以及對應的輸出。

圖1 二維互相關(guān)運算
import torch 
import torch.nn as nn

def corr2d(X, K):
    H, W = X.shape
    h, w = K.shape
    Y = torch.zeros(H - h + 1, W - w + 1)
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            Y[i, j] = (X[i: i + h, j: j + w] * K).sum()
    return Y

二維卷積層

二維卷積層將輸入和卷積核做互相關(guān)運算,并加上一個標量偏置來得到輸出。卷積層的模型參數(shù)包括卷積核和標量偏置。

class Conv2D(nn.Module):
    def __init__(self, kernel_size):
        super(Conv2D, self).__init__()
        self.weight = nn.Parameter(torch.randn(kernel_size))
        self.bias = nn.Parameter(torch.randn(1))
    def forward(self, x):
        return corr2d(x, self.weight) + self.bias

舉個例子說明一下

X = torch.ones(6, 8)
Y = torch.zeros(6, 7)
X[:, 2: 6] = 0
Y[:, 1] = 1
Y[:, 5] = -1
print(X)
print(Y)
conv2d = Conv2D(kernel_size=(1, 2))
step = 30
lr = 0.01
for i in range(step):
    Y_hat = conv2d(X)
    l = ((Y_hat - Y) ** 2).sum()
    l.backward()
    # 梯度下降
    conv2d.weight.data -= lr * conv2d.weight.grad
    conv2d.bias.data -= lr * conv2d.bias.grad
    
    # 梯度清零
    conv2d.weight.grad.zero_()
    conv2d.bias.grad.zero_()
    if (i + 1) % 5 == 0:
        print('Step %d, loss %.3f' % (i + 1, l.item()))
        
print(conv2d.weight.data)
print(conv2d.bias.data)

tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.]])
tensor([[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.]])
Step 5, loss 26.487
Step 10, loss 5.822
Step 15, loss 1.448
Step 20, loss 0.384
Step 25, loss 0.105
Step 30, loss 0.029
tensor([[ 0.9587, -0.9551]])
tensor([-0.0020])

互相關(guān)運算與卷積運算

卷積層得名于卷積運算,但卷積層中用到的并非卷積運算而是互相關(guān)運算。我們將核數(shù)組上下翻轉(zhuǎn)、左右翻轉(zhuǎn),再與輸入數(shù)組做互相關(guān)運算,這一過程就是卷積運算。由于卷積層的核數(shù)組是可學習的,所以使用互相關(guān)運算與使用卷積運算并無本質(zhì)區(qū)別。

特征圖與感受野

二維卷積層輸出的二維數(shù)組可以看作是輸入在空間維度(寬和高)上某一級的表征,也叫特征圖(feature map)。影響元素x的前向計算的所有可能輸入?yún)^(qū)域(可能大于輸入的實際尺寸)叫做x的感受野(receptive field)。

以圖1為例,輸入中陰影部分的四個元素是輸出中陰影部分元素的感受野。我們將圖中形狀為2 \times 2的輸出記為Y,將Y與另一個形狀為2 \times 2的核數(shù)組做互相關(guān)運算,輸出單個元素z。那么,zY上的感受野包括Y的全部四個元素,在輸入上的感受野包括其中全部9個元素。可見,我們可以通過更深的卷積神經(jīng)網(wǎng)絡(luò)使特征圖中單個元素的感受野變得更加廣闊,從而捕捉輸入上更大尺寸的特征。

填充和步幅

我們介紹卷積層的兩個超參數(shù),即填充和步幅,它們可以對給定形狀的輸入和卷積核改變輸出形狀。

填充

填充(padding)是指在輸入高和寬的兩側(cè)填充元素(通常是0元素),圖2里我們在原輸入高和寬的兩側(cè)分別添加了值為0的元素。

圖2 在輸入的高和寬兩側(cè)分別填充了0元素的二維互相關(guān)計算

如果原輸入的高和寬是n_hn_w,卷積核的高和寬是k_hk_w,在高的兩側(cè)一共填充p_h行,在寬的兩側(cè)一共填充p_w列,則輸出形狀為:

(n_h+p_h-k_h+1)\times(n_w+p_w-k_w+1)

我們在卷積神經(jīng)網(wǎng)絡(luò)中使用奇數(shù)高寬的核,比如3 \times 35 \times 5的卷積核,對于高度(或?qū)挾龋榇笮?img class="math-inline" src="https://math.jianshu.com/math?formula=2%20k%20%2B%201" alt="2 k + 1" mathimg="1">的核,令步幅為1,在高(或?qū)挘﹥蓚?cè)選擇大小為k的填充,便可保持輸入與輸出尺寸相同。

步幅

在互相關(guān)運算中,卷積核在輸入數(shù)組上滑動,每次滑動的行數(shù)與列數(shù)即是步幅(stride)。此前我們使用的步幅都是1,圖3展示了在高上步幅為3、在寬上步幅為2的二維互相關(guān)運算。

圖3 高和寬上步幅分別為3和2的二維互相關(guān)運算

一般來說,當高上步幅為s_h,寬上步幅為s_w時,輸出形狀為:

\lfloor(n_h+p_h-k_h+s_h)/s_h\rfloor \times \lfloor(n_w+p_w-k_w+s_w)/s_w\rfloor

如果p_h=k_h-1,p_w=k_w-1,那么輸出形狀將簡化為\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor。更進一步,如果輸入的高和寬能分別被高和寬上的步幅整除,那么輸出形狀將是(n_h / s_h) \times (n_w/s_w)

p_h = p_w = p時,我們稱填充為p;當s_h = s_w = s時,我們稱步幅為s

多輸入通道和多輸出通道

之前的輸入和輸出都是二維數(shù)組,但真實數(shù)據(jù)的維度經(jīng)常更高。例如,彩色圖像在高和寬2個維度外還有RGB(紅、綠、藍)3個顏色通道。假設(shè)彩色圖像的高和寬分別是hw(像素),那么它可以表示為一個3 \times h \times w的多維數(shù)組,我們將大小為3的這一維稱為通道(channel)維。

多輸入通道

卷積層的輸入可以包含多個通道,圖4展示了一個含2個輸入通道的二維互相關(guān)計算的例子。

假設(shè)輸入數(shù)據(jù)的通道數(shù)為c_i,卷積核形狀為k_h\times k_w,我們?yōu)槊總€輸入通道各分配一個形狀為k_h\times k_w的核數(shù)組,將c_i個互相關(guān)運算的二維輸出按通道相加,得到一個二維數(shù)組作為輸出。我們把c_i個核數(shù)組在通道維上連結(jié),即得到一個形狀為c_i\times k_h\times k_w的卷積核。

多輸出通道

卷積層的輸出也可以包含多個通道,設(shè)卷積核輸入通道數(shù)和輸出通道數(shù)分別為c_ic_o,高和寬分別為k_hk_w。如果希望得到含多個通道的輸出,我們可以為每個輸出通道分別創(chuàng)建形狀為c_i\times k_h\times k_w的核數(shù)組,將它們在輸出通道維上連結(jié),卷積核的形狀即c_o\times c_i\times k_h\times k_w
對于輸出通道的卷積核,我們提供這樣一種理解,一個c_i \times k_h \times k_w的核數(shù)組可以提取某種局部特征,但是輸入可能具有相當豐富的特征,我們需要有多個這樣的c_i \times k_h \times k_w的核數(shù)組,不同的核數(shù)組提取的是不同的特征。

卷積層與全連接層的對比

二維卷積層經(jīng)常用于處理圖像,與此前的全連接層相比,它主要有兩個優(yōu)勢:

  • 一是全連接層把圖像展平成一個向量,在輸入圖像上相鄰的元素可能因為展平操作不再相鄰,網(wǎng)絡(luò)難以捕捉局部信息。而卷積層的設(shè)計,天然地具有提取局部信息的能力。
  • 二是卷積層的參數(shù)量更少。不考慮偏置的情況下,一個形狀為(c_i, c_o, h, w)的卷積核的參數(shù)量是c_i \times c_o \times h \times w,與輸入圖像的寬高無關(guān)。假如一個卷積層的輸入和輸出形狀分別是(c_1, h_1, w_1)(c_2, h_2, w_2),如果要用全連接層進行連接,參數(shù)數(shù)量就是c_1 \times c_2 \times h_1 \times w_1 \times h_2 \times w_2。使用卷積層可以以較少的參數(shù)數(shù)量來處理更大的圖像。

卷積層的簡潔實現(xiàn)

使用Pytorch中的nn.Conv2d類來實現(xiàn)二維卷積層,主要關(guān)注以下幾個構(gòu)造函數(shù)參數(shù):

  • in_channels (python:int) – Number of channels in the input imag
  • out_channels (python:int) – Number of channels produced by the convolution
  • kernel_size (python:int or tuple) – Size of the convolving kernel
  • stride (python:int or tuple, optional) – Stride of the convolution. Default: 1
  • padding (python:int or tuple, optional) – Zero-padding added to both sides of the input. Default: 0
  • bias (bool, optional) – If True, adds a learnable bias to the output. Default: True

forward函數(shù)的參數(shù)為一個四維張量,形狀為(N, C_{in}, H_{in}, W_{in}),返回值也是一個四維張量,形狀為(N, C_{out}, H_{out}, W_{out}),其中N是批量大小,C, H, W分別表示通道數(shù)、高度、寬度。

conv2d = nn.Conv2d(in_channels=2, out_channels=3, kernel_size=(3, 5), stride=1, padding=(1, 2))
Y = conv2d(X)

池化

二維池化層

\color{red}{池化層有參與模型的正向計算,同樣也會參與反向傳播}
\color{red}{池化層直接對窗口內(nèi)的元素求最大值或平均值,并沒有模型參數(shù)參與計算}
池化層主要用于緩解卷積層對位置的過度敏感性。同卷積層一樣,池化層每次對輸入數(shù)據(jù)的一個固定形狀窗口(又稱池化窗口)中的元素計算輸出,池化層直接計算池化窗口內(nèi)元素的最大值或者平均值,該運算也分別叫做最大池化或平均池化。圖6展示了池化窗口形狀為2\times 2的最大池化。

二維平均池化的工作原理與二維最大池化類似,但將最大運算符替換成平均運算符。池化窗口形狀為p \times q的池化層稱為p \times q池化層,其中的池化運算叫作p \times q池化。

池化層也可以在輸入的高和寬兩側(cè)填充并調(diào)整窗口的移動步幅來改變輸出形狀。池化層填充和步幅與卷積層填充和步幅的工作機制一樣。

在處理多通道輸入數(shù)據(jù)時,池化層對每個輸入通道分別池化,但不會像卷積層那樣將各通道的結(jié)果按通道相加。這意味著池化層的輸出通道數(shù)與輸入通道數(shù)相等。

池化層的簡潔實現(xiàn)

我們使用Pytorch中的nn.MaxPool2d實現(xiàn)最大池化層,關(guān)注以下構(gòu)造函數(shù)參數(shù):

  • kernel_size – the size of the window to take a max over
  • stride – the stride of the window. Default value is kernel_size
  • padding – implicit zero padding to be added on both sides

forward函數(shù)的參數(shù)為一個四維張量,形狀為(N, C, H_{in}, W_{in}),返回值也是一個四維張量,形狀為(N, C, H_{out}, W_{out}),其中N是批量大小,C, H, W分別表示通道數(shù)、高度、寬度。

pool2d = nn.MaxPool2d(kernel_size=3, padding=1, stride=(2, 1))
Y = pool2d(X)

\color{Aqua}{卷積神經(jīng)網(wǎng)絡(luò)就是含卷積層的網(wǎng)絡(luò)。}

LeNet交替使用卷積層和最大池化層后接全連接層來進行圖像分類。

LeNet

使用全連接層的局限性:

  • 圖像在同一列鄰近的像素在這個向量中可能相距較遠。它們構(gòu)成的模式可能難以被模型識別。
  • 對于大尺寸的輸入圖像,使用全連接層容易導致模型過大。

使用卷積層的優(yōu)勢:

  • 卷積層保留輸入形狀。
  • 卷積層通過滑動窗口將同一卷積核與不同位置的輸入重復計算,從而避免參數(shù)尺寸過大。

LeNet 模型

LeNet分為卷積層塊和全連接層塊兩個部分。下面我們分別介紹這兩個模塊。

Image Name

卷積層塊里的基本單位是卷積層后接平均池化層:卷積層用來識別圖像里的空間模式,如線條和物體局部,之后的平均池化層則用來降低卷積層對位置的敏感性。
卷積層塊由兩個這樣的基本單位重復堆疊構(gòu)成。在卷積層塊中,每個卷積層都使用5 \times 5的窗口,并在輸出上使用sigmoid激活函數(shù)。第一個卷積層輸出通道數(shù)為6,第二個卷積層輸出通道數(shù)則增加到16。
全連接層塊含3個全連接層。它們的輸出個數(shù)分別是120、84和10,其中10為輸出的類別個數(shù)。

下面我們通過Sequential類來實現(xiàn)LeNet模型。

#net
class Flatten(torch.nn.Module):  #展平操作
    def forward(self, x):
        return x.view(x.shape[0], -1)

class Reshape(torch.nn.Module): #將圖像大小重定型
    def forward(self, x):
        return x.view(-1,1,28,28)      #(B x C x H x W)
    
net = torch.nn.Sequential(     #Lelet                                                  
    Reshape(),
    nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2), #b*1*28*28  =>b*6*28*28
    nn.Sigmoid(),                                                       
    nn.AvgPool2d(kernel_size=2, stride=2),                              #b*6*28*28  =>b*6*14*14
    nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5),           #b*6*14*14  =>b*16*10*10
    nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),                              #b*16*10*10  => b*16*5*5
    Flatten(),                                                          #b*16*5*5   => b*400
    nn.Linear(in_features=16*5*5, out_features=120),
    nn.Sigmoid(),
    nn.Linear(120, 84),
    nn.Sigmoid(),
    nn.Linear(84, 10)
)

可以看到,在卷積層塊中輸入的高和寬在逐層減小。卷積層由于使用高和寬均為5的卷積核,從而將高和寬分別減小4,而池化層則將高和寬減半,但通道數(shù)則從1增加到16。全連接層則逐層減少輸出個數(shù),直到變成圖像的類別數(shù)10。

Image Name

接下來使用網(wǎng)絡(luò)訓練模型

# 數(shù)據(jù)
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(
    batch_size=batch_size, root='/home/kesci/input/FashionMNIST2065')
print(len(train_iter))

為了使讀者更加形象的看到數(shù)據(jù),添加額外的部分來展示數(shù)據(jù)的圖像
#數(shù)據(jù)展示
import matplotlib.pyplot as plt
def show_fashion_mnist(images, labels):
    d2l.use_svg_display()
    # 這里的_表示我們忽略(不使用)的變量
    _, figs = plt.subplots(1, len(images), figsize=(12, 12))
    for f, img, lbl in zip(figs, images, labels):
        f.imshow(img.view((28, 28)).numpy())
        f.set_title(lbl)
        f.axes.get_xaxis().set_visible(False)
        f.axes.get_yaxis().set_visible(False)
    plt.show()

for Xdata,ylabel in train_iter:
    break
X, y = [], []
for i in range(10):
    print(Xdata[i].shape,ylabel[i].numpy())
    X.append(Xdata[i]) # 將第i個feature加到X中
    y.append(ylabel[i].numpy()) # 將第i個label加到y(tǒng)中
show_fashion_mnist(X, y)
因為卷積神經(jīng)網(wǎng)絡(luò)計算比多層感知機要復雜,建議使用GPU來加速計算。我們查看看是否可以用GPU,如果成功則使用`cuda:0`,否則仍然使用`cpu`。
# This function has been saved in the d2l package for future use
#use GPU
def try_gpu():
    """If GPU is available, return torch.device as cuda:0; else return torch.device as cpu."""
    if torch.cuda.is_available():
        device = torch.device('cuda:0')
    else:
        device = torch.device('cpu')
    return device

device = try_gpu()
device
我們實現(xiàn)`evaluate_accuracy`函數(shù),該函數(shù)用于計算模型`net`在數(shù)據(jù)集`data_iter`上的準確率。

#計算準確率
'''
(1). net.train()
  啟用 BatchNormalization 和 Dropout,將BatchNormalization和Dropout置為True
(2). net.eval()
不啟用 BatchNormalization 和 Dropout,將BatchNormalization和Dropout置為False
'''

def evaluate_accuracy(data_iter, net,device=torch.device('cpu')):
    """Evaluate accuracy of a model on the given data set."""
    acc_sum,n = torch.tensor([0],dtype=torch.float32,device=device),0
    for X,y in data_iter:
        # If device is the GPU, copy the data to the GPU.
        X,y = X.to(device),y.to(device)
        net.eval()
        with torch.no_grad():
            y = y.long()
            acc_sum += torch.sum((torch.argmax(net(X), dim=1) == y))  #[[0.2 ,0.4 ,0.5 ,0.6 ,0.8] ,[ 0.1,0.2 ,0.4 ,0.3 ,0.1]] => [ 4 , 2 ]
            n += y.shape[0]
    return acc_sum.item()/n
我們定義函數(shù)`train_ch5`,用于訓練模型。
#訓練函數(shù)
def train_ch5(net, train_iter, test_iter,criterion, num_epochs, batch_size, device,lr=None):
    """Train and evaluate a model with CPU or GPU."""
    print('training on', device)
    net.to(device)
    optimizer = optim.SGD(net.parameters(), lr=lr)
    for epoch in range(num_epochs):
        train_l_sum = torch.tensor([0.0],dtype=torch.float32,device=device)
        train_acc_sum = torch.tensor([0.0],dtype=torch.float32,device=device)
        n, start = 0, time.time()
        for X, y in train_iter:
            net.train()
            
            optimizer.zero_grad()
            X,y = X.to(device),y.to(device) 
            y_hat = net(X)
            loss = criterion(y_hat, y)
            loss.backward()
            optimizer.step()
            
            with torch.no_grad():
                y = y.long()
                train_l_sum += loss.float()
                train_acc_sum += (torch.sum((torch.argmax(y_hat, dim=1) == y))).float()
                n += y.shape[0]
        test_acc = evaluate_accuracy(test_iter, net,device)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, '
              'time %.1f sec'
              % (epoch + 1, train_l_sum/n, train_acc_sum/n, test_acc,
                 time.time() - start))
# 訓練
lr, num_epochs = 0.9, 10

def init_weights(m):
    if type(m) == nn.Linear or type(m) == nn.Conv2d:
        torch.nn.init.xavier_uniform_(m.weight)

net.apply(init_weights)
net = net.to(device)

criterion = nn.CrossEntropyLoss()   #交叉熵描述了兩個概率分布之間的距離,交叉熵越小說明兩者之間越接近
train_ch5(net, train_iter, test_iter, criterion,num_epochs, batch_size,device, lr)
# test
for testdata,testlabe in test_iter:
    testdata,testlabe = testdata.to(device),testlabe.to(device)
    break
print(testdata.shape,testlabe.shape)
net.eval()
y_pre = net(testdata)
print(torch.argmax(y_pre,dim=1)[:10])
print(testlabe[:10])
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容