動手學深度學習2——實現卷積層和池化層

卷積神經網絡

卷積神經網絡是一種主要用於圖像識別和分類的網絡,也可以用於其他的領域,比如文本處理和alphago下棋。CNN是經典的深度學習網絡,它的靈感來源於對生物的視覺系統的研究,卷積神經網絡是受生物學上感受野的機制而提出的,感受野(receptive field)主要是指聽覺、視覺等神經系統中一些神經元的特性,即神經元只接受其所支配的刺激區域內的信號。一個神經元的感受野是指視網膜上的特定區域,只有區域內的刺激纔會激活該神經元。
目前卷積神經網絡一般由卷積層、匯聚層和全連接層交叉堆疊而成,利用反向傳播算法進行訓練。卷積神經網絡有三個特性:局部連接、權重共享以及子採樣,這些特性使得卷積神經網絡具有一定程度上的平移、縮放和旋轉不變性,這也符合人的視覺特點,和前饋神經網絡相比,其參數更少。
本篇博客我將簡單講述卷積層和池化層的運作機理,並用numpy實現它們。最後,我們將使用它們和上一篇博客:
https://blog.csdn.net/qq_41858347/article/details/104742634
講到的affine全連接層實現一個極爲簡單的二分類網絡。

卷積層

卷積層是網絡的靈魂所在,爲了迎合圖像識別的任務最主要的特徵,即平移不變性,我們把卷積核在整張圖片上“掃過”,圖片上的任何位置出現某種pattern都會讓卷積核和pattern共同形成一種更明顯更清晰的特徵,然後被傳遞到後幾層的網絡。
卷積核的結構如下面所示
在這裏插入圖片描述
它的參數有長寬、維度、步長几種。它處理圖片的方式是與局部圖進行像素的相乘,然後累加各個位置的乘積。維度是根據輸入圖像的維度設置的,輸入的每一個維度都是一張2D圖,也就需要獨立的卷積核進行卷積操作。
在這裏插入圖片描述
有的時候爲了卷積核能囊括所有位置的像素,我們在邊緣對圖像進行0-padding填充,也就是留白。這是一種訓練時爲了不丟失特徵而進行的特殊操作。
在這裏插入圖片描述
簡單計算一下就得到輸出長寬的公式
在這裏插入圖片描述
前面說了,輸入的圖像可能有多維,這裏我們一般叫”多通道“;對多個通道都進行了卷積運算後還要把它們做加和,得到新的一張2D特徵圖。
爲了提取一張圖片的多種特徵,我們在一層”卷積層“裏可能需要設置多個卷積核。設置N個卷積核就得到N個2D特徵圖,也就進行了至少N種特徵提取。

用Numpy實現卷積層

學習完了理論我們動手實踐一下,用numpy矩陣包寫一個能使用的卷積層類。
在此之前還要講一下誤差反向傳播,反向傳播是在沒有torch和tf幫我們進行計算圖自動求導時,獲取每一層梯度的原始方式。這裏直接給出卷積層的dw、dx、db的公式;上一層的梯度dx(圖中的δ(l-1)),計算公式爲
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
dw是由前向傳播時的x和dz,也就是δ(l)直接卷積
dw = X * δ(l)
在這裏插入圖片描述
b作用於z的每個像素,因此db是dz的平均值。
然後我們就可以設計一個擁有前向傳播計算和反向傳播更新參數功能的,可以自我學習的卷積層。記得先導入numpy。

def conv(A,B):
    '''
    圖片卷積方法,操作兩個二維圖像
    '''
    #A,B:np.array
    H,W = A.shape
    h,w = B.shape
    res = np.zeros([H-h+1,W-w+1])
    for i in range(len(res)):
        for j in range(len(res[0])):
            a = A[i:i+h,j:j+w]#用於和B卷積的局部圖
            res[i][j] = np.sum(np.sum(a*B))
    return res

