欢迎光临
我们一直在努力

人工神经网络算法详解及代码实例

1. 引言:从生物神经元到人工神经网络

人工神经网络(Artificial Neural Network,ANN)是深度学习的基石。它受生物神经系统启发,通过大量简单计算单元(神经元)的相互连接,从数据中自动学习特征与规律。

本文将从单个神经元出发,逐步推导前向传播、损失函数、反向传播与梯度下降,最后用 NumPy 从零实现一个可训练的多层感知机(MLP),并在手写数字数据集上完成分类任务。全文代码均可直接运行。

2. 单个神经元:线性组合 + 非线性激活

一个人工神经元做的事情非常朴素:对输入做加权求和,加上偏置,再经过一个非线性激活函数。

z=∑i=1nwixi+b=w⊤x+b,a=σ(z)
z = \\sum_{i=1}^{n} w_i x_i + b = \\mathbf{w}^\\top \\mathbf{x} + b,\\qquad a = \\sigma(z)
z=i=1∑n​wi​xi​+b=w⊤x+b,a=σ(z)

其中:

  • x\\mathbf{x}x:输入向量
  • w\\mathbf{w}w:权重向量,模型要学习的参数
  • bbb:偏置,控制激活阈值
  • σ(⋅)\\sigma(\\cdot)σ(⋅):激活函数,负责引入非线性

如果没有激活函数,无论网络堆叠多少层,整体仍然是输入的线性变换,表达能力极其有限。这正是「非线性」的关键意义。

2.1 常用激活函数对比

激活函数公式优点缺点
Sigmoid 11+e−z\\frac{1}{1+e^{-z}}1+e−z1​ 输出在 (0,1),可解释为概率 易梯度消失,输出非零均值
Tanh ez−e−zez+e−z\\frac{e^{z}-e^{-z}}{e^{z}+e^{-z}}ez+e−zez−e−z​ 零均值,收敛快于 Sigmoid 两端仍会梯度消失
ReLU max⁡(0,z)\\max(0, z)max(0,z) 计算简单,缓解梯度消失 存在神经元「死亡」问题
Leaky ReLU max⁡(αz,z)\\max(\\alpha z, z)max(αz,z) 缓解死亡问题 需额外超参 α\\alphaα

实践中,隐藏层默认优先尝试 ReLU,输出层根据任务选择:二分类用 Sigmoid,多分类用 Softmax,回归用线性输出。

3. 网络结构:前向传播

多层感知机由输入层、若干隐藏层和输出层组成。以两层隐藏层为例,前向传播就是逐层做「线性变换 + 激活」:

h1=σ(W1x+b1)h2=σ(W2h1+b2)y^=softmax(W3h2+b3)
\\begin{aligned}
\\mathbf{h}_1 &= \\sigma(\\mathbf{W}_1 \\mathbf{x} + \\mathbf{b}_1) \\\\
\\mathbf{h}_2 &= \\sigma(\\mathbf{W}_2 \\mathbf{h}_1 + \\mathbf{b}_2) \\\\
\\hat{\\mathbf{y}} &= \\mathrm{softmax}(\\mathbf{W}_3 \\mathbf{h}_2 + \\mathbf{b}_3)
\\end{aligned}
h1​h2​y^​​=σ(W1​x+b1​)=σ(W2​h1​+b2​)=softmax(W3​h2​+b3​)​

用矩阵形式书写的好处是:一次矩阵乘法即可并行计算整个 batch 的所有样本,这也是 GPU 加速神经网络的根本原因。

4. 损失函数:衡量预测与真实标签的差距

多分类任务最常用的是交叉熵损失(Cross Entropy)。设真实标签为 one-hot 向量 y\\mathbf{y}y,预测概率为 y^\\hat{\\mathbf{y}}y^​:

L=−∑k=1Kyklog⁡y^k
L = -\\sum_{k=1}^{K} y_k \\log \\hat{y}_k
L=−k=1∑K​yk​logy^​k​

当 Softmax 与交叉熵组合使用时,对 logits 的梯度会化简为极其简洁的形式:

