
✍ 本专题假设读者已有相关基础知识储备,目标是帮助读者以更高效的方式快速回顾每个关键知识点。本专题汇集了个人在准备多模态、大模型、强化学习等前沿岗位面试过程中总结的核心知识点,同时记录了本人在真实面试中面试官的提问。通过高效回顾这些内容,读者可以快速掌握关键概念和实践经验,为面试和工作打下坚实基础。
对于 Transformer,相信读者已经相当熟悉,因为当前大多数 LLM 都沿用了其基本架构。然而,一旦涉及具体细节,很多人可能一时难以回答。因此,本节将回顾 Transformer 的核心架构与设计要点,帮助读者在面试或实际应用中快速理清思路。
Transformer 采用了Encoder-Decoder架构,用来取代 RNN/LSTM 在长距离依赖建模上的局限。核心思想是通过注意力计算任意位置之间的信息交互,而不依赖递归或卷积的顺序处理,使得训练并行化更容易、捕捉长程依赖能力更强。
这时,面试官可能就会咨询你第一个问题了:

在了解了Transformer架构后,我们还需要知道为什么是Encoder-Decoder这样子的架构模式,在现在的主流LLM中,也依然存在着不少Decoder-Only的架构,因此面试官还可能问:

模型 | 架构类型 | 输入类型 | 输出类型 | 典型应用 |
|---|---|---|---|---|
GPT 系列 | Decoder-Only | 文本 | 文本 | 语言建模、文本生成、自回归任务 |
BERT | Encoder-Only | 文本 | 表征向量 | 表征学习、分类、问答 |
T5 | Encoder-Decoder | 文本 | 文本 | 机器翻译、摘要、seq2seq 任务 |
FLAN-T5 | Encoder-Decoder | 文本 | 文本 | 指令微调任务、seq2seq |
LLaMA | Decoder-Only | 文本 | 文本 | LLM 训练、文本生成 |
GPT-4 | Decoder-Only | 文本/多模态 | 文本/多模态 | 大规模 LLM、通用生成 |
PaLM | Decoder-Only | 文本 | 文本 | LLM 训练、文本生成 |
BLIP-2 | Encoder-Decoder | 图像+文本 | 文本 | 图文理解、图文生成 |
OpenFlamingo | Encoder-Decoder | 图像+文本 | 文本 | 多模态理解 |
MiniGPT-4 | Encoder-Decoder | 图像+文本 | 文本 | 多模态生成、问答 |
Transformer的核心内容还包括:
与传统的RNN、LSTM时序模型不同的是,为了充分利用序列之间的绝对以及相对位置信息,Transformer在对文本序列进行向量化的同时,加入了位置编码。

这是,面试官可能又会问了:
Transformer 论文中使用了 绝对位置编码(Absolute Positional Encoding)。通过对每个 token 添加固定的向量来表示其在序列中的位置。使用正弦和余弦函数实现固定位置编码:
其中 $pos$ 是位置,$i$ 是维度索引,$d{model}$ 是 embedding 维度。每个维度对应一个不同频率的正弦或余弦函数,波长呈几何级数,从 $2\pi$ 到 $10000 \cdot 2\pi$。这种编码方式允许模型学习相对位置,因为对任意固定偏移 $k$,$PE{pos+k}$ 可以通过 $PE_{pos}$ 的线性组合表示。
✅ 注意: 论文中提出实验过 可学习的位置嵌入(learned positional embeddings),效果与正弦-余弦编码几乎相同,但正弦-余弦编码可让模型更好地外推到比训练序列更长的长度。这是由于绝对正弦-余弦位置编码是一个固定函数,波长随维度指数增长,因此即使序列长度超过训练长度,也可以直接计算对应的编码向量而可学习位置嵌入是训练中针对每个位置独立优化的向量,如果在训练中未出现过的更长序列位置,就没有对应的向量,模型需要“猜测”或插值,可能不如固定函数平滑。
原始的正弦(Sinusoidal)位置编码理论上可以通过三角函数的周期性处理更长的序列,但在实践中,模型往往“过拟合”了训练时的特定相位组合,导致外推时性能也会下降。为了进一步解决模型外推问题,面试官可能会进一步问你其他类型的位置编码,例如:

自注意力是面试官最喜欢提问的模块,因为他包括了更加深入的数学原理。我们知道,自注意力机制其实就是包括了$Q, K, V$ 以及一个缩放因子。数学公式如下:
$$
Attention=\text{softmax}(\frac{QK^T}{\sqrt{d_k}})V
$$

