在深度學習模型訓練過程中,梯度計算扮演著至關重要的角色,它引導模型引數朝著損失函式最小化的方向調整。本文將聚焦於卷積神經網路(CNN)的核心環節,詳細闡述如何使用 Python 和 NumPy 計算輸入和引數梯度,並逐步剖析 Conv2D、Flatten 層以及 Conv2DOperation 的具體實作方式。

藉由 Python 程式碼示例,我們將深入理解如何計算輸入資料和卷積核引數的梯度,這些梯度資訊在反向傳播過程中用於更新網路權重,進而提升模型的預測準確性。文章同時提供圖表輔助說明梯度計算流程,並討論如何運用 NumPy 的矩陣運算最佳化程式碼效能,提升訓練效率。此外,我們也將探討 Conv2D 和 Flatten 層的運作機制,以及它們在構建 CNN 模型中的作用,並提供一個簡化的 CNN 模型建構示例,幫助讀者快速上手實作。

程式碼實作

以下是計算輸入梯度的 Python 程式碼片段:

def _compute_grads_obs(inp, output_grad, param):
    param_size = param.shape[2]
    param_mid = param_size // 2
    img_size = inp.shape[1]
    in_channels = inp.shape[0]
    out_channels = param.shape[1]
    output_obs_pad = _pad_2d_channel(output_grad, param_mid)
    input_grad = np.zeros_like(inp)
    
    for c_in in range(in_channels):
        for c_out in range(out_channels):
            for i_w in range(inp.shape[1]):
                for i_h in range(inp.shape[2]):
                    for p_w in range(param_size):
                        for p_h in range(param_size):
                            input_grad[c_in, i_w, i_h] += \
                                output_obs_pad[c_out, i_w+param_size-p_w-1, i_h+param_size-p_h-1] * \
                                param[c_in, c_out, p_w, p_h]
    return input_grad

def _input_grad(inp: np.ndarray, output_grad: np.ndarray, param: np.ndarray) -> np.ndarray:
    grads = [_compute_grads_obs(inp[i], output_grad[i], param) for i in range(inp.shape[0])]
    return np.array(grads)

圖表翻譯

此圖示

  graph LR
    A[輸入觀測值] -->|shape: (C, H, W)|> B[取得引數尺寸]
    B --> C[計算引數中心點索引]
    C --> D[填充輸出梯度]
    D --> E[計算輸入梯度]
    E --> F[傳回輸入梯度]

圖表翻譯:

此圖表展示了計算輸入梯度的過程。首先,從輸入觀測值中取得其形狀(通道數、寬度和高度)。然後,計算引數的尺寸和中心點索引。接下來,對輸出梯度進行填充,以便於後續的計算。隨後,透過迴圈計算每個位置的梯度,並累加到輸入梯度中。最後,傳回計算出的輸入梯度。這個過程是卷積神經網路反向傳播中的一個關鍵步驟,用於更新模型引數以最小化損失函式。

卷積神經網路中的梯度計算

在卷積神經網路中,梯度計算是反向傳播演算法的關鍵步驟。下面,我們將實作卷積層引數的梯度計算。

引數梯度計算

給定輸入資料 inp、輸出梯度 output_grad 和卷積核引數 param,我們可以計算引數梯度如下:

def _param_grad(inp: np.ndarray, output_grad: np.ndarray, param: np.ndarray) -> np.ndarray:
    """
    計算卷積層引數的梯度。

    引數:
    - inp (np.ndarray): 輸入資料,形狀為 (in_channels, img_width, img_height)
    - output_grad (np.ndarray): 輸出梯度,形狀為 (out_channels, img_width, img_height)
    - param (np.ndarray): 卷積核引數,形狀為 (in_channels, out_channels, img_width, img_height)

    傳回:
    - param_grad (np.ndarray): 引數梯度,形狀與 param 相同
    """
    param_grad = np.zeros_like(param)
    param_size = param.shape[2]
    param_mid = param_size // 2
    img_size = inp.shape[2]
    in_channels = inp.shape[1]
    out_channels = output_grad.shape[1]

    # 對輸入資料進行填充
    inp_pad = _pad_conv_input(inp, param_mid)

    # 進行梯度計算
    for i in range(in_channels):
        for j in range(out_channels):
            for x in range(img_size):
                for y in range(img_size):
                    # 計算梯度
                    grad = output_grad[j, x, y] * inp_pad[i, x:x+param_size, y:y+param_size]
                    param_grad[i, j, :, :] += grad

    return param_grad

