Happy-LLM 学习笔记 03:从 Embedding 到 Encoder-Decoder,手拆 Transformer

读完 Attention 之后,再看完整 Transformer,我最大的变化是:不再把它当成一张复杂结构图,而是把它拆成一条清晰的数据流。文本先被 tokenizer 变成 token id,再经过 Embedding 变成向量;向量加上位置编码后进入一层层 Encoder 和 Decoder;每层内部由注意力、前馈网络、残差连接和 LayerNorm 共同维持信息流动。这样看,Transformer 并不是“很多模块堆在一起”,而是一套围绕序列表示不断交换、融合、稳定的工程结构。

Embedding 是模型世界的入口

Embedding 层做的事情很朴素:把离散 token id 映射成连续向量。它内部可以理解成一个可训练的查找表,词表中的每个 token 对应一行向量。输入变成带 hidden size 的张量后,模型才真正有了可计算的语义空间。

这一步容易被忽略,但它决定了后面所有模块的输入形状。Attention 计算向量关系,FFN 处理每个位置的特征维度,残差连接要求子层输入输出一致。

位置编码补上了注意力的短板

Self-Attention 的优点是并行、能直接看任意两个 token 的关系;短板是它天然不关心顺序。如果只看 token 集合,“我喜欢你”和“你喜欢我”会失去方向差异。位置编码就是为了解决这个问题:把位置信息加到词向量里,让模型在计算相关性时也能感知 token 出现的位置。

Happy-LLM 里对正余弦位置编码的解释对我很有启发。它不是随便选一个函数,而是同时满足几个工程要求:同一位置的编码要稳定,不同句子里的相对距离要可比较,还要能推广到训练时没见过的更长序列。sincos 的组合能把 pos+k 表达成已有位置的线性组合,这让“泛化到更长位置”不只是经验直觉,而有了数学上的理由。

我还注意到一个细节:位置编码只在底层加一次,但并不会轻易丢失。原因在于残差连接会把底层输入一路带到高层,每层学习的是对已有表示的修正,而不是完全覆盖原始信息。这也让我更理解为什么残差连接不是训练技巧,而是信息传递结构本身的一部分。

Encoder 更像理解器,Decoder 更像生成器

Encoder 的结构相对直接:每个 Encoder Layer 先做多头自注意力,再做前馈网络,中间配合残差连接和归一化。它的目标是把完整源序列编码成上下文表示,所以每个位置可以看见其他所有有效位置。对机器翻译这类任务来说,Encoder 就是在尽可能充分地理解原文。

Decoder 的任务更复杂。它不仅要看已经生成的目标序列,还要看 Encoder 输出的源序列表示。因此 Decoder Layer 通常有三块:第一块是带因果 mask 的自注意力,保证生成时不能偷看未来;第二块是交叉注意力,用 Decoder 当前表示作为 query,用 Encoder 输出作为 key 和 value;第三块才是前馈网络。这样拆开后,Decoder 的职责就很清楚:先整理自己已经生成的上下文,再去源序列里取相关信息,最后对当前位置的特征做非线性加工。

这也解释了为什么后来会分出 BERT 和 GPT 两条路线:前者偏理解,后者偏生成。

FFN、LayerNorm 和残差流是稳定性的关键

如果说注意力负责“位置之间的信息交流”,那么 FFN 更像“每个位置内部的特征加工”。它对序列中每个位置共享同一套参数,常见做法是先升维、激活,再降回 hidden size。升维增加表达空间,降维保证输出还能回到残差流里。

LayerNorm 和残差连接则解决深层网络训练的问题。Happy-LLM 的实现采用更接近现代 LLM 的 Pre-Norm:先归一化,再进入注意力或 FFN,最后把结果加回原输入。我的理解是,这样残差主干更稳定,梯度和原始信息都更容易沿着主路径传递。Post-Norm 在结构图里更常见,但真正写训练代码时,Pre-Norm 往往更符合大模型实践。

工程上最值得盯住的是形状

手拆 Transformer 后,我觉得最容易出错的不是公式,而是张量形状。多头注意力要把 hidden size 拆成 n_head * head_dim,mask 要能广播到注意力分数,Encoder 输出要能作为 Decoder 交叉注意力的 memory,FFN 输出还必须回到同一个 hidden size。只要其中一个维度没对齐,轻则运行时报错,重则模型能跑但语义学偏。

所以以后我读 Transformer 代码,会按一条线检查:token id 到 embedding,embedding 加位置,进入 N 层 Encoder 或 Decoder,注意力输出回残差流,FFN 再回残差流。抓住这条主线后,复杂结构图就不再吓人。

这篇给我的工程启发是:理解大模型结构时,不要只背模块名,而要能说清楚每个模块为什么存在、输入输出是什么、它和残差主干怎么连接。只有这样,后面再看 RoPE、GPT-only 架构、KV Cache 或推理优化时,才不会把新概念看成孤立知识点。

文章摘自:https://www.cnblogs.com/hazy-star/p/21765540