搭建深度學習框架(二) 卷積層, 池化層

爲什麼使用卷積

我們前一課 搭建深度學習框架(一) 線性層,激活函數,損失函數.學習瞭如何使用全連接網實現最簡單的圖像識別和分類, 儘管MNIST是非常容易訓練的數據集, 但親自開發一個可用的AI還是很激動人心的. 但是使用全連接網絡雖然能解決很多傳統機器學習方法面向的問題, 比如簡單的模式分類和函數擬合. 但是設想一下, 我們實際要面對的可不是MNIST這種簡單數據集, 而是特徵更豐富, 分辨率更高的真實圖像, 我們在網絡上隨便找一找就能找到大把的1000x800量級的圖片. 如果我們把這種圖片用來訓練神經網絡, 那麼我們僅在第一層需要的參數就會達到1000x800xhidden size那麼多, 這幾乎是不可能接受的. 儘管我們也可以用預處理的方式降低分辨率, 但是這不免會丟失很多本應有的特徵. 爲此我們提出了二維卷積的方法幫助我們在圖像中提取特徵, 同時用池化方法實現圖像壓縮和數據降維.
在這裏插入圖片描述
在這裏插入圖片描述
上圖是卷積運算的演算法, 以及某種卷積核作用於圖像後的結果. 卷積運算可以提取圖像的高級特徵, 比如上面的卷積就能實現邊緣檢測的工作. 如果我們用級聯的卷積核形成卷積層, 它還能得到某些更高級的特徵. 比如我們可能在上面的這種特徵圖上再做卷積, 得到橫線的邊緣和豎線的邊緣. 再卷積可能就能識別直角邊緣的特徵.
另外, 卷積並不是像上面那樣只能操作一維圖像, 通常我們的輸入圖像是RGB的三通道圖像. 那麼我們的卷積實際上是一個同爲3維的卷積核按上圖的方式滑動, 然後把這三個通道卷積運算得到的1維特徵圖相加. 如果我們用N個這樣的3維卷積核把圖片做N次卷積, 就能得到N通道的特徵圖. 那麼下次卷積, 每個卷積核的維度就必須是N維, 如此類推.
卷積運算conv2d接收C維的特徵圖, 尺度爲NxCxHxW. 卷積核的數量爲FN, 通道數爲C, 尺寸filter size自主給定, 步長stride一般爲1. 輸出特徵圖爲NFN(Hsize)+1stride(Wsize)+1strideN*FN*\frac{(H-size)+1}{stride}*\frac{(W-size)+1}{stride}.
在這裏插入圖片描述
上圖就是卷積神經網絡的基本架構, 我們用這種方式實現特徵的快速提取, 並用這些特徵來進行模式分類, 分類任務就交給全連接層進行.

Im2col

雖然我們上面的圖是把卷積核一步一步移動, 一步一步點積並求和, 但是實際實現時我們並不會用for循環來做這件事情, 我們通常的做法是選擇最能並行化的方式, 把卷積運算轉爲矩陣運算. 因爲今天的硬件運算性能的提高已經不是以電子器件的尺度和頻率取勝了, 而是以更高程度的並行化加速運算. 把卷積轉爲矩陣乘法的方式如下圖.
在這裏插入圖片描述
在這裏插入圖片描述
COL=im2col(IM)COL = im2col(IM)
OM=COLFCOL+bOM = COL\cdot FCOL+b
當然執行完矩陣運算後, 這還不是結束.我們conv2d輸出的是特徵圖, 所以我們還要有一個把矩陣重塑成NxCxHxW的特徵圖的reshape和permute操作.

conv2d的反向傳播