def rotate(Arr):
    '''
    把矩陣旋轉180度的方法
    '''
    A = deepcopy(Arr)
    H,W = A.shape
    for i in range(H//2):
        A[i],A[H-1-i] = A[H-1-i],A[i]
    return A

def inv_conv(A,B,exth,extw):
    '''
    從卷積核和輸出逆向出和輸入維度相同的delta,用於反向傳播
    '''
    A = np.pad(A,((exth,exth),(extw,extw)),'constant', constant_values=(0,0))
    return conv(A,B)

首先我們實現上面提到的conv卷積函數,rotate旋轉180度函數,還有填充函數。

class Convolution:
    def __init__(self, N, D, H, W):
        '''
        給出filter高度和寬度H W
        給出filter維度D和輸出通道數N
        至於stride,因爲我們要在cifar上測試,圖片比較小32*32,stride直接默認爲1
        padding上則是默認爲0,步長1的filter不存在信息丟失
        '''
        #隨機初始化卷積核和偏置
        self.W = np.random.rand(H*W*D*N)*0.1
        self.W = self.W.reshape(N,D,H,W)
        self.B = np.zeros(N)
        #設置dw和db,並額外設置計數器,用於批量梯度下降
        self.dw = np.zeros(self.W.shape)
        self.db = np.zeros(self.B.shape)
        self.batch_size = 0
        
    def forward(self, input_X):
        '''
        input_X爲單個圖像,擁有不定的維度
        滿足input.shape = [D,H,W]
        '''
        self.X = input_X
        D,H,W = input_X.shape
        FN,FD,FH,FW = self.W.shape
        outH,outW = H-FH+1,W-FW+1
        
        assert FD==D
        res = np.zeros((FN,outH,outW))
        for i in range(FN):
            for j in range(D):
                res[i] += conv(input_X[j],self.W[i][j])+self.B[i]
        return res
        
        
    def backward(self, dz):
        '''
        dz爲接受的後一層的反向傳播誤差
        lr是學習率
        反向傳播方法對dW和dB進行推導,並由dz推導出dx傳播到上一層
        '''
        N,D,H,W = self.W.shape
        
        #首先計算dx,即δx,從上一輸出的誤差到本次輸入self.X的誤差
        dx = np.zeros(self.X.shape)
        for i in range(D):
            for j in range(N):
                dx[i] += inv_conv(dz[j],rotate(self.W[j][i]),H-1,W-1)
        
        #然後計算dw,用於修正W的δw
        dw = np.zeros(self.W.shape)
        for i in range(N):
            for j in range(D):
                dw[i][j] = conv(self.X[j],dz[i])
                
        #最後計算db,db事實上就是把dz做分塊部分和
        db = dz.reshape(N,-1)
        num = db.shape[1]
        db = np.sum(db,axis = 1)/num
        self.dw += dw
        self.db += db
        self.batch_size += 1
        
        return dx
        
    
    def update(self, lr):
        '''
        把積累的誤差清零並用於修正參數
        '''
        dw = self.dw/self.batch_size
        db = self.db/self.batch_size
        self.dw -= self.dw
        self.db -= self.db
        self.batch_size = 0
        self.W -= dw*lr
        self.B -= db*lr

按照上面的邏輯實現forward和backward方法,額外設立update方法,用於進行多次backward後再更新一次參數,也就是常規的批量梯度下降。

我們可以試着把兩個這樣的卷積層級聯形成網絡,並用特定的輸入輸出訓練,看一看網絡能不能正常工作。首先我們自己定義兩張圖片作爲輸入,兩張更小的圖片作爲輸出。

input_X1 = np.array(
[[
    [0,0,1,1,0,0],
    [0,0,1,1,0,0],
    [1,1,1,1,1,1],
    [1,1,1,1,1,1],
    [0,0,1,1,0,0],
    [0,0,1,1,0,0],
]]
)

input_X2 = np.array(
[[
    [1,0,0,0,0,1],
    [0,1,0,0,1,0],
    [0,0,1,1,0,0],
    [0,0,1,1,0,0],
    [0,1,0,0,1,0],
    [1,0,0,0,0,1],
]]
)

targ1 = np.array(
[[
    [1,0],
    [0,1]
]]
)
targ2 = np.array(
[[
    [0,1],
    [1,0]
]]
)

然後寫一個convlayers類,用於訓練。我們希望網絡的輸入和輸出能有一一對應的關係。注意這裏的參數設置,爲了提煉兩種特徵,我們用2個卷積核放在第一層,第二層放置一個卷積核歸一輸出。

class Conv_Layers:    
    def __init__(self, LEARNING_RATE=0.01):
        #輸入:3*32*32的cifar圖片
        self.lr = LEARNING_RATE
        self.Conv1 = Convolution(2,1,3,3)
        self.Relu1 = Relu()
        self.Conv2 = Convolution(1,2,3,3)
        
    def forward(self, X):
        '''
        連接各層,需要在連接處reshape數據
        '''
        X = self.Conv1.forward(X)
        X = self.Relu1.forward(X)
        X = self.Conv2.forward(X)
        self.out = X
        return self.out
    
    def backward(self,y):
        '''
        反向傳播
        '''
        dz = self.out-y
        dz = self.Conv2.backward(dz)
        dz = self.Relu1.backward(dz)
        dz = self.Conv1.backward(dz)
        return dz
     
    
    def update(self):
        '''
        用各層積累的誤差更新網絡參數
        '''
        self.Conv1.update(self.lr)
        self.Conv2.update(self.lr)

訓練1000次,使用的學習率是0.02.

conv_model = Conv_Layers(0.02)

for epoch in range(1000):
    x = conv_model.forward(input_X1)
    conv_model.backward(targ1)
    conv_model.update()
    x = conv_model.forward(input_X2)
    conv_model.backward(targ2)
    conv_model.update()
    
print(conv_model.forward(input_X1))
print(conv_model.forward(input_X2))

得到的輸出如圖,可以看見儘管沒有完全收斂,但是網絡已經能把輸入的兩種圖像分別轉換爲特定的特徵圖,可以認爲網絡能夠正常工作。
在這裏插入圖片描述

池化層

池化層是爲了快速壓縮特徵而設置的,可以認爲它是把當前的特徵圖進行圖像壓縮。工作方式也是和卷積層一樣,用一個核在圖片上滑動,但是它沒有參數,它每滑倒一個位置,就執行Max-Pooling(取窗口內的最大值作爲輸出,常用方式)
Mean-Pooling(取窗口內的所有值的均值作爲輸出)的邏輯。這樣,區域內的最明顯特徵,或是所有特徵的平均就被提取出來。根據參數設置的不同,一般會從原始圖像獲得一張更小的圖像。
在這裏插入圖片描述
池化層意義在於:
1.降低了後續網絡層的輸入維度,從而減少計算量。
2.增強了 Feature Map 的健壯性(Robust),防止過擬合。

用Numpy實現池化層

一樣考慮一下反向傳播,以maxpooling爲例,輸入位置傳來的dz,是池化過程找到的幾個max的參數的delta。因此我們只需要把dz每個位置的delta放回原圖像的對應位置即可。
首先實現maxpool函數

def pool(A, size, stride):
    assert size>=stride
    H,W = A.shape
    outH,outW = (H-(size-stride))//stride,(W-(size-stride))//stride
    arg_max = np.zeros((outH,outW))
    where_max = np.zeros((outH,outW,2))
    for i in range(0,H-size+1,stride):
        for j in range(0,W-size+1,stride):
            ii = i//stride
            jj = j//stride
            arg_max[ii][jj] = -float("inf")
            for m in range(i,i+size):
                for n in range(j,j+size):
                    if A[m][n]>arg_max[ii][jj]:
                        where_max[ii][jj] = [m,n]
                        arg_max[ii][jj] = A[m][n]
    return arg_max,where_max

它會返回最大值組成的矩陣,以及這些最大值在原圖像種的下標矩陣,以便我們反向傳播時使用。
然後我們像上面一樣寫出池化層的類

class Pooling:
    def __init__(self, size=3, stride=2):
        '''
        maxpooling,用於壓縮特徵
        這裏只考慮H=W型的池化層
        '''
        self.size = size
        self.stride = stride
        self.X = None
        self.arg_max = None
        self.where_max = None
    
    def forward(self, input_X):
        '''
        根據步長和size進行0-padding,以免丟失特徵
        池化過程中記錄max元素的下標,反向傳播時會用它來複現dx
        '''
        self.arg_max = []
        self.where_max = []
        D,H,W = input_X.shape
        self.X = input_X
        padH,padW = 0,0
        if (H-self.size)%self.stride:
            padH = self.stride-(H-self.size)%self.stride
        if (W-self.size)%self.stride:
            padW = self.stride-(W-self.size)%self.stride
        
        A = -100*np.ones((D,H+padH,W+padW))
        for i in range(D):
            A[i][:H,:W] = input_X[i]
            t1,t2 = pool(A[i], self.size, self.stride)
            self.arg_max.append(t1)
            self.where_max.append(t2)
        
        self.arg_max = np.array(self.arg_max)
        self.where_max = np.array(self.where_max)
        return self.arg_max
    
    def backward(self, dz):
        '''
        反向傳播,不需要修改參數
        只需要計算dx並返回
        '''
        dx = []
        D,H,W = self.X.shape
        zD,zH,zW = dz.shape
        dx = np.zeros((D,H,W))
        for m in range(D):
            for i in range(zH):
                for j in range(zW):
                    x,y = self.where_max[m][i][j]
                    if x>=H or y>=W:
                        continue
                    dx[m][int(x)][int(y)] += dz[m][i][j]
        return dx

隨便設計一下輸入和delta,用於驗證池化層的正確性。

#對池化層的反向傳播梯度進行檢測
P = Pooling()
X = np.array([x for x in range(16)]).reshape(1,4,4)
np.random.shuffle(X[0])
print(X)

print(P.forward(X))
Delta = np.array([[[1,2],[3,4]]])
print(P.backward(Delta))

#進一步測試
P = Pooling(4,2)
X = np.array([x for x in range(25)])
np.random.shuffle(X)
X = X.reshape(1,5,5)

print(X)
print(P.forward(X))
Delta = np.array([[[1,2],[4,5]]])
P.backward(Delta)

輸出如下,可以看見池化層的輸出和反向傳播正常運行。
在這裏插入圖片描述

激活層

CNN中廣泛使用的激勵函數不是sigmoid而是relu,因爲卷積網絡不需要負數,所有負數規約爲0也不會影響網絡的正常運行。使用relu可以更好的讓網絡收斂。
在這裏插入圖片描述
同時relu也能引人非線性。無論是卷積層、池化層還是全連接層它們都是線性的,激活層主要爲了引入非線性因素,提升模型的表達能力,一般在卷積或池化之後引入非線性激活(卷積後激活或者池化後激活都可以,順序不同影響不大)。
Relu的實現就很簡單了,我在這裏使用了一點點小技巧;讓relu輸入爲負數時也不輸出0,而是把原輸入乘一個很小的係數,這個技巧可以防止某些結點在訓練時意外更新到了一個很差的參數,導致後面任何輸入都會讓它輸出0,進而該結點再也無法更新,成爲了”死結點“。

class Relu:
    '''
    激勵層,使用relu函數
    用於扭曲映射,實現非線性
    這裏爲了防止出現死結點,使用鬆弛的relu
    '''
    def __init__(self):
        self.mask = None
        
    def forward(self ,X):
        self.mask = X <= 0
        out = X
        out[self.mask] = 0.01*out[self.mask]
        return out
    
    def backward(self,dz):
        dz[self.mask] = 0.01*dz[self.mask]
        return dz

實踐二分類網絡

到這裏,卷積網絡的素材已經全部給出,把它們串聯起來就可以得到卷積神經網絡。在卷積和池化的後面一般還要加上全連接層用於特徵分類,這個我上一篇博客有講到,這裏我直接再貼一遍代碼。

class Affine:
    def __init__(self,X_size,Y_size):
        '''
        仿射層,全連接線性變換層
        用於對卷積網絡部分提取的特徵做進一步分類
        '''
        self.W = np.random.randn(X_size,Y_size)*0.02
        self.b  = np.random.randn(1)*0.01
        self.X = None
        self.X_size = X_size
        self.Y_size = Y_size
        
        self.dW = np.zeros(self.W.shape)
        self.db = np.zeros(self.b.shape)
        self.batch_size = 0
        
        self.out_shape =None
        
    def forward(self,X):
        X = X.reshape(1,-1)
        self.X = X
        out =  np.dot(self.X,self.W)+self.b
        return out
    
    def backward(self,dz):
        """
        dz-- 前面的導數
        lr 學習率
        """  
        dz = dz.reshape(1,-1)
        
        self.dW += np.dot(self.X.T,dz)
        self.db += np.sum(dz,axis=1)
        dx = np.dot(dz,self.W.T)
        self.batch_size+=1
        
        return dx
    
    def update(self, lr):
        dw = self.dW/self.batch_size
        db = self.db/self.batch_size
        #更新W和b
        self.W = self.W-lr*dw
        self.b = self.b-lr*db
        self.dW -= self.dW
        self.db -= self.db
        self.batch_size = 0
        
        return

我們用一個非常簡單的任務測試CNN能否正常工作,數據集就使用上面的兩張圖片,它們一張是十字形,一張是X形,我們希望網絡能正確分類它們。
在這裏插入圖片描述
首先設計網絡,使用兩層卷積層、一層池化層和兩層全連接層,最後還加上一層softmax層用於歸一化,把數值轉概率。

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):
        dx = (self.y_hat-labels)
        return dx

