前馈神经网络(理论和从0手搓)

AI

什么是前馈神经网络

前馈神经网络(FFN),神经网络中最简单基础的一种

image-20260509215819065

信息只向前流动,经过输入层,隐藏层,输出层。没有任何反馈回路,是最基础的神经网络。
它的每一层只做一件事,输入,线性变换(加权求和),非线性激活,输出给下一层。

  1. 线性组合: z = w₁x₁ + w₂x₂ + w₃x₃ + b

  2. 非线性激活: a = σ(z) (σ是[[激活函数]],如ReLU、Sigmoid)

为什么这么简单可以完成复杂任务?

因为很多个神经元堆叠后,第一层网络识别颜色边缘,第二层识别形状纹理,第三层识别物体部件,等等。靠着层数的叠加来完成复杂识别。

它有哪些缺陷

问题 说明 解决方案
全连接参数爆炸 输入1000维,隐藏层1000,就有1M参数 CNN/局部连接
无法处理序列 输入长度必须固定 RNN/Transformer
梯度消失/爆炸 深层网络难训练 残差连接 + LayerNorm
过拟合 参数多,容易记住数据 Dropout + 正则化
忽略空间结构 把图像展平会丢失邻域关系 CNN

虽然有着缺陷,但它仍旧很重要
前馈神经网络加上卷积是CNN,加上循环是RNN,加自注意力机制是Transformer

FNN的纬度问题(初始化部分怎么写)

公式Z = X @ W + b

实现矩阵乘法,X @ W 要能相乘,X最后一维必须等于W第一维

符号 含义 形状
X 输入 [batch, 输入维度]
W 权重 [输入维度, 输出维度]
b 偏置 [1, 输出维度] 或 [输出维度]
Z 输出 [batch, 输出维度]
假设现在三个神经元(输出三个值),输入纬度784
1
2
3
4
5
6
7
8
9
10
import numpy as np

# 假设
batch = 5#表示一次处理的数据样本数量
X = np.random.randn(5, 784) # [5, 784]
W = np.random.randn(784, 3) # [784, 3]
b = np.random.randn(3) # [3]

# 计算
Z = X @ W + b # [5, 784] @ [784, 3] = [5, 3] + [3] = [5, 3]

给你一个3层网络

  • 输入层:784维
  • 隐藏层1:128个神经元
  • 隐藏层2:64个神经元
  • 输出层:10个神经元
1
2
3
4
5
6
7
8
W1.shape = [784, 128]   # 输入 → 隐藏层1
b1.shape = [128] 或 (1, 128)

W2.shape = [128, 64] # 隐藏层1 → 隐藏层2
b2.shape = [64] 或 (1, 64)

W3.shape = [64, 10] # 隐藏层2 → 输出层
b3.shape = [10] 或 (1, 10)

前向传播

1
2
3
X → Z1 → A1 → Z2 → A2 → Z3 → A3 (输出)
↓ ↓ ↓
ReLU ReLU Softmax

除了最后一层输出层,都是线性变换然后激活,由于我要写的是分类任务所以需要softmax把数值转为概率分布

前向传播的softmax怎么写

image-20260509215908970 代码是这样的
1
2
exp_z3 = np.exp(z3 - np.max(z3, axis=1, keepdims=True))
a3 = exp_z3 / np.sum(exp_z3, axis=1, keepdims=True)

为什么要减去一个最大值呢(z3-np.max)

因为直接e^z3 可能会溢出,然后分子分母同除不会改变大小(在e的指数上操作的所以是减)
axis=1沿着列的方向走,在每一行也就是每个样本里操作。
keepdims=true是因为需要保持维度,作减法时需要维度对齐
image-20260509215939713
softmax之后,可以通过a3得出[[损失函数]]
损失: L = CrossEntropy(y_true, A₃) (一个数字)
怎么直观理解呢,y_true表示实际类别(标签)

1
y_true = [0,0,0,0,0,1,0,0,0,0](第5类)在数字识别里代表数字5

1
L=-log(a3[5])

反向传播

反向传播就是把前向传播倒着来一遍更新参数,也就是说,每一层反向传播,我们都要计算三个东西

  1. dw2:w2的梯度用来更新w2
  2. db2:b2的梯度用来更新b2
  3. dA1:传递给前一层的梯度
    本质是链式法则,从后往前一层层计算梯度,然后根据每层梯度大小改变权重。

输出层

从输出层开始传播,由之前的前向传播我们知道b3,w3,loss。由定义可知

1
W3' = W3 - 学习率 × ∂Loss/∂W3(这一块是w3梯度)

经过一系列数学化简
image-20260509220036098
image-20260509220047929
但这里有形状问题:dZ3 是 [batch, 10],而 db3 应该是 [10]
所以必须把 batch 维度消掉,最简单的方法就是求和(或平均)

1
2
3
4
5
6
7
8
dZ3 = [
[0.01, 0.02, -0.10], # 样本1
[0.03, 0.01, -0.05] # 样本2
]
# 对 axis=0 求和
sum_dZ3 = [0.01+0.03=0.04, 0.02+0.01=0.03, -0.10-0.05=-0.15]
# 除以 m=2
db3 = [0.02, 0.015, -0.075]

所以我们写出代码,注意字母d代表求导

1
2
dW3 = (A₂.T @ dZ3) / m
db3 = np.sum(dZ3, axis=0, keepdims=True) / m

/m 是因为我们计算的是平均梯度,而不是总梯度。损失函数是平均值。
image-20260509220059876

后续层