填充輸入資料

在進行梯度計算之前,我們需要對輸入資料進行填充,以確保卷積運算的正確性。以下是填充函式的實作:

def _pad_conv_input(inp: np.ndarray, pad_size: int) -> np.ndarray:
    """
    對輸入資料進行填充。

    引數:
    - inp (np.ndarray): 輸入資料
    - pad_size (int): 填充大小

    傳回:
    - inp_pad (np.ndarray): 填充後的輸入資料
    """
    inp_pad = np.pad(inp, ((0, 0), (pad_size, pad_size), (pad_size, pad_size)), mode='constant')
    return inp_pad

圖表解釋

以下是梯度計算過程的圖表解釋:

  graph LR
    A[輸入資料] -->|填充|> B[填充後輸入資料]
    B -->|梯度計算|> C[引數梯度]
    C -->|傳回|> D[最終結果]

圖表翻譯:

上述圖表展示了梯度計算過程。首先,輸入資料經過填充處理,然後進行梯度計算,最終傳回引數梯度。這個過程是卷積神經網路中反向傳播演算法的關鍵步驟。

Conv2D 層的實作

Conv2D 層是卷積神經網路(CNN)中的一個基本組成部分,負責對輸入資料進行卷積運算。以下是 Conv2D 層的實作:

class Conv2D(Layer):
    def __init__(self, 
                 out_channels: int, 
                 param_size: int, 
                 activation: Operation = Sigmoid(), 
                 flatten: bool = False) -> None:
        super().__init__()
        self.out_channels = out_channels
        self.param_size = param_size
        self.activation = activation
        self.flatten = flatten

    def _output(self) -> ndarray:
        # 進行卷積運算
        output = self._convolve(self.input)
        # 啟用啟用函式
        output = self.activation._output(output)
        # 如果需要,進行flatten運算
        if self.flatten:
            output = self._flatten(output)
        return output

    def _convolve(self, input: ndarray) -> ndarray:
        # 初始化輸出資料
        output = np.zeros((input.shape[0], self.out_channels, input.shape[2] - self.param_size + 1, input.shape[3] - self.param_size + 1))
        # 進行卷積運算
        for i in range(input.shape[0]):
            for c_in in range(input.shape[1]):
                for c_out in range(self.out_channels):
                    for o_w in range(input.shape[2] - self.param_size + 1):
                        for o_h in range(input.shape[3] - self.param_size + 1):
                            for p_w in range(self.param_size):
                                for p_h in range(self.param_size):
                                    output[i, c_out, o_w, o_h] += input[i, c_in, o_w + p_w, o_h + p_h] * self.params[c_in, c_out, p_w, p_h]
        return output

    def _flatten(self, input: ndarray) -> ndarray:
        # 進行flatten運算
        return input.reshape(input.shape[0], -1)

    def _input_grad(self, output_grad: ndarray) -> ndarray:
        # 計算輸入梯度
        input_grad = np.zeros_like(self.input)
        for i in range(self.input.shape[0]):
            for c_in in range(self.input.shape[1]):
                for c_out in range(self.out_channels):
                    for o_w in range(self.input.shape[2] - self.param_size + 1):
                        for o_h in range(self.input.shape[3] - self.param_size + 1):
                            for p_w in range(self.param_size):
                                for p_h in range(self.param_size):
                                    input_grad[i, c_in, o_w + p_w, o_h + p_h] += output_grad[i, c_out, o_w, o_h] * self.params[c_in, c_out, p_w, p_h]
        return input_grad

    def _param_grad(self, output_grad: ndarray) -> ndarray:
        # 計算引數梯度
        param_grad = np.zeros_like(self.params)
        for i in range(self.input.shape[0]):
            for c_in in range(self.input.shape[1]):
                for c_out in range(self.out_channels):
                    for o_w in range(self.input.shape[2] - self.param_size + 1):
                        for o_h in range(self.input.shape[3] - self.param_size + 1):
                            for p_w in range(self.param_size):
                                for p_h in range(self.param_size):
                                    param_grad[c_in, c_out, p_w, p_h] += self.input[i, c_in, o_w + p_w, o_h + p_h] * output_grad[i, c_out, o_w, o_h]
        return param_grad

