首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【多模态大模型面经】 Transformer 专题面经

【多模态大模型面经】 Transformer 专题面经

原创
作者头像
九年义务漏网鲨鱼
发布2025-11-16 11:25:46
发布2025-11-16 11:25:46
1170
举报

前言

✍ 本专题假设读者已有相关基础知识储备,目标是帮助读者以更高效的方式快速回顾每个关键知识点。本专题汇集了个人在准备多模态、大模型、强化学习等前沿岗位面试过程中总结的核心知识点,同时记录了本人在真实面试中面试官的提问。通过高效回顾这些内容,读者可以快速掌握关键概念和实践经验,为面试和工作打下坚实基础。

一、Transformer 基本架构

对于 Transformer,相信读者已经相当熟悉,因为当前大多数 LLM 都沿用了其基本架构。然而,一旦涉及具体细节,很多人可能一时难以回答。因此,本节将回顾 Transformer 的核心架构与设计要点,帮助读者在面试或实际应用中快速理清思路。

Transformer 采用了Encoder-Decoder架构,用来取代 RNN/LSTM 在长距离依赖建模上的局限。核心思想是通过注意力计算任意位置之间的信息交互,而不依赖递归或卷积的顺序处理,使得训练并行化更容易、捕捉长程依赖能力更强。

这时,面试官可能就会咨询你第一个问题了:

🧠 1. Transformer的Encoder和Decoder分别是怎么设计的
  • Encoder结构:每一层 Encoder Layer 包含两个主要的子层(Sub-layers):① 多头注意力层 ② 前馈网络层;Output = LayerNorm(X + SubLayer(X))
  • Decoder结构:每一层 Decoder Layer 包含三个主要的子层(Sub-layers):① 掩码多头自注意力 ② 编码器-解码器注意力 ③ 前馈网络层;
  • 连接方式:Encoder和Decoder层与层之间的连接方式都相同,每个子层都使用了残差连接 (Residual Connection)LayerNorm,即

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

🧠 2. 为什么有 Encoder-Decoder 和 Decoder-Only 两种不同的架构?它们各自适合什么场景?
  • Encoder-Decoder 架构:适用于序列到序列(seq2seq)任务,如机器翻译、摘要生成。Encoder 编码完整输入序列,需要通过 Encoder 理解图文或文本知识,再通过 Decoder 生成输出序列,能够显式建模输入-输出对应关系,表现更强。
  • Decoder-Only 架构:适合语言建模、文本生成和自回归任务。整个模型只包含 Decoder,通过自回归预测下一个 token,结构更简洁,便于大规模训练,但在输入-输出对齐和条件生成任务上通常不如 Encoder-Decoder 强。因此,在纯生成任务中,使用 Decoder-Only 架构是可行且高效的,因为模型只需要预测下一个 token,不需要显式处理输入-输出对齐。对于这些任务,Decoder-Only 能够充分利用自回归生成能力,并且结构简单便于大规模训练。

📝 主流 LLM 和多模态大模型架构对比表

模型

架构类型

输入类型

输出类型

典型应用

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在对文本序列进行向量化的同时,加入了位置编码。

这是,面试官可能又会问了:

🧠 3. 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)位置编码理论上可以通过三角函数的周期性处理更长的序列,但在实践中,模型往往“过拟合”了训练时的特定相位组合,导致外推时性能也会下降。为了进一步解决模型外推问题,面试官可能会进一步问你其他类型的位置编码,例如:

🧠 4. 你还了解过什么位置编码 (真实面试提问)
  • 相对位置编码 (Relative Positional Encoding - RPE)
    • 代表模型:Transformer-XL, T5, DeBERTa
    • 实现方式:为不同相对距离创建可学习嵌入表;长距离通过分桶共享嵌入。在注意力分数($QK^\top$)前加上对应的距离偏置。
    • 解决外推问题:距离概念是通用的,任意序列长度都能共享相同嵌入,实现泛化。
  • 旋转位置编码 (Rotary Positional Embedding - RoPE)
    • 代表模型:LLaMA, Mistral, Qwen, PaLM 等主流 LLM
    • 实现方式:将 Q、K 向量按两维一组旋转角度,点积结果只依赖相对位置 $(m-n)$。
    • 解决外推问题:旋转后的相对角度概念通用,对长序列外推稳定。

三、多头注意力机制

自注意力是面试官最喜欢提问的模块,因为他包括了更加深入的数学原理。我们知道,自注意力机制其实就是包括了$Q, K, V$ 以及一个缩放因子。数学公式如下:

$$

Attention=\text{softmax}(\frac{QK^T}{\sqrt{d_k}})V

$$

自注意力机制的数学设计完美地模拟了人类“分配注意力”的过程:

  1. 匹配($Q K^T$): 我要查什么 (Q)?它和我的资料 (K) 有多相关? $\rightarrow$ 得出相关性分数。
  2. 归一化($\text{softmax}$): 将分数转化为注意力权重(贡献度比例)。
  3. 提取($\times V$): 根据贡献度比例,从实际信息 (V) 中加权抽取所需的内容。
🧠 5. 为什么需要缩放因子 (真实面试)

$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$$