既然卷積可以寫成矩陣乘法, 那麼矩陣乘法的求導一樣適用於卷積運算. 我們把卷積核展開成列向量, 與圖像的col矩陣做乘法, 每個列向量的元素和卷積核中的元素有一一對應的關係, 那麼我們保存卷積核時也不必用2d的形式保存, 而可以用列向量來保存. 這樣只需要用矩陣乘法的求導方法計算FM和b的導數即可 LFCOL=COLTLOM\frac{\partial L}{\partial FCOL} = COL^T \cdot \frac{\partial L}{\partial OM}
Lb=SUMROWLOM\frac{\partial L}{\partial b} = SUMROW\frac{\partial L}{\partial OM}
比較麻煩的是計算損失函數關於輸入圖像的導數, 也就是反向傳播. 我們前向傳播使用im2col的方法得到一張把原圖像im中的元素反覆使用並填充到對應位置的矩陣COL, 我們計算完矩陣COL的導數後還需要一個col2im的逆變換把它變成圖像導數. 這個col2im其實就是im2col的逆操作, 原來是怎樣拆分矩陣的, 現在就怎樣把它拼裝回去. 這個過程中會有重疊的存在, 因此處理時要格外小心.
LCOL=LOMFCOLT \frac{\partial L}{\partial COL} = \frac{\partial L}{\partial OM} \cdot FCOL^T
LW=col2im(LCOL) \frac{\partial L}{\partial W} = col2im(\frac{\partial L}{\partial COL})

技術細節

上面對conv2d的im2col實現方法做了簡述, 但是實際做起來還是蠻麻煩蠻繞的. 首先把接口的輸入輸出寫好.
conv2d: input, 要操作的4維圖像張量a, 輸入通道數in_channels, 輸出通道數out_channels, 卷積核尺寸kernel_size, 步長stride和填充尺度padding.
輸入張量a的size爲(N,C,H,W), 其中C = in_channels, 我們會對它做im2col操作, 得到一個(N*H_o*W_o, C*kernel_size*kernel_size)的二維矩陣COL. 其中H_o和W_o表示經過卷積運算後得到的特徵圖的高和寬. 在不考慮填充的情況下, 它的尺寸可以由公式計算得出.
Ho=Hkernel_sizestride+1 H_o = \frac{H - kernel\_size}{stride} + 1
Wo=Hkernel_sizestride+1 W_o = \frac{H - kernel\_size}{stride} + 1
每個卷積核都是C維, 按照上面所說可以寫成一個長(C*kernel_size*kernel_size)的列向量, 因此卷積層(多個卷積核)就可以表示爲(C*kernel_size*kernel_size,out_channels)的二維矩陣W_k. 我們把這兩個矩陣相乘就能得到輸出的矩陣Out, size = (N*H_o*W_o, out_channels), 最後我們還要把它通過reshape和permute變成(N,out_channels,H_o,W_o)的四維特徵圖, 然後做下一次卷積運算.
按照上面的步驟就能實現卷積前向傳播, 然後我們實現上面提到的im2col. 我們先用一一對應的關係把它寫成for循環的形式, 再用一些pytorch操作把它最大可能並行化.
輸入: a,size=(N,C,H,W). 輸出: COL,size=(N*H_o*W_o, C*kernel_size*kernel_size)
注意到COL中的六個屬性是獨立的, 我們不妨先把它寫成六維矩陣, 最後再用reshape和permute操作把它重塑成二維矩陣. 即現在我們尋找一個a到6維矩陣COL6[N][C][Ho][Wo][kernelsize][kernelsize]COL6[N][C][H_o][W_o][kernel_size][kernel_size]的映射. 我們用for循環, 遍歷每個圖片(N), 每個通道©, 在每張圖中遍歷每個卷積核可能處於的位置(H,W)和卷積核內對應的每個位置.

for n in range(n):  
	for c in range(C):  
		for h in range(0,H-kernel_size+1,stride):  
			for w in range(0,W-kernel_size+1,stride):  
				for i in range(0,kernel_size):  
					for j in range(0,kernel_size):  
						COL6[n][c][h][w][i][j] = a[n][c][h+i][w+j]

上面的循環是完全可以並行化的, 我們可以把最小的循環(kernel_size一般遠小於其他4個變量)拿到最上面, 把下面其餘的循環都用矩陣賦值的形式並行.

for i in range(0,kernel_size):  
	for j in range(0,kernel_size):  
		COL6[:][:][:][:][i][j] = a[:][:][i+range(0,H-kernel_size+1,stride)][range(0,W-kernel_size+1,stride)]  

