AI 解读算法实践

从 Encoder-Decoder 到 Decoder-Only:Transformer 架构进化史与选型指南

Michael Meng· 2026年7月23日· ◷ 8 分钟阅读
从 Encoder-Decoder 到 Decoder-Only:Transformer 架构进化史与选型指南

一、问题引入:都是 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?

三个原因:

  1. 统一性:同一个结构既能理解(通过 Prompt)又能生成(自带因果生成能力),不需要两个子网络。
  2. 可扩展性:GPT 的 Next-Token-Prediction 训练目标极简——只需要语言数据,不需要标注,Scaling Law 驱动了参数量井喷。
  3. 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(如「我北京回来」),让模型还原被掩蔽的内容。相比 BERT 的单个 token 掩码,Span Corruption 迫使模型学会生成连贯的文本片段,是连接理解和生成的「一步到位」方案。

六、决策框架

你的任务是什么?
├── 文本分类 / 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 留言讨论

还没有评论,来抢个沙发,聊聊你的看法~

Michael.Meng

michaelnews@126.com
用 AI 记录,用文字沉淀

© 2026 Michael Meng · 保留所有权利 · Powered by FastAPI + Nuxt