Flatten 層的實作

Flatten 層是一種特殊的層,負責將輸入資料從三維陣列轉換為一維陣列。

class Flatten(Operation):
    def __init__(self):
        super().__init__()

    def _output(self) -> ndarray:
        # 進行flatten運算
        return self.input.reshape(self.input.shape[0], -1)

    def _input_grad(self, output_grad: ndarray) -> ndarray:
        # 計算輸入梯度
        return output_grad.reshape(self.input.shape)

Conv2DOperation 的實作

Conv2DOperation 是 Conv2D 層的核心,負責進行卷積運算。

class Conv2DOperation(Operation):
    def __init__(self, 
                 out_channels: int, 
                 param_size: int):
        super().__init__()
        self.out_channels = out_channels
        self.param_size = param_size

    def _output(self) -> ndarray:
        # 進行卷積運算
        output = np.zeros((self.input.shape[0], self.out_channels, self.input.shape[2] - self.param_size + 1, self.input.shape[3] - self.param_size + 1))
        for i in range(self.input.shape[0]):
            for c_in in range(self.input.shape[1]):
                for c_out in range(self.out_channels):
                    for o_w in range(self.input.shape[2] - self.param_size + 1):
                        for o_h in range(self.input.shape[3] - self.param_size + 1):
                            for p_w in range(self.param_size):
                                for p_h in range(self.param_size):
                                    output[i, c_out, o_w, o_h] += self.input[i, c_in, o_w + p_w, o_h + p_h] * self.params[c_in, c_out, p_w, p_h]
        return output

    def _input_grad(self, output_grad: ndarray) -> ndarray:
        # 計算輸入梯度
        input_grad = np.zeros_like(self.input)
        for i in range(self.input.shape[0]):
            for c_in in range(self.input.shape[1]):
                for c_out in range(self.out_channels):
                    for o_w in range(self.input.shape[2] - self.param_size + 1):
                        for o_h in range(self.input.shape[3] - self.param_size + 1):
                            for p_w in range(self.param_size):
                                for p_h in range(self.param_size):
                                    input_grad[i, c_in, o_w + p_w, o_h + p_h] += output_grad[i, c_out, o_w, o_h] * self.params[c_in, c_out, p_w, p_h]
        return input_grad

    def _param_grad(self, output_grad: ndarray) -> ndarray:
        # 計算引數梯度
        param_grad = np.zeros_like(self.params)
        for i in range(self.input.shape[0]):
            for c_in in range(self.input.shape[1]):
                for c_out in range(self.out_channels):
                    for o_w in range(self.input.shape[2] - self.param_size + 1):
                        for o_h in range(self.input.shape[3] - self.param_size + 1):
                            for p_w in range(self.param_size):
                                for p_h in range(self.param_size):
                                    param_grad[c_in, c_out, p_w, p_h] += self.input[i, c_in, o_w + p_w, o_h + p_h] * output_grad[i, c_out, o_w, o_h]
        return param_grad

圖表翻譯:

  graph LR
    A[Conv2D 層] --> B[Flatten 層]
    B --> C[輸出]
    A --> D[Conv2DOperation]
    D --> E[輸入梯度]
    E --> F[引數梯度]

內容解密:

上述程式碼實作了 Conv2D 層、Flatten 層和 Conv2DOperation。Conv2D 層負責進行卷積運算,Flatten 層負責將輸入資料從三維陣列轉換為一維陣列,Conv2DOperation 是 Conv2D 層的核心,負責進行卷積運算和計算梯度。這些層和操作共同構成了卷積神經網路的基礎。

