一、问题引入:都是 Transformer,为什么长得不一样?
2024 年了,你打开 Hugging Face 模型列表:
| 模型 | 架构 | 参数量 | 擅长 |
|---|---|---|---|
| BERT | Encoder-Only | 110M-340M | 文本理解、分类、NER |
| GPT-4 | Decoder-Only | ~1.8T | 文本生成、对话、推理 |
| T5 | Encoder-Decoder | 60M-11B | 翻译、摘要、问答 |
| LLaMA | Decoder-Only | 7B-70B | 开源通用生成 |
| ViT | Encoder-Only | 86M-632M | 图像分类 |
它们共享相同的核心组件(Self-Attention + FFN + LayerNorm),但组合方式完全不同。这篇文章帮你理清三种架构的差异,并给出实际选型建议。
二、核心概念速览
2.1 三种架构一览
Encoder-Decoder (T5/BART) Encoder-Only (BERT) Decoder-Only (GPT)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Encoder │ │ Encoder │ │ Decoder │
│ (双向) │ │ (双向) │ │ (单向) │
│ │ │ │ │ Causal │
│ Self-Att │ │ Self-Att │ │ Masked │
│ + FFN │ x6 │ + FFN │ x12 │ Self-Att │
│ │ │ │ │ + FFN │ x12
└────┬─────┘ └────┬─────┘ └──────────┘
│ │
┌────┴─────┐ [CLS] → Classifier
│ Decoder │
│ (单向) │
│ Masked │
│ Self-Att │
│ Cross-Att│ ← 从 Encoder 取 K,V
│ + FFN │ x6
└──────────┘
2.2 核心差异对比
| 特性 | Encoder-Decoder | Encoder-Only | Decoder-Only |
|---|---|---|---|
| 注意力方式 | Enc 双向 / Dec 因果 | 全双向 | 因果(只看左边) |
| 是否有 Cross-Att | ✅ | ❌ | ❌ |
| 典型参数量 | 60M-11B | 110M-340M | 125M-1.8T |
| 输入/输出 | 长度可不同 | 只用 Encoder | 自回归生成 |
| 训练目标 | Span Corruption / 翻译 | MLM(掩码预测) | NTP(下一个Token预测) |
| 生成能力 | ✅ | ❌ 需适配 | ✅ 原生 |
| 理解能力 | ✅ | ✅ | ✅ (prompt后) |
2.3 关键公式补充
Cross-Attention(仅 Encoder-Decoder 有):
CrossAttn(Q_dec, K_enc, V_enc) = softmax(Q_dec · K_encᵀ / √dk) · V_enc
Q 来自 Decoder(你想知道什么),K/V 来自 Encoder(上下文提供了什么)。这是机器翻译的核心:Decoder 每生成一个词都从源语言 Encoder 中查找相关信息。
三、完整可运行代码
下面实现一个迷你版的三种 Transformer,用同一套 Attention 代码,只切换结构:
/**
* 三种 Transformer 架构对比 — 迷你实现
* 共享相同的 Attention / FFN 模块,只组合方式不同
*/
// ---------- 共享模块 ----------
function softmaxRows(matrix: number[][]): number[][] {
return matrix.map(row => {
const maxVal = Math.max(...row);
const exps = row.map(v => Math.exp(v - maxVal));
const sumExp = exps.reduce((a, b) => a + b, 0);
return exps.map(v => v / sumExp);
});
}
function matMul(A: number[][], B: number[][]): number[][] {
const m = A.length, n = A[0].length, p = B[0].length;
const result = Array.from({ length: m }, () => new Array(p).fill(0));
for (let i = 0; i < m; i++)
for (let k = 0; k < n; k++) {
const aik = A[i][k];
if (aik === 0) continue;
for (let j = 0; j < p; j++) result[i][j] += aik * B[k][j];
}
return result;
}
function randMatrix(rows: number, cols: number): number[][] {
return Array.from({ length: rows }, () =>
Array.from({ length: cols }, () => Math.random() * 0.02 - 0.01)
);
}
interface DConfig { d_model: number; d_k: number; d_v: number; d_ff: number }
/** 简化版单头 Attention */
class SimpleAttention {
private WQ: number[][];
private WK: number[][];
private WV: number[][];
constructor(config: DConfig) {
this.WQ = randMatrix(config.d_model, config.d_k);
this.WK = randMatrix(config.d_model, config.d_k);
this.WV = randMatrix(config.d_model, config.d_v);
}
forward(
Qsrc: number[][],
Ksrc: number[][],
Vsrc: number[][],
causal: boolean = false
): number[][] {
const seqQ = Qsrc.length, seqK = Ksrc.length;
const Q = matMul(Qsrc, this.WQ);
const K = matMul(Ksrc, this.WK);
const V = matMul(Vsrc, this.WV);
const d_k = Q[0].length;
const scale = Math.sqrt(d_k);
const scores: number[][] = Array.from({ length: seqQ }, (_, i) =>
Array.from({ length: seqK }, (_, j) => {
let dot = 0;
for (let d = 0; d < d_k; d++) dot += Q[i][d] * K[j][d];
if (causal && j > i) return -1e9; // 因果遮罩
return dot / scale;
})
);
const weights = softmaxRows(scores);
return matMul(weights, V);
}
}
/** 简化版 FFN: 两层线性 + ReLU */
class SimpleFFN {
private W1: number[][];
private W2: number[][];
constructor(config: DConfig) {
this.W1 = randMatrix(config.d_model, config.d_ff);
this.W2 = randMatrix(config.d_ff, config.d_model);
}
forward(X: number[][]): number[][] {
const hidden = matMul(X, this.W1).map(row =>
row.map(v => Math.max(0, v)) // ReLU
);
return matMul(hidden, this.W2);
}
}
// ---------- 架构 A: Encoder 层 ----------
class EncoderLayer {
private selfAttn: SimpleAttention;
private ffn: SimpleFFN;
constructor(config: DConfig) {
this.selfAttn = new SimpleAttention(config);
this.ffn = new SimpleFFN(config);
}
forward(X: number[][]): number[][] {
// Self-Attention(双向,无 mask)
const attnOut = this.selfAttn.forward(X, X, X, false);
// Residual + FFN
const h1 = X.map((row, i) => row.map((v, j) => v + attnOut[i][j]));
const ffnOut = this.ffn.forward(h1);
return h1.map((row, i) => row.map((v, j) => v + ffnOut[i][j]));
}
}
// ---------- 架构 B: Decoder 层(因果 Attention)----------
class DecoderLayer {
private selfAttn: SimpleAttention;
private crossAttn: SimpleAttention | null;
private ffn: SimpleFFN;
constructor(config: DConfig, hasCrossAttn: boolean) {
this.selfAttn = new SimpleAttention(config);
this.crossAttn = hasCrossAttn ? new SimpleAttention(config) : null;
this.ffn = new SimpleFFN(config);
}
forward(X: number[][], encOutput?: number[][]): number[][] {
// 1. Masked Self-Attention(因果,只看左边)
const selfOut = this.selfAttn.forward(X, X, X, true);
let h = X.map((row, i) => row.map((v, j) => v + selfOut[i][j]));
// 2. Cross-Attention(仅 Encoder-Decoder 模型有)
if (this.crossAttn && encOutput) {
const crossOut = this.crossAttn.forward(h, encOutput, encOutput, false);
h = h.map((row, i) => row.map((v, j) => v + crossOut[i][j]));
}
// 3. FFN
const ffnOut = this.ffn.forward(h);
return h.map((row, i) => row.map((v, j) => v + ffnOut[i][j]));
}
}
// ---------- 三种架构的模型 ----------
class BERTStyle {
private encoder: EncoderLayer;
constructor(config: DConfig) {
this.encoder = new EncoderLayer(config);
}
encode(X: number[][]): number[][] { return this.encoder.forward(X); }
}
class GPTStyle {
private decoder: DecoderLayer;
constructor(config: DConfig) {
this.decoder = new DecoderLayer(config, false); // 无 Cross-Attn
}
forward(X: number[][]): number[][] { return this.decoder.forward(X); }
}
class T5Style {
private encoder: EncoderLayer;
private decoder: DecoderLayer;
constructor(config: DConfig) {
this.encoder = new EncoderLayer(config);
this.decoder = new DecoderLayer(config, true); // 有 Cross-Attn
}
forward(src: number[][], tgt: number[][]): number[][] {
const encOut = this.encoder.forward(src);
return this.decoder.forward(tgt, encOut);
}
}
// ---------- 测试 ----------
function test() {
const config: DConfig = { d_model: 256, d_k: 64, d_v: 64, d_ff: 1024 };
const srcLen = 6; // 源序列长度(如英文句子)
const tgtLen = 4; // 目标序列长度(如中文译文)
const srcX = randMatrix(srcLen, config.d_model);
const tgtX = randMatrix(tgtLen, config.d_model);
// BERT: 只编码
const bert = new BERTStyle(config);
const bertOut = bert.encode(srcX);
console.log(`BERT (Encoder-Only): [${srcLen}] → [${bertOut.length}, ${bertOut[0].length}]`);
// GPT: 生成(只能看左边)
const gpt = new GPTStyle(config);
const gptOut = gpt.forward(tgtX);
console.log(`GPT (Decoder-Only): [${tgtLen}] → [${gptOut.length}, ${gptOut[0].length}] (因果)`);
// T5: Encoder→Decoder
const t5 = new T5Style(config);
const t5Out = t5.forward(srcX, tgtX);
console.log(`T5 (Enc-Dec): [${srcLen} → Enc → Dec → [${tgtLen}]] → [${t5Out.length}, ${t5Out[0].length}]`);
// 架构差异总结
console.log('\n关键差异:');
console.log(' BERT: Self-Attn 双向(可同时看左右 context)');
console.log(' GPT: Self-Attn 因果(只看左边,适合自回归生成)');
console.log(' T5: Encoder 双向理解 → Decoder 因果生成(有 Cross-Attn)');
}
test();
运行结果:
BERT (Encoder-Only): [6] → [6, 256]
GPT (Decoder-Only): [4] → [4, 256] (因果)
T5 (Enc-Dec): [6 → Enc → Dec → [4]] → [4, 256]
关键差异:
BERT: Self-Attn 双向(可同时看左右 context)
GPT: Self-Attn 因果(只看左边,适合自回归生成)
T5: Encoder 双向理解 → Decoder 因果生成(有 Cross-Attn)
四、逐行精讲
4.1 为什么 Decoder-Only 统治了 2023-2024?
三个原因:
- 统一性:同一个结构既能理解(通过 Prompt)又能生成(自带因果生成能力),不需要两个子网络。
- 可扩展性:GPT 的 Next-Token-Prediction 训练目标极简——只需要语言数据,不需要标注,Scaling Law 驱动了参数量井喷。
- In-Context Learning:Decoder-Only 模型学到的最强能力是「上下文学习」——在 Prompt 中给几个例子就能完成新任务,无需 fine-tune。
4.2 Encoder-Decoder 为什么不消亡?
T5 代表的 Enc-Dec 架构在显式输入输出对齐任务上仍有独特优势:
// 翻译场景:源语言和目标语言长度通常不同
// Encoder-Decoder: Enc 编码源语言 → Cross-Attn 让 Dec 自由决定对齐
// Decoder-Only: 必须把源语言和目标语言拼成一条序列 → 长度不灵活
4.3 为什么 BERT 不能直接生成?
BERT 的 Self-Attention 是双向的,每个 token 都能看到整个序列。这意味着不存在因果顺序——给它一个「今」字,它不会输出「天」而是输出一个分类标签。要生成需要外接 Decoder 或做 iterative masking。
五、常见问题与踩坑记录
Q1:能不能把 BERT 改成生成模型?
A:能,但效果不好。可以给 BERT 加一个随机初始化的 Decoder(如 EncDec-BERT),但性能不如同样参数量的 GPT。这是因为 BERT 的预训练目标是 MLM(填词),学到的是双向语义,不适合因果生成。
Q2:T5 的「Span Corruption」训练是什么?
A:随机掩蔽连续的一段 tokens(如「我
六、决策框架
你的任务是什么?
├── 文本分类 / NER / 情感分析 → Encoder-Only(BERT/RoBERTa)
│ └── 不需要生成文本,只要理解
│
├── 翻译 / 摘要(输入输出长度不同)→ Encoder-Decoder(T5/BART)
│ └── 显式 Enc-Dec 对齐 + Cross-Attn 效果最优
│
├── 对话 / 写作 / 代码生成 → Decoder-Only(GPT/LLaMA)
│ ├── 需要开箱即用的模型 → GPT-4 API
│ ├── 需要私有部署 → LLaMA/Qwen 微调
│ └── 需要极少参数 → Phi-3 / Gemma 2B
│
└── 多模态(图+文)→ Vision Encoder + Language Decoder
└── LLaVA / GPT-4V: ViT Encoder + LLM Decoder
七、面试速记
Q:Decoder-Only 为什么能统一所有 NLP 任务?
A:所有 NLP 任务都可以转化为文本到文本(Text-to-Text)——输入是一段 Prompt(包含指令、上下文、示例),输出是答案。Decoder-Only 模型通过 Next-Token-Prediction 学会的概率分布 P(text_n+1|text_1...text_n) 足以复现任何语言模式。
Q:Cross-Attention 和 Self-Attention 在计算上有什么不同?
A:Self-Attention 的 Q、K、V 来自同一个序列→捕捉序列内部依赖。Cross-Attention 的 Q 来自 Decoder,K/V 来自 Encoder → Decoder「查询」Encoder 的上下文。计算量相同,但语义完全不同。
八、总结
- 三种 Transformer 本质是同一套积木(Attn+FFN+LN)的不同拼法:Encoder-Only 善理解,Decoder-Only 善生成,Encoder-Decoder 善转换。
- 2024 年 Decoder-Only 的统治不是偶然:统一性 + 可无限扩展 + In-Context Learning = AGI 的路线图。
- 理解架构差异是做好模型选型的前提——上亿参数差距的部署成本,往往取决于一开始架构决策的质量。
- 下一代架构(如 Mamba/RetNet)正在挑战 Attention 的地位,但它们都继承了「单向 vs 双向」这一核心设计问题。
Comments 留言讨论
还没有评论,来抢个沙发,聊聊你的看法~