Contents

动手学DL

Beautiful landscape

Nature's Beauty

Explore the wonders of the world

参考资料

线性神经网络

  • 矢量化加速
    在训练模型时,若希望能够 同时处理整个小批量的样本
    需要对计算进行矢量化,从而利用 线性代数库
    而不是在 Python 中编写开销高昂的 for 循环
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
n = 10000
a = torch.ones(n)
b = torch.ones(n)


# 使用 Python 的 for 循环遍历全为 1 的 10000 维向量
for i in range(n):
    c[i] = a[i] + b[i]

# torch支持直接相加
# 矢量化代码通常会带来数量级的加速
c = a + b
  • 正态分布(normal distribution)
    也称为高斯分布(Gaussian distribution)
    最早由德国数学家高斯(Gauss)应用于天文学研究
1
2
3
4
5
6
7
def normal(x, mu, sigma):
    # mu:均值,控制峰值出现的横坐标位置
    # sigma:标准差,控制数值的集中度
    p = 1 / math.sqrt(2 * math.pi * sigma**2)

    # 正态分布的概率密度函数
    return p * np.exp(-0.5 / sigma**2 * (x - mu)**2)
  • 均方误差 损失函数(简称均方损失)
    $\frac{1}{2}(\hat{y}-y)^{2}$
    可以用于线性回归
    我们假设了观测中包含 噪声,且噪声 服从正态分布
    可以写出通过给定的 x 观测到特定 y 的似然(likelihood)
    根据极大似然估计法,参数 w 和 b 的最优值是 使整个数据集的似然达到最大的值,根据极大似然估计法选择的估计量称为极大似然估计量
    问题可以通过最大化似然对数来简化。但由于历史原因,优化通常是说最小化而不是最大化,所以可以改为 最小化负对数似然
    在高斯噪声的假设下,最小化均方误差等价于对线性模型的极大似然估计

  • 读取数据集
    每次抽取一小批量样本,并使用它们来更新我们的模型,叫做 batch

    通常,我们利用 GPU 并行运算 的优势,处理合理大小的“小批量”
    每个样本都可以并行地进行模型计算,且每个样本损失函数的梯度也可以被并行计算
    GPU 可以使得 处理几百个样本所花费的时间 和 处理一个样本时 差不多

  • 定义模型

  • 初始化模型参数
    首先初始化参数 –> 更新参数 –> 直到参数对应的模型能较好地拟合数据

    1
    2
    3
    4
    5
    6
    
    # 随机化初始参数
    # 服从均值为 0,方差为 0.01 的,形状为两行一列的正态分布的随机数
    # 截距 b 初始化为张量 0
    # requires_grad=True 方便在反向传播中计算梯度
    w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
    b = torch.zeros(1, requires_grad=True)
    

    每次更新都需要计算 损失函数关于模型参数的梯度
    有了这个梯度,我们就可以 向减小损失的方向 去更新每个参数

  • 定义损失函数

  • 定义优化算法

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    def sgd(params, lr, batch_size):
        """
        小批量随机梯度下降
        """
        with torch.no_grad():
            for param in params:
                # 更新参数
                param -= lr * param.grad / batch_size
                # 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
                param.grad.zero_()
    
  • 训练

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    lr =  # 学习率
    num_epochs =  # 迭代周期个数
    net =  # 神经网络线性模型
    loss =  # 损失函数
    
    
    for epoch in range(num_epochs):
        for X, y in data_iter(batch_size, features, labels):
            l = loss(net(X, w, b), y)
            # 因为 l 形状是(batch_size, 1),而不是一个标量
            # 所以 l 中的所有元素被加到一起,并以此计算关于[w, b]的梯度
            l.sum().backward()
            # 更新参数
            sgd([w, b], lr, batch_size)
    
        with torch.no_grad():
            train_l = loss(net(features, w, b), labels)
            print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
    

    参数总的更新次数:batch 的个数 * 迭代周期个数

  • softmax 回归
    回归可以用于预测,关注 多少 的问题
    分类问题则是关注 哪一类 的问题
    机器学习中通常使用的是软性类别的模型,可以 得到属于每一类的概率

    对于任何具有 $d$ 个输入和 $q$ 个输出的全连接层,参数开销为 $O(dq)$ ,这个数字在实践中可能高得令人望而却步

    1
    2
    3
    4
    5
    
    # 有时也称为配分函数(其对数称为对数-配分函数)
    def softmax(X):
        X_exp = torch.exp(X)
        partition = X_exp.sum(1, keepdim=True)
        return X_exp / partition
    
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    y = torch.tensor([0, 2])
    y_hat = torch.tensor([
        [0.1, 0.3, 0.6],
        [0.3, 0.2, 0.5]
    ])
    y_hat[[0, 1], y]
    # 选出 y_hat 第一个样本 [0.1, 0.3, 0.6] 的第 1 类的概率 0.1;第二个样本 [0.3, 0.2, 0.5] 的第 3 类的概率 0.5
    
    
    # 定义交叉熵损失函数
    def cross_entropy(y_hat, y):
        return -torch.log(y_hat[range(len(y_hat)), y])
    
  • 一些代码操作

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    # 生成均值为 0,方差为 1 的正态分布随机数
    # 形状为 num_examples*len(w)
    X = torch.normal(0, 1, (num_examples, len(w)))
    
    # 计算 y=Xw+b
    y = torch.matmul(X, w) + b
    
    # 生成均值为 0,方差为 0.01 的正态分布随机数,形状同 y
    # 相当于添加了噪声
    y += torch.normal(0, 0.01, y.shape)
    y.reshape((-1, 1))
    
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    # 数据迭代器
    # 注意随机读取和 yield 的使用
    def data_iter(batch_size, feature, label, ...):
        for i in range(...):
            ind = 随机读取
            yield feature[ind], label[ind]
    
    
    for X, y in data_iter(...):
        ...
    

    使用 PyTorch 的高级 API 更简洁地实现模型:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    # data 模块提供了数据处理工具
    # nn 模块定义了大量的神经网络层和常见损失函数
    from torch.utils import data
    from torch import nn
    
    
    def load_array(data_arrays, batch_size, is_train=True):
        """构造一个 PyTorch数据迭代器"""
        dataset = data.TensorDataset(*data_arrays)
        return data.DataLoader(dataset, batch_size, shuffle=is_train)
        # shuffle=True 会让数据迭代器对象在每个迭代周期内打乱数据的顺序
    
    batch_size = 10
    data_iter = load_array((features, labels), batch_size) #调用迭代器
    net = nn.Sequential(nn.Linear(2, 1))
    
    # 以 _结尾的方法将参数替换,从而初始化参数
    net[0].weight.data.normal_(0, 0.01)
    net[0].bias.data.fill_(0)
    
    loss = nn.MSELoss()
    trainer = torch.optim.SGD(net.parameters(), lr=0.03)
    
  • 问题

    • 关于 torch.no_grad()
      torch.no_grad() 是一个上下文管理器,用于禁用梯度计算
      在 with torch.no_grad() 块内,所有计算不会记录在计算图中,PyTorch 不会跟踪这些操作的梯度
      它的主要作用是减少内存消耗并加速计算,适用于那些不需要反向传播/梯度计算的场景
      例如在模型推理(预测)时,你只需要前向计算得到输出,不需要计算梯度

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      
      import torch
      
      # 模拟一个简单的模型
      model = torch.nn.Linear(10, 2)
      x = torch.randn(5, 10)
      y_true = torch.randint(0, 2, (5,))
      
      # 训练时需要梯度
      outputs = model(x)
      loss = torch.nn.functional.cross_entropy(outputs, y_true)
      loss.backward()
      
      # 推理时不需要梯度
      with torch.no_grad():
          test_output = model(x)
          print("Predictions:", test_output.argmax(dim=1))
      
    • 随机梯度下载的随机体现在哪里
      每次迭代时随机采样一个小批量计算梯度,相较于批量梯度下降,SGD 的计算速度更快,因为并没有使用全部数据计算梯度
      并且随机选择的样本,计算得出的梯度可能存在噪声,可能帮助模型跳出局部最优

    • 均方损失函数 $\frac{1}{2}(\hat{y}-y)^{2}$ 中前面的 1/2 有什么用
      对均方损失函数求导时,平方项会产生系数 2,而 1/2 就是用于抵消这个系数简化计算
      这样不影响优化结果,只是缩放损失值,不影响模型收敛性

    • 这句话是什么原理?为什么 相加 求损失函数关于参数的梯度?

      1
      
      l = loss(net(X, w, b), y),因为 l形状是(batch_size, 1),而不是一个标量,所以 l中的所有元素被加到一起,并以此计算关于[w, b]的梯度。
      

      PyTorch 的自动求导机制要求标量损失(loss 必须是一个标量)
      l 表示的是每个样本的损失值,故求和或均值得到标量,再反向传播。梯度方向是一致

      • mean()sum() 的区别仅在于梯度缩放:
      • sum():梯度是单个样本梯度的总和
      • mean():梯度是单个样本梯度的平均(相当于总和除以 batch_size)。
      • 不影响优化方向:两者梯度方向相同,仅步长(学习率需相应调整)。
    • softmax 函数本身是非线性的,不改变决策边界,这句话怎么理解?

      1
      2
      
      $\hat{Y} = softmax(o)$ ,其中 $\hat{y}_j = \cfrac{exp(o_j)}{\sum_{k}exp(o_k)}$
      是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定
      

      假设输入特征 $X$ 通过仿射变换得到输出 $o = WX + b$
      Softmax 仅对 $o$ 做归一化($\hat{y}_j = \frac{e^{o_j}}{\sum_k e^{o_k}}$),不改变决策边界
      分类边界还是由 $WX + b$ 决定,即超平面

      如果模型包含隐藏层或非线性激活(如 ReLU),则成为非线性模型

    • l.sum().backward() 反向传播后会自动计算梯度吗?
      是的,这是 PyTorch 自动微分(Autograd)机制的核心功能,无需手动推导梯度公式
      调用 backward() 后,PyTorch 会从该标量损失出发,沿着计算图(Computational Graph)反向传播,自动计算所有参与运算的张量(如 wb)的梯度
      梯度会存储在张量的 .grad 属性中(例如 w.gradb.grad

      完整流程示例:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      
      import torch
      
      # 定义模型参数(需要梯度追踪)
      w = torch.randn(3, requires_grad=True)  # 例如:线性层的权重
      b = torch.zeros(1, requires_grad=True)  # 偏置
      
      # 模拟输入和标签
      X = torch.randn(5, 3)  # batch_size=5, 特征维度=3
      y = torch.randn(5, 1)  # 标签
      
      # 前向计算
      y_pred = X @ w + b          # 仿射变换
      l = (y_pred - y).pow(2)     # 每个样本的损失,形状 (5, 1)
      
      # 反向传播
      l.sum().backward()          # 关键步骤:求和后反向传播
      
      # 查看梯度
      print(w.grad)               # 形状与 w 一致 (3,)
      print(b.grad)               # 形状与 b 一致 (1,)
      

      如果多次调用 backward(),梯度会累加.grad 中(例如在 RNN 中)。如果需要清空梯度,需手动调用:

      1
      
      w.grad.zero_()  # 清零梯度
      

      只有 requires_grad=True 的张量(如模型参数 wb)会计算梯度。输入数据 X 默认不需要梯度。

      • 数学原理
        设批量损失 $L = \sum_{i=1}^n l_i$,则参数 $w$ 的梯度为: $$ \frac{\partial L}{\partial w} = \sum_{i=1}^n \frac{\partial l_i}{\partial w} $$ PyTorch 的 l.sum().backward() 正是计算了这一求和后的梯度。

多层感知机

隐藏层

由于线性模型可能出错,故添加隐藏层,但是还是有可能退化成线性模型 故需要激活函数,使得模型从线性到非线性,一般来说,有了激活函数,就不可能再将多层感知机退化成线性模型 输出层 隐藏层 输入层 输入层没有涉及计算,所以最简易的多层感知机的层数为 2,即一层隐藏层和一层输出层 三层之间是全连接,故参数开销很高

ReLU 图像是分段线性的 当输入为负时,ReLU 函数的导数为 0 当输入为正时,ReLU 函数的导数为 1 当输入值精确等于 0 时,ReLU 函数不可导。在此时,我们默认使用左侧的导数,即当输入为 0 时导数为 0

sigmoid 能将其输入压缩转换到区间 (0, 1)上 可以将 sigmoid 视为 softmax 的特例 通常在想要将输出视作二元分类问题的概率时使用 但是大部分时候已经被更简单、更容易训练的 ReLU 所取代

tanh 函数,双曲正切函数 能将其输入压缩转换到区间 (-1, 1)上

通常,我们选择 2 的若干次幂作为层的宽度。因为内存在硬件中的分配和寻址方式,这么做往往可以在计算上更高效

卷积神经网络

图像数据的每个样本都由一个 二维像素网格 组成
每个像素可能是一个或者多个数值,取决于是 黑白还是彩色 图像

多层感知机十分适合处理表格数据,其中行对应样本,列对应特征,但是缺少结构
对于图像数据,我们仅仅通过 将图像数据展平成一维向量,再将数据送入一个全连接的多层感知机中
这会忽略每个图像的 空间结构信息

这些网络特征元素的顺序是不变的,因此最优的结果是利用先验知识
即利用 相近像素之间的相互关联性,从图像数据中学习得到有效的模型

卷积神经网络(convolutional neural networks,CNN)是包含 卷积层 的一类特殊的神经网络
卷积核(convolution kernel)也称滤波器(filter),亦或简单地称之为该卷积层的 权重,通常该权重是可学习的参数
卷积神经网络的应用:图像识别、目标检测或语义分割

  • 不变性

    • 平移不变性(translation invariance)
      不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对 相同的图像区域具有相似的反应
    • 局部性(locality)
      神经网络的前面几层应该只探索输入图像中的 局部区域,而不过度在意图像中相隔较远区域的关系
      最终 聚合这些局部特征 以在整个图像级别进行预测

    好处:参数大幅减少
    代价:当图像不满足平移不变时,模型可能 难以拟合 训练数据

  • 卷积的定义
    连续情况:$(f * g)(\mathbf{x}) = \int f(\mathbf{z}) g(\mathbf{x}-\mathbf{z}) d\mathbf{z}.$
    离散情况:$(f * g)(i) = \sum_a f(a) g(i-a).$

  • 通道
    图像不是二维张量,而是一个由 高度、宽度和颜色 组成的三维张量
    比如包含 1024x1024x3 个像素

    $$ [\mathsf{H}]{i,j,d} = \sum{a = -\Delta}^{\Delta} \sum_{b = -\Delta}^{\Delta} \sum_c [\mathsf{V}]{a, b, c, d} [\mathsf{X}]{i+a, j+b, c} $$

  • 互相关运算
    严格来说,卷积层是个错误的叫法
    因为它所表达的运算其实是 互相关运算(cross-correlation),而不是卷积运算
    二维卷积层的核心计算是二维互相关运算【对应元素相乘再求和】
    最简单的形式是,对二维输入数据和卷积核执行互相关操作,然后添加一个偏置

     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
    
    # 二维互相关运算
    def corr2d(X, K):
        # 获取卷积核的大小
        h, w = K.shape
    
        # 计算卷积层输出的大小
        Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
    
        for i in range(Y.shape[0]):
            for j in range(Y.shape[1]):
                # 做互相关运算,对应元素相乘再求和
                Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
        return Y
    
    
    # 定义卷积层
    class Conv2D(nn.Module):
        def __init__(self, kernel_size):
            super().__init__()
            # 对卷积核中的参数进行随机初始化
            self.weight = nn.Parameter(torch.rand(kernel_size))
            # 初始化偏置为 0
            self.bias = nn.Parameter(torch.zeros(1))
        # 前向传播,做卷积
        def forward(self, x):
            return corr2d(x, self.weight) + self.bias
    

    注意:原输入大小为【X.shape[0], X.shape[1]】
    卷积核大小为【h, w】
    卷积层输出大小为【X.shape[0]-h+1, X.shape[1]-w+1】

  • 卷积核参数的学习

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)
    # 学习率
    lr = 3e-2
    for i in range(10):
        # 做卷积
        Y_hat = conv2d(X)
        # 计算损失,这里是用均方差衡量
        l = (Y_hat - Y) ** 2
        # 梯度清零
        conv2d.zero_grad()
        # 向后传播计算梯度
        l.sum().backward()
        # 梯度下降法更新卷积核的参数
        conv2d.weight.data[:] -= lr * conv2d.weight.grad
        if (i + 1) % 2 == 0:
            # 第偶数次更新时,打印结果
            print(f'epoch {i+1}, loss {l.sum():.3f}')
    
  • feature map
    卷积层的输出,一个输入映射到下一层的 空间维度的转换器
    网络越深,检测输入特征中的区域越广

  • padding 和 stride

    • 填充
      卷积的 输出形状 取决于 输入形状卷积核的形状
      在应用了连续的卷积之后,我们最终得到的输出远小于输入大小
      这会导致原始图像的边界丢失了许多有用信息
      填充是解决此问题最有效的方法

    • 步幅
      有时希望大幅降低图像的宽度和高度
      例如原始的输入分辨率存在很多冗余

    https://img-blog.csdnimg.cn/20200825171851456.gif#pic_center

    一个特征图尺寸为 6x6 的输入,卷积核大小为 3x3,步幅为 2,填充为 1
    输出的尺寸 $=(6-3+2*1)/2+1$ 向下取整得到 3
    即:(输入大小-卷积核大小+2*padding)/步幅+1,再向下取整
    当填充数为:(卷积核大小-1)/2 且当步幅为 1 时,可以使输入和输出具有相同的高度和宽度

  • 池化层
    最大、最小、平均池化

    1
    2
    3
    4
    5
    
    # 设定汇聚层大小为 2x3
    # 步幅也为 2x3
    # padding=(0, 1) 表示仅对 X 的两侧进行填充
    pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
    pool2d(X)
    
  • LeNet(LeNet-5)

    • 卷积编码器
      两个卷积层

    • 全连接层密集块
      三个全连接层

       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
      
      net = nn.Sequential(
          #卷积 + 激活函数
          #输入通道数为 1,输出通道数为 6,卷积核大小为 5x5,填充为 2
          nn.Conv2d(1, 6, kernel_size=5, padding=2),
          nn.Sigmoid(),
          #平均池化
          #池化层大小为 2x2,步幅为 2
          nn.AvgPool2d(kernel_size=2, stride=2),
      
          #卷积 + 激活函数
          #输入通道数为 6,输出通道数为 16,卷积核大小为 5x5
          nn.Conv2d(6, 16, kernel_size=5),
          nn.Sigmoid(),
          #平均池化
          #池化层大小为 2x2,步幅为 2
          nn.AvgPool2d(kernel_size=2, stride=2),
      
          #展平为向量
          nn.Flatten(),
      
          #全连接层 + 激活函数
          nn.Linear(16 * 5 * 5, 120),
          nn.Sigmoid(),
      
          #全连接层 + 激活函数
          nn.Linear(120, 84),
          nn.Sigmoid(),
      
          #全连接层
          nn.Linear(84, 10))
      
    • 具体过程
      输入为 (1, 1, 28, 28)
      经过第一个卷积层,通道数变为 6,大小还是 $28=(28-5+22)/1+1$,经过激活函数大小不变
      再经过第一个池化层,通道数不变,由于两倍降采样,大小变为 $14=28/2$
      再经过第二个卷积层,通道数变为 16,大小变为 $10=(14-5+2
      0)/1+1$,经过激活函数大小不变
      再经过第二个池化层,通道数不变,由于两倍降采样,大小变为 $5=10/2$
      16 个 5x5 大小的特征图展平变为长 $400=1655$ 的向量
      经过第一个全连接层变为长 $120$ 的向量
      经过第二个全连接层变为长 $84$ 的向量
      经过第三个全连接层变为长 $10$ 的向量

  • 一些代码操作

    1
    2
    3
    
    # 张量转置
    X = torch.ones((6, 8))
    X.t()
    
    1
    2
    3
    4
    
    conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
    # 前面两个 1 分别表示输入的通道数,和希望输出的通道数
    # padding=1 填充 1 行或 1 列,因此整张图片总共添加了 2 行或 2 列
    # stride=2 两倍降采样
    