🧠 6. 为什么要采用多头注意力机制

Transformer 使用多头注意力是为了让模型在不同的子空间中独立学习不同类型的关系。多个小头可以从不同角度捕捉语义信息,增强模型的表达能力和稳定性,比单头更鲁棒。


🧠 7. Encoder和Decoder中用的注意力机制有什么不同
  • Encoder 用的是 双向(bidirectional)自注意力,让每个位置能看到序列中所有 token 的信息;
  • Decoder 包含 因果(causal / masked)自注意力(只看到当前及过去位置)以及 Encoder–Decoder(cross)注意力(把 Decoder 的查询与 Encoder 的键值对接),两者合起来保证生成时的自回归性并能利用输入上下文。

我们在注意力权重计算的过程中会先匹配($Q K^T$),这一过程会导致当前的token看到未来的token。因此,为了避免信息泄露,在计算完 $Q K^T$ 之后加入一个 MASK, 形成因果注意力机制。

MASK是一个与 $Attention$ 大小相同的上三角矩阵作为 Mask。主对角线之上(即未来位置)的元素,用一个极大的负数(如 $-10^{9}$)进行填充。在遇到softmax后会趋近于0。

四、LayerNorm 与 残差连接

Transformer 的稳定训练,很大程度上依赖于 残差连接(Residual Connection) 与 层归一化(Layer Normalization, LN) 的配合。它们共同解决了深层网络的梯度消失与分布漂移问题

🧱 1. 残差连接(Residual Connection)

在每个子层(Self-Attention 或 Feed-Forward 层)中,Transformer 都采用如下结构:

$$x' = \text{LayerNorm}(x + \text{Sublayer}(x))$$

📌 作用:: 允许梯度从更深层直接反向传播到浅层,避免梯度消失,使深层 Transformer(如 96 层的 GPT-3)仍能稳定训练。

⚖️ 2. Layer Normalization(LN)

LayerNorm 是在特征维度(feature dimension)上归一化的操作:

$$\text{LN}(x_i) = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \beta$$

其中:

  • $\mu, \sigma$ 是单个样本在特征维度上的均值与方差;
  • $\gamma, \beta$ 是可学习缩放与平移参数。

📌 直觉:

LayerNorm 让每个 token 的特征分布稳定,不会因为输入尺度或激活偏移而导致模型震荡。

面试官可能会问:

🧠 8. 为什么 Transformer 不使用 BatchNorm(BN)

BatchNorm 对比 LayerNorm 的关键区别是 归一化的维度不同

归一化方法

归一化范围

是否依赖 Batch

是否受序列长度影响

BatchNorm

同一通道、跨样本

✅ 是

✅ 受影响

LayerNorm

单样本、跨通道

❌ 否

❌ 不受影响

BN 的统计量(均值、方差)来自整个 batch,因此在 Transformer 中有以下问题:

  1. 变长序列问题undefined每个样本的 token 数量不同,BN 无法在时间维度上统一统计。
  2. 小 batch 或自回归推理不稳定undefined在推理(inference)阶段,batch size 往往为 1。BN 的统计量与训练时分布不匹配,容易导致性能下降。
  3. 分布漂移(Distribution Shift)undefined自注意力中,不同 token 的激活分布差异大,BN 在 batch 内混合这些统计会引入噪声,破坏 token 表征独立性。

五、手撕代码

Scaled Dot-Product Attention

代码语言:python
复制
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, attn

Multi-Head Attention

代码语言:python
复制
class 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, attn

Position-wise Feed Forward

代码语言:python
复制
class 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 x

Add & Norm (Residual + LayerNorm)

代码语言:python
复制
class 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))

Positional Encoding

代码语言:python
复制
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)

Encoder Layer

代码语言:python
复制
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 x

Transformer Encoder 堆叠

代码语言:python
复制
class 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)

构造 padding mask & 简单测试

代码语言:python
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、Transformer 基本架构
    • 🧠 1. Transformer的Encoder和Decoder分别是怎么设计的
    • 🧠 2. 为什么有 Encoder-Decoder 和 Decoder-Only 两种不同的架构?它们各自适合什么场景?
    • 📝 主流 LLM 和多模态大模型架构对比表
  • 二、位置编码
    • 🧠 3. Transformer提出了什么位置编码,具体怎么实现的
    • 🧠 4. 你还了解过什么位置编码 (真实面试提问)
  • 三、多头注意力机制
    • 🧠 5. 为什么需要缩放因子 (真实面试)
    • 🧠 6. 为什么要采用多头注意力机制
    • 🧠 7. Encoder和Decoder中用的注意力机制有什么不同
  • 四、LayerNorm 与 残差连接
    • 🧱 1. 残差连接(Residual Connection)
    • ⚖️ 2. Layer Normalization(LN)
      • 🧠 8. 为什么 Transformer 不使用 BatchNorm(BN)
  • 五、手撕代码
    • Scaled Dot-Product Attention
    • Multi-Head Attention
    • Position-wise Feed Forward
    • Add & Norm (Residual + LayerNorm)
    • Positional Encoding
    • Encoder Layer
    • Transformer Encoder 堆叠
    • 构造 padding mask & 简单测试
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档