天天看點

自己動手實作深度學習架構-6 卷積層和池化層目标卷積層的設計和實作最大池化層的設計和實作驗證

代碼倉庫: https://github.com/brandonlyg/cute-dl

(轉載請注明出處!)

目标

        上個階段使用MLP模型在在MNIST資料集上實作了92%左右的準确率,達到了tensorflow同等模型的水準。這個階段要讓cute-dl架構支援最簡單的卷積神經網絡, 并在MNIST和CIFA10資料上驗證,具體來說要達到如下目标:

  1. 添加2D卷積層。
  2. 添加2D最大池化層。
  3. CNN模型在MNIST資料集上達到99%以上的準确率。
  4. CNN模型在CIFA10資料集上達到70%以上在準确率。

卷積層的設計和實作

卷積運算

        卷積運算有兩個關鍵要素: 卷積核(過濾器), 卷積運算步長。如果卷積運算的目标是二維的那麼卷積核可以用矩陣表示,卷積運算步長可以用二維向量。例如用kernel_size=(3,3)表示卷積核的尺寸,strides=(1,1)表示卷積運算的步長, 假如卷積核是這樣的:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-FyIuVwhj-1589796084050)(https://img2020.cnblogs.com/blog/895062/202005/895062-20200518175425078-2061957728.png)]

        可以把它看成 R 3 X 3 R^{3 X 3} R3X3矩陣。在步長strides=(1,1)的情況下卷積運算如下所示:

自己動手實作深度學習架構-6 卷積層和池化層目标卷積層的設計和實作最大池化層的設計和實作驗證

        其中

128 ∗ 0 + 97 ∗ 1 + 53 ∗ 0 + 35 ∗ 1 + 22 ∗ 0 + 25 ∗ 1 + 37 ∗ 0 + 24 ∗ 1 + 28 ∗ 0 = 181 97 ∗ 0 + 53 ∗ 1 + 201 ∗ 0 + 22 ∗ 1 + 25 ∗ 0 + 200 ∗ 1 + 24 ∗ 0 + 28 ∗ 1 + 197 ∗ 0 = 303 . . . 28 ∗ 0 + 197 ∗ 1 + 182 ∗ 0 + 92 ∗ 1 + 195 ∗ 0 + 179 ∗ 1 + 100 ∗ 0 + 192 ∗ 1 + 177 ∗ 0 = 660 \begin{matrix} 128*0 + 97*1 + 53*0 + 35*1 + 22*0 + 25*1 + 37*0 + 24*1 + 28 * 0 = 181 \\ 97*0 + 53*1 + 201*0 + 22*1 + 25*0 + 200*1 + 24*0 + 28*1 + 197 * 0 = 303 \\ ... \\ \\ 28*0 + 197*1 + 182*0 + 92*1 + 195*0 + 179*1 + 100*0 + 192*1 + 177 * 0 = 660 \end{matrix} 128∗0+97∗1+53∗0+35∗1+22∗0+25∗1+37∗0+24∗1+28∗0=18197∗0+53∗1+201∗0+22∗1+25∗0+200∗1+24∗0+28∗1+197∗0=303...28∗0+197∗1+182∗0+92∗1+195∗0+179∗1+100∗0+192∗1+177∗0=660​

       (注意示意圖中最後次運算計算有誤應該是660)

        這個卷積運算輸入的是一個高h=5, 寬w=5的特征圖, 卷積運算輸出的是一個高h_=3, 寬w_=3的特征圖。 如果已知w, h, kernel_size= ( h k , w k ) (h_k, w_k) (hk​,wk​), strides= ( h s , w s ) (h_s, w_s) (hs​,ws​), 那麼輸出特征圖的高和寬分别為:

h _ = h − h k h s + 1 w _ = w − w k w s + 1 h , w , h k , w k , h s , w s 都是正整數, , 1 < = h k < h , 1 < = w k < w , h _ , w _ 向 下 取 整 \begin{matrix} h\_ = \frac{h - h_k}{h_s} + 1 \\ w\_= \frac{w - w_k}{w_s} + 1 \\ \\ h, w, h_k, w_k, h_s, w_s \text{都是正整數,}, 1<=h_k<h, 1<=w_k<w, h\_, w\_向下取整 \end{matrix} h_=hs​h−hk​​+1w_=ws​w−wk​​+1h,w,hk​,wk​,hs​,ws​都是正整數,,1<=hk​<h,1<=wk​<w,h_,w_向下取整​

        由上面兩個等式可以得出: h_ <= h, w_ <= w。經過卷積運算後特征圖會變小。

卷積層設計

        卷積層的設計主要考慮以下幾個問題:

  1. 輸入層的圖像資料可能有多個(顔色)通道, 卷積層要能夠處理多通道的輸入。
  2. 當疊加多個卷積層時,一般情況下特征圖會逐層變小, 可以疊加的層數很少, 進而限制了模型的表達能力。卷積層要能夠通過填充改變輸入特征圖的大小, 控制輸出特征圖的大小。
  3. 如果用循環實作卷積運算, 會導緻性能很低, 要把卷積運算轉換成矩陣運算。
  4. 卷積層輸出的特征圖最後會輸入到全連接配接層中, 而全連接配接層值隻支援矩陣輸入,是以需要一個過渡層把特征圖展平成矩陣。

初始化參數

        卷積層輸入是特征圖,它的形狀為(m,c,h,w), 其中m是批次大小,c是通道數,h,w分别為特征圖的高、寬。輸出也是特征圖,形狀為(m,c_,h_,w_)。前面已經給出了h_和h, w_和w的關系,已知h,w情況下确定h_,w_還需要給出 h k , w k , h s , w s h_k,w_k, h_s, w_s hk​,wk​,hs​,ws​, c_是獨立的量,可以根據需要設定。是以卷積層初始化時需要給出的層參數有:卷積核大小kernel_size= ( h k , w k ) (h_k, w_k) (hk​,wk​), 卷積運算步長strides= ( h s , w s ) (h_s, w_s) (hs​,ws​), 輸出通道數c_。 另外還需要padding參數指定填充方式來控制輸出特征圖的大小。

填充

        一般情況下, 特征圖經過卷積層時會縮小,當h=h_時, 則有:

h k = h ( 1 − h s ) + h s h_k = h(1-h_s) + hs hk​=h(1−hs​)+hs

        其中 0 < h s h_s hs​ < h, 隻有當 h s h_s hs​=1時這個等式才有意義, h k = 1 h_k=1 hk​=1。同理可以得到當w=w_時, w k = w s = 1 w_k=w_s=1 wk​=ws​=1。是以在沒有填充的情況下如果要得到大小不變的輸出,必須把卷積核設定成kernel_size=(1,1), 步長設定成strides=(1,1), 這限制了卷積層的表達能力。

        為了能夠比較自由地設定kernel_size和strides, 我們把特征圖輸入輸出大小關系略作調整:

h _ = h + 2 ∗ h p − h k h s + 1 w _ = w + 2 ∗ w p − w k w s + 1 \begin{matrix} h\_ = \frac{h + 2*h_p - h_k}{h_s} + 1 \\ w\_= \frac{w + 2*w_p - w_k}{w_s} + 1 \\ \\ \end{matrix} h_=hs​h+2∗hp​−hk​​+1w_=ws​w+2∗wp​−wk​​+1​

        其中 h p , w p h_p, w_p hp​,wp​分别是在高度和寬度上的填充。在高度上,需要在頂部和底部分别填充 h p h_p hp​的高度。在寬度上也是一樣。

        以高度方向的填充為例:

h p = h ( h s − 1 ) + h k − h s 2 h_p = \frac{h(h_s-1) + h_k - h_s}{2} hp​=2h(hs​−1)+hk​−hs​​

        如果 h s = 1 h_s=1 hs​=1, h k h_k hk​選擇[1, h]之内的任意一個奇數都等讓上式有意義。當 h s > 1 h_s > 1 hs​>1時, h k h_k hk​會有更多的選擇。然而在工程上,太多的選擇并不一定是好事。太多的選擇可能意味着有很多方法可以處理目标問題,也有肯能意味着很難找到有效的方法。

        前面讨論了通過填充實作h_=h, w_=w的情況。 填充也可以實作h_>h, w_>h, 這種情況沒有多大意義,這裡不予考慮。是以padding隻支援兩種不同的參數: 'valid’不填充; 'same’填充,使輸入輸出特征圖同樣大。

把卷積運算轉換成矩陣運算

        一個輸入輸出别為 ( m , c , h , w ) (m, c, h, w) (m,c,h,w), ( m , c _ , h _ , w _ ) (m, c\_, h\_, w\_) (m,c_,h_,w_)的卷積層, 可以把權重參數W的形狀設計成 ( c ∗ h k ∗ w k , c _ ) (c*h_k*w_k, c\_) (c∗hk​∗wk​,c_), 輸入特征圖轉換成卷積矩陣F, 形狀為 ( m ∗ w _ ∗ h _ , c ∗ h k ∗ w k ) (m*w\_*h\_, c*h_k*w_k) (m∗w_∗h_,c∗hk​∗wk​), W, F進行矩陣運算,轉換成輸出特征圖的步驟如下:

  • W, F進行矩陣運算得到形狀為 ( m ∗ w _ ∗ h _ , c _ ) (m*w\_*h\_, c\_) (m∗w_∗h_,c_)矩陣。
  • 轉換形狀 ( m ∗ w _ ∗ h _ , c _ ) (m*w\_*h\_, c\_) (m∗w_∗h_,c_)-> ( m , h _ , w _ , c _ ) (m, h\_, w\_, c\_) (m,h_,w_,c_)。
  • 移動通道次元 ( m , h _ , w _ , c _ ) (m, h\_, w\_, c\_) (m,h_,w_,c_)-> ( m , c _ , h _ , w _ , ) (m, c\_, h\_, w\_,) (m,c_,h_,w_,)。

卷積層實作

        卷積層代碼位于cutedl/cnn_layers.py中, 類名為Conv2D, 它支援2維特征圖。

初始化

'''
  channels 輸出通道數 int
  kernel_size 卷積核形狀 (kh, kw)
  strids  卷積運算步長(sh, sw)
  padding 填充方式 'valid': 步填充. 'same': 使輸出特征圖和輸入特征圖形狀相同
  inshape 輸入形狀 (c, h, w)
          c 輸入通道數
          h 特征圖高度
          w 特征圖寬度
  kernel_initialier 卷積核初始化器
          uniform 均勻分布
          normal  正态分布
  bias_initialier 偏移量初始化器
          uniform 均勻分布
          normal 正态分布
          zeros  0
  '''
  def __init__(self, channels, kernel_size, strides=(1,1),
              padding='same',
              inshape=None,
              activation='relu',
              kernel_initializer='uniform',
              bias_initializer='zeros'):
      #pdb.set_trace()
      self.__ks = kernel_size
      self.__st = strides
      self.__pad = (0, 0)
      self.__padding = padding

      #參數
      self.__W = self.weight_initializers[kernel_initializer]
      self.__b = self.bias_initializers[bias_initializer]

      #輸入輸出形狀
      self.__inshape = (-1, -1, -1)
      self.__outshape = None

      #輸出形狀
      outshape = self.check_shape(channels)
      if outshape is None or type(channels) != type(1):
          raise Exception("invalid channels: "+str(channels))

      self.__outshape = outshape

      #輸入形狀
      inshape = self.check_shape(inshape)
      if self.valid_shape(inshape):
          self.__inshape = self.check_shape(inshape)
          if self.__inshape is None or len(self.__inshape) != 3:
              raise Exception("invalid inshape: "+str(inshape))

          outshape, self.__pad = compute_2D_outshape(self.__inshape, self.__ks, self.__st, self.__padding)
          self.__outshape = self.__outshape + outshape

      super().__init__(activation)

      self.__in_batch_shape = None
      self.__in_batch = None
           

        如目前層是輸入層, 需要inshape參數,在初始類初始化時會調用compute_2D_outshape方法計算輸出形狀,如果目前層不是輸入層,會在set_prev方法中計算輸出形狀。下面是compute_2D_outshape函數的實作:

'''
計算2D卷積層的輸輸出和填充
'''
def compute_2D_outshape(inshape, kernel_size, strides, padding):
    #pdb.set_trace()
    _, h, w = inshape
    kh, kw = kernel_size
    sh, sw = strides

    h_ = -1
    w_ = -1
    pad = (0, 0)
    if 'same' == padding:
        #填充, 使用輸入輸出形狀一緻
        _, h_, w_ = inshape
        pad = (((h_-1)*sh - h + kh )//2, ((w_-1)*sw - w + kw)//2)
    elif 'valid' == padding:
        #不填充
        h_ = (h - kh)//sh + 1
        w_ = (w - kw)//sw + 1
    else:
        raise Exception("invalid padding: "+padding)

    #pdb.set_trace()
    outshape = (h_, w_)
    return outshape, pad
           

        這個函數除了傳回輸出形狀還傳回填充值,這是因為,特征圖和矩陣之間進行轉換時需要知道填充的大小。

卷積運算

        卷積運算會在向前傳播是執行,代碼如下:

'''
  向前傳播
  in_batch: 一批輸入資料
  training: 是否正在訓練
  '''
  def forward(self, in_batch, training=False):
      #pdb.set_trace()
      W = self.__W.value
      b = self.__b.value
      self.__in_batch_shape = in_batch.shape

      #把輸入特征圖展開成卷積運算的矩陣矩陣(m*h_*w_, c*kh*kw)
      in_batch = img2D_mat(in_batch, self.__ks, self.__pad, self.__st)
      #計算輸出值(m*h_*w_, c_) = (m*h_*w_, c*kh*kw) @ (c*kh*kw, c_) + (c_,)
      out = in_batch @ W + b
      #把(m*h_*w_, c_) 轉換成(m, h_, w_, c_)
      c_, h_, w_ = self.__outshape
      out = out.reshape((-1, h_, w_, c_))
      #把輸出值還原成(m, c_, h_, w_)
      out = np.moveaxis(out, -1, 1)

      self.__in_batch = in_batch

      return self.activation(out)
           

        其中img2D_mat函數把輸入特征圖轉換成用于卷積運算的矩陣,這個函數的實作如下:

'''
把2D特征圖轉換成友善卷積運算的矩陣, 形狀(m*h_*w_, c*kh*kw)
img 特征圖 shape=(m,c,h,w)
kernel_size 核形狀 shape=(kh, kw)
pad 填充大小 shape=(ph, pw)
strides 步長 shape=(sh, sw)
'''
def img2D_mat(img, kernel_size, pad, strides):
    #pdb.set_trace()
    kh, kw = kernel_size
    ph, pw = pad
    sh, sw = strides
    #pdb.set_trace()
    m, c, h, w = img.shape
    kh, kw = kernel_size

    #得到填充的圖
    pdshape = (m, c) + (h + 2*ph, w + 2*pw)
    #得到輸出大小
    h_ = (pdshape[2] - kh)//sh + 1
    w_ = (pdshape[3] - kw)//sw + 1
    #填充
    padded = np.zeros(pdshape)
    padded[:, :, ph:(ph+h), pw:(pw+w)] = img

    #轉換成卷積矩陣(m, h_, w_, c, kh, kw)
    #pdb.set_trace()
    out = np.zeros((m, h_, w_, c, kh, kw))
    for i in range(h_):
        for j in range(w_):
            #(m, c, kh, kw)
            cov = padded[:, :, i*sh:i*sh+kh, j*sw:j*sw+kw]
            out[:, i, j] = cov

    #轉換成(m*h_*w_, c*kh*kw)
    out = out.reshape((-1, c*kh*kw))

    return out
           

反向傳播

        方向方向傳播沒什麼特别的地方,主要把梯度矩陣還原到特征圖上, 代碼如下。

'''
矩陣形狀的梯度轉換成2D特征圖梯度
mat 矩陣梯度 shape=(m*h_*w_, c*kh*kw)
特征圖形狀 imgshape=(m, c, h, w)
'''
def matgrad_img2D(mat, imgshape, kernel_size, pad, strides):
    #pdb.set_trace()
    m, c, h, w = imgshape
    kh, kw = kernel_size
    sh, sw = strides
    ph, pw = pad

    #得到填充形狀
    pdshape = (m, c) + (h + 2*ph, w + 2 * pw)
    #得到輸出大小
    h_ = (pdshape[2] - kh)//sh + 1
    w_ = (pdshape[3] - kw)//sw + 1

    #轉換(m*h_*w_, c*kh*kw)->(m, h_, w_, c, kh, kw)
    mat = mat.reshape(m, h_, w_, c, kh, kw)

    #還原成填充後的特征圖
    padded = np.zeros(pdshape)
    for i in range(h_):
        for j in range(w_):
            #(m, c, kh, kw)
            padded[:, :, i*sh:i*sh+kh, j*sw:j*sw+kw] += mat[:, i, j]

    #pdb.set_trace()
    #得到原圖(m,c,h,w)
    out = padded[:, :, ph:ph+h, pw:pw+w]

    return out
           

最大池化層的設計和實作

最大池化運算

        最大池化計算和卷積運算算的過程幾乎一樣,隻有一點不同,卷積運算是把一個卷積核矩形區域的元素、權重參數按元素相乘後取和,池化層沒有權重參數,它的運算結果是取池矩形區域内的最大元素值。下面是池化運算涉及到的概念:

  1. pool_size: 池大小, 形如 ( h p , w p ) (h_p, w_p) (hp​,wp​), 其中 h p , w p h_p, w_p hp​,wp​是池的高度和寬度。pool_size含義和卷積層的kernel_size類似.
  2. strides: 步長。和卷積層一樣。
  3. padding: 填充方式,和卷積層一樣。

        假設最大池化層的參數為: pool_size=(2,2), strides=(1,1), padding=‘valid’.

        輸入資料為:

自己動手實作深度學習架構-6 卷積層和池化層目标卷積層的設計和實作最大池化層的設計和實作驗證

        池化運算之後的輸入為:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-mp4QiSoY-1589796084065)(https://img2020.cnblogs.com/blog/895062/202005/895062-20200518175536428-1325958594.jpg)]

最大池化層實作

        最大池化層的代碼在cutedl/cnn_layers.py中,類名: MaxPool2D.

        相比于Conv2D, MaxPool2D要簡單許多,其代碼主要集中在forward和backward方法中。

        forward實作:

def forward(self, in_batch, training=False):
    m, c, h, w = in_batch.shape
    _, h_, w_ = self.outshape
    kh, kw = self.__ks
    #把特征圖轉換成矩陣(m, c, h, w)->(m*h_*w_, c*kh*kw)
    in_batch = img2D_mat(in_batch, self.__ks, self.__pad, self.__st)
    #轉換形狀(m*w_*h_, c*kh*kw)->(m*h_*w_*c,kh*kw)
    in_batch = in_batch.reshape((m*h_*w_*c, kh*kw))
    #得到最大最索引
    idx = in_batch.argmax(axis=1).reshape(-1, 1)
    #轉成in_batch相同的形狀
    idx = idx @ np.ones((1, in_batch.shape[1]))
    temp = np.ones((in_batch.shape[0], 1)) @ np.arange(in_batch.shape[1]).reshape(1, -1)
    #得到boolean的标記
    self.__mark = idx == temp

    #得到最大值
    max = in_batch[self.__mark]
    max = max.reshape((m, h_, w_, c))
    max = np.moveaxis(max, -1, 1)

    return max
           

        這個方法的關鍵是得到最大值索引__mark, 有了它,在反向傳播的時候就能知道梯度值和輸入元素的對應關系。

驗證

        卷積層驗證代碼位于examples/cnn目錄下,mnis_recognize.py是手寫數字識别模型,cifar10_fit.py是圖檔分類模型,下面是兩個模型的訓練報告。

        cifar10資料集下載下傳連結: https://pan.baidu.com/s/1FIBWvJ446ta7CI5_RHdeOw 密碼: mhni

mnist資料集上的分類模型

        模型定義:

model = Model([
              cnn.Conv2D(32, (5,5), inshape=inshape),
              cnn.MaxPool2D((2,2), strides=(2,2)),
              cnn.Conv2D(64, (5,5)),
              cnn.MaxPool2D((2,2), strides=(2,2)),
              nn.Flatten(),
              nn.Dense(1024),
              nn.Dropout(0.5),
              nn.Dense(10)
          ])
           

        訓練報告:

自己動手實作深度學習架構-6 卷積層和池化層目标卷積層的設計和實作最大池化層的設計和實作驗證

        經過2.6小時的訓練,模型有了99.2%的準确率,達到預期目标。

cifar10資料集上的分類模型

        模型定義:

model = Model([
              cnn.Conv2D(32, (3,3), inshape=inshape),
              cnn.MaxPool2D((2,2), strides=(2,2)),
              cnn.Conv2D(64, (3,3)),
              cnn.MaxPool2D((2,2), strides=(2,2)),
              cnn.Conv2D(64, (3, 3)),
              nn.Flatten(),
              nn.Dense(64),
              nn.Dropout(0.5),
              nn.Dense(10)
          ])
           

        訓練報告:

自己動手實作深度學習架構-6 卷積層和池化層目标卷積層的設計和實作最大池化層的設計和實作驗證

        經過9小時的訓練,模型有了72.2%的準确率,達到預期目标。

繼續閱讀