因爲使用pytorch的大型矩陣賦值一般有着很好的可並行化性, 而我們使用的for循環次數又並不多(比如3x3的卷積核只使用了9次循環), 這個運算效率是可以接受的. 賦值完畢後我們就可以把COL6重塑成我們需要的COL, 我們把COL6的C維放到倒數第三維, 然後用reshape(N*H_o*W_o, C*kernel_size*kernel_size)就能計算出目標輸出.在上面的步驟中, 如果要最大化空間局部性, 就把i, j兩維預設成最高的兩個維度.
至於col2im, 使用的是相似的方法. 我們把上面的操作反過來, 先把COL變成COL6, 然後用類似的循環把COL6中的元素不斷加到IM中即可.

CONV2D的IM2COL實現

import torch
import math
import numpy as np
import matplotlib.pyplot as plt
import torch.nn.functional as F

def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    # Padding
    img = F.pad(input_data,(0,0,0,0,pad,pad,pad,pad),"constant")
    N, C, H, W = input_data.shape
    out_h = (H - filter_h)//stride + 1  # 輸出數據的高
    out_w = (W - filter_w)//stride + 1  # 輸出數據的長
    col = torch.zeros((filter_h, filter_w, N, C, out_h, out_w))
    
    for y in range(filter_h):
        y_indices = torch.arange(0,H-filter_h+1,stride)+y
        for x in range(filter_w):
            x_indices = torch.arange(0,W-filter_w+1,stride)+x
            col[y, x] = img[:, :, y_indices][:,:,:,x_indices]
    # 交換col的列,然後改變形狀
    col = col.permute(2, 4, 5, 3, 0, 1).reshape(N*out_h*out_w, -1)
    return col

def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
    N, C, H, W = input_shape
    out_h = (H + 2*pad - filter_h)//stride + 1
    out_w = (W + 2*pad - filter_w)//stride + 1
    col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).permute(4, 5, 0, 3, 1, 2)

    img = torch.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
    for y in range(filter_h):
        y_max = y + stride*out_h
        # y_indices = torch.arange(0,H-filter_h+1,stride)+y
        for x in range(filter_w):
            x_max = x + stride*out_w
            # x_indices = torch.arange(0,W-filter_w+1,stride)+x
            img[:, :, y:y_max:stride, x:x_max:stride] += col[y, x]

    return img[:, :, pad:H + pad, pad:W + pad]


class Convolution:
    # 初始化權重(卷積核4維)、偏置、步幅、填充
    def __init__(self, in_channels, out_channels, kernel_size = 3,
                 stride=1, padding=0, LEARNING_RATE = 0.01, momentum = 0.9):
        self.W = None
        self.b = None
        self.in_channels = in_channels
        self.out_channels = out_channels
        self.kernel_size = kernel_size
        self.stride = stride
        self.pad = padding
        
        # 中間數據(backward時使用)
        self.x = None   
        self.col = None
        self.col_W = None
        
        # 權重和偏置參數的梯度
        self.dW = None
        self.db = None
        
        self.lr = LEARNING_RATE
        self.momentum = momentum

    def init_params_if_needed(self):
        # 參數初始化
        if type(self.W) == type(None):
            n = self.in_channels*self.kernel_size*self.kernel_size
            stdv = math.sqrt(2/(n*(1+0.2**2)))
            self.W = stdv*torch.randn(self.out_channels,self.in_channels,
                                     self.kernel_size,self.kernel_size)
        if type(self.b) == type(None):
            self.b = torch.zeros(self.out_channels)
        if type(self.dW) == type(None):
            self.dW = torch.zeros_like(self.W)
        if type(self.db) == type(None):
            self.db = torch.zeros_like(self.b)
        
        
    def forward(self, x):
        # 數據大小
        N, C, H, W = x.shape
        # 初始化參數
        self.init_params_if_needed()
        # 卷積核大小
        FN, C, FH, FW = self.W.shape
        # 計算輸出數據大小
        out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
        out_w = 1 + int((W + 2*self.pad - FW) / self.stride)
        # 利用im2col轉換爲行
        col = im2col(x, FH, FW, self.stride, self.pad)
        # 卷積核轉換爲列,展開爲2維數組
        col_W = self.W.reshape(FN, -1).T
        # 計算正向傳播
        out = torch.mm(col, col_W) + self.b 
        # size = (N*out_h*out_w,FN)
        out = out.reshape(N, out_h, out_w, -1).permute(0, 3, 1, 2) 
        # size = (N,FN,out_h,out_w)

        self.x = x
        self.col = col
        self.col_W = col_W

        return out

    def backward(self, dout):
        # 卷積核大小
        FN, C, FH, FW = self.W.shape
        dout = dout.permute(0,2,3,1).reshape(-1, FN)
        # size = (N*out_h*out_w,FN)

        db = torch.sum(dout, axis=0)
        # size(FN,) 與b的維度相同
        dW = torch.mm(self.col.T, dout)
        # size = torch.mm(size(C*ker*ker,N*out_h*out_w), (N*out_h*out_w,FN))
        # = size(C*ker*ker, FN)
        dW = dW.permute(1, 0).reshape(FN, C, FH, FW)
        # size(FN,C,ker,ker), 與W的維度相同
        
        self.dW, self.db = self.dW*self.momentum, self.db*self.momentum
        self.dW += self.lr*dW*(1-self.momentum)
        self.db += self.lr*db*(1-self.momentum)

        dcol = torch.mm(dout, self.col_W.T)
        # size = torch.mm(size(N*out_h*out_w,FN), (FN,C*ker*ker))
        # = size(N*out_h*out_w, C*ker*ker)
        # 經過col2im轉換爲圖像
        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
        # size(N,C,H,W) = self.x.shape

        return dx
    
    def update(self):
        #更新W和b
        self.W = self.W-self.dW
        self.b = self.b-self.db
        
    def zero_delta(self):
        # 清零dW和db
        self.dW = torch.zeros_like(self.W)
        self.db = torch.zeros_like(self.b)
    
    def __call__(self, X):
        return self.forward(X)