class SimpleCNN:    
    def __init__(self, LEARNING_RATE=0.01):
        '''
        設計用於驗證網絡能否正常工作的網絡,
        使用上面的X型和十字形圖片嘗試二分類任務
        '''
        #輸入:1*6*6
        self.lr = LEARNING_RATE
        self.Conv1 = Convolution(2,1,2,2)
        self.Relu1 = Relu()
        #輸出:2*5*5
        self.MaxPool1 = Pooling(2,2)
        #輸出:2*3*3
        self.Conv2 = Convolution(1,2,2,2)
        self.Relu2 = Relu()
        #輸出:1*2*2
        self.Affine1 = Affine(4,2)
        self.Relu3 = Relu()
        self.Affine2 = Affine(2,2)
        self.Softmax = SoftMax()
        self.print = False
        #輸出:2個特徵
        
    def forward(self, X):
        '''
        連接各層,需要在連接處reshape數據
        '''
        X = self.Conv1.forward(X)
        X = self.Relu1.forward(X)
        if self.print:print("conv1 out:",X)
        X = self.MaxPool1.forward(X)
        if self.print:print("maxpool1 out:",X)
        X = self.Conv2.forward(X)
        X = self.Relu2.forward(X)
        if self.print:print("conv2 out:",X)
        X = X.reshape(1,-1)
        X = self.Affine1.forward(X)
        X = self.Relu3.forward(X)
        if self.print:print("affine1 out:",X)
        X = self.Affine2.forward(X)
        if self.print:print("affine2 out:",X)
        X = self.Softmax.forward(X)
        if self.print:print("softmax out:",X)
        return X
    
    def backward(self,y):
        '''
        反向傳播
        '''
        y = y.reshape(1,-1)
        dz = self.Softmax.backward(y)
        if self.print:print("softmax delta:",dz)
        dz = self.Affine2.backward(dz)
        if self.print:print("affine2 delta:",dz)
        dz = self.Relu3.backward(dz)
        dz = self.Affine1.backward(dz)
        if self.print:print("affine1 delta:",dz)
        dz = dz.reshape(1,2,2)
        dz = self.Relu2.backward(dz)
        dz = self.Conv2.backward(dz)
        if self.print:print("conv2 delta:",dz)
        dz = self.MaxPool1.backward(dz)
        dz = self.Relu1.backward(dz)
        dz = self.Conv1.backward(dz)
        if self.print:print("conv1 delta:",dz)
        return dz
     
    
    def update(self):
        '''
        用各層積累的誤差更新網絡參數
        '''
        self.Affine2.update(self.lr)
        self.Affine1.update(self.lr)
        self.Conv2.update(self.lr)
        self.Conv1.update(self.lr)