自注意力机制的数学设计完美地模拟了人类“分配注意力”的过程:
$d_\text{model}$维度越大,方差越大,结果数值范围越极端。没有缩放时,$QK^T$的数值随维度$d_k$增大而增大,使 softmax 输入过大。由于 softmax 是一个指数函数,当输入过大时,输出接近 one-hot,梯度趋于0。从而导致梯度消失。
在标准的自注意力中,我们通过 $QK^T / \sqrt{d_k}$ 来计算不同 token 之间的注意力权重。但作者发现,仅用一个注意力头往往难以同时捕捉多种语义关系(如词法、语义、句法等)。因此,Transformer 提出了 多头注意力机制 (Multi-Head Attention, MHA)。
将输入特征通过不同的线性投影矩阵,映射到多个低维子空间中:
$$\text{head}_i = \text{Attention}(QW_i^Q, \, KW_i^K, \, VW_i^V)$$
然后将所有头拼接(concatenate)再线性变换:
$$\text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_1, \dots, \text{head}_h) W^O$$
Transformer 使用多头注意力是为了让模型在不同的子空间中独立学习不同类型的关系。多个小头可以从不同角度捕捉语义信息,增强模型的表达能力和稳定性,比单头更鲁棒。
我们在注意力权重计算的过程中会先匹配($Q K^T$),这一过程会导致当前的token看到未来的token。因此,为了避免信息泄露,在计算完 $Q K^T$ 之后加入一个 MASK, 形成因果注意力机制。
MASK是一个与 $Attention$ 大小相同的上三角矩阵作为 Mask。主对角线之上(即未来位置)的元素,用一个极大的负数(如 $-10^{9}$)进行填充。在遇到softmax后会趋近于0。