卷積層梯度驗證

我們用pytorch自帶的梯度計算工具計算梯度, 並和我們的卷積層計算結果比較. 使用的誤差函數是MSEloss, torch中的定義是
Loss(output,target)=(outputtarget)2n Loss(output,target) = \frac{(output-target)^2}{n}
Loutput=2(outputtarget)n 所以導數計算是\frac{\partial L}{\partial output} = \frac{2(output-target)}{n}

conv_torch = torch.nn.Conv2d(in_channels=3,out_channels=2,kernel_size=2,stride=1,padding=0)
conv_my = Convolution(in_channels=3,out_channels=2,kernel_size=2,stride=1,padding=0,LEARNING_RATE = 1,
                     momentum = 0)
conv_my.W = conv_torch.weight.clone()
conv_my.b = conv_torch.bias.clone()

x = torch.randn(2,3,3,3)
xc = x.clone()
xc.requires_grad = True
target = torch.randn(2,2,2,2)
n = 16

output_my = conv_my.forward(x)
output_torch = conv_torch.forward(xc)
loss = F.mse_loss(output_torch,target)
loss.backward()
print(conv_torch.weight.grad)
print(conv_torch.bias.grad)
print(xc.grad)
dx = conv_my.backward(2*(output_my-target)/n)
print(conv_my.dW)
print(conv_my.db)
print(dx)

池化層

池化的操作和卷積很類似, 它也是用一個窗口在圖像上掃描. 掃描的過程中, 池化層做的不是用自帶的參數去與圖像點積, 而是用max或者average等算術運算操作對當前窗口. 最常用的池化方法是max pooling, 它用一個窗口在圖像上滑動, 並輸出每個窗口最大的像素值. 因爲池化層的意義是高效的圖像壓縮和特徵萃取, 所以一般不像卷積核一樣, 使用1步長, 而一般會使用kernel_size=stride的定義方法. 比如2x2, 2步長的池化層就會把圖像大小壓縮到四分之一.
在這裏插入圖片描述
具體實現時, 因爲我們已經有了im2col這種強大的工具, 因此池化就只需要先把IM變成COL矩陣, 調整矩陣的每一行是kernel_size x kernel_size, 即單個窗口囊括的元素. 然後在COL矩陣的每一行上做argmax, 記錄下這些座標的位置. 在反向傳播時, 我們把dz放回COL對應的位置, 其他位置爲0. 然後用col2im重塑即可.