用上面的兩張圖片作爲網絡輸入,兩種類別的獨熱編碼作爲輸出,訓練網絡。

simple_model = SimpleCNN(0.1)

for epoch in range(5000):
    if epoch%100==0:
        simple_model.print = True
    x = simple_model.forward(input_X1)
    simple_model.backward(np.array([[1,0]]))
    simple_model.update()
    x = simple_model.forward(input_X2)
    simple_model.backward(np.array([[0,1]]))
    simple_model.update()
    simple_model.print = False

設計一個爲圖片加噪音的函數,看一看網絡能否正常分類。

def addnoise(mat):
    img = deepcopy(mat)
    shape = img.shape
    img = img.reshape(-1)
    for i in range(len(img)):
        if np.random.rand()>0.9:
            img[i] = abs(img[i]-1)
    img = img.reshape(shape)
    return img
    
noiseX1 = addnoise(input_X1)
plt.figure()
plt.matshow(noiseX1[0])
print(simple_model.forward(noiseX1))
noiseX2 = addnoise(input_X2)
plt.figure()
plt.matshow(noiseX2[0])
print(simple_model.forward(noiseX2))

在這裏插入圖片描述
即使輸入有噪聲的圖片,網絡也能正確分類。
我們多測試幾次試試看

ri = 0;wr = 0
for t in range(200):
    noiseX1 = addnoise(input_X1)
    out = simple_model.forward(noiseX1)
    if out[0][0]>out[0][1]:
        ri+=1
    else:
        wr+=1
    noiseX2 = addnoise(input_X2)
    out = simple_model.forward(noiseX2)
    if out[0][0]<out[0][1]:
        ri+=1
    else:
        wr+=1