Transformer 的稳定训练,很大程度上依赖于 残差连接(Residual Connection) 与 层归一化(Layer Normalization, LN) 的配合。它们共同解决了深层网络的梯度消失与分布漂移问题
在每个子层(Self-Attention 或 Feed-Forward 层)中,Transformer 都采用如下结构:
$$x' = \text{LayerNorm}(x + \text{Sublayer}(x))$$
📌 作用:: 允许梯度从更深层直接反向传播到浅层,避免梯度消失,使深层 Transformer(如 96 层的 GPT-3)仍能稳定训练。
LayerNorm 是在特征维度(feature dimension)上归一化的操作:
$$\text{LN}(x_i) = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \beta$$
其中:
📌 直觉:
LayerNorm 让每个 token 的特征分布稳定,不会因为输入尺度或激活偏移而导致模型震荡。
面试官可能会问:
BatchNorm 对比 LayerNorm 的关键区别是 归一化的维度不同:
归一化方法 | 归一化范围 | 是否依赖 Batch | 是否受序列长度影响 |
|---|---|---|---|
BatchNorm | 同一通道、跨样本 | ✅ 是 | ✅ 受影响 |
LayerNorm | 单样本、跨通道 | ❌ 否 | ❌ 不受影响 |
BN 的统计量(均值、方差)来自整个 batch,因此在 Transformer 中有以下问题:
import math
from typing import Optional, Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
# -----------------------------
# 1. Scaled Dot-Product Attention
# -----------------------------
def scaled_dot_product_attention(
Q: torch.Tensor,
K: torch.Tensor,
V: torch.Tensor,
mask: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Scaled Dot-Product Attention
Args:
Q: [B, h, L_q, d_k]
K: [B, h, L_k, d_k]
V: [B, h, L_k, d_v]
mask: [B, 1, L_q, L_k] or [B, h, L_q, L_k]
Returns:
out: [B, h, L_q, d_v]
attn: [B, h, L_q, L_k]
"""
d_k = K.size(-1)
scores = Q @ K.transpose(-2, -1) / math.sqrt(d_k) # [B, h, L_q, L_k]
if mask is not None:
# mask == 0 的位置填 -inf,softmax 后概率为 0
scores = scores.masked_fill(mask == 0, float("-inf"))
attn = F.softmax(scores, dim=-1)
out = attn @ V # [B, h, L_q, d_v]
return out, attnclass MultiHeadAttention(nn.Module):
def __init__(self, d_model: int, num_heads: int, dropout: float = 0.1):
super().__init__()
assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_heads
self.W_q = nn.Linear(d_model, d_model)
self.W_k = nn.Linear(d_model, d_model)
self.W_v = nn.Linear(d_model, d_model)
self.W_o = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
def _split_heads(self, x: torch.Tensor) -> torch.Tensor:
"""
输入: [B, L, d_model]
输出: [B, h, L, d_k]
"""
B, L, _ = x.shape
x = x.view(B, L, self.num_heads, self.d_k)
return x.transpose(1, 2) # [B, h, L, d_k]
def _combine_heads(self, x: torch.Tensor) -> torch.Tensor:
"""
输入: [B, h, L, d_k]
输出: [B, L, d_model]
"""
B, h, L, d_k = x.shape
x = x.transpose(1, 2).contiguous().view(B, L, h * d_k)
return x
def forward(
self,
Q: torch.Tensor,
K: torch.Tensor,
V: torch.Tensor,
mask: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Args:
Q, K, V: [B, L, d_model]
mask: [B, 1, 1, L] 或 [B, 1, L, L]
Returns:
out: [B, L, d_model]
attn: [B, h, L, L]
"""
B, L, _ = Q.shape
Q = self._split_heads(self.W_q(Q)) # [B, h, L, d_k]
K = self._split_heads(self.W_k(K)) # [B, h, L, d_k]
V = self._split_heads(self.W_v(V)) # [B, h, L, d_k]
if mask is not None:
# mask 形状适配到 [B, 1, L_q, L_k]
if mask.dim() == 3:
mask = mask.unsqueeze(1)
out, attn = scaled_dot_product_attention(Q, K, V, mask)
out = self._combine_heads(out) # [B, L, d_model]
out = self.W_o(out)
out = self.dropout(out)
return out, attnclass PositionwiseFeedForward(nn.Module):
def __init__(
self,
d_model: int,
d_ff: int = 2048,
dropout: float = 0.1,
activation: str = "relu",
):
super().__init__()
self.fc1 = nn.Linear(d_model, d_ff)
self.fc2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
if activation == "relu":
self.act = F.relu
elif activation == "gelu":
self.act = F.gelu
else:
raise ValueError(f"Unsupported activation: {activation}")
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.fc1(x)
x = self.act(x)
x = self.dropout(x)
x = self.fc2(x)
return xclass AddNorm(nn.Module):
def __init__(self, d_model: int, dropout: float = 0.1):
super().__init__()
self.norm = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x: torch.Tensor, sublayer_out: torch.Tensor) -> torch.Tensor:
"""
x: 残差分支输入
sublayer_out: 子层输出 (self-attn / ffn)
"""
return self.norm(x + self.dropout(sublayer_out))class PositionalEncoding(nn.Module):
def __init__(self, d_model: int, max_len: int = 5000, dropout: float = 0.1):
super().__init__()
self.dropout = nn.Dropout(dropout)
pe = torch.zeros(max_len, d_model) # [L, d_model]
position = torch.arange(0, max_len).unsqueeze(1) # [L, 1]
div_term = torch.exp(
torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model)
) # [d_model/2]
pe[:, 0::2] = torch.sin(position * div_term) # 偶数维
pe[:, 1::2] = torch.cos(position * div_term) # 奇数维
pe = pe.unsqueeze(0) # [1, L, d_model]
self.register_buffer("pe", pe) # 不参与训练
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
x: [B, L, d_model]
"""
L = x.size(1)
x = x + self.pe[:, :L, :]
return self.dropout(x)class EncoderLayer(nn.Module):
def __init__(
self,
d_model: int,
num_heads: int,
d_ff: int = 2048,
dropout: float = 0.1,
):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads, dropout)
self.ffn = PositionwiseFeedForward(d_model, d_ff, dropout)
self.addnorm1 = AddNorm(d_model, dropout)
self.addnorm2 = AddNorm(d_model, dropout)
def forward(self, x: torch.Tensor, src_mask: Optional[torch.Tensor] = None):
# Self-Attention + Add & Norm
attn_out, _ = self.self_attn(x, x, x, src_mask)
x = self.addnorm1(x, attn_out)
# FFN + Add & Norm
ffn_out = self.ffn(x)
x = self.addnorm2(x, ffn_out)
return xclass TransformerEncoder(nn.Module):
def __init__(
self,
vocab_size: int,
d_model: int = 512,
num_layers: int = 6,
num_heads: int = 8,
d_ff: int = 2048,
dropout: float = 0.1,
max_len: int = 5000,
):
super().__init__()
self.d_model = d_model
self.embed = nn.Embedding(vocab_size, d_model)
self.pos_encoding = PositionalEncoding(d_model, max_len, dropout)
self.layers = nn.ModuleList(
[
EncoderLayer(d_model, num_heads, d_ff, dropout)
for _ in range(num_layers)
]
)
self.norm = nn.LayerNorm(d_model)
def forward(self, src: torch.Tensor, src_mask: Optional[torch.Tensor] = None):
"""
Args:
src: [B, L] token id 序列
src_mask: [B, 1, 1, L] padding mask(1 为保留,0 为忽略)
Returns:
out: [B, L, d_model]
"""
x = self.embed(src) * math.sqrt(self.d_model) # 缩放嵌入
x = self.pos_encoding(x)
for layer in self.layers:
x = layer(x, src_mask)
return self.norm(x)def build_padding_mask(src: torch.Tensor, pad_idx: int = 0) -> torch.Tensor:
"""
src: [B, L]
返回: [B, 1, 1, L]
"""
mask = (src != pad_idx).unsqueeze(1).unsqueeze(2)
return mask # 1 表示保留,0 表示被 mask
if __name__ == "__main__":
# 假设词表大小为 10000,批大小 B=2,序列长度 L=10
vocab_size = 10000
B, L = 2, 10
model = TransformerEncoder(
vocab_size=vocab_size,
d_model=256,
num_layers=2,
num_heads=8,
d_ff=512,
dropout=0.1,
max_len=100,
)
# 构造一个假输入,0 作为 pad
src = torch.randint(low=1, high=vocab_size, size=(B, L))
src[:, -2:] = 0 # 人为加两个 pad token
src_mask = build_padding_mask(src, pad_idx=0)
out = model(src, src_mask) # [B, L, d_model]
print("input shape :", src.shape)
print("output shape:", out.shape)总结
本章总结了Transformer的基本知识点以及问法,但也通常不会直接问了,更多的是融合其他LLM进行提问,例如:Transformer、Bert、GPT在架构上分别有什么差别。在面试或科研问答中,Transformer 相关问题往往不再单独考察“公式细节”,而是考查你是否理解:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。