class Pooling:
    def __init__(self, kernel_size = 2, stride = 2, padding=0):
        self.pool_h = kernel_size
        self.pool_w = kernel_size
        self.stride = stride
        self.pad = padding
        
        self.x = None
        self.arg_max = None

    def forward(self, x):
        N, C, H, W = x.shape
        out_h = int(1 + (H - self.pool_h) / self.stride)
        out_w = int(1 + (W - self.pool_w) / self.stride)
        # 用im2col展開圖像爲二維矩陣
        col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad) 
        # size(N*out_h*out_w,C*ker*ker)
        col = col.reshape(-1, self.pool_h*self.pool_w) 
        # size(N*out_h*out_w*C,ker*ker)
        # 在每行取最大值,計算argmax並保存下來
        arg_max = torch.argmax(col, axis=1)
        out = col[range(col.shape[0]),arg_max]
        # size(N*out_h*out_w*C,)
        # 重塑成圖像
        out = out.reshape(N, out_h, out_w, C).permute(0, 3, 1, 2) 
        # size = (N,C,out_h,out_w)

        self.x = x
        self.arg_max = arg_max

        return out

    def backward(self, dout):
        dout = dout.permute(0, 2, 3, 1) 
        # size(N,out_h,out_w,C)
        
        pool_size = self.pool_h * self.pool_w
        n = dout.size().numel()
        dmax = torch.zeros((n, pool_size))
        # 把之前最大元素對應位置用dout填充, 其他置0
        dmax[torch.arange(n), self.arg_max.flatten()] = dout.flatten()
        # size(N*out_h*out_w*C,ker*ker)
        dmax = dmax.reshape(dout.shape + (pool_size,))
        # size(N,out_h,out_w,C,ker*ker)
        dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
        # size(N*out_h*out_w,C*ker*ker)
        # 調用col2im復原圖像
        dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
        # size(N,C,H,W)
        return dx
    
    def __call__(self, X):
        return self.forward(X)

池化層梯度驗證

MaxPool_torch = torch.nn.MaxPool2d(kernel_size=2)
MaxPool_my = Pooling(kernel_size=2)

x = torch.randn(2,2,4,4)
xc = x.clone()
xc.requires_grad = True
target = torch.randn(2,2,2,2)
n = 16

output_my = MaxPool_my.forward(x)
output_torch = MaxPool_torch.forward(xc)
loss = F.mse_loss(output_torch,target)
loss.backward()
print(xc.grad)
dx = MaxPool_my.backward(2*(output_my-target)/n)
print(dx)

任務:搭建面向CIFAR-10的CNN分類器

使用上面寫好的卷積層和池化層, 還有之前實現過的激活函數和softmax, 線性層等, 實現簡單的卷積神經網絡, 並用反向傳播訓練網絡. base line是在cifar-10上達到50 per的準確率. 一種最基本的模型架構爲:
(input) - Conv1 - ReLU - Pooling - Conv2 - ReLU - Pooling - FC1 - ReLU - FC2 - (output)
其中卷積層使用5x5的size, Conv1的通道數爲6, Conv2的通道數爲16, 如果希望能讓模型做得更好, 可以適當增加捲積層數和通道數, 當然相對的需要的內存和訓練時間也更多. 一種VGG型的卷積網絡搭建方法是, 用兩層3x3的卷積核代替5x5的卷積核. 這樣使用的參數更少而且層數更深, 更容易提取高級特徵來讓CNN學得更好.

class ReLU:
    def __init__(self, slope = 0.2):
        self.mask = None
        self.leak = slope
        
    def forward(self, x):
        self.mask = x<0
        x[self.mask] *= self.leak
        return x
    
    def backward(self, dz):
        dz[self.mask] *= self.leak
        return dz
    
    def __call__(self, x):
        return self.forward(x)
    