print("Accracy:",ri/(ri+wr))

Accracy: 0.91

得到的正確率是0.91.

在Cifar-10數據集上測試

cifar是經典的圖片分類任務數據集,我們使用其中的貓和狗圖片來訓練,實現一個更復雜的二分類網絡。
cifar的數據集是33232的圖片,而且像素值處在0~255之間,而上面我們測試用的圖片是0-1的值域,如果想要把cifar像上面一樣訓練,要設計新的網絡結構並把圖片做適當歸一化。

import pickle

def load_file(filename):
    with open(filename, 'rb') as fo:
        data = pickle.load(fo, encoding='latin1')
    return data

data = load_file('E:/cifar-10-batches-py/data_batch_1')
print(data.keys())

train_X = data['data'].reshape(-1,3,32,32)
train_y = np.array(data['labels'])
choose = np.logical_or(train_y==3,train_y==5)

X_train = train_X[choose]
y_train = np.array(train_y[choose]==5).astype(np.int)


def one_hot_label(y):
    one_hot_label = np.zeros((y.shape[0],2))
    y = y.reshape(y.shape[0])
    one_hot_label[range(y.shape[0]),y] = 1
    return one_hot_label

y_train = one_hot_label(y_train)

我們的代碼沒有用多線程或者CUDA來優化計算,因此實踐起來非常慢。因爲我們只使用100張圖片用作訓練。

