手撕卷积神经网络+反向传播梯度更新

手撕卷积神经网络

一个简单的卷积神经网络,就4层,分别是InputLayer, conv_layer, hidden_layer(全连接层 + ReLU激活), output_layer

import numpy as np

class InputLayer:
    """输入层 - 简单的数据传递层"""
    def __init__(self):
        pass
    
    def forward(self, x):
        """前向传播:直接传递输入"""
        self.x = x
        return x
    
    def backward(self, dout):
        """反向传播:直接传递梯度"""
        return dout
    
    def update_params(self):
        """输入层没有参数需要更新"""
        pass


class HiddenLayer:
    """隐藏层 - 全连接层 + ReLU激活"""
    def __init__(self, input_size, output_size, learning_rate=0.001):
        self.learning_rate = learning_rate
        
        # 初始化权重和偏置
        self.weights = np.random.randn(input_size, output_size) * 0.01
        self.bias = np.zeros((1, output_size))
        
        # 梯度缓存
        self.dweights = np.zeros_like(self.weights)
        self.dbias = np.zeros_like(self.bias)
        
        # 前向传播缓存
        self.x = None
        self.z = None  # 激活前的值
        self.a = None  # 激活后的值
        self.original_shape = None  # 保存原始形状以便反向传播时恢复
    
    def forward(self, x):
        """前向传播"""
        # 保存原始形状
        self.original_shape = x.shape
        
        # 如果输入是4D张量(来自卷积层),需要flatten
        if len(x.shape) == 4:
            batch_size = x.shape[0]
            x = x.reshape(batch_size, -1)
        
        self.x = x
        # 线性变换: z = x * W + b
        self.z = np.dot(x, self.weights) + self.bias
        # ReLU激活: a = max(0, z)
        self.a = np.maximum(0, self.z)
        return self.a
    
    def backward(self, dout):
        """反向传播"""
        batch_size = self.x.shape[0]
        
        # ReLU梯度
        da_dz = (self.z > 0).astype(float)
        dz = dout * da_dz
        
        # 计算梯度
        self.dweights = np.dot(self.x.T, dz)
        self.dbias = np.sum(dz, axis=0, keepdims=True)
        dx = np.dot(dz, self.weights.T)
        
        # 如果原始输入是4D,需要恢复形状
        if len(self.original_shape) == 4:
            dx = dx.reshape(self.original_shape)
        
        return dx
    
    def update_params(self):
        """更新参数"""
        self.weights -= self.learning_rate * self.dweights
        self.bias -= self.learning_rate * self.dbias


class OutputLayer:
    """输出层 - 全连接层 + Softmax激活"""
    def __init__(self, input_size, output_size, learning_rate=0.001):
        self.learning_rate = learning_rate
        
        # 初始化权重和偏置
        self.weights = np.random.randn(input_size, output_size) * 0.01
        self.bias = np.zeros((1, output_size))
        
        # 梯度缓存
        self.dweights = np.zeros_like(self.weights)
        self.dbias = np.zeros_like(self.bias)
        
        # 前向传播缓存
        self.x = None
        self.z = None
        self.a = None
    
    def forward(self, x):
        """前向传播"""
        # 如果输入是4D张量,需要flatten
        if len(x.shape) == 4:
            batch_size = x.shape[0]
            x = x.reshape(batch_size, -1)
        
        self.x = x
        # 线性变换
        self.z = np.dot(x, self.weights) + self.bias
        # Softmax激活
        self.a = self.softmax(self.z)
        return self.a
    
    def softmax(self, z):
        """Softmax激活函数"""
        exp_z = np.exp(z - np.max(z, axis=1, keepdims=True))  # 防止溢出
        return exp_z / np.sum(exp_z, axis=1, keepdims=True)
    
    def backward(self, dout):
        """反向传播"""
        batch_size = self.x.shape[0]
        
        # 对于softmax + 交叉熵,梯度简化为 dz = y_pred - y_true
        dz = dout
        
        # 计算梯度
        self.dweights = np.dot(self.x.T, dz)
        self.dbias = np.sum(dz, axis=0, keepdims=True)
        dx = np.dot(dz, self.weights.T)
        
        return dx
    
    def update_params(self):
        """更新参数"""
        self.weights -= self.learning_rate * self.dweights
        self.bias -= self.learning_rate * self.dbias


class Conv2D:
    def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, learning_rate=0.001):
        self.in_channels = in_channels 
        self.out_channels = out_channels
        self.kernel_size = kernel_size
        self.stride = stride
        self.padding = padding
        self.learning_rate = learning_rate

        # 初始化权重和偏置
        self.weights = np.random.randn(out_channels, in_channels, kernel_size, kernel_size) * 0.01
        self.bias = np.zeros((out_channels, 1))

        # 梯度缓存
        self.dweights = np.zeros_like(self.weights)
        self.dbias = np.zeros_like(self.bias)
        self.x = None

    def forward(self, x):
        self.x = x
        batch_size, in_channels, in_height, in_width = x.shape

        out_height = (in_height + 2 * self.padding - self.kernel_size) // self.stride + 1
        out_width = (in_width + 2 * self.padding - self.kernel_size) // self.stride + 1

        x_padded = np.pad(x, ((0, 0), (0, 0), (self.padding, self.padding), (self.padding, self.padding)), mode='constant')
        output = np.zeros((batch_size, self.out_channels, out_height, out_width))

        for b in range(batch_size):
            for oc in range(self.out_channels):
                for h in range(out_height):
                    for w in range(out_width):
                        h_start = h * self.stride
                        h_end = h_start + self.kernel_size
                        w_start = w * self.stride
                        w_end = w_start + self.kernel_size

                        window = x_padded[b, :, h_start:h_end, w_start:w_end]
                        output[b, oc, h, w] = np.sum(window * self.weights[oc]) + self.bias[oc]
        return output
    
    def backward(self, dout):
        batch_size, out_channels, out_height, out_width = dout.shape
        _, _, in_height, in_width = self.x.shape

        x_padded = np.pad(self.x, ((0, 0), (0, 0), (self.padding, self.padding), (self.padding, self.padding)), mode='constant')
        dx_padded = np.zeros_like(x_padded)
        self.dweights = np.zeros_like(self.weights)
        self.dbias = np.zeros_like(self.bias)

        for b in range(batch_size):
            for oc in range(out_channels):
                for h in range(out_height):
                    for w in range(out_width):
                        h_start = h * self.stride
                        h_end = h_start + self.kernel_size
                        w_start = w * self.stride
                        w_end = w_start + self.kernel_size

                        grad = dout[b, oc, h, w]
                        self.dweights[oc] += grad * x_padded[b, :, h_start:h_end, w_start:w_end]
                        self.dbias[oc] += grad
                        dx_padded[b, :, h_start:h_end, w_start:w_end] += grad * self.weights[oc]

        # 移除填充
        if self.padding > 0:
            dx = dx_padded[:, :, self.padding:-self.padding, self.padding:-self.padding]
        else:
            dx = dx_padded
        
        return dx
    
    def update_params(self):
        """更新参数"""
        self.weights -= self.learning_rate * self.dweights
        self.bias -= self.learning_rate * self.dbias


def loss_function(y_pred, y_true):
    """交叉熵损失函数"""
    batch_size = y_pred.shape[0]
    # 防止log(0)
    y_pred_clipped = np.clip(y_pred, 1e-15, 1 - 1e-15)
    # 计算交叉熵损失
    loss = -np.sum(y_true * np.log(y_pred_clipped)) / batch_size
    return loss


def loss_gradient(y_pred, y_true):
    """交叉熵损失的梯度"""
    # 对于softmax + 交叉熵,梯度为 y_pred - y_true
    return y_pred - y_true


if __name__ == "__main__":
    # 示例数据
    x0 = np.random.randn(2, 1, 28, 28)  # batch_size=2, channels=1, height=28, width=28
    y = np.array([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],  # one-hot编码
                  [0, 1, 0, 0, 0, 0, 0, 0, 0, 0]])
    
    # 创建网络层 - 修正网络架构
    input_layer = InputLayer()
    conv_layer = Conv2D(in_channels=1, out_channels=32, kernel_size=3, padding=1)
    hidden_layer = HiddenLayer(input_size=32*28*28, output_size=64)  # 32通道*28*28
    output_layer = OutputLayer(input_size=64, output_size=10)  # 10个类别
    
    print("Network architecture:")
    print("Input -> Conv2D -> HiddenLayer -> OutputLayer")
    print(f"Input shape: {x0.shape}")
    
    # 前向传播
    x1 = input_layer.forward(x0)       # (2, 1, 28, 28)
    print(f"After input_layer: {x1.shape}")
    
    x2 = conv_layer.forward(x1)        # (2, 32, 28, 28)
    print(f"After conv_layer: {x2.shape}")
    
    x3 = hidden_layer.forward(x2)      # (2, 64)
    print(f"After hidden_layer: {x3.shape}")
    
    y_pred = output_layer.forward(x3)  # (2, 10)
    print(f"After output_layer: {y_pred.shape}")

    # 计算损失和梯度
    loss = loss_function(y_pred, y)
    dout = loss_gradient(y_pred, y)

    print(f"\nLoss: {loss:.4f}")
    print(f"Predictions:\n{y_pred}")

    # 反向传播
    dx3 = output_layer.backward(dout)     
    dx2 = hidden_layer.backward(dx3)      
    dx1 = conv_layer.backward(dx2)        
    dx0 = input_layer.backward(dx1)       

    # 更新参数
    output_layer.update_params()
    hidden_layer.update_params()
    conv_layer.update_params()
    input_layer.update_params()

    print("Training step completed!")

反向传播梯度更新推导

以下是四层网络中每层的反向传播公式(以损失函数 L L 为起点,使用偏导数符号):

OutputLayer

正向传播公式:

  1. Z=XW+b Z = XW + b
  2. A=Softmax(Z) A = \text{Softmax}(Z)
  3. L=CrossEntropy(A,Y) L = \text{CrossEntropy}(A, Y)

反向传播公式(链式法则展开):

  1. ∂L∂Z=∂L∂A⋅∂A∂Z=A−Y \frac{\partial L}{\partial Z} = \frac{\partial L}{\partial A} \cdot \frac{\partial A}{\partial Z} = A - Y (Softmax+交叉熵的梯度简化)
  2. ∂L∂W=∂L∂Z⋅∂Z∂W=XT⋅∂L∂Z \frac{\partial L}{\partial W} = \frac{\partial L}{\partial Z} \cdot \frac{\partial Z}{\partial W} = X^T \cdot \frac{\partial L}{\partial Z}
  3. ∂L∂b=∂L∂Z⋅∂Z∂b=∑∂L∂Z \frac{\partial L}{\partial b} = \frac{\partial L}{\partial Z} \cdot \frac{\partial Z}{\partial b} = \sum \frac{\partial L}{\partial Z}
  4. ∂L∂X=∂L∂Z⋅∂Z∂X=∂L∂Z⋅WT \frac{\partial L}{\partial X} = \frac{\partial L}{\partial Z} \cdot \frac{\partial Z}{\partial X} = \frac{\partial L}{\partial Z} \cdot W^T

HiddenLayer

正向传播公式:

  1. Z=XW+b Z = XW + b
  2. A=ReLU(Z) A = \text{ReLU}(Z)

反向传播公式(链式法则展开):

  1. ∂L∂Z=∂L∂A⋅∂A∂Z=∂L∂A⋅ReLU′(Z) \frac{\partial L}{\partial Z} = \frac{\partial L}{\partial A} \cdot \frac{\partial A}{\partial Z} = \frac{\partial L}{\partial A} \cdot \text{ReLU}'(Z)
  2. ∂L∂W=∂L∂Z⋅∂Z∂W=XT⋅∂L∂Z \frac{\partial L}{\partial W} = \frac{\partial L}{\partial Z} \cdot \frac{\partial Z}{\partial W} = X^T \cdot \frac{\partial L}{\partial Z}
  3. ∂L∂b=∂L∂Z⋅∂Z∂b=∑∂L∂Z \frac{\partial L}{\partial b} = \frac{\partial L}{\partial Z} \cdot \frac{\partial Z}{\partial b} = \sum \frac{\partial L}{\partial Z}
  4. ∂L∂X=∂L∂Z⋅∂Z∂X=∂L∂Z⋅WT \frac{\partial L}{\partial X} = \frac{\partial L}{\partial Z} \cdot \frac{\partial Z}{\partial X} = \frac{\partial L}{\partial Z} \cdot W^T

Conv2D

正向传播公式:

  1. Z=X⋆W+b Z = X \star W + b (⋆ \star 表示卷积操作)

反向传播公式(链式法则展开):

  1. ∂L∂W=∂L∂Z⋅∂Z∂W=X⋆rot∂L∂Z \frac{\partial L}{\partial W} = \frac{\partial L}{\partial Z} \cdot \frac{\partial Z}{\partial W} = X \star_{\text{rot}} \frac{\partial L}{\partial Z} (旋转卷积,等价于 X X 与 ∂L∂Z \frac{\partial L}{\partial Z} 的卷积)
  2. ∂L∂b=∂L∂Z⋅∂Z∂b=∑∂L∂Z \frac{\partial L}{\partial b} = \frac{\partial L}{\partial Z} \cdot \frac{\partial Z}{\partial b} = \sum \frac{\partial L}{\partial Z}
  3. ∂L∂X=∂L∂Z⋅∂Z∂X=∂L∂Z⋆Wrot \frac{\partial L}{\partial X} = \frac{\partial L}{\partial Z} \cdot \frac{\partial Z}{\partial X} = \frac{\partial L}{\partial Z} \star W_{\text{rot}} (旋转权重卷积,等价于 ∂L∂Z \frac{\partial L}{\partial Z} 与旋转后的 W W 的卷积)

InputLayer

正向传播公式:

  1. A=X A = X

反向传播公式(链式法则展开):

  1. ∂L∂X=∂L∂A⋅∂A∂X=∂L∂A \frac{\partial L}{\partial X} = \frac{\partial L}{\partial A} \cdot \frac{\partial A}{\partial X} = \frac{\partial L}{\partial A} (恒等传递梯度)

说明

卷积层的特殊处理:

  • ⋆rot \star_{\text{rot}} 表示旋转180度后的卷积(等价于数学上的相关操作),用于计算权重梯度;
  • Wrot W_{\text{rot}} 表示旋转后的权重,用于计算输入梯度。
评论区已延迟加载滑到评论区附近时会自动加载,也可以现在手动打开。