class Linear:
    def __init__(self,input_sz,output_sz, LEARNING_RATE=0.01,
                 momentum = 0.9):
        '''
        使用kaiming初始化策略, W = normal(0,2/(input_sz+output_sz))
        '''
        self.W = torch.randn(input_sz,output_sz)*(2/(input_sz+output_sz))
        self.b  = torch.randn(output_sz)*(2/output_sz)
        
        self.dW = torch.zeros_like(self.W)
        self.db = torch.zeros_like(self.b)
        
        self.lr = LEARNING_RATE
        self.momentum = momentum
        
        self.X = None
        
    def forward(self,X):
        self.X = X
        out =  torch.mm(self.X,self.W)+self.b
        return out
    
    def backward(self,dz):
        """
        dz-- 前面的導數
        基於反向傳播的dz和動量、學習率,更新W和b
        """
        n,m = self.X.shape
        
        self.dW, self.db = self.dW*self.momentum, self.db*self.momentum
        dw = torch.mm(self.X.T,dz)#/n
        self.dW += self.lr*dw*(1-self.momentum)
        
        #db = torch.mean(dz, axis = 0)
        db = torch.sum(dz, axis = 0)
        self.db += self.lr*db*(1-self.momentum)
        
        dx = torch.mm(dz,self.W.T)
        
        return dx
    
    def update(self):
        #更新W和b
        self.W = self.W-self.dW
        self.b = self.b-self.db
        
    def zero_delta(self):
        # 清零dW和db
        self.dW = torch.zeros_like(self.W)
        self.db = torch.zeros_like(self.b)
    
    def __call__(self, X):
        return self.forward(X)
    
def cross_entropy_error(y_pred,labels):
    n,m = y_pred.shape
    return -torch.mean(torch.log(y_pred[range(n),labels]))
    
def softmax(X):
    n,m = X.shape
    exp_x = torch.exp(X)
    sum_exp_x = torch.sum(exp_x,axis=1)
    return (exp_x.T/sum_exp_x).T

def one_hot_encode(labels):
    '''
    labels:一維數組,返回獨熱編碼後的二維數組
    '''
    n = len(labels)
    m = 10
    ret = torch.zeros((n,m))
    ret[range(n),labels] = 1.
    return ret


class SoftMax:
    '''
    softmax,歸一化層,把輸出轉概率
    該層的輸出代表每一分類的概率
    '''
    def __init__ (self):
        self.y_hat = None
        
    def forward(self,X):
        self.X = X
        self.y_hat = softmax(X)
        return self.y_hat
    
    def backward(self,labels):
        # 使用cross entropy loss
        dx = (self.y_hat-one_hot_encode(labels))
        return dx
    
    def __call__(self, x):
        return self.forward(x)
    

    
class Sequential:
    def __init__(self, module_list):
        self.layers = module_list
        
    def forward(self, x):
        for layer in self.layers:
            x = layer(x)
        return x
    
    def backward(self, dz):
        for layer in self.layers[::-1]:
            dz = layer.backward(dz)
        return dz
    
    def update(self):
        for layer in self.layers:
            if type(layer)==Linear or type(layer)==Convolution:
                layer.update()
                
    def zero_delta(self):
        for layer in self.layers:
            if type(layer)==Linear or type(layer)==Convolution:
                layer.zero_delta()
    
    def __call__(self, x):
        return self.forward(x)

模型架構設計

class CNN:    
    def __init__(self, LEARNING_RATE=0.01, MOMENTUM = 0.9):
        lr = LEARNING_RATE
        mom = MOMENTUM
        # 輸入:CIFAR-10圖像, 3x32x32
        self.conv = Sequential(
            [
                Convolution(in_channels=3,out_channels=16,kernel_size=3,
                            stride=1,padding=0,LEARNING_RATE = lr,momentum = mom),
                ReLU(),
                Convolution(in_channels=16,out_channels=16,kernel_size=3,
                            stride=1,padding=0,LEARNING_RATE = lr,momentum = mom),
                ReLU(),
                Pooling(kernel_size=2),
                # 6x14x14
                Convolution(in_channels=16,out_channels=32,kernel_size=3,
                            stride=1,padding=0,LEARNING_RATE = lr,momentum = mom),
                ReLU(),
                Convolution(in_channels=32,out_channels=32,kernel_size=3,
                            stride=1,padding=0,LEARNING_RATE = lr,momentum = mom),
                ReLU(),
                Pooling(kernel_size=2)
                # 32x5x5
            ]
        )
        self.fc = Sequential(
            [
                Linear(32*5*5,128,lr,mom),
                ReLU(),
                Linear(128,10,lr,mom),
                SoftMax()
            ]
        )
        # 輸出:10維分類

    def forward(self, X):
        self.out = self.conv(X)
        self.out = self.out.reshape(self.out.shape[0],-1)
        self.out = self.fc(self.out)
        return self.out

    def backward(self,y):
        # 使用softmax和交叉熵
        self.loss = cross_entropy_error(self.out,y)
        dz = self.fc.backward(y)
        dz = dz.reshape(-1,32,5,5)
        dz = self.conv.backward(dz)
        return dz

    def update(self):
        self.conv.update()
        self.fc.update()
        
    def zero_delta(self):
        self.conv.zero_delta()
        self.fc.zero_delta()
        
    def __call__(self, x):
        return self.forward(x)