大模型的自注意力机制如何工作?

自注意力机制是Transformer架构的核心,它让模型能像人类阅读时自然关注关键词那样,动态计算句子中每个词与其他所有词的关联强度。这种机制通过查询(Query)、键(Key)、值(Value) 三个向量的交互实现,类比图书馆检索系统:Q是你想找的书的主题(如“机器学习”),K是每本书的目录标签,V是书的具体内容。

一、核心原理:从向量变换到权重计算

  1. 生成QKV向量
    输入句子中的每个词(如“我爱你”)先通过嵌入层转为向量,再与三个可训练的权重矩阵($W_Q, W_K, W_V$)相乘,得到对应的Q、K、V向量。例如,“我”的向量经$W_Q$变换后成为Q,代表它需要“查询”的信息;经$W_K$变换为K,作为被查询的“索引”;经$W_V$变换为V,包含实际语义内容。

  2. 计算注意力得分
    用Q与所有词的K做点积(内积),得到相似度矩阵。例如“我”的Q与“爱”的K点积值越高,说明两者关联越强。为避免维度过高导致数值溢出(如768维向量点积可能很大),需除以$\sqrt{d_k}$($d_k$是K的维度),这一步叫“缩放”,能稳定softmax函数的梯度。公式如下:
    $$\text{Attention Scores} = \frac{QK^T}{\sqrt{d_k}}$$

  3. 归一化与加权求和
    将得分矩阵通过softmax转为概率分布(权重和为1),再与V矩阵相乘,得到每个词的上下文向量。例如“爱”的权重高,其V向量在“我”的输出中占比更大,最终“我”的表示就融合了“爱”的语义。完整公式:
    $$\text{Attention}(Q,K,V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$

二、多头注意力:平行捕捉多维度关系

单头注意力可能只关注一种关联(如语法),而多头注意力将QKV拆分成多个子空间(如8个头),每个头独立计算注意力,最后拼接结果。例如:

  • 头1专注“主谓关系”(如“我”→“爱”)
  • 头2专注“动宾关系”(如“爱”→“你”)
    这种机制类似集成学习,让模型从不同角度理解语言,最终通过线性变换合并输出。公式为:
    $$\text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_1,…,\text{head}_h)W^O$$

