手撕卷积神经网络+反向传播梯度更新
手撕卷积神经网络
一个简单的卷积神经网络,就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!")
反向传播梯度更新推导
以下是四层网络中每层的反向传播公式(以损失函数
OutputLayer
正向传播公式:
反向传播公式(链式法则展开):
(Softmax+交叉熵的梯度简化)
HiddenLayer
正向传播公式:
反向传播公式(链式法则展开):
Conv2D
正向传播公式:
( 表示卷积操作)
反向传播公式(链式法则展开):
(旋转卷积,等价于 与 的卷积) (旋转权重卷积,等价于 与旋转后的 的卷积)
InputLayer
正向传播公式:
反向传播公式(链式法则展开):
(恒等传递梯度)
说明
卷积层的特殊处理:
表示旋转180度后的卷积(等价于数学上的相关操作),用于计算权重梯度; 表示旋转后的权重,用于计算输入梯度。
评论区已延迟加载滑到评论区附近时会自动加载,也可以现在手动打开。