數據集

num_epochs = 10
total_step = len(train_loader)
step_size = 50
model = CNN(LEARNING_RATE = 0.001,MOMENTUM = 0.9)
loss_list = []

for epoch in range(num_epochs):
    running_loss = 0.
    for i, (images, labels) in enumerate(train_loader):
        # Forward pass
        outputs = model(images)
        
        # Backward and optimize
        model.backward(labels)
        model.update()
        
        running_loss += model.loss
        
        if (i+1) % step_size == 0:
            print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
                   .format(epoch+1, num_epochs, i+1, total_step, running_loss.item()/step_size))
            loss_list.append(running_loss)
            running_loss = 0.

plt.plot(loss_list)
plt.title('Train loss on cifar-10 (CrossEntropy)')

訓練

因爲cifar-10不像MNIST那麼容易學習, 所以我們設置10個epoch的訓練, 以及較小的學習率.

num_epochs = 10
total_step = len(train_loader)
step_size = 50
model = CNN(LEARNING_RATE = 0.001,MOMENTUM = 0.9)
loss_list = []

for epoch in range(num_epochs):
    running_loss = 0.
    for i, (images, labels) in enumerate(train_loader):
        # Forward pass
        outputs = model(images)
        
        # Backward and optimize
        model.backward(labels)
        model.update()
        
        running_loss += model.loss
        
        if (i+1) % step_size == 0:
            print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
                   .format(epoch+1, num_epochs, i+1, total_step, running_loss.item()/step_size))
            loss_list.append(running_loss)
            running_loss = 0.

plt.plot(loss_list)
plt.title('Train loss on cifar-10 (CrossEntropy)')

在這裏插入圖片描述
看一看測試集正確率

correct = 0
total = 0
for images, labels in test_loader:
    outputs = model(images)
    predicted = torch.argmax(outputs.data, axis = 1)
    total += labels.size(0)
    correct += (predicted == labels).sum().item()

print('Accuracy of the network on the 10000 test images: {} %'.format(100 * correct / total))

Accuracy of the network on the 10000 test images: 71.8 %

雖然71.8好像並沒有很強, 但是如果用全連接網絡來處理這個數據集, 我們會發現我們需要非常多的參數和相對更長的訓練時間來取得同樣的效果. 這裏使用的網絡是16-32通道的卷積層堆疊, 如果使用更深層, 更多通道的卷積層來訓練會取得更好的效果. 不過考慮到我們自己的模型只能在CPU上跑, 爲了快速看到結果就沒有選擇更大的網絡.

小結

本小節我們學習了基於im2col實現的卷積層和池化層, 事實上這種技術是遠古版本的caffe(今天的Pytorch)的實現方法, 今天我們已經有了更好的計算方法, 可以在上面這種實現的基礎上提速數倍. 但是im2col是一種非常好的理解圖像卷積的方法, 有很好的學習價值. 同時因爲時間關係我們也沒有機會用CUDA敲一個能在GPU上運行卷積網絡的真-深度學習架構, 但是沒關係, 只要學到東西了不就好了?
卷積網絡的本質是權值共享和局部感受野, 正因如此它在2D圖像數據上有着遠超全連接網絡的效率. 卷積網絡還有其他的變種, 比如處理文本數據使用的charCNN, 中間的卷積方法是1D卷積. 圖像分割使用的全卷積網絡, 目標檢測使用的RCNN. 這些新潮的卷積神經網絡已經在工業界大放異彩, 後面如果有機會, 我會嘗試都做一下實現.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章