三、实例:“我爱你”的注意力计算

假设“我爱你”三个词的嵌入向量为$X_1,X_2,X_3$,经矩阵变换后得到:

  • $Q = [Q_1, Q_2, Q_3]$(每个词的查询向量)
  • $K = [K_1, K_2, K_3]$(每个词的键向量)
  • $V = [V_1, V_2, V_3]$(每个词的值向量)
  1. 计算得分矩阵:$QK^T$得到3×3矩阵,其中$(i,j)$元素是$Q_i$与$K_j$的相似度。
  2. 缩放与softmax:除以$\sqrt{d_k}$后归一化,假设得到权重矩阵$\begin{bmatrix}0.1&0.8&0.1\0.3&0.5&0.2\0.2&0.7&0.1\end{bmatrix}$,表示“我”对“爱”的权重(0.8)最高。
  3. 加权求和:每个词的输出 = 权重 × V向量,例如“我”的输出 = $0.1V_1 + 0.8V_2 + 0.1V_3$。

四、关键设计:为何需要缩放与掩码?

  • 缩放因子$\sqrt{d_k}$:当K维度(如512)增大时,点积结果方差会膨胀,导致softmax输出接近0或1(梯度消失)。除以$\sqrt{d_k}$可将方差控制为1,保证训练稳定。
  • 掩码机制:在文本生成(如GPT)中,需用“因果掩码”确保预测第i个词时仅关注前i-1个词,避免“未来信息泄露”。实现方式是将得分矩阵上三角置为负无穷,使softmax后权重为0。

自注意力机制的本质是用数据驱动的权重替代人工设计的规则,让模型自动学习“哪些词需要重点关注”。它不仅支撑了大语言模型的崛起,还启发了图像(如ViT)、推荐系统等领域的注意力应用。理解它的核心,就掌握了Transformer的“灵魂”。