∂L∂z=y^−y
\\frac{\\partial L}{\\partial \\mathbf{z}} = \\hat{\\mathbf{y}} – \\mathbf{y}
∂z∂L​=y^​−y

这个性质让反向传播的实现变得非常干净,也是分类网络几乎标配 Softmax + 交叉熵的原因。

5. 反向传播:链式法则的工程化

反向传播(Backpropagation)本质就是链式法则的高效组织方式:从损失出发,逐层向前计算每个参数的梯度。

以单层为例,设 z=Waprev+b\\mathbf{z} = \\mathbf{W}\\mathbf{a}_{prev} + \\mathbf{b}z=Waprev​+b,a=σ(z)\\mathbf{a} = \\sigma(\\mathbf{z})a=σ(z),上游传来的梯度为 δ=∂L∂a\\boldsymbol{\\delta} = \\frac{\\partial L}{\\partial \\mathbf{a}}δ=∂a∂L​,则:

∂L∂z=δ⊙σ′(z)∂L∂W=∂L∂z aprev⊤∂L∂b=∂L∂z∂L∂aprev=W⊤∂L∂z
\\begin{aligned}
\\frac{\\partial L}{\\partial \\mathbf{z}} &= \\boldsymbol{\\delta} \\odot \\sigma'(\\mathbf{z}) \\\\
\\frac{\\partial L}{\\partial \\mathbf{W}} &= \\frac{\\partial L}{\\partial \\mathbf{z}} \\, \\mathbf{a}_{prev}^\\top \\\\
\\frac{\\partial L}{\\partial \\mathbf{b}} &= \\frac{\\partial L}{\\partial \\mathbf{z}} \\\\
\\frac{\\partial L}{\\partial \\mathbf{a}_{prev}} &= \\mathbf{W}^\\top \\frac{\\partial L}{\\partial \\mathbf{z}}
\\end{aligned}
∂z∂L​∂W∂L​∂b∂L​∂aprev​∂L​​=δ⊙σ′(z)=∂z∂L​aprev⊤​=∂z∂L​=W⊤∂z∂L​​

其中 ⊙\\odot⊙ 表示逐元素相乘。可以看到,每一层只需要保存前向时的输入 aprev\\mathbf{a}_{prev}aprev​ 和中间结果 z\\mathbf{z}z,就能完成梯度计算——这就是训练时需要缓存激活值的原因。

6. 参数更新:梯度下降及其变体

拿到梯度后,最基础的更新方式是批量梯度下降:

θ←θ−η∂L∂θ
\\theta \\leftarrow \\theta – \\eta \\frac{\\partial L}{\\partial \\theta}
θ←θ−η∂θ∂L​

其中 η\\etaη 是学习率。实际训练中常用小批量(mini-batch)随机梯度下降,兼顾收敛速度与内存占用。更进一步的优化器包括:

  • Momentum:累积历史梯度方向,抑制震荡
  • RMSProp:按参数维度自适应缩放学习率
  • Adam:结合动量与自适应学习率,通常是默认首选

7. 代码实例:NumPy 从零实现 MLP

下面用纯 NumPy 实现一个「784 → 128 → 64 → 10」的三层网络,并在 MNIST 手写数字数据集上训练。代码不依赖任何深度学习框架,便于理解每一步在做什么。

7.1 环境与数据准备

import numpy as np

def load_mnist(path="mnist.npz"):
"""加载 MNIST,返回归一化后的训练/测试集"""
data = np.load(path)
x_train = data["x_train"].reshape(–1, 784).astype(np.float32) / 255.0
y_train = data["y_train"].astype(np.int64)
x_test = data["x_test"].reshape(–1, 784).astype(np.float32) / 255.0
y_test = data["y_test"].astype(np.int64)
return x_train, y_train, x_test, y_test

def one_hot(y, num_classes=10):
out = np.zeros((y.shape[0], num_classes), dtype=np.float32)
out[np.arange(y.shape[0]), y] = 1.0
return out

说明:mnist.npz 可从 Keras 数据集或公开镜像获取,键名保持 x_train / y_train / x_test / y_test 即可。

