第三章 大模型基础原理:Transformer架构精讲

说实话,Transformer 这个东西,我第一次看论文的时候也懵了。2017年那篇《Attention Is All You Need》,我翻了三遍才大概明白它在干什么。但后来我在嵌入式端做语音识别模型移植时,发现这东西真香——它把序列处理的复杂度降下来了,而且特别适合并行计算。

今天咱们就把它拆开揉碎了讲。你想想看,一个 Transformer 其实就两大块:编码器解码器。编码器负责理解输入,解码器负责生成输出。每一块又由一堆相同的层堆叠而成。

核心要点:Transformer 抛弃了传统的 RNN/CNN,完全基于注意力机制。这意味着它不再需要按顺序处理数据,可以一次看完整个序列。

3.1 自注意力机制——Transformer 的灵魂

自注意力机制,说白了就是让模型学会「关注哪里」。比如你读句子「我在河边钓鱼」,模型要知道「我」和「钓鱼」关系更近,而不是「河边」和「钓鱼」。

具体怎么做的?我给你画个图就明白了。

自注意力机制计算流程 输入 X Q 查询 K 键 V 值 注意力分数 = Q × K^T / √d Softmax 归一化 → 权重分布 输出 = 权重 × V 的加权和 每个输入向量 通过三个矩阵变换 得到 Q、K、V d 是向量维度 除以 √d 防止梯度消失 权重越大,对应 V 的 贡献越多

嗯,这里要注意:Q、K、V 都是从同一个输入 X 通过三个不同的权重矩阵算出来的。我刚开始总搞混,以为 Q、K、V 是三个不同的东西。其实它们「同源不同权」。

我的经验:在嵌入式端部署时,自注意力机制的计算量主要卡在 Q×K^T 这一步。如果序列长度是 L,复杂度就是 O(L²)。我曾在 STM32 上跑过一个小模型,L=128 时还能接受,超过 256 就明显卡顿了。

3.2 多头注意力——让模型从不同角度看问题

为什么需要多头?你想想看,一个注意力头只能关注一种关系。比如「我在河边钓鱼」这句话,一个头可能关注了主语和动词的关系,另一个头可能关注了地点和动作的关系。

多头注意力就是把 Q、K、V 切成 h 份,每份独立做自注意力,最后再拼起来。我习惯用 h=8,这是论文里的默认值,也是工程上性价比最高的选择。

# 伪代码:多头注意力实现
def multi_head_attention(Q, K, V, h=8):
    # 1. 线性变换
    Q = linear(Q, W_q)  # 形状: [batch, seq_len, d_model]
    K = linear(K, W_k)
    V = linear(V, W_v)
    
    # 2. 切分成 h 个头
    # 每个头的维度 d_k = d_model / h
    Q = reshape(Q, [batch, seq_len, h, d_k])
    K = reshape(K, [batch, seq_len, h, d_k])
    V = reshape(V, [batch, seq_len, h, d_k])
    
    # 3. 每个头独立计算注意力
    for i in range(h):
        head_i = attention(Q[:,:,:,i], K[:,:,:,i], V[:,:,:,i])
    
    # 4. 拼接所有头
    concat = concat([head_1, ..., head_h], axis=-1)
    
    # 5. 最终线性变换
    output = linear(concat, W_o)
    return output

避坑指南:我曾经在模型量化时踩过一个坑——多头注意力的拼接层如果量化精度不够,不同头之间的信息融合会出问题。建议对拼接后的线性层保留 FP16 精度,或者用逐通道量化。

3.3 位置编码——给模型装上「顺序感」

Transformer 没有 RNN 那种天然的时序结构,它看输入就像看一堆散落的词。所以我们需要给每个词加上位置信息。

论文里用的是正弦余弦编码。为什么用这个?因为正弦函数有周期性,模型可以学到相对位置关系。比如位置 5 和位置 7 的关系,跟位置 105 和位置 107 的关系,在编码上是一致的。

位置 维度 0 (sin) 维度 1 (cos) 维度 2 (sin) 维度 3 (cos)
0 0.000 1.000 0.000 1.000
1 0.841 0.540 0.010 0.999
2 0.909 -0.416 0.020 0.999
3 0.141 -0.990 0.030 0.999

你看,低维度的变化快,高维度的变化慢。这样模型既能捕捉局部顺序,也能感知全局位置。

3.4 预训练与微调——站在巨人肩膀上

预训练就是让模型在海量数据上先学一遍「通用知识」。比如 BERT 在维基百科上训练,学会了语法、常识、逻辑关系。然后微调就是在你的特定任务上再学一遍。

我在做嵌入式语音命令识别时,就是这么干的:

  1. 预训练阶段:用大规模语音数据训练一个 Transformer 编码器,学会通用的语音特征提取
  2. 微调阶段:只保留编码器,加上一个简单的分类头,用 1000 条命令语音微调
  3. 部署阶段:把模型量化到 INT8,烧到 STM32 上

关键点:微调时不要全量更新参数。我建议只微调最后 2-3 层和新增的分类头。前面的层已经学到了通用特征,动它们反而容易过拟合。

3.5 模型量化与蒸馏入门——把大象塞进冰箱

大模型动辄几百 MB,嵌入式设备根本装不下。所以我们要做两件事:量化和蒸馏。

量化就是把 FP32 的权重变成 INT8。说白了,就是用更少的比特表示同样的数值。我常用的方法是:

# 量化示例:对称量化
def quantize(weights, bits=8):
    # 找到权重的最大值
    max_val = max(abs(weights))
    # 计算缩放因子
    scale = max_val / (2^(bits-1) - 1)
    # 量化
    q_weights = round(weights / scale)
    # 限制范围
    q_weights = clip(q_weights, -128, 127)
    return q_weights, scale

# 反量化
def dequantize(q_weights, scale):
    return q_weights * scale

蒸馏就更巧妙了。用一个大的教师模型教一个小学生模型。教师模型输出软标签(概率分布),学生模型学着模仿。我做过实验,蒸馏后的 3 层 Transformer 能达到 6 层模型 95% 的精度,但参数量只有一半。

我的经验:蒸馏时温度参数 T 很关键。T 越大,软标签越平滑,学生学到的知识越丰富。我一般从 T=5 开始试,然后逐步降低。在嵌入式场景下,T=3 左右效果最好。

3.6 嵌入式部署的实战建议

最后,我总结几条在嵌入式端部署 Transformer 的实战经验:

  • 剪枝优先:先剪掉注意力头中权重小的头,一般能剪掉 30% 而不损失精度
  • 量化时机:微调后再量化,不要预训练完就量化。微调会让权重分布更集中,量化损失更小
  • 内存优化:自注意力的中间结果(Q、K、V)不要全部存下来,算一个头就释放一个头
  • 硬件加速:如果芯片有 SIMD 指令,矩阵乘法能快 3-5 倍。我曾在 ARM Cortex-M7 上用 CMSIS-DSP 库优化过

嗯,Transformer 的内容就讲到这里。记住,理解自注意力机制是基础,量化蒸馏是落地的关键。下次你看到一个大模型,别慌,先想想怎么把它拆成 Q、K、V,再想想怎么把它塞进你的嵌入式设备里。


公众号:蓝海资料掘金营,微信deep3321