#預處理
X_training = deepcopy(X_train[:100])
y_training = y_train[:100]
X_training = X_training/128

然後我們像上面一樣設計網絡,不過這次要設計更大規模的。

class CNN:    
    def __init__(self, LEARNING_RATE=0.01):
        '''
        設計處理cifar數據集的網絡
        '''
        #輸入:3*32*32的cifar圖片
        self.lr = LEARNING_RATE
        self.Conv1 = Convolution(3,3,5,5)
        self.Relu1 = Relu()
        #輸出:3*28*28的維特徵圖
        self.MaxPool1 = Pooling(3,2)
        #輸出:3*14*14的3維特徵圖
        self.Conv2 = Convolution(5,3,3,3)
        self.Relu2 = Relu()
        #輸出:5*12*12的3維特徵圖
        self.MaxPool2 = Pooling(3,2)
        #輸出:5*6*6的50維特徵圖
        #展開:180維的向量
        self.Affine1 = Affine(180,20)
        self.Relu3 = Relu()
        #輸入:10維的向量
        self.Affine2 = Affine(20,2)
        self.Softmax = SoftMax()
        #輸出:2個特徵
        self.print = False
        
    def forward(self, X):
        '''
        連接各層,需要在連接處reshape數據
        '''
        X = self.Conv1.forward(X)
        X = self.Relu1.forward(X)
        X = self.MaxPool1.forward(X)
        X = self.Conv2.forward(X)
        X = self.Relu2.forward(X)
        X = self.MaxPool2.forward(X)
        X = X.reshape(1,-1)
        X = self.Affine1.forward(X)
        X = self.Relu3.forward(X)
        if self.print:print("affine1 out:",X)
        X = self.Affine2.forward(X)
        if self.print:print("affine2 out:",X)
        X = self.Softmax.forward(X)
        if self.print:print("softmax out:",X)
        return X
    
    def backward(self,y):
        '''
        反向傳播
        '''
        y = y.reshape(1,-1)
        dz = self.Softmax.backward(y)
        if self.print:print("softmax delta:",dz)
        dz = self.Affine2.backward(dz)
        if self.print:print("affine2 delta:",dz)
        dz = self.Relu3.backward(dz)
        dz = self.Affine1.backward(dz)
        if self.print:print("affine1 delta:",dz)
        dz = dz.reshape(5,6,6)
        dz = self.MaxPool2.backward(dz)
        dz = self.Relu2.backward(dz)
        dz = self.Conv2.backward(dz)
        if self.print:print("conv2 delta:",dz)
        dz = self.MaxPool1.backward(dz)
        dz = self.Relu1.backward(dz)
        dz = self.Conv1.backward(dz)
        if self.print:print("conv1 delta:",dz)
        return dz
     
    
    def update(self):
        '''
        用各層積累的誤差更新網絡參數
        '''
        self.Affine2.update(self.lr)
        self.Affine1.update(self.lr)
        self.Conv2.update(self.lr*3)
        self.Conv1.update(self.lr*3)
    
    
    def onehot(self, Y):
        shape = Y.shape
        Y = Y.reshape(-1)
        max_idx = np.argmax(Y)
        out = np.zeros(shape=Y.shape)
        out[max_idx] = 1
        return out
    
    def predict(self,X):
        '''
        進行分類的預測,返回的是一維向量,形式是one-hot
        '''
        y = self.forward(X)
        return self.onehot(y)
    
    def fit(self,train_X,train_y,batch_size=5, num_iters=500):
        '''
        根據batch_size每次從訓練集中隨機抽樣
        對抽樣到的batch中每個圖像都進行正向和反向傳播
        最後用累積的誤差去更新參數
        '''
        costs = []
        avg_size = 100
        N = train_X.shape[0]
        for epoch in range(num_iters):
            #隨機取用例
            idx = np.random.randint(0,N,size=batch_size)
            cost = 0
            for i in idx:
                X = train_X[i]
                y = train_y[i]
                #前向與反向傳播
                A = self.forward(X)
                cost += cross_entropy_error(y,A)
                self.backward(y)
            #更新
            self.update()
            cost/=batch_size
            costs.append(cost)
            
        
            if((epoch+1)%avg_size == 0):
                #把後avg_size次的cost做平均
                S = 0
                for _ in range(avg_size):
                    S+=costs.pop()
                
                cost_avg = S/avg_size
                costs.append(cost_avg)
                print("After %d iters ,cost is :%g" %(epoch+1,cost_avg))
                
                
        #畫出損失函數圖
        plt.plot(costs)
        plt.xlabel("iterations/%d" %avg_size)
        plt.ylabel("costs")
        plt.show()

