機器學習小社-6

講師 000

  • 你可以叫我 000 / Lucas

  • 建國中學資訊社37th學術長

  • 建國中學電子計算機研習社44th學術

  • 校際交流群創群者

  • 不會音遊不會競程不會數學的笨

  • 資訊技能樹亂點但都一樣爛

  • 專案爛尾大師

  • IZCC x SCINT x Ruby Taiwan 聯課負責人

講師 章魚

  • 建國中學電子計算機研習社44th學術+總務

  • 是的,我是總務。在座的你各位下次記得交社費束脩給我

  • 技能樹貧乏

  • 想拿機器學習做專題結果只學會使用API

  • 上屆社展烙跑到資訊社的叛徒

  • 科班墊神

  • 卷積神經網路

  • 卷積

  • 池化

  • 前向傳播

  • 反向傳播

  • 實作

目錄

卷積神經網路

一張圖在經過卷積層時

會被截取出許多特徵

並且將圖片資訊壓縮

最後將這些圖片放入我們原本的神經網路

也稱為全連接層(Fully-Connected)

卷積

\displaystyle (f*g)(t)\triangleq \int _{-\infty }^{\infty }f(\tau )g(t-\tau )\,\mathrm {d} \tau

卷積

Convolution

\displaystyle (f*g)(t)\triangleq \int _{-\infty }^{\infty }f(\tau )g(t-\tau )\,\mathrm {d} \tau

卷積

Convolution

看著是挺抽象

但是你可以發現當f, g皆為純量的離散資料時

也就是我們在做多維的離散卷積時

 

可以視為各元素積構成的新離散資料

卷積

Convolution

換成我們看得懂的形式也就是

卷積

Convolution

換成我們看得懂的形式也就是

卷積

Convolution

他可以讓一張圖片的每個像素進行一種固定改變

如銳化、浮雕

經常被使用在濾鏡上面

卷積

Convolution

import os

import cv2
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image


path = os.path.join(os.path.dirname(__file__), 'test.jpg')

x = Image.open(path)
x = np.asarray(x)
kernels = []

kernels.append(np.array([
    [0, 0, 0],
    [0, 1, 0],
    [0, 0, 0]
]))

kernels.append(np.array([
    [0, -1, 0],
    [-1, 5, -1],
    [0, -1, 0]
]))

kernels.append(np.array([
    [1, 1, 1],
    [1, -7, 1],
    [1, 1, 1]
]))

kernels.append(np.array([
    [-1, -1, -1],
    [-1, 8, -1],
    [-1, -1, -1]
]))

kernels.append(np.array([
    [-1, -1, 0],
    [-1, 0, 1],
    [0, 1, 1]
]))

for kernel in kernels:
    image = cv2.filter2D(x, -1, kernel)
    plt.imshow(image)
    plt.show()

池化

池化

Pooling

一種卷積變體

將卷積的kernel換成特定條件

來達到縮小圖片 保留資訊

Max Pooling: 取區塊內最大值

Mean/Average Pooling: 取區塊平均值

池化

Pooling

通常我們在卷積層會經過

一次卷積 一次池化

因此圖片會縮小尺寸

前向傳播

首先我們要知道卷積層像是神經元一樣

可以橫豎擴張

對同張圖過多個卷積層

產生出來的圖像分別向後傳遞

過好幾次卷積層

然後我們需要知道

不管是卷積還是池化

我們可以控制他在橫向與縱向上的步伐

以上述的卷積&池化為例

step_x = kernel_x \\ step_y = kernel_y

最後你就可以開始實作了

def conv(self, x: np.ndarray, kernel: np.ndarray) -> np.ndarray:
    c, cx, cy = \
    self.conv_kernel_size, self.conv_kernel_size[0], self.conv_kernel_size[1]
        
    output_shape = [int((x.shape[i]-c[i])/(self.conv_strides[i])+1) for i in [0, 1]]
    output = np.zeros(output_shape)
        
    for i, ix in enumerate(range(0, x.shape[0]-cx+1, self.conv_strides[0])):
        for j, jx in enumerate(range(0, x.shape[1]-cy+1, self.conv_strides[1])):
            output[i, j] = np.sum(x[ix:ix+cx, jx:jx+cy] * kernel)

    return output
def max_pooling(self, x: np.ndarray, pooling_maximum: list[list[tuple]]) -> np.ndarray:
    p, px, py = \
   	self.pool_kernel_size, self.pool_kernel_size[0], self.pool_kernel_size[1]
        
    output_shape = [int((x.shape[i]-p[i])/(self.pool_strides[i])+1) for i in [0, 1]]
    output = np.zeros(output_shape)
        
    for i, ix in enumerate(range(0, x.shape[0]-px+1, self.pool_strides[0])):
        pooling_maximum.append([])
        for j, jx in enumerate(range(0, x.shape[1]-px+1, self.pool_strides[1])):
            t = x[ix:ix+px, jx:jx+py]
            output[i, j] = np.max(t)
            pooling_maximum[i].append([])
            pooling_maximum[i][j].append(t.argmax())
                
    return output