7.2 网络层与激活函数

class Linear:
"""全连接层:y = xW + b"""
def __init__(self, in_dim, out_dim):
# He 初始化,适合 ReLU
self.W = np.random.randn(in_dim, out_dim) * np.sqrt(2.0 / in_dim)
self.b = np.zeros(out_dim, dtype=np.float32)
self.x = None

def forward(self, x):
self.x = x
return x @ self.W + self.b

def backward(self, grad_out):
self.grad_W = self.x.T @ grad_out
self.grad_b = grad_out.sum(axis=0)
return grad_out @ self.W.T

class ReLU:
def forward(self, x):
self.mask = x > 0
return x * self.mask

def backward(self, grad_out):
return grad_out * self.mask

def softmax(z):
z = z – z.max(axis=1, keepdims=True) # 数值稳定
exp_z = np.exp(z)
return exp_z / exp_z.sum(axis=1, keepdims=True)

7.3 组装网络与训练循环

class MLP:
def __init__(self, dims=(784, 128, 64, 10)):
self.layers = []
for i in range(len(dims) – 1):
self.layers.append(Linear(dims[i], dims[i + 1]))
if i < len(dims) – 2:
self.layers.append(ReLU())

def forward(self, x):
for layer in self.layers:
x = layer.forward(x)
return softmax(x)

def backward(self, probs, y_onehot):
# Softmax + 交叉熵的梯度
grad = (probs – y_onehot) / probs.shape[0]
for layer in reversed(self.layers):
grad = layer.backward(grad)

def step(self, lr):
for layer in self.layers:
if isinstance(layer, Linear):
layer.W -= lr * layer.grad_W
layer.b -= lr * layer.grad_b

def accuracy(model, x, y):
pred = model.forward(x).argmax(axis=1)
return (pred == y).mean()

def train(model, x_train, y_train, x_test, y_test,
epochs=10, batch_size=64, lr=0.1):
y_train_oh = one_hot(y_train)
n = x_train.shape[0]
for epoch in range(epochs):
idx = np.random.permutation(n)
for start in range(0, n, batch_size):
batch = idx[start:start + batch_size]
probs = model.forward(x_train[batch])
model.backward(probs, y_train_oh[batch])
model.step(lr)
print(f"Epoch {epoch + 1:02d} | "
f"train acc={accuracy(model, x_train, y_train):.4f} | "
f"test acc={accuracy(model, x_test, y_test):.4f}")

if __name__ == "__main__":
x_train, y_train, x_test, y_test = load_mnist()
net = MLP()
train(net, x_train, y_train, x_test, y_test)

7.3.1 用 NumPy 实现 Adam 优化器

朴素 SGD 对每个参数使用同一个学习率,遇到不同量级的梯度时容易震荡或收敛缓慢。Adam 通过维护梯度的一阶矩(动量)和二阶矩(梯度平方的滑动平均),并对二者做偏差修正,实现按参数维度的自适应步长。核心公式如下:

mt=β1mt−1+(1−β1)gtvt=β2vt−1+(1−β2)gt2m^t=mt1−β1t,v^t=vt1−β2tθt=θt−1−ηm^tv^t+ϵ
\\begin{aligned}
m_t &= \\beta_1 m_{t-1} + (1-\\beta_1) g_t \\\\
v_t &= \\beta_2 v_{t-1} + (1-\\beta_2) g_t^2 \\\\
\\hat{m}_t &= \\frac{m_t}{1-\\beta_1^t},\\qquad \\hat{v}_t = \\frac{v_t}{1-\\beta_2^t} \\\\
\\theta_t &= \\theta_{t-1} – \\eta \\frac{\\hat{m}_t}{\\sqrt{\\hat{v}_t} + \\epsilon}
\\end{aligned}
mt​vt​m^t​θt​​=β1​mt−1​+(1−β1​)gt​=β2​vt−1​+(1−β2​)gt2​=1−β1t​mt​​,v^t​=1−β2t​vt​​=θt−1​−ηv^t​​+ϵm^t​​​