開始訓練,設置0.015的學習率、3的batch_size和10000次迭代。

model = CNN(0.015)
model.fit(X_training,y_training,batch_size=3,num_iters=10000)

跑了40分鐘…最後的損失函數圖如下
在這裏插入圖片描述
訓練時出現了一些震盪,但可以看見總誤差還是下降趨勢,說明網絡還是收斂的。
在訓練集上跑一下正確率

right = 0;
wrong = 0;
for i in range(len(y_training)):
    yhat = model.predict(X_training[i])
    if (yhat==y_training[i]).all():
        right+=1
    else:
        wrong+=1
print("Accuracy:",right/(right+wrong))

Accuracy: 0.98

正確率達到0.98,我們再試一下在其他數據上的正確率。

X_test = deepcopy(X_train[100:200])
y_test = y_train[100:200]
X_test = X_test/128
for i in range(len(y_test)):
    yhat = model.predict(X_test[i])
    if (yhat==y_test[i]).all():
        right+=1
    else:
        wrong+=1
print("Accuracy:",right/(right+wrong))

```python
right = 0;
wrong = 0;
verify = 50
for i in range(verify):
    i = random.randint(0,1000)
    x = deepcopy(X_train[i])/128
    yhat = model.predict(x)
    if (yhat==y_train[i]).all():
        right+=1
    else:
        wrong+=1
print("Accuracy:",right/(right+wrong))

Accuracy: 0.76

0.76,不是很理想。一是因爲我們使用的訓練集太少,另外也是因爲網絡產生了一些過擬合。當然,我們只是爲了學習卷積神經網絡的理論才手動實現。當要實際解決問題時,還是乖乖用tensorflow和pytorch這些環境完善、性能高的軟件包。

訓練完的權值可以導出保存以免下次還要訓練。

model.Conv1.W.tofile('Cat_dog_cnn_Conv1_W.bin')
model.Conv1.B.tofile('Cat_dog_cnn_Conv1_B.bin')
model.Conv2.W.tofile('Cat_dog_cnn_Conv2_W.bin')
model.Conv2.B.tofile('Cat_dog_cnn_Conv2_B.bin')
model.Affine1.W.tofile('Cat_dog_cnn_Affine1_W.bin')
model.Affine1.b.tofile('Cat_dog_cnn_Affine1_B.bin')
model.Affine2.W.tofile('Cat_dog_cnn_Affine2_W.bin')
model.Affine2.b.tofile('Cat_dog_cnn_Affine2_B.bin')

model.Conv1.W = np.fromfile('Cat_dog_cnn_Conv1_W.bin',dtype=np.float)
model.Conv1.B = np.fromfile('Cat_dog_cnn_Conv1_B.bin',dtype=np.float)
model.Conv2.W = np.fromfile('Cat_dog_cnn_Conv2_W.bin',dtype=np.float)
model.Conv2.B = np.fromfile('Cat_dog_cnn_Conv2_B.bin',dtype=np.float)
model.Affine1.W = np.fromfile('Cat_dog_cnn_Affine1_W.bin',dtype=np.float)
model.Affine1.b = np.fromfile('Cat_dog_cnn_Affine1_B.bin',dtype=np.float)
model.Affine2.W = np.fromfile('Cat_dog_cnn_Affine2_W.bin',dtype=np.float)
model.Affine2.b = np.fromfile('Cat_dog_cnn_Affine2_B.bin',dtype=np.float)

model.Conv1.W = model.Conv1.W.reshape(3,3,5,5)
model.Conv2.W = model.Conv2.W.reshape(5,3,3,3)
model.Affine1.W = model.Affine1.W.reshape(180,20)
model.Affine2.W = model.Affine2.W.reshape(20,2)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章