def forward(self, x: np.ndarray) -> np.ndarray:
    assert x.shape[0] == self.data_size[0] * self.data_size[1]
    x = x.reshape(self.data_size[0], self.data_size[1])
    x_conv = []
    self.pooling_maximum = []
    self.X = []
        
    for i in range(len(self.kernels)):
        self.pooling_maximum.append([])
        _x = x.copy()
        self.X.append([])
        for j in range(len(self.kernels[i])):
            self.pooling_maximum[i].append([])
            self.X[i].append(_x.copy())
            _x = self.conv(_x, self.kernels[i][j])
            _x = self.max_pooling(_x, self.pooling_maximum[i][j])
            
        x_conv.extend(_x.reshape(-1))
            
    x_conv = np.array(x_conv).astype("float64")
    assert x_conv.shape[0] == self.layers[0]
    return super().forward(x_conv)

反向傳播

除了FC的反向傳播外

kernel也可以透過反向傳播更新

當然你也可以選擇固定kernel
來在特定資料上抓指定的特徵

卷積的部分

我們需要兩個操作

一個是更新參數(供當前的kernel更新)

一個是反向卷積(供更前面的資訊使用)

    def dconv(self, x: np.ndarray, dy: np.ndarray) -> np.ndarray:
        cx, cy = self.conv_kernel_size[0], self.conv_kernel_size[1]
        d_kernel = np.zeros(self.conv_kernel_size)

        for i, ix in enumerate(range(0, x.shape[0]-cx+1, self.conv_strides[0])):
            for j, jx in enumerate(range(0, x.shape[1]-cy+1, self.conv_strides[1])):
                d_kernel += x[ix:ix+cx, jx:jx+cy] * dy[i, j]
                
        return d_kernel

更新參數

	def bconv(self, x: np.ndarray, kernel: np.ndarray) -> np.ndarray:
        kernel = np.flip(np.flip(kernel, axis=0), axis=1)
        padded = np.pad(x, ((kernel.shape[0]-1, kernel.shape[0]-1), (kernel.shape[1]-1, kernel.shape[1]-1)), mode="constant")
        return self.conv(padded, kernel)

反向卷積

接著是池化

池化沒有需要更新的東西

所以做個反向池化即可

那麼問題來了

要怎麼反向呢

還記得我們紀錄了每次池化的最大值位置

argmax()

    def max_pooling(self, x: np.ndarray, pooling_maximum: list[list[tuple]]) -> np.ndarray:
        p, px, py = \
      	self.pool_kernel_size, self.pool_kernel_size[0], self.pool_kernel_size[1]
        
        output_shape = [int((x.shape[i]-p[i])/(self.pool_strides[i])+1) for i in [0, 1]]
        output = np.zeros(output_shape)
        
        for i, ix in enumerate(range(0, x.shape[0]-px+1, self.pool_strides[0])):
            pooling_maximum.append([])
            for j, jx in enumerate(range(0, x.shape[1]-px+1, self.pool_strides[1])):
                t = x[ix:ix+px, jx:jx+py]
                output[i, j] = np.max(t)
                pooling_maximum[i].append([])
                pooling_maximum[i][j].append(t.argmax())
                
        return output

還記得我們紀錄了每次池化的最大值位置

argmax()

而逆向操作時就是把池化後的值填回去

當初拿值的位置

Q: 其他資訊呢?
A: 跟Loss沒關聯 更新不會用到

def bmax_pooling(self, x: np.ndarray, pooling_maximum: list[list[tuple]]) -> np.ndarray:
    p, px, py = self.pool_kernel_size, self.pool_kernel_size[0], self.pool_kernel_size[1]
    output_shape = [int(((x.shape[i]-1)*self.pool_strides[i])+p[i]) for i in [0, 1]]
    output = np.zeros(output_shape)
        
    for i in range(x.shape[0]):
        for j in range(x.shape[1]):
            argmax = pooling_maximum[i][j][0]
            output[i*px+argmax//px, j*py+argmax%px] = x[i, j]
                
    return output
def backward(self, y: np.ndarray) -> None:
    x_conv = super().backward(y)
    size = self.conv_data_size / self.conv_layer[1]
    x_conv = x_conv.reshape(self.conv_layer[1], int(np.sqrt(size)), int(np.sqrt(size)))
        
    for i in reversed(range(len(self.kernels))):
        x = x_conv[i].copy()
        for j in reversed(range(len(self.kernels[i]))):
            x = self.bmax_pooling(x, self.pooling_maximum[i][j])
            self.kernels[i][j] -= self.learning_rate * self.dconv(self.X[i][j], x)
            x = self.bconv(x, self.kernels[i][j])

實作

你可以

但如果要更有效率

你還可以

機器學習社課 第六堂

By lucasw

機器學習社課 第六堂

  • 143