1. 程式人生 > 實用技巧 >神經網路學習筆記7

神經網路學習筆記7

C7 卷積神經網路
CNN中新增了Convolution層和Pooling層,其連線順序是:Convolution-Relu-Pooling,靠近輸出層使用Affine-ReLU組合
①全連線:相鄰層的所有神經元之間都有連線
②卷積層:以多維的資料形式接收輸入資料,其輸入輸出資料稱為“特徵圖”
卷積運算:相當於影象處理中的濾波器運算;對於輸入資料,以一定間隔滑動濾波器視窗進行運算,將各個位置上過濾的元素和輸入的對應元素相乘,再求和,把結果儲存到輸入的對應位置(乘積累加運算)
偏置:在應用了濾波器的資料上加上偏置
填充:向資料周圍填入固定資料,主要是為了調整輸出的大小。如果每次進行卷積運算都會縮小空間,則在某個時刻輸出大小就可能變為1,導致無法再應用卷積運算。為避免這樣的情況,就要使用填充。

步幅:應用濾波器位置的間隔稱為步幅
假設輸入大小為(H,W),濾波器大小為(FH,FW),輸出大小為(OH,OW),填充為P,步幅為S
輸出大小計算公式:
OH=(H+2P-FH)/S+1
OW=(W+2P-FW)/S+1

多維資料的卷積運算:對於多通道方向資料,當通道方向上有多個特徵圖時,會按照通道進行輸入資料和濾波器的卷積運算,並將結果相加從而得到輸出。輸入資料和濾波器的通道數要設定為相同的值。
批處理:對神經網路的處理中進行輸入資料的打包處理,能夠實現處理的高效化和學習時對mini-batch的對應。
在卷積運算中同樣對應批處理,需要將各層間傳遞的資料儲存為4維資料,按照(batch_num,channel,height,width)順序儲存資料。批處理將n次處理彙總成1次進行。

池化層:是縮小高、長方向上的空間運算。有Average池化、Max池化
池化層特徵:沒有要學習的引數,通道數不發生變化,對於微小的位置變化具有魯棒性。

卷積層與池化層實現
1、四維陣列:對於CNN中各層間傳遞的四維資料,使用python實現
x=np.random.rand(10,1,28,28)
訪問第一個資料:x[0]
x[0].shape #(1,28,28)

2、基於im2col的展開
如果老老實實地實現卷積運算,估計要重複好幾層的for語句,這樣的實現比較麻煩。而且NumPy中存在使用for語句後處理變慢的缺點。使用im2col這個不便利的函式進行簡單實現。
im2col函式可以將輸入資料展開以是和濾波器,對三維的輸入資料應用im2col後,資料轉換為二維矩陣

當濾波器的運用區域重疊的情況下,使用im2col展開以後,展開後的元素會多於原方塊的元素個數;im2col實現存在比普通實現消耗更多記憶體的缺點,彙總成一個大矩陣進行計算,對計算機計算頗有益處。

使用im2col展開資料後,就只需將卷積層的濾波器縱向展開為1列,並且計算2個矩陣的乘積即可。與全連線層Affine層進行處理基本相同。

※卷積層實現
初始化方法將濾波器(權重)、偏置、步幅、填充作為引數接收。濾波器是(FN,C,FH,FW)的4維形狀。
使用im2col展開輸入資料,並用reshape將濾波器展開為二維陣列。然後計算展開後的矩陣的乘積。
其中,通過使用reshape(FN,-1),將引數指定為-1,reshape函式會自動計算-1維度上的元素個數,以使得多維陣列元素個數前後一致
transpose:更改多維陣列的軸的順序,將輸出大小轉換為合適的形狀。
卷積層進行反向轉播時,必須進行im2col的逆處理

class Convolution:
    def __init__(self, W, b, stride=1, pad=0):
        self.W = W
        self.b = b
        self.stride = stride
        self.pad = pad
        
        # 中間資料(backward時使用)
        self.x = None   
        self.col = None
        self.col_W = None
        
        # 權重和偏置引數的梯度
        self.dW = None
        self.db = None

    def forward(self, x):
        FN, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
        out_w = 1 + int((W + 2*self.pad - FW) / self.stride)

        col = im2col(x, FH, FW, self.stride, self.pad)
        col_W = self.W.reshape(FN, -1).T

        out = np.dot(col, col_W) + self.b
        out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)

        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.transpose(0,2,3,1).reshape(-1, FN)

        self.db = np.sum(dout, axis=0)
        self.dW = np.dot(self.col.T, dout)
        self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)

        dcol = np.dot(dout, self.col_W.T)
        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)

        return dx

※池化層實現
一樣使用im2col展開輸入資料,不過池化層中通道方向是獨立的,池化的應用區域按照通道單獨展開。
進行階段:
①展開輸入資料,②求各行的最大值,③轉換為合適的輸出大小

class Pooling:
    def __init__(self, pool_h, pool_w, stride=1, pad=0):
        self.pool_h = pool_h
        self.pool_w = pool_w
        self.stride = stride
        self.pad = pad
        
        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)

        col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
        col = col.reshape(-1, self.pool_h*self.pool_w)

        arg_max = np.argmax(col, axis=1)
        out = np.max(col, axis=1)
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

        self.x = x
        self.arg_max = arg_max

        return out

    def backward(self, dout):
        dout = dout.transpose(0, 2, 3, 1)
        
        pool_size = self.pool_h * self.pool_w
        dmax = np.zeros((dout.size, pool_size))
        dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
        dmax = dmax.reshape(dout.shape + (pool_size,)) 
        
        dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
        dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
        
        return dx

CNN的實現:
引數解釋
①input_dim:輸入資料的維度(通道,高,長)
②conv_param:卷積層的超引數(字典),字典的關鍵字如下:
filter_num:濾波器的數量
filter_size:濾波器的大小
stride:步幅,
pad:填充
③hidden_size:隱藏層(全連線)的神經元數量
④output_size:輸出層(全連線)的神經元數量
⑤weight_int_std:初始化時權重的標準差

將由初始化引數傳入的卷積層出號引數從字典中取了出來,計算卷積層輸出大小,接下來是權重引數的初始化部分。
學習所需的引數是第一層的卷積層和剩餘兩個全連線的權重和偏置。這些引數儲存在例項變數中params字典中。將第一層的卷積層權重設為關鍵字w1,偏置設定為關鍵字b1,分別使用關鍵字w2,b2和關鍵字w3,b3儲存第二個和第三個全連線的權重與偏置。

然後按照順序生成必要的層,向有序字典(orderedDict)的layers中新增層,只有最後的softmaxWithLoss層被新增到別的變數lastLayer中。

推理:predict方法,從頭開始依次呼叫已經新增的層,將結果傳遞給下一個層。
在求損失函式函式的loss方法中,除了使用predict方法進行的forward處理以外,還會繼續進行forward處理,知道到達最後的softmaxWithLoss層

接下來是基於誤差反差傳播法求梯度的程式碼實現
引數的梯度通過誤差反向傳播法(反向傳播)求出,通過把正向傳播和反向傳播組裝在一起完成。因為已經再各層中正確實現了正向傳播和反向傳播功能,這裡只需要以合適的順序呼叫即可。最後把各個權重引數梯度儲存到grads字典中。

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 為了匯入父目錄的檔案而進行的設定
import pickle
import numpy as np
from collections import OrderedDict
from common.layers import *
from common.gradient import numerical_gradient


