第三十六 神经网络(Neural Network)
神经网络是深度学习的核心模型,灵感来源于生物神经系统的结构与功能。从手写数字识别到大语言模型,神经网络已经成为现代人工智能的基石。本章将从生物神经元讲起,逐步构建人工神经网络的完整知识体系,并用 Rust 代码实现核心算法。
一、从生物神经元到人工神经元
1.1 生物神经元
人脑由约 $8.6 \times 10^{10}$ 个神经元组成,每个神经元通过突触与其他神经元相连。生物神经元的基本结构包括:
| 组成部分 | 功能 |
|---|---|
| 树突(Dendrite) | 接收来自其他神经元的信号 |
| 细胞体(Soma) | 整合输入信号 |
| 轴突(Axon) | 传递输出信号 |
| 突触(Synapse) | 神经元之间的连接,信号在此传递 |
当输入信号的加权和超过阈值时,神经元被“激活“,产生输出信号。这一过程可以用数学模型来抽象。
1.2 人工神经元(McCulloch-Pitts 模型)
1943 年,Warren McCulloch 和 Walter Pitts 提出了第一个人工神经元模型。一个神经元接收 $n$ 个输入 $x_1, x_2, \ldots, x_n$,每个输入对应一个权重 $w_1, w_2, \ldots, w_n$,加上偏置 $b$,经过激活函数 $f$ 后产生输出:
$$y = f\left(\sum_{i=1}^{n} w_i x_i + b\right) = f(\mathbf{w}^T \mathbf{x} + b)$$
/// 人工神经元:加权求和 + 激活函数
struct Neuron {
weights: Vec<f64>,
bias: f64,
}
impl Neuron {
fn new(weights: Vec<f64>, bias: f64) -> Self {
Neuron { weights, bias }
}
/// 加权求和
fn weighted_sum(&self, inputs: &[f64]) -> f64 {
self.weights.iter()
.zip(inputs.iter())
.map(|(w, x)| w * x)
.sum::<f64>() + self.bias
}
/// 前向传播(使用阶跃激活函数)
fn forward(&self, inputs: &[f64]) -> f64 {
let sum = self.weighted_sum(inputs);
if sum >= 0.0 { 1.0 } else { 0.0 }
}
}
fn main() {
// AND 门:w1=1, w2=1, bias=-1.5
let and_gate = Neuron::new(vec![1.0, 1.0], -1.5);
println!("AND(0,0) = {}", and_gate.forward(&[0.0, 0.0])); // 0
println!("AND(0,1) = {}", and_gate.forward(&[0.0, 1.0])); // 0
println!("AND(1,1) = {}", and_gate.forward(&[1.0, 1.0])); // 1
// OR 门:w1=1, w2=1, bias=-0.5
let or_gate = Neuron::new(vec![1.0, 1.0], -0.5);
println!("OR(0,1) = {}", or_gate.forward(&[0.0, 1.0])); // 1
}
二、感知机
2.1 感知机的定义
1957 年,Frank Rosenblatt 提出了感知机(Perceptron),这是最简单的神经网络模型。感知机使用阶跃函数作为激活函数:
$$y = \text{sign}(\mathbf{w}^T \mathbf{x} + b) = \begin{cases} 1 & \text{if } \mathbf{w}^T \mathbf{x} + b \ge 0 \ 0 & \text{otherwise} \end{cases}$$
2.2 感知机学习规则
感知机通过迭代更新权重来学习。对于误分类的样本 $(x_i, y_i)$,更新规则为:
$$w_j \leftarrow w_j + \eta (y_i - \hat{y}i) x{ij}$$
$$b \leftarrow b + \eta (y_i - \hat{y}_i)$$
其中 $\eta$ 是学习率,$y_i$ 是真实标签,$\hat{y}_i$ 是预测值。
/// 感知机分类器
struct Perceptron {
weights: Vec<f64>,
bias: f64,
learning_rate: f64,
}
impl Perceptron {
fn new(n_inputs: usize, lr: f64) -> Self {
Perceptron {
weights: vec![0.0; n_inputs],
bias: 0.0,
learning_rate: lr,
}
}
fn predict(&self, inputs: &[f64]) -> i32 {
let sum = self.weights.iter()
.zip(inputs.iter())
.map(|(w, x)| w * x)
.sum::<f64>() + self.bias;
if sum >= 0.0 { 1 } else { 0 }
}
/// 训练一个 epoch
fn train_epoch(&mut self, samples: &[(Vec<f64>, i32)]) {
for (inputs, label) in samples {
let prediction = self.predict(inputs);
let error = (*label - prediction) as f64 * self.learning_rate;
for w in &mut self.weights {
*w += error * 1.0; // 简化:假设 inputs 为 0/1
}
for (w, x) in self.weights.iter_mut().zip(inputs.iter()) {
*w += error * x;
}
self.bias += error;
}
}
}
fn main() {
// AND 逻辑门的训练数据
let samples = vec![
(vec![0.0, 0.0], 0),
(vec![0.0, 1.0], 0),
(vec![1.0, 0.0], 0),
(vec![1.0, 1.0], 1),
];
let mut p = Perceptron::new(2, 0.1);
for epoch in 0..20 {
p.train_epoch(&samples);
}
println!("AND(0,0) = {}", p.predict(&[0.0, 0.0])); // 0
println!("AND(1,1) = {}", p.predict(&[1.0, 1.0])); // 1
println!("权重: {:?}", p.weights);
println!("偏置: {}", p.bias);
}
局限性: 单层感知机只能解决线性可分问题(如 AND、OR),无法解决 XOR 等非线性问题。这一局限促使了多层神经网络的发展。
三、神经网络的结构
3.1 多层感知机(MLP)
多层感知机由输入层、一个或多个隐藏层、输出层组成:
输入层 隐藏层 输出层
x1 ──╲ ╱── w1' ──╲
╲╱ ╲╱── y
x2 ──╱ ╲── w2' ──╱
- 输入层:接收原始特征,节点数等于特征维度
- 隐藏层:提取高阶特征,层数和宽度决定模型容量
- 输出层:产生最终预测
3.2 权重与偏置
一个 $L$ 层神经网络中,第 $l$ 层的参数为:
- 权重矩阵 $W^{(l)} \in \mathbb{R}^{n_l \times n_{l-1}}$
- 偏置向量 $\mathbf{b}^{(l)} \in \mathbb{R}^{n_l}$
3.3 前向传播
数据从输入层逐层传递到输出层的过程:
$$\mathbf{z}^{(l)} = W^{(l)} \mathbf{a}^{(l-1)} + \mathbf{b}^{(l)}$$
$$\mathbf{a}^{(l)} = f(\mathbf{z}^{(l)})$$
其中 $\mathbf{a}^{(0)} = \mathbf{x}$(输入),$\mathbf{a}^{(L)}$ 是最终输出。
#![allow(unused)]
fn main() {
/// 简易多层感知机(纯 Rust 实现)
struct Layer {
weights: Vec<Vec<f64>>, // [output_dim][input_dim]
bias: Vec<f64>, // [output_dim]
}
struct MLP {
layers: Vec<Layer>,
}
impl MLP {
/// 创建一个 784 -> 128 -> 10 的网络(MNIST 结构)
fn new_mnist() -> Self {
let mut rng = rand_simple();
MLP {
layers: vec![
Layer::random(128, 784, &mut rng),
Layer::random(10, 128, &mut rng),
],
}
}
/// 前向传播
fn forward(&self, input: &[f64]) -> Vec<f64> {
let mut activations = input.to_vec();
for (i, layer) in self.layers.iter().enumerate() {
let z = layer.forward(&activations);
// 最后一层用 softmax,隐藏层用 relu
if i == self.layers.len() - 1 {
activations = softmax(&z);
} else {
activations = z.iter().map(|&v| relu(v)).collect();
}
}
activations
}
}
impl Layer {
fn random(output_dim: usize, input_dim: usize, rng: &mut impl Fn() -> f64) -> Self {
let scale = (2.0 / input_dim as f64).sqrt(); // He 初始化
let weights = (0..output_dim)
.map(|_| (0..input_dim).map(|_| rng() * 2.0 * scale - scale).collect())
.collect();
let bias = vec![0.0; output_dim];
Layer { weights, bias }
}
fn forward(&self, input: &[f64]) -> Vec<f64> {
self.weights.iter().zip(self.bias.iter())
.map(|(w_row, &b)| {
w_row.iter().zip(input.iter())
.map(|(w, x)| w * x)
.sum::<f64>() + b
})
.collect()
}
}
/// 简易伪随机数生成器
fn rand_simple() -> impl Fn() -> f64 {
let mut state: u64 = 42;
move || {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
(state >> 33) as f64 / u32::MAX as f64
}
}
}
四、激活函数
激活函数为神经网络引入非线性,使其能够拟合复杂的函数。
4.1 常见激活函数
| 激活函数 | 公式 | 导数 | 特点 |
|---|---|---|---|
| Sigmoid | $\sigma(x) = \frac{1}{1+e^{-x}}$ | $\sigma’(x) = \sigma(x)(1-\sigma(x))$ | 输出 (0,1),易梯度消失 |
| Tanh | $\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}$ | $1 - \tanh^2(x)$ | 输出 (-1,1),零中心 |
| ReLU | $\text{ReLU}(x) = \max(0, x)$ | $\begin{cases}1 & x>0\0 & x<0\end{cases}$ | 计算高效,缓解梯度消失 |
| Leaky ReLU | $\max(\alpha x, x)$ | $\begin{cases}1 & x>0\\alpha & x<0\end{cases}$ | 解决 ReLU 神经元死亡 |
| Softmax | $\frac{e^{z_i}}{\sum_j e^{z_j}}$ | — | 多分类输出层 |
4.2 Rust 实现
/// 激活函数集合
fn sigmoid(x: f64) -> f64 {
1.0 / (1.0 + (-x).exp())
}
fn sigmoid_derivative(x: f64) -> f64 {
let s = sigmoid(x);
s * (1.0 - s)
}
fn tanh_activation(x: f64) -> f64 {
x.tanh()
}
fn relu(x: f64) -> f64 {
if x > 0.0 { x } else { 0.0 }
}
fn relu_derivative(x: f64) -> f64 {
if x > 0.0 { 1.0 } else { 0.0 }
}
fn leaky_relu(x: f64, alpha: f64) -> f64 {
if x > 0.0 { x } else { alpha * x }
}
/// Softmax:将向量转为概率分布
fn softmax(logits: &[f64]) -> Vec<f64> {
let max = logits.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let exps: Vec<f64> = logits.iter().map(|&x| (x - max).exp()).collect();
let sum: f64 = exps.iter().sum();
exps.iter().map(|e| e / sum).collect()
}
fn main() {
// Sigmoid 测试
println!("sigmoid(0) = {:.4}", sigmoid(0.0)); // 0.5000
println!("sigmoid(10) = {:.4}", sigmoid(10.0)); // ≈1.0000
println!("sigmoid(-10) = {:.4}", sigmoid(-10.0)); // ≈0.0000
// ReLU 测试
println!("relu(-3) = {}", relu(-3.0)); // 0
println!("relu(5) = {}", relu(5.0)); // 5
// Softmax 测试
let logits = [2.0, 1.0, 0.1];
let probs = softmax(&logits);
println!("softmax([2, 1, 0.1]) = {:?}", probs);
// ≈ [0.6590, 0.2424, 0.0986]
}
4.3 激活函数的选择
- 隐藏层:优先使用 ReLU 及其变体(Leaky ReLU、GELU),计算高效且缓解梯度消失
- 二分类输出层:使用 Sigmoid,输出概率值
- 多分类输出层:使用 Softmax,输出概率分布
- 回归输出层:不使用激活函数(线性输出)
五、损失函数
损失函数衡量模型预测值与真实值之间的差距,是训练过程中需要最小化的目标。
5.1 均方误差(MSE)
用于回归任务:
$$\mathcal{L}{\text{MSE}} = \frac{1}{n} \sum{i=1}^{n} (y_i - \hat{y}_i)^2$$
5.2 交叉熵损失
用于分类任务。二分类交叉熵:
$$\mathcal{L}{\text{CE}} = -\frac{1}{n} \sum{i=1}^{n} \left[ y_i \ln(\hat{y}_i) + (1 - y_i) \ln(1 - \hat{y}_i) \right]$$
多分类交叉熵:
$$\mathcal{L}{\text{CE}} = -\sum{i=1}^{n} \sum_{j=1}^{C} y_{ij} \ln(\hat{y}_{ij})$$
其中 $C$ 是类别数,$y_{ij}$ 是 one-hot 编码的真实标签。
/// 损失函数
fn mse_loss(predictions: &[f64], targets: &[f64]) -> f64 {
let n = predictions.len() as f64;
predictions.iter()
.zip(targets.iter())
.map(|(p, t)| (p - t).powi(2))
.sum::<f64>() / n
}
/// 二分类交叉熵
fn binary_cross_entropy(predictions: &[f64], targets: &[f64]) -> f64 {
let n = predictions.len() as f64;
predictions.iter()
.zip(targets.iter())
.map(|(p, &t)| {
let p_clamped = p.clamp(1e-7, 1.0 - 1e-7);
-t * p_clamped.ln() - (1.0 - t) * (1.0 - p_clamped).ln()
})
.sum::<f64>() / n
}
/// 多分类交叉熵(targets 为 one-hot 编码)
fn cross_entropy_loss(predictions: &[f64], targets: &[f64]) -> f64 {
predictions.iter()
.zip(targets.iter())
.map(|(p, &t)| {
if t > 0.0 {
-t * p.clamp(1e-7, 1.0).ln()
} else {
0.0
}
})
.sum()
}
fn main() {
let preds = vec![0.9, 0.1, 0.3];
let targets = vec![1.0, 0.0, 0.0];
println!("MSE = {:.6}", mse_loss(&preds, &targets));
println!("CE = {:.6}", cross_entropy_loss(&preds, &targets));
}
六、梯度下降与优化器
6.1 梯度下降法
梯度下降通过沿损失函数梯度的反方向更新参数来最小化损失:
$$\theta \leftarrow \theta - \eta \nabla_\theta \mathcal{L}$$
其中 $\eta$ 是学习率,$\nabla_\theta \mathcal{L}$ 是损失对参数的梯度。
| 变体 | 特点 |
|---|---|
| 批量梯度下降(BGD) | 使用全部样本计算梯度,稳定但慢 |
| 随机梯度下降(SGD) | 每次只用一个样本,快速但震荡 |
| 小批量梯度下降(Mini-batch SGD) | 折中方案,实践中最常用 |
6.2 动量法(Momentum)
加入动量项来加速收敛、减少震荡:
$$v_t = \gamma v_{t-1} + \eta \nabla_\theta \mathcal{L}$$
$$\theta \leftarrow \theta - v_t$$
其中 $\gamma$ 通常取 0.9。
6.3 Adam 优化器
Adam(Adaptive Moment Estimation)结合了动量法和自适应学习率:
$$m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t \quad \text{(一阶矩估计)}$$
$$v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2 \quad \text{(二阶矩估计)}$$
$$\hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1 - \beta_2^t} \quad \text{(偏差修正)}$$
$$\theta \leftarrow \theta - \eta \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}$$
默认参数:$\beta_1 = 0.9$,$\beta_2 = 0.999$,$\epsilon = 10^{-8}$。
/// Adam 优化器的 Rust 实现
struct AdamOptimizer {
lr: f64,
beta1: f64,
beta2: f64,
epsilon: f64,
m: Vec<f64>, // 一阶矩
v: Vec<f64>, // 二阶矩
t: usize, // 时间步
}
impl AdamOptimizer {
fn new(params: &[f64], lr: f64) -> Self {
AdamOptimizer {
lr,
beta1: 0.9,
beta2: 0.999,
epsilon: 1e-8,
m: vec![0.0; params.len()],
v: vec![0.0; params.len()],
t: 0,
}
}
/// 单步更新:params 就地修改
fn step(&mut self, params: &mut [f64], grads: &[f64]) {
self.t += 1;
for i in 0..params.len() {
// 更新一阶矩和二阶矩
self.m[i] = self.beta1 * self.m[i] + (1.0 - self.beta1) * grads[i];
self.v[i] = self.beta2 * self.v[i] + (1.0 - self.beta2) * grads[i].powi(2);
// 偏差修正
let m_hat = self.m[i] / (1.0 - self.beta1.powi(self.t as i32));
let v_hat = self.v[i] / (1.0 - self.beta2.powi(self.t as i32));
// 参数更新
params[i] -= self.lr * m_hat / (v_hat.sqrt() + self.epsilon);
}
}
}
fn main() {
// 示例:用 Adam 最小化 f(x) = (x-3)^2
let mut x = vec![0.0];
let mut optimizer = AdamOptimizer::new(&x, 0.1);
for step in 1..=100 {
// 梯度:df/dx = 2(x-3)
let grad = vec![2.0 * (x[0] - 3.0)];
optimizer.step(&mut x, &grad);
if step % 20 == 0 {
println!("Step {}: x = {:.6}, f(x) = {:.6}", step, x[0], (x[0] - 3.0).powi(2));
}
}
// 最终 x ≈ 3.0
}
七、反向传播(链式法则)
7.1 链式法则
反向传播是训练神经网络的核心算法,其数学基础是微积分的链式法则。对于复合函数 $y = f(g(x))$:
$$\frac{\partial y}{\partial x} = \frac{\partial y}{\partial g} \cdot \frac{\partial g}{\partial x}$$
在神经网络中,损失 $\mathcal{L}$ 对第 $l$ 层权重 $W^{(l)}$ 的梯度需要从输出层逐层回传:
$$\frac{\partial \mathcal{L}}{\partial W^{(l)}} = \frac{\partial \mathcal{L}}{\partial \mathbf{z}^{(l)}} \cdot \frac{\partial \mathbf{z}^{(l)}}{\partial W^{(l)}}$$
其中 $\boldsymbol{\delta}^{(l)} = \frac{\partial \mathcal{L}}{\partial \mathbf{z}^{(l)}}$ 称为误差项。
7.2 反向传播的四个步骤
- 前向传播:计算每一层的 $\mathbf{z}^{(l)}$ 和 $\mathbf{a}^{(l)}$
- 计算输出层误差:$\boldsymbol{\delta}^{(L)} = \nabla_{\mathbf{a}^{(L)}} \mathcal{L} \odot f’(\mathbf{z}^{(L)})$
- 反向传播误差:$\boldsymbol{\delta}^{(l)} = (W^{(l+1)})^T \boldsymbol{\delta}^{(l+1)} \odot f’(\mathbf{z}^{(l)})$
- 计算梯度:$\frac{\partial \mathcal{L}}{\partial W^{(l)}} = \boldsymbol{\delta}^{(l)} (\mathbf{a}^{(l-1)})^T$
#![allow(unused)]
fn main() {
/// 反向传播的 Rust 实现(两层网络:输入 -> 隐藏 -> 输出)
fn backward(
x: &[f64], // 输入
y: &[f64], // 真实标签(one-hot)
w1: &[Vec<f64>], // 隐藏层权重
w2: &[Vec<f64>], // 输出层权重
z1: &[f64], // 隐藏层线性输出
a1: &[f64], // 隐藏层激活输出
a2: &[f64], // 输出层激活输出
) -> (Vec<Vec<f64>>, Vec<Vec<f64>>, Vec<f64>, Vec<f64>) {
let n_hidden = w1.len();
let n_input = w1[0].len();
let n_output = w2.len();
// 步骤2:输出层误差 delta2 = (a2 - y) * softmax_derivative
// softmax + 交叉熵的梯度简化为 (a2 - y)
let mut delta2 = vec![0.0; n_output];
for i in 0..n_output {
delta2[i] = a2[i] - y[i];
}
// 步骤3:隐藏层误差 delta1 = (W2^T * delta2) * relu'(z1)
let mut delta1 = vec![0.0; n_hidden];
for j in 0..n_hidden {
let mut sum = 0.0;
for k in 0..n_output {
sum += w2[k][j] * delta2[k];
}
delta1[j] = sum * relu_derivative(z1[j]);
}
// 步骤4:计算梯度
// dW2 = delta2 * a1^T
let mut dw2 = vec![vec![0.0; n_hidden]; n_output];
for k in 0..n_output {
for j in 0..n_hidden {
dw2[k][j] = delta2[k] * a1[j];
}
}
// dW1 = delta1 * x^T
let mut dw1 = vec![vec![0.0; n_input]; n_hidden];
for j in 0..n_hidden {
for i in 0..n_input {
dw1[j][i] = delta1[j] * x[i];
}
}
// db2 = delta2
let db2 = delta2.clone();
// db1 = delta1
let db1 = delta1.clone();
(dw1, dw2, db1, db2)
}
}
计算图视角: 反向传播本质上是自动微分在计算图上的应用。PyTorch 的
autograd和 Rust 框架 burn 的自动微分系统都基于这一原理。
八、经典神经网络架构
8.1 卷积神经网络(CNN)
CNN 专为处理网格状数据(如图像)设计,核心操作是卷积:
$$(f * g)(i, j) = \sum_m \sum_n f(m, n) \cdot g(i-m, j-n)$$
CNN 的关键组件:
| 组件 | 作用 |
|---|---|
| 卷积层 | 提取局部特征(边缘、纹理等) |
| 池化层 | 降低空间维度,增强平移不变性 |
| 全连接层 | 综合特征进行分类 |
经典 CNN 架构:
| 模型 | 年份 | 特点 |
|---|---|---|
| LeNet-5 | 1998 | 手写数字识别,7 层 |
| AlexNet | 2012 | ImageNet 冠军,ReLU 激活 |
| VGGNet | 2014 | 使用小卷积核 (3x3) 堆叠 |
| ResNet | 2015 | 残差连接,解决退化问题 |
/// 简易 2D 卷积操作(无 padding,stride=1)
fn conv2d(input: &[Vec<f64>], kernel: &[Vec<f64>]) -> Vec<Vec<f64>> {
let (h_in, w_in) = (input.len(), input[0].len());
let (k_h, k_w) = (kernel.len(), kernel[0].len());
let h_out = h_in - k_h + 1;
let w_out = w_in - k_w + 1;
let mut output = vec![vec![0.0; w_out]; h_out];
for i in 0..h_out {
for j in 0..w_out {
let mut sum = 0.0;
for ki in 0..k_h {
for kj in 0..k_w {
sum += input[i + ki][j + kj] * kernel[ki][kj];
}
}
output[i][j] = sum;
}
}
output
}
/// 最大池化(2x2)
fn max_pool2d(input: &[Vec<f64>]) -> Vec<Vec<f64>> {
let h = input.len() / 2;
let w = input[0].len() / 2;
let mut output = vec![vec![0.0; w]; h];
for i in 0..h {
for j in 0..w {
output[i][j] = [
input[2*i][2*j], input[2*i][2*j+1],
input[2*i+1][2*j], input[2*i+1][2*j+1],
].into_iter().cloned().fold(f64::NEG_INFINITY, f64::max);
}
}
output
}
fn main() {
// 5x5 输入图像
let image = vec![
vec![1.0, 2.0, 0.0, 1.0, 3.0],
vec![0.0, 1.0, 2.0, 3.0, 1.0],
vec![1.0, 3.0, 1.0, 0.0, 2.0],
vec![2.0, 0.0, 3.0, 1.0, 0.0],
vec![1.0, 2.0, 1.0, 2.0, 1.0],
];
// 3x3 边缘检测卷积核
let kernel = vec![
vec![-1.0, -1.0, -1.0],
vec![-1.0, 8.0, -1.0],
vec![-1.0, -1.0, -1.0],
];
let feature_map = conv2d(&image, &kernel);
println!("卷积结果 (3x3):");
for row in &feature_map {
println!(" {:?}", row.iter().map(|v| format!("{:6.1}", v)).collect::<Vec<_>>());
}
}
8.2 循环神经网络(RNN)
RNN 专为处理序列数据设计,具有“记忆“能力。在每个时间步 $t$:
$$\mathbf{h}t = \tanh(W_h \mathbf{h}{t-1} + W_x \mathbf{x}_t + \mathbf{b})$$
$$\mathbf{y}_t = W_y \mathbf{h}_t + \mathbf{b}_y$$
RNN 的问题: 长序列训练时会出现梯度消失或梯度爆炸,难以捕捉长期依赖。
8.3 LSTM(长短期记忆网络)
LSTM 通过引入门控机制解决了 RNN 的长期依赖问题:
| 门 | 公式 | 作用 |
|---|---|---|
| 遗忘门 $f_t$ | $\sigma(W_f [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_f)$ | 决定丢弃哪些旧信息 |
| 输入门 $i_t$ | $\sigma(W_i [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_i)$ | 决定写入哪些新信息 |
| 候选记忆 $\tilde{C}_t$ | $\tanh(W_C [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_C)$ | 候选的新记忆内容 |
| 输出门 $o_t$ | $\sigma(W_o [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_o)$ | 决定输出哪些信息 |
细胞状态更新:
$$C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t$$
$$h_t = o_t \odot \tanh(C_t)$$
#![allow(unused)]
fn main() {
/// 简易 LSTM 单元(Rust 实现)
struct LSTMCell {
// 合并权重矩阵 [4 * hidden_size, input_size + hidden_size]
w_xh: Vec<Vec<f64>>, // 输入权重
w_hh: Vec<Vec<f64>>, // 循环权重
b: Vec<f64>, // 偏置
hidden_size: usize,
}
impl LSTMCell {
fn new(input_size: usize, hidden_size: usize) -> Self {
let scale = (2.0 / (input_size + hidden_size) as f64).sqrt();
let mut rng = rand_simple();
let w_xh = (0..4 * hidden_size)
.map(|_| (0..input_size).map(|_| rng() * 2.0 * scale - scale).collect())
.collect();
let w_hh = (0..4 * hidden_size)
.map(|_| (0..hidden_size).map(|_| rng() * 2.0 * scale - scale).collect())
.collect();
LSTMCell {
w_xh, w_hh,
b: vec![0.0; 4 * hidden_size],
hidden_size,
}
}
/// 单步前向传播
fn step(&self, x: &[f64], h_prev: &[f64], c_prev: &[f64])
-> (Vec<f64>, Vec<f64>)
{
let hs = self.hidden_size;
let mut gates = vec![0.0; 4 * hs];
// 计算 gates = W_xh * x + W_hh * h_prev + b
for i in 0..4 * hs {
let mut sum = self.b[i];
for j in 0..x.len() {
sum += self.w_xh[i][j] * x[j];
}
for j in 0..hs {
sum += self.w_hh[i][j] * h_prev[j];
}
gates[i] = sum;
}
// 分离四个门
let (f, rest) = gates.split_at(hs);
let (i, rest) = rest.split_at(hs);
let (c_candidate, o) = rest.split_at(hs);
// 更新细胞状态和隐藏状态
let mut c_next = vec![0.0; hs];
let mut h_next = vec![0.0; hs];
for j in 0..hs {
let f_j = sigmoid(f[j]);
let i_j = sigmoid(i[j]);
let c_hat_j = c_candidate[j].tanh();
c_next[j] = f_j * c_prev[j] + i_j * c_hat_j;
h_next[j] = sigmoid(o[j]) * c_next[j].tanh();
}
(h_next, c_next)
}
}
}
8.4 Transformer 与注意力机制
2017 年,Vaswani 等人提出了 Transformer 架构(《Attention Is All You Need》),彻底改变了自然语言处理领域。
自注意力机制(Self-Attention) 的核心公式:
$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V$$
其中 $Q$(Query)、$K$(Key)、$V$(Value)均由输入线性变换得到:
$$Q = X W^Q, \quad K = X W^K, \quad V = X W^V$$
多头注意力(Multi-Head Attention) 允许模型同时关注不同位置的不同表示子空间:
$$\text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h) W^O$$
$$\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)$$
/// 简易自注意力机制(Rust 实现)
fn self_attention(x: &[Vec<f64>], w_q: &[Vec<f64>], w_k: &[Vec<f64>], w_v: &[Vec<f64>])
-> Vec<Vec<f64>>
{
let seq_len = x.len();
let d_k = w_q[0].len();
// 计算 Q, K, V
let q: Vec<Vec<f64>> = x.iter()
.map(|xi| mat_vec_mul(w_q, xi)).collect();
let k: Vec<Vec<f64>> = x.iter()
.map(|xi| mat_vec_mul(w_k, xi)).collect();
let v: Vec<Vec<f64>> = x.iter()
.map(|xi| mat_vec_mul(w_v, xi)).collect();
// 计算注意力分数:scores = Q * K^T / sqrt(d_k)
let scale = (d_k as f64).sqrt();
let mut scores = vec![vec![0.0; seq_len]; seq_len];
for i in 0..seq_len {
for j in 0..seq_len {
let dot: f64 = q[i].iter().zip(k[j].iter())
.map(|(a, b)| a * b).sum();
scores[i][j] = dot / scale;
}
}
// Softmax(按行)
for row in &mut scores {
let max = row.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let exps: Vec<f64> = row.iter().map(|s| (s - max).exp()).collect();
let sum: f64 = exps.iter().sum();
for (r, e) in row.iter_mut().zip(exps.iter()) {
*r = e / sum;
}
}
// 输出 = scores * V
let mut output = vec![vec![0.0; v[0].len()]; seq_len];
for i in 0..seq_len {
for j in 0..v[0].len() {
output[i][j] = scores[i].iter().zip(v.iter())
.map(|(s, vk)| s * vk[j]).sum();
}
}
output
}
/// 矩阵-向量乘法
fn mat_vec_mul(mat: &[Vec<f64>], vec: &[f64]) -> Vec<f64> {
mat.iter().map(|row| {
row.iter().zip(vec.iter()).map(|(a, b)| a * b).sum()
}).collect()
}
fn main() {
// 3 个 token,每个 4 维
let x = vec![
vec![1.0, 0.0, 1.0, 0.0],
vec![0.0, 1.0, 0.0, 1.0],
vec![1.0, 1.0, 0.0, 0.0],
];
let d_model = 4;
// 简化:W_q = W_k = W_v = 单位矩阵
let w = vec![vec![0.0; d_model]; d_model];
let identity: Vec<Vec<f64>> = (0..d_model)
.map(|i| (0..d_model).map(|j| if i == j { 1.0 } else { 0.0 }).collect())
.collect();
let output = self_attention(&x, &identity, &identity, &identity);
println!("自注意力输出:");
for row in &output {
println!(" {:?}", row.iter().map(|v| format!("{:.4}", v)).collect::<Vec<_>>());
}
}
Transformer 是 GPT、BERT、LLaMA 等大语言模型的基石架构。从 2017 年至今,Transformer 及其变体主导了整个 AI 领域。
九、实战:手写数字识别(MNIST)
MNIST 是深度学习的“Hello World“,包含 60000 张训练图像和 10000 张测试图像,每张为 $28 \times 28$ 像素的灰度手写数字。
9.1 使用 candle 框架
candle 是 HuggingFace 开发的 Rust 深度学习框架,支持 GPU 加速,API 风格类似 PyTorch。
# Cargo.toml
[dependencies]
candle-nn = "0.8"
candle-core = "0.8"
use candle_core::{Tensor, Device, Result};
use candle_nn::{Linear, VarBuilder, Module};
/// MNIST 分类网络:784 -> 256 -> 128 -> 10
struct MnistModel {
fc1: Linear,
fc2: Linear,
fc3: Linear,
}
impl MnistModel {
fn new(vs: VarBuilder) -> Result<Self> {
Ok(MnistModel {
fc1: candle_nn::linear(784, 256, vs.pp("fc1"))?,
fc2: candle_nn::linear(256, 128, vs.pp("fc2"))?,
fc3: candle_nn::linear(128, 10, vs.pp("fc3"))?,
})
}
fn forward(&self, x: &Tensor) -> Result<Tensor> {
let x = self.fc1.forward(x)?;
let x = x.relu()?;
let x = self.fc2.forward(x)?;
let x = x.relu()?;
self.fc3.forward(&x) // 交叉熵损失自带 softmax
}
}
fn main() -> Result<()> {
let device = Device::cuda_if_available(0)?;
// 创建模型(随机初始化)
let vs = VarBuilder::new(
candle_nn::Init::KaimingUniform,
1.0 / 256.0,
candle_core::DType::F32,
&device,
);
let model = MnistModel::new(vs)?;
// 模拟输入:batch_size=4, 每张图展平为 784 维
let dummy_input = Tensor::randn(0f32, 1f32, (4, 784), &device)?;
let logits = model.forward(&dummy_input)?;
println!("输出形状: {:?}", logits.shape()); // [4, 10]
// 预测类别
let predicted = logits.argmax(1)?;
println!("预测类别: {:?}", predicted.to_vec1::<u32>()?);
Ok(())
}
9.2 使用 burn 框架
burn 是另一个优秀的 Rust 深度学习框架,支持多种后端(CPU/CUDA/WASM)。
#![allow(unused)]
fn main() {
use burn::{
module::Module,
tensor::{backend::{AutodiffBackend, Backend}, Tensor},
nn::{Linear, LinearConfig, ReLU},
};
/// 使用 burn 的 MNIST 模型定义
#[derive(Module, Debug)]
pub struct MnistModel<B: Backend> {
fc1: Linear<B>,
fc2: Linear<B>,
fc3: Linear<B>,
relu: ReLU,
}
impl<B: Backend> MnistModel<B> {
pub fn new(device: &B::Device) -> Self {
Self {
fc1: LinearConfig::new(784, 256).init(device),
fc2: LinearConfig::new(256, 128).init(device),
fc3: LinearConfig::new(128, 10).init(device),
relu: ReLU::new(),
}
}
pub fn forward(&self, x: Tensor<B, 2>) -> Tensor<B, 2> {
let x = self.fc1.forward(x);
let x = self.relu.forward(x);
let x = self.fc2.forward(x);
let x = self.relu.forward(x);
self.fc3.forward(x)
}
}
}
十、Rust AI 生态
10.1 主要框架对比
| 框架 | 特点 | 适用场景 |
|---|---|---|
| candle | HuggingFace 出品,PyTorch 风格,支持 CUDA/Metal | 推理、微调、NLP/CV 任务 |
| burn | 模块化设计,支持多后端,自动微分 | 研究、训练、生产部署 |
| ndarray | 纯 Rust 数值计算库,类似 NumPy | 底层数值运算、数据分析 |
| smartcore | 传统机器学习算法库(SVM、随机森林等) | 经典 ML 任务 |
| linfa | Rust 机器学习工具箱(类似 scikit-learn) | 传统 ML、数据分析 |
| ort | ONNX Runtime 绑定,可运行训练好的模型 | 跨框架模型推理 |
10.2 Rust 在 AI 领域的优势
- 内存安全:无数据竞争,适合长时间运行的推理服务
- 零成本抽象:性能媲美 C++,适合高性能推理
- 无 GC 停顿:稳定的延迟,适合实时推理场景
- WASM 支持:可在浏览器中运行 ML 模型
- 易于部署:单一二进制文件,无依赖地狱
/// 使用 ndarray 进行矩阵运算(神经网络基础)
use ndarray::{Array2, Array1, array};
fn main() {
// 权重矩阵 3x2
let w = array![[0.5, 0.3],
[0.2, 0.8],
[0.1, 0.6]];
// 输入向量 2x1
let x = array![1.0, 2.0];
// 矩阵乘法:z = W * x + b
let b = array![0.1, -0.1, 0.2];
let z = w.dot(&x) + b;
println!("线性输出 z = {:?}", z); // [1.0, 1.6, 1.5]
// ReLU 激活
let a: Array1<f64> = z.mapv(|v| v.max(0.0));
println!("ReLU 激活后 a = {:?}", a); // [1.0, 1.6, 1.5]
// Softmax
let exp_a = a.mapv(|v| v.exp());
let sum_exp = exp_a.sum();
let probs = exp_a / sum_exp;
println!("Softmax 输出 = {:?}", probs);
}
十一、神经网络与密码学的交叉
11.1 侧信道攻击
神经网络可被用于分析密码系统的侧信道信息(如功耗、电磁辐射、时间),从而推断密钥:
- 功耗分析攻击:通过分析加密设备在不同操作下的功耗差异,推断内部密钥位
- 电磁辐射分析:捕获设备运算时的电磁泄漏,提取敏感信息
- 模板攻击:使用大量样本训练攻击模型,对目标设备的侧信道信号进行分类
11.2 AI 辅助密码分析
- 使用 CNN 分析功耗轨迹,自动提取特征
- 使用 RNN/LSTM 分析时间序列侧信道数据
- 使用 GAN 生成对抗样本来测试密码系统的鲁棒性
11.3 防御策略
| 防御方法 | 原理 |
|---|---|
| 掩码技术 | 在计算中引入随机掩码,隐藏真实数据 |
| 隐藏技术 | 使所有操作的功耗/时间恒定 |
| 噪声注入 | 向侧信道信号中添加随机噪声 |
| 对抗训练 | 使用对抗样本增强模型的鲁棒性 |
十二、总结
| 主题 | 核心要点 |
|---|---|
| 人工神经元 | $y = f(\mathbf{w}^T \mathbf{x} + b)$,加权求和 + 激活 |
| 感知机 | 最简单的线性分类器,无法解决 XOR |
| 激活函数 | ReLU(隐藏层)、Sigmoid/Softmax(输出层) |
| 前向传播 | 逐层计算 $\mathbf{z}^{(l)} = W^{(l)}\mathbf{a}^{(l-1)} + \mathbf{b}^{(l)}$ |
| 损失函数 | MSE(回归)、交叉熵(分类) |
| 梯度下降 | SGD、Momentum、Adam(最常用) |
| 反向传播 | 基于链式法则,逐层计算梯度 |
| CNN | 卷积 + 池化,擅长图像处理 |
| RNN/LSTM | 处理序列数据,门控机制解决长期依赖 |
| Transformer | 自注意力机制,大语言模型的基石 |
| Rust 生态 | candle(推理)、burn(训练)、ndarray(数值计算) |
神经网络是现代人工智能的核心技术。从简单的感知机到复杂的 Transformer,从 MNIST 手写识别到大语言模型,神经网络的发展深刻改变了技术世界。Rust 凭借其安全性和高性能,正在成为 AI 基础设施的重要选择。
练习建议:
- 从零实现一个两层 MLP,在 MNIST 子集上完成训练和推理
- 用 Rust 实现 SGD 和 Adam 优化器,对比收敛速度
- 使用 candle 框架加载预训练模型,完成文本分类或图像识别任务
- 实现一个简易的自注意力模块,理解 Transformer 的核心机制