卷積神經網路(Convolutional Neural Network, CNN)實作

在本文中,我們將實作一個簡單的卷積神經網路(CNN),並探討其運作原理。

卷積層(Conv2D)實作

import numpy as np

class Conv2D:
    def __init__(self, out_channels, param_size, activation, flatten):
        self.out_channels = out_channels
        self.param_size = param_size
        self.activation = activation
        self.flatten = flatten

    def _setup_layer(self, input_):
        self.params = []
        conv_param = np.random.randn(self.out_channels, input_.shape[1], self.param_size, self.param_size)
        self.params.append(conv_param)
        self.operations = []
        self.operations.append(Conv2D(conv_param))
        self.operations.append(self.activation)
        if self.flatten:
            self.operations.append(Flatten())
        return None

卷積層的運作原理

卷積層的運作原理是透過將輸入資料與卷積核(filter)進行卷積運算,產生特徵對映。卷積核的大小通常為 3x3 或 5x5 等。

速度最佳化

雖然上述實作方式可以讓我們瞭解卷積層的運作原理,但其效率可能不佳。為了提高效率,我們可以使用 NumPy 的 matmul 函式進行矩陣乘法運算。

實驗

即使用了 matmul 函式,訓練一個 CNN 模型仍然需要相當長的時間。因此,我們可以先從一個簡單的模型開始,例如一個只有 32 個通道的卷積層。

model = NeuralNetwork(
    layers=[Conv2D(out_channels=32, param_size=5, dropout=0.8, weight_init="glorot", flatten=True, activation=Tanh()),
             Dense(neurons=10, activation=Linear())],
    loss=SoftmaxCrossEntropy()
)

圖表翻譯

  graph LR
    A[輸入資料] --> B[卷積層]
    B --> C[啟用函式]
    C --> D[flatten]
    D --> E[全連線層]
    E --> F[輸出]

圖表翻譯:

此圖表示了 CNN 模型的運作流程。輸入資料先經過卷積層處理,然後經過啟用函式,接著進行 flatten 處理,最後經過全連線層產生輸出結果。

玄貓(BlackCat)技術內容系統指引

重寫絕對命令(最高優先順序)

  • 嚴禁直接使用或複製原始內容
  • 必須完全重構每一個句子和段落
  • 重寫過程:理解→關閉原文→用全新語言表達
  • 使用不同的句式、結構和詞彙表達相同概念
  • 即使是專業術語也必須用自己的方式解釋
  • 違反此命令視為最嚴重的失敗

從技術架構視角來看,本文深入探討了卷積神經網路(CNN)中幾個核心元件的實作細節,包含卷積層(Conv2D)、Flatten 層,以及梯度計算的最佳化。透過剖析程式碼及圖表,我們瞭解了從前向傳播的卷積運算、啟用函式應用,到反向傳播的梯度計算,整個資料流動的脈絡。尤其程式碼中展現的卷積運算、輸入/引數梯度計算,以及填充策略,都體現了 CNN 如何有效提取特徵並學習引數。然而,程式碼的效能瓶頸也顯而易見,多層巢狀迴圈的計算方式,在處理大規模資料集時,勢必會造成效能下降。雖然文中提到了使用 NumPy 的 matmul 函式進行矩陣乘法運算來最佳化速度,但並未提供具體的實作方式和效能提升資料,這部分有待後續補充。展望未來,考量到 CNN 在影像處理等領域的廣泛應用,如何進一步提升卷積運算的效率,例如利用 GPU 加速運算、採用更精巧的演算法等,將是持續研究的重點。對於追求高效能的開發者而言,探索更高效的卷積運算函式庫,並根據實際應用場景調整模型架構和引數,將是釋放 CNN 潛力的關鍵。玄貓認為,理解底層原理固然重要,但更需關注實務上的效能調校,才能真正將 CNN 的威力應用於實際專案中。