Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第三十六 神经网络(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 反向传播的四个步骤

  1. 前向传播:计算每一层的 $\mathbf{z}^{(l)}$ 和 $\mathbf{a}^{(l)}$
  2. 计算输出层误差:$\boldsymbol{\delta}^{(L)} = \nabla_{\mathbf{a}^{(L)}} \mathcal{L} \odot f’(\mathbf{z}^{(L)})$
  3. 反向传播误差:$\boldsymbol{\delta}^{(l)} = (W^{(l+1)})^T \boldsymbol{\delta}^{(l+1)} \odot f’(\mathbf{z}^{(l)})$
  4. 计算梯度:$\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-51998手写数字识别,7 层
AlexNet2012ImageNet 冠军,ReLU 激活
VGGNet2014使用小卷积核 (3x3) 堆叠
ResNet2015残差连接,解决退化问题
/// 简易 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 主要框架对比

框架特点适用场景
candleHuggingFace 出品,PyTorch 风格,支持 CUDA/Metal推理、微调、NLP/CV 任务
burn模块化设计,支持多后端,自动微分研究、训练、生产部署
ndarray纯 Rust 数值计算库,类似 NumPy底层数值运算、数据分析
smartcore传统机器学习算法库(SVM、随机森林等)经典 ML 任务
linfaRust 机器学习工具箱(类似 scikit-learn)传统 ML、数据分析
ortONNX 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 基础设施的重要选择。

练习建议:

  1. 从零实现一个两层 MLP,在 MNIST 子集上完成训练和推理
  2. 用 Rust 实现 SGD 和 Adam 优化器,对比收敛速度
  3. 使用 candle 框架加载预训练模型,完成文本分类或图像识别任务
  4. 实现一个简易的自注意力模块,理解 Transformer 的核心机制