class SimpleConvNet:
    """簡單的ConvNet

    conv - relu - pool - affine - relu - affine - softmax
    
    Parameters
    ----------
    input_size : 輸入大小(MNIST的情況下為784)
    hidden_size_list : 隱藏層的神經元數量的列表(e.g. [100, 100, 100])
    output_size : 輸出大小(MNIST的情況下為10)
    activation : 'relu' or 'sigmoid'
    weight_init_std : 指定權重的標準差(e.g. 0.01)
        指定'relu'或'he'的情況下設定“He的初始值”
        指定'sigmoid'或'xavier'的情況下設定“Xavier的初始值”
    """
    def __init__(self, input_dim=(1, 28, 28), 
                 conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1},
                 hidden_size=100, output_size=10, weight_init_std=0.01):
        filter_num = conv_param['filter_num']
        filter_size = conv_param['filter_size']
        filter_pad = conv_param['pad']
        filter_stride = conv_param['stride']
        input_size = input_dim[1]
        conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1
        pool_output_size = int(filter_num * (conv_output_size/2) * (conv_output_size/2))

        # 初始化權重
        self.params = {}
        self.params['W1'] = weight_init_std * \
                            np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
        self.params['b1'] = np.zeros(filter_num)
        self.params['W2'] = weight_init_std * \
                            np.random.randn(pool_output_size, hidden_size)
        self.params['b2'] = np.zeros(hidden_size)
        self.params['W3'] = weight_init_std * \
                            np.random.randn(hidden_size, output_size)
        self.params['b3'] = np.zeros(output_size)

        # 生成層
        self.layers = OrderedDict()
        self.layers['Conv1'] = Convolution(self.params['W1'], self.params['b1'],
                                           conv_param['stride'], conv_param['pad'])
        self.layers['Relu1'] = Relu()
        self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
        self.layers['Affine1'] = Affine(self.params['W2'], self.params['b2'])
        self.layers['Relu2'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3'])

        self.last_layer = SoftmaxWithLoss()

    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)

        return x

    def loss(self, x, t):
        """求損失函式
        引數x是輸入資料、t是教師標籤
        """
        y = self.predict(x)
        return self.last_layer.forward(y, t)

    def accuracy(self, x, t, batch_size=100):
        if t.ndim != 1 : t = np.argmax(t, axis=1)
        
        acc = 0.0
        
        for i in range(int(x.shape[0] / batch_size)):
            tx = x[i*batch_size:(i+1)*batch_size]
            tt = t[i*batch_size:(i+1)*batch_size]
            y = self.predict(tx)
            y = np.argmax(y, axis=1)
            acc += np.sum(y == tt) 
        
        return acc / x.shape[0]

    def numerical_gradient(self, x, t):
        """求梯度(數值微分)

        Parameters
        ----------
        x : 輸入資料
        t : 教師標籤

        Returns
        -------
        具有各層的梯度的字典變數
            grads['W1']、grads['W2']、...是各層的權重
            grads['b1']、grads['b2']、...是各層的偏置
        """
        loss_w = lambda w: self.loss(x, t)

        grads = {}
        for idx in (1, 2, 3):
            grads['W' + str(idx)] = numerical_gradient(loss_w, self.params['W' + str(idx)])
            grads['b' + str(idx)] = numerical_gradient(loss_w, self.params['b' + str(idx)])

        return grads

    def gradient(self, x, t):
        """求梯度(誤差反向傳播法)

        Parameters
        ----------
        x : 輸入資料
        t : 教師標籤

        Returns
        -------
        具有各層的梯度的字典變數
            grads['W1']、grads['W2']、...是各層的權重
            grads['b1']、grads['b2']、...是各層的偏置
        """
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.last_layer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 設定
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db
        grads['W2'], grads['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W3'], grads['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads
        
    def save_params(self, file_name="params.pkl"):
        params = {}
        for key, val in self.params.items():
            params[key] = val
        with open(file_name, 'wb') as f:
            pickle.dump(params, f)

    def load_params(self, file_name="params.pkl"):
        with open(file_name, 'rb') as f:
            params = pickle.load(f)
        for key, val in params.items():
            self.params[key] = val

        for i, key in enumerate(['Conv1', 'Affine1', 'Affine2']):
            self.layers[key].W = self.params['W' + str(i+1)]
            self.layers[key].b = self.params['b' + str(i+1)]


CNN的視覺化
學習前的濾波器是隨機進行初始化的 所以在黑白的濃淡上沒有規律可循,但學習後的濾波器變成了有規律的影象。對於有規律的濾波器是在觀察邊緣和斑塊,會對圖中對應方向的邊緣有響應。
卷積層的濾波器會提取邊緣或者斑塊等原始資訊,剛才實現的CNN會將這些原始資訊傳遞給後面的層

具有代表性的CNN
1、LeNet
具有連續的卷積層和池化層,最後經過全連線層輸出結果。使用sigmoid函式,原始的LeNet中使用子取樣縮小中間資料的大小。
2、AlexNet
有多個卷積層和池化層,最後經由全連線層輸出結果。啟用函式使用ReLU,使用進行區域性正規化的LRN,使用Dropout層