其中 gtg_tgt​ 是当前步梯度,β1\\beta_1β1​、β2\\beta_2β2​ 通常取 0.9 与 0.999,ϵ\\epsilonϵ 取 10−810^{-8}10−8 防止除零。偏差修正的作用是:训练初期 mtm_tmt​、vtv_tvt​ 从 0 起步会偏小,除以 1−βt1-\\beta^t1−βt 后即可还原为无偏估计。

class Adam:
"""Adam 优化器:一阶矩 + 二阶矩 + 偏差修正"""
def __init__(self, params, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8):
self.params = params # [(param_array, grad_attr_name), …]
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.t = 0 # 全局步数,用于偏差修正
# 为每个参数分配一阶矩与二阶矩缓存
self.m = [np.zeros_like(p) for p, _ in params]
self.v = [np.zeros_like(p) for p, _ in params]

def step(self):
self.t += 1
bias1 = 1.0 – self.beta1 ** self.t
bias2 = 1.0 – self.beta2 ** self.t
for i, (param, grad_attr) in enumerate(self.params):
g = getattr(param, grad_attr) if not isinstance(param, np.ndarray) else None
# 这里 param 为数组,grad 由外部通过闭包传入,见下方用法
raise NotImplementedError

上面的骨架为了说明缓存结构,实际使用时更简洁的写法是把「参数数组 + 梯度数组」成对传入:

class Adam:
"""Adam 优化器:一阶矩 + 二阶矩 + 偏差修正"""
def __init__(self, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.t = 0
self.m = {} # 一阶矩缓存:id(param) -> array
self.v = {} # 二阶矩缓存:id(param) -> array

def update(self, param, grad):
"""就地更新单个参数数组 param,grad 为对应梯度"""
key = id(param)
if key not in self.m:
self.m[key] = np.zeros_like(param)
self.v[key] = np.zeros_like(param)

self.t += 1
m, v = self.m[key], self.v[key]

# 一阶矩与二阶矩的滑动平均
m *= self.beta1
m += (1.0 – self.beta1) * grad
v *= self.beta2
v += (1.0 – self.beta2) * (grad * grad)

# 偏差修正
m_hat = m / (1.0 – self.beta1 ** self.t)
v_hat = v / (1.0 – self.beta2 ** self.t)

# 参数更新
param -= self.lr * m_hat / (np.sqrt(v_hat) + self.eps)

注意:self.t 是全局步数,每次调用 update 都会自增。若希望每个参数独立计数,可把 t 也放进字典按 id(param) 维护,但实践中全局步数是标准做法。

7.3.2 替换 MLP.step 中的朴素 SGD

原 MLP.step 直接执行 W -= lr * grad_W。要接入 Adam,只需把优化器实例挂在模型上,并在 step 中把每个 Linear 层的 W、b 及其梯度交给优化器:

class MLP:
def __init__(self, dims=(784, 128, 64, 10), optimizer="sgd", lr=0.1):
self.layers = []
for i in range(len(dims) – 1):
self.layers.append(Linear(dims[i], dims[i + 1]))
if i < len(dims) – 2:
self.layers.append(ReLU())
self.optimizer = optimizer
self.lr = lr
self.adam = Adam(lr=lr) if optimizer == "adam" else None

def forward(self, x):
for layer in self.layers:
x = layer.forward(x)
return softmax(x)

def backward(self, probs, y_onehot):
grad = (probs – y_onehot) / probs.shape[0]
for layer in reversed(self.layers):
grad = layer.backward(grad)

def step(self):
for layer in self.layers:
if not isinstance(layer, Linear):
continue
if self.adam is None:
# 朴素 SGD
layer.W -= self.lr * layer.grad_W
layer.b -= self.lr * layer.grad_b
else:
# Adam 更新
self.adam.update(layer.W, layer.grad_W)
self.adam.update(layer.b, layer.grad_b)

对应地把训练循环里的 model.step(lr) 改为 model.step(),学习率在构造模型时传入:

if __name__ == "__main__":
x_train, y_train, x_test, y_test = load_mnist()
# 使用 Adam,学习率通常取 1e-3 量级
net = MLP(optimizer="adam", lr=1e-3)
train(net, x_train, y_train, x_test, y_test)

7.3.3 收敛速度对比

在 MNIST 上使用相同的「784 → 128 → 64 → 10」结构与 batch size 64,两种更新方式的典型表现如下:

优化器学习率达到 95% 测试准确率所需 epoch10 epoch 后测试准确率特点
SGD 0.1 约 3~4 约 97% 前期下降快,后期震荡明显
Adam 1e-3 约 1~2 约 97.5%~98% 前期收敛更快,曲线更平滑

几点说明:

  • 前期速度:Adam 的自适应步长让每个参数按自身梯度量级缩放,训练初期损失下降明显更快,通常 1~2 个 epoch 即可越过 95%。
  • 后期精度:两者最终精度接近,Adam 略高且波动更小;但 Adam 在部分任务上泛化略逊于精调后的 SGD + Momentum,需要结合验证集判断。
  • 学习率敏感度:SGD 对学习率非常敏感,0.1 与 0.01 的表现差异很大;Adam 在 1e-3 附近较稳健,但不宜直接沿用 SGD 的 0.1,否则会因步长过大而发散。
  • 显存与计算开销:Adam 为每个参数额外保存两份同形状缓存,内存约为 SGD 的 3 倍,计算量也略高,但在本示例规模下可忽略。

7.4 运行结果参考

在 MNIST 上训练 10 个 epoch,测试集准确率通常可以达到 97% 以上。若想进一步提升,可以尝试:

  • 增大隐藏层宽度或层数
  • 加入 Dropout 或权重衰减抑制过拟合
  • 使用 Adam 优化器替代朴素 SGD
  • 对输入做标准化处理

8. 训练中的常见问题与调优建议

现象可能原因处理方式
损失不下降 学习率过小、初始化不当 调大学习率,改用 He/Xavier 初始化
损失震荡剧烈 学习率过大、batch 太小 降低学习率,增大 batch size
训练集好、测试集差 过拟合 加正则化、Dropout、数据增强
输出全为同一类 梯度消失、标签错误 检查数据与标签,换 ReLU 激活
出现 NaN 数值溢出、学习率过大 使用数值稳定的 Softmax,降低学习率

9. 总结

本文从单个神经元出发,完整梳理了人工神经网络的核心算法链路:

  • 前向传播:线性变换 + 非线性激活,逐层提取特征
  • 损失函数:交叉熵衡量预测与标签的差距
  • 反向传播:链式法则逐层回传梯度
  • 梯度下降:沿负梯度方向更新参数
  • 并用 NumPy 从零实现了一个可训练的三层 MLP,在 MNIST 上取得 97% 以上的准确率。理解这套「前向—损失—反向—更新」的闭环,是进一步学习 CNN、RNN、Transformer 等复杂结构的前提。

    10. 参考资料

  • Rumelhart, D. E., Hinton, G. E., & Williams, R. J. Learning representations by back-propagating errors. Nature, 323(6088), 533–536, 1986.(反向传播的奠基性论文)
  • LeCun, Y., Bottou, L., Bengio, Y., & Haffner, P. Gradient-Based Learning Applied to Document Recognition. Proceedings of the IEEE, 86(11), 2278–2324, 1998.(MNIST 数据集与卷积网络的开创性工作)
  • Kingma, D. P., & Ba, J. Adam: A Method for Stochastic Optimization. ICLR, 2015.(Adam 优化器原始论文,arXiv:1412.6980)
  • Goodfellow, I., Bengio, Y., & Courville, A. Deep Learning. MIT Press, 2016.(神经网络与深度学习的系统教材,第 6~8 章对应本文内容)
  • Stanford University. CS231n: Convolutional Neural Networks for Visual Recognition(课程讲义与作业). Stanford, 2017–2024. 链接:https://cs231n.stanford.edu/(反向传播、优化器与网络训练的经典教学资源)
  • 赞(0)
    未经允许不得转载:171主机测评 » 人工神经网络算法详解及代码实例
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址