以下的都是数学推导,由链式法则求导得出,转置则是因为维度不匹配

1
2
3
4
5
dA2 = dZ3 @ self.params['W3'].T
dZ2 = dA2 * (z2 > 0).astype(float)#relu函数,因为(z2>0)的结果是布尔值,需要显示转换float,当然不转也行。

dW2 = (a1.T @ dZ2) / m
db2 = np.sum(dZ2, axis=0, keepdims=True) / m

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import numpy as np
import gzip
import torchvision
import os
from sklearn.preprocessing import LabelBinarizer

# 设置随机种子(随机是为了打破对称,全0初始化为让同一层的神经元完全相同。设定固定种子是保证结果可复现)
np.random.seed(42)


class FFN: # 注意:class 是小写
# 初始化
def __init__(self, layer_sizes):
self.params = {}

for i in range(1, len(layer_sizes)):
in_dim = layer_sizes[i-1] # 输入维度
out_dim = layer_sizes[i] # 输出维度
# np.random.randn(in_dim,out_dim)生成一个形状为(in_dim, out_dim)的数组,元素符合标准正态分布
# np.sqrt(2.0/in_dim)是He初始化,把随机数缩小到合适的范围,以避免激活函数饱和(尤其是ReLU)。让每一层输出的方差保持一致,防止信号消失爆炸
# 原来w的数是[-1,1],×上He初始化,假设in_dim=100,那么数就变成了[-0.141,0.141],这样就更适合ReLU激活函数了
self.params[f'W{i}'] = np.random.randn(in_dim, out_dim) * np.sqrt(2.0 / in_dim)

# b初始化为0,因为偏置项不参与对称性破坏,纬度为(1,out_dim),前置知识有讲
self.params[f'b{i}'] = np.zeros((1, out_dim))

# 前向传播
def forward(self, X):
# 第一层是输入层到隐藏层,z1=x@W1+b1
z1 = X @ self.params['W1'] + self.params['b1']
# 激活函数,ReLU
a1 = np.maximum(0, z1)

# 以此类推,第二层
z2 = a1 @ self.params['W2'] + self.params['b2']
a2 = np.maximum(0, z2)

# 第三层是输出层,没有激活函数而是softmax,因为输出层是分类问题,softmax可以把输出转化为概率分布
z3 = a2 @ self.params['W3'] + self.params['b3']

# 计算softmax
# softmax(x_i) = exp(x_i) / sum(exp(x_j))是计算公式,axis=1是按行计算,keepdims=True是保持维度不变,这样输出的a3的形状和z3一样
exp = np.exp(z3 - np.max(z3, axis=1, keepdims=True))
a3 = exp / np.sum(exp, axis=1, keepdims=True)

# 你是不是觉得到这里就结束?如果只有前向传播是这样,但是为了后面的反向传播,我们还需要把每一层的输入输出都保存下来,这样在反向传播的时候才能用到这些值来计算梯度。所以我们可以把这些值保存在一个字典里,比如self.cache,这样就可以在反向传播的时候访问到它们了。
# 保存 cache(注意:把 a3 也存进去,反向传播要用)
cache = {
'X': X,
'z1': z1, 'a1': a1,
'z2': z2, 'a2': a2,
'z3': z3, 'a3': a3 # 加上 a3
}

return a3, cache # 返回两个东西

# 反向传播
def backward(self, y_true, cache):
#y_true: 真实标签 [batch, 10]
#cache: 前向传播保存的中间值

m = y_true.shape[0] # 样本数量,比如 batch_size
gradients = {}
X = cache['X']
a1 = cache['a1']
a2 = cache['a2']
a3 = cache['a3']
z1 = cache['z1']
z2 = cache['z2']

# 输出层,softmax和交叉熵的组合
dz3 = a3 - y_true # 输出层的误差,a3是预测值,y_true是真实值
dW3 = a2.T @ dz3 / m # 输出层权重的梯度,a2是隐藏层的输出
db3 = np.sum(dz3, axis=0, keepdims=True) / m # 输出层偏置的梯度

# 隐藏层2
dA2 = dz3 @ self.params['W3'].T # 注意:W3 是大写
dz2 = dA2 * (z2 > 0).astype(float) # ReLU的梯度,z2>0是ReLU的导数
dW2 = (a1.T @ dz2) / m
db2 = np.sum(dz2, axis=0, keepdims=True) / m

# 隐藏层1
dA1 = dz2 @ self.params['W2'].T # 注意:W2 是大写
dz1 = dA1 * (z1 > 0).astype(float) # ReLU的梯度
dW1 = (X.T @ dz1) / m
db1 = np.sum(dz1, axis=0, keepdims=True) / m

# 存梯度
gradients['dW3'] = dW3
gradients['db3'] = db3
gradients['dW2'] = dW2
gradients['db2'] = db2
gradients['dW1'] = dW1
gradients['db1'] = db1

return gradients

# 参数更新
def update(self, gradients, lr):
self.params['W1'] -= lr * gradients['dW1']
self.params['b1'] -= lr * gradients['db1']
self.params['W2'] -= lr * gradients['dW2']
self.params['b2'] -= lr * gradients['db2']
self.params['W3'] -= lr * gradients['dW3']
self.params['b3'] -= lr * gradients['db3']

# 预测
def predict(self, X):
a3, _ = self.forward(X)
return np.argmax(a3, axis=1)

# 计算准确率
def accuracy(self, X, y):
y_pred = self.predict(X)
return np.mean(y_pred == y)
站内搜索
常搜: