2017 年,Transformer 横空出世,直接颠覆了之前 RNN、LSTM 主导的 NLP 领域 —— 它就像一座高效运转的智能翻译工厂,不用像之前的 "流水线"(RNN)那样逐词处理,而是能 "并行开工",同时分析整个句子的语义,还能精准抓住重点。今天咱们就深入这座工厂,看看每个车间、每个工位是怎么分工协作,把一种语言完美转换成另一种的~

一、工厂核心优势:为什么它比老流水线(RNN)强?

在 Transformer 出现前,翻译模型靠 RNN/LSTM"逐词翻译":先处理第一个词,再处理第二个,像流水线一样串行工作,效率低还记不住长句子细节。而 Transformer 的核心突破是:

  1. 并行计算:所有词同时分析,不用排队,训练速度提升 N 倍;

  2. 全局注意力:能同时看到句子里所有词的关系(比如 "他" 到底指前面的 "小明" 还是 "小红"),翻译更准确;

  3. 更强的特征提取:通过 "多头注意力" 和 "前馈网络",能挖掘更深层的语义(比如区分 "银行" 是 "存钱的地方" 还是 "河岸")。

这也是为什么 2018 年 BERT(基于 Transformer)能横扫 NLP 各项任务 —— 底子打得好!

二、工厂架构全景:四大核心车间 + 关键组件

Transformer 这座工厂的完整流程是:输入车间编码车间解码车间输出车间,中间还有 "注意力机制" 这个核心协作工具。咱们逐个拆解:

(一)输入车间:给文字 "数字化 + 贴位置标签"

输入车间的任务是把人类语言(比如 "我爱中国")变成机器能懂的 "数字信号",分两步走:

1. 第一步:词嵌入(Word Embedding)—— 给每个词发 "数字身份证"

每个词都会被转换成一个固定维度的向量(比如 512 维),就像给每个词办了一张独一无二的数字身份证。比如:

  • "我" → [0.2, 0.5, -0.1, ..., 0.3](512 个数字);

  • "爱" → [0.7, -0.3, 0.4, ..., -0.2]。

但这里有个小技巧:词嵌入后要乘以√嵌入维度(比如√512≈22.6)。原因很实在:

  • 防止后面的 "位置编码" 信息盖过词本身的含义;

  • 让向量值符合标准正态分布,训练更稳定。

代码实现超直观:

class Embeddings(nn.Module):
    def __init__(self, vocab_size, embed_dim):
        super().__init__()
        # 词嵌入层:vocab_size个词,每个词embed_dim维向量
        self.embed = nn.Embedding(vocab_size, embed_dim)
        self.embed_dim = embed_dim

    def forward(self, x):
        # x是词的索引(比如[1,4,10]),输出是词向量×√embed_dim
        return self.embed(x) * math.sqrt(self.embed_dim)
2. 第二步:位置编码(Positional Encoding)—— 给词按顺序贴标签

机器本身不知道词的顺序(比如 "我爱中国" 和 "中国爱我" 词向量一样,但意思完全不同),所以需要给每个词的位置编上号。

Transformer 用了个聪明的办法:三角函数编码(正弦 sin + 余弦 cos),好处有两个:

  • 同一词在不同位置有不同编码(比如 "我" 在第 1 位和第 3 位的编码不一样);

  • 三角函数的周期性让模型能理解 "位置远近"(比如第 2 个词和第 4 个词比第 10 个词更近)。

代码里是这么实现的:

class PositionEmbeddings(nn.Module):
    def __init__(self, embed_dim, dropout_p, max_len=60):
        super().__init__()
        self.dropout = nn.Dropout(dropout_p)
        # 位置编码矩阵:max_len个词,每个词embed_dim维
        pe = torch.zeros(max_len, embed_dim)
        # 生成位置序列:[0,1,2,...,59](共60个位置)
        position = torch.arange(0, max_len).unsqueeze(1)
        # 生成变换因子:让不同位置的编码有区分度
        div_term = torch.exp(torch.arange(0, embed_dim, 2) * -(math.log(10000.0) / embed_dim))
        # 奇数位置用sin,偶数位置用cos
        pe[:, 0::2] = torch.sin(position * div_term)  # 0、2、4...列
        pe[:, 1::2] = torch.cos(position * div_term)  # 1、3、5...列
        # 升维:[60,512]→[1,60,512],方便和词嵌入相加
        pe = pe.unsqueeze(0)
        # 注册为缓存:不参与训练,但模型保存时会一起保存
        self.register_buffer('pe', pe)

    def forward(self, x):
        # 词嵌入 + 位置编码(只取前x.size(1)个位置,适配句子长度)
        x = x + self.pe[:, :x.size(1)]
        return self.dropout(x)

最终输入车间的输出:词嵌入向量 + 位置编码向量,形状是[批量大小, 句子长度, 嵌入维度](比如 [2, 4, 512]:2 个句子,每个句子 4 个词,每个词 512 维)。

(二)核心工具:注意力机制 —— 工厂内部的 "重点讨论会"

这是 Transformer 最灵魂的部分!就像工厂讨论时,大家会重点关注重要信息,忽略无关内容,注意力机制就是让模型学会 "该关注什么"。

1. 注意力的核心逻辑:QKV 三兄弟协作

注意力机制靠三个角色配合(可以用 "查字典" 理解):

  • Q(Query):要查询的内容(比如我想知道 "他" 指谁,Q 就是 "他" 的向量);

  • K(Key):关键词索引(比如句子里的 "小明"、"小红" 的向量);

  • V(Value):关键词对应的具体内容(比如 "小明" 是 "男孩","小红" 是 "女孩")。

核心公式(记不住也没关系,懂逻辑就行):

注意力结果 = Softmax( (Q × K^T) / √d_k ) × V

拆解成三步:

  1. 算 "关注度":用 Q 和 K 的转置相乘,除以√d_k(d_k 是 Q/K 的维度)—— 防止数值太大导致 Softmax"饱和"(所有概率都集中在一个词上);

  2. 标准化:用 Softmax 把关注度变成 0-1 的概率(总和为 1),比如 "他" 对应 "小明" 的概率 0.8,对应 "小红" 的概率 0.2;

  3. 提信息:用概率乘以 V,得到加权后的信息(重点提取 "小明" 的内容)。

举个具体例子:在句子 “小明爱小红,他送了她一束花” 中,当模型处理 "他" 时:

  • Q 是 "他" 的向量,K 是 "小明"、"小红"、"爱"、"送" 等所有词的向量;

  • 计算后发现 "他" 与 "小明" 的 K 相似度最高(权重 0.8),与 "小红" 的相似度低(权重 0.1);

  • 最终注意力结果主要由 "小明" 的 V 向量构成,模型因此知道 "他" 指 "小明"。

def attention(query, key, value, mask=None, dropout=None):
    d_k = query.size(-1)  # Q的维度(比如64)
    # 1. 计算关注度分数
    scores = torch.matmul(query, key.transpose(-1, -2)) / math.sqrt(d_k)
    # 2. 掩码(可选):遮住不重要的词(比如补齐的PAD词)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)  # 遮掉的位置分数设为极小值
    # 3. 标准化得到概率
    attn_weight = F.softmax(scores, dim=-1)
    # 4. 防止过拟合:随机失活
    if dropout is not None:
        attn_weight = dropout(attn_weight)
    # 5. 提取加权信息
    return torch.matmul(attn_weight, value), attn_weight
2. 多头注意力(Multi-Head Attention):多视角讨论

一个注意力头看问题可能片面,就像一个人只能注意到句子的一个角度(比如只关注主谓关系)。Transformer 用 "多头注意力" 解决这个问题:把注意力机制分成多个 "小团队"(比如 8 个),每个头独立关注不同的语言特征,最后汇总所有头的意见,得到更全面的理解。

为什么要分多头?单头注意力可能会 "一叶障目"。比如翻译 "苹果手机很好用" 时:

  • 单头可能只关注 "苹果" 和 "手机" 的关联,忽略 "好用" 的语义;

  • 多头则可以让不同头分工:头 1 关注 "苹果" 与 "手机" 的修饰关系,头 2 关注 "好用" 与 "手机" 的描述关系,头 3 关注整体句式结构。

多头注意力的详细工作流程

  1. 拆分 QKV:把输入的 Q、K、V(512 维)拆成 8 个小头,每个头处理 64 维向量(512÷8=64)。这一步就像把一个复杂问题拆成 8 个小问题,让每个小团队专注解决一部分;

  2. 每个头独立计算注意力:8 个小团队并行工作,各自输出 64 维的注意力结果。例如:

    • 头 1:专注于语法关系(主谓、动宾),比如 "我爱中国" 中 "我" 和 "爱" 的主谓关系;

    • 头 2:专注于语义关联(近义词、上下位词),比如 "苹果" 和 "水果" 的关系;

    • 头 3:专注于位置依赖(比如 "的" 前后的修饰关系)。

  3. 合并多头结果:把 8 个头的 64 维结果拼接起来,重新组成 512 维向量(8×64=512)。这一步类似 8 个小团队汇报结论,汇总成完整方案;

  4. 线性变换:通过一个全连接层对合并后的向量进行微调,确保输出格式统一,便于后续处理。

代码实现(核心部分)

class MutiHeadAttention(nn.Module):
    def __init__(self, head, embed_dim, dropout_p=0.1):
        super().__init__()
        assert embed_dim % head == 0  # 确保能整除
        self.d_k = embed_dim // head  # 每个头的维度(512÷8=64)
        self.head = head  # 头数(8)
        # 4个全连接层:3个用于QKV拆分,1个用于最终合并
        self.linears = clones(nn.Linear(embed_dim, embed_dim), 4)  
        self.dropout = nn.Dropout(p=dropout_p)

    def forward(self, query, key, value, mask=None):
        if mask is not None:
            mask = mask.unsqueeze(0)  # 扩展维度适配多头
        batch_size = query.size(0)

        # 1. 线性变换+拆分多头:[2,4,512]→[2,4,8,64]→[2,8,4,64]
        query, key, value = [
            model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2)
            for model, x in zip(self.linears, (query, key, value))
        ]

        # 2. 8个小头并行计算注意力
        attn_output, attn_weight = attention(query, key, value, mask, self.dropout)

        # 3. 合并多头:[2,8,4,64]→[2,4,8,64]→[2,4,512]
        attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)

        # 4. 最终线性变换,统一输出格式
        return self.linears[-1](attn_output)
3. 注意力机制如何发现 token 之间的关系?

Transformer 通过注意力权重的分布,直接 "显式" 地捕捉 token 间的关联,这是它比 RNN 更透明的优势。具体来说,有三种典型关系会被捕捉:

  1. 语法依赖关系:比如主谓、动宾、修饰关系。在 "美丽的花朵绽放了" 中,注意力机制会让 "美丽的" 对 "花朵" 的权重很高(修饰关系),"花朵" 对 "绽放了" 的权重很高(主谓关系)。

  2. 语义关联关系:比如指代、同义、反义关系。在 "小明丢了伞,他很着急" 中,"他" 对 "小明" 的注意力权重接近 1(明确指代);在 "冷和热是反义词" 中,"冷" 和 "热" 的相互注意力权重会显著高于其他词。

  3. 长距离依赖关系:即使两个词相隔很远,也能被关联。在 "虽然雨下得很大,但他还是坚持跑完了全程,这让我很感动" 中,"这" 对 "坚持跑完" 的注意力权重会很高,模型能跨越多个词捕捉到指代关系 —— 这是 RNN 很难做到的(RNN 处理长句子时容易 "遗忘" 前面的信息)。

可视化注意力权重:通过热力图可以直观看到这种关联。横轴和纵轴都是句子中的 token,颜色越深表示注意力权重越高。例如在翻译 "She loves cats because they are cute" 时,法文翻译 "Elle aime les chats parce qu'ils sont mignons" 的注意力热力图中,"they" 对应 "ils" 的位置会呈现深色,清晰显示指代关系。

4. 掩码(Mask):防止 "作弊" 和 "分心"

注意力机制需要掩码来解决两个问题:

  • Padding Mask(补齐掩码):句子长度不一样时,会用 PAD 词补齐(比如一个句子 4 个词,一个 6 个词,短的补 2 个 PAD),掩码会遮住这些 PAD 词,不让模型关注它们;

  • Subsequent Mask(后续掩码):解码时,模型不能 "偷看" 后面还没生成的词(比如翻译 "我爱吃苹果",生成 "我" 时不能看 "爱"、"吃"、"苹果"),所以用下三角矩阵遮住后面的位置。

代码里的后续掩码实现:

def seq_mask(size):
    # 生成上三角矩阵(1表示要遮的位置),转成下三角矩阵(0表示遮)
    return 1 - torch.triu(torch.ones(1, size, size), diagonal=1)

这里的clones函数是个小工具,复制多个相同的层(比如 4 个全连接层),避免重复代码:

def clones(module, N):
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

(三)编码车间:深入理解原文的 "分析团队"

编码车间由 6 个相同的 "编码小组"(EncoderLayer)组成(可以理解为 6 轮深度分析),每个小组做两件事,还配有 "残差连接" 和 "规范化" 两个辅助工具。

1. 编码小组的工作流程(每个小组):
输入 → 多头自注意力 → 残差连接+规范化 → 前馈网络 → 残差连接+规范化 → 输出
2. 关键组件解析:
  • 多头自注意力:Q=K=V(都是当前句子的向量),分析句子内部的关系(比如 "我爱中国" 中 "我" 和 "中国" 的关系);

  • 前馈网络(FeedForward):两层全连接层,增强模型的拟合能力(比如把注意力结果进一步提炼,挖掘深层语义)。代码实现:

class FeedForward(nn.Module):
    def __init__(self, embed_dim, hidden_dim, dropout_p=0.1):
        super().__init__()
        self.linear1 = nn.Linear(embed_dim, hidden_dim)  # 512→1024
        self.linear2 = nn.Linear(hidden_dim, embed_dim)  # 1024→512
        self.dropout = nn.Dropout(p=dropout_p)

    def forward(self, x):
        # 线性变换→ReLU激活→ dropout→线性变换
        return self.linear2(self.dropout(F.relu(self.linear1(x))))
  • 残差连接(Residual Connection):把输入直接加到输出上(x + 注意力结果),防止深层网络的梯度消失(就像给信息加了条 "捷径",不会越传越弱);

  • 规范化(LayerNorm):把数据归一化到固定范围(比如均值 0、方差 1),避免参数过大或过小,让训练更稳定。代码实现:

class LayerNorm(nn.Module):
    def __init__(self, embed_dim, eps=1e-6):
        super().__init__()
        self.a = nn.Parameter(torch.ones(embed_dim))  # 可学习的缩放系数
        self.b = nn.Parameter(torch.zeros(embed_dim))  # 可学习的偏移系数
        self.eps = eps  # 防止分母为0

    def forward(self, x):
        mean = x.mean(-1, keepdim=True)  # 按最后一维求均值
        std = x.std(-1, keepdim=True)   # 按最后一维求方差
        # 归一化:(x-mean)/std × a + b
        return self.a * (x - mean) / (std + self.eps) + self.b
3. 子层连接(SublayerConnection):把组件串起来

编码小组里,多头注意力和前馈网络都是 "子层",用SublayerConnection把 "子层 + 残差连接 + 规范化" 打包:

class SubLayerConnection(nn.Module):
    def __init__(self, embed_dim, dropout_p=0.1):
        super().__init__()
        self.norm = LayerNorm(embed_dim)
        self.dropout = nn.Dropout(dropout_p)

    def forward(self, x, sublayer):
        # 先规范化→子层处理→dropout→残差连接(x + 处理结果)
        return x + self.dropout(sublayer(self.norm(x)))
4. 编码车间整体(6 个小组堆叠):
class Encoder(nn.Module):
    def __init__(self, layer, N):
        super().__init__()
        self.layers = clones(layer, N)  # 复制N个编码小组(N=6)
        self.norm = LayerNorm(layer.embed_dim)  # 最终规范化

    def forward(self, x, mask):
        # 依次经过6个编码小组
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)

编码车间的输出:原文的深层语义表示(形状和输入一样:[2,4,512]),包含了句子的所有关键信息。

(四)解码车间:生成译文的 "创作团队"

解码车间和编码车间类似,由 6 个 "解码小组"(DecoderLayer)组成,但每个小组要做三件事(比编码小组多一件),因为要同时结合 "原文信息" 和 "已生成的译文信息"。

1. 解码小组的工作流程(每个小组):
输入 → 多头自注意力(带后续掩码) → 残差连接+规范化 → 
多头注意力(编码-解码注意力) → 残差连接+规范化 → 
前馈网络 → 残差连接+规范化 → 输出
2. 关键差异点:
  • 多头自注意力(带后续掩码):Q=K=V(已生成的译文向量),但要加后续掩码,防止 "偷看" 后面没生成的词;

  • 编码 - 解码注意力(Cross Attention):Q 是已生成的译文向量,K=V 是编码车间的输出(原文语义)—— 这一步是 "结合原文生成译文" 的关键(比如生成 "love" 时,关注原文的 "爱")。

3. 解码车间整体:
class Decoder(nn.Module):
    def __init__(self, layer, N):
        super().__init__()
        self.layers = clones(layer, N)  # 6个解码小组
        self.norm = LayerNorm(layer.embed_dim)

    def forward(self, x, encoder_out, src_mask, target_mask):
        for layer in self.layers:
            # x:已生成的译文向量;encoder_out:编码车间输出;src_mask:原文掩码;target_mask:译文掩码
            x = layer(x, encoder_out, src_mask, target_mask)
        return self.norm(x)

解码车间的输出:译文的语义表示(形状:[2,6,512]:2 个句子,每个句子 6 个词,每个词 512 维)。

(五)输出车间:把机器语言变回人类文字

最后一步,把解码车间的语义表示转换成具体的词:

  1. 线性变换:用全连接层把 512 维的语义向量,转换成 "词汇表大小" 的向量(比如词汇表有 2000 个词,输出 2000 维);

  2. 标准化:用 LogSoftmax 把向量变成概率(方便计算损失);

  3. 选词:取概率最高的词作为最终输出(比如 2000 维中第 356 位概率最高,对应词 "love")。

代码实现:

class Generator(nn.Module):
    def __init__(self, embed_dim, vocab_size):
        super().__init__()
        self.linear = nn.Linear(embed_dim, vocab_size)  # 512→2000

    def forward(self, x):
        # 线性变换→LogSoftmax归一化
        return F.log_softmax(self.linear(x), dim=-1)

三、工厂完整运转流程(以 "我爱中国"→"I love China" 为例)

  1. 输入车间

    • "我爱中国"→词嵌入(4 个 512 维向量)+ 位置编码→[1,4,512];

    • "I love China"→词嵌入(3 个 512 维向量)+ 位置编码→[1,3,512](训练时的目标译文)。

  2. 编码车间

    • 6 个编码小组依次处理 [1,4,512],分析 "我 - 爱 - 中国" 的关系→输出原文语义表示 [1,4,512]。

  3. 解码车间

    • 第一步:用多头自注意力分析已生成的译文前缀(比如先生成 "I",分析 "I" 的语义);

    • 第二步:用编码 - 解码注意力,让 "I" 关注原文的 "我";

    • 第三步:前馈网络提炼语义,生成 "I" 的向量;

    • 重复上述步骤,依次生成 "love"(关注原文 "爱")、"China"(关注原文 "中国")→输出译文语义表示 [1,3,512]。

  4. 输出车间

    • [1,3,512]→线性变换→[1,3,2000]→LogSoftmax→取概率最高的词→"I love China"。

四、总结:Transformer 为什么这么强?

  1. 并行计算:摆脱了 RNN 的串行依赖,训练速度飞起;

  2. 全局注意力:能同时看到整个句子的所有词,语义理解更准;

  3. 模块化设计:编码器、解码器、注意力机制、前馈网络都是模块化的,可复用、可扩展(比如 BERT 只用了编码器,GPT 只用了解码器)。

现在你知道了,Transformer 这座 "智能翻译工厂" 之所以厉害,不是靠复杂的公式,而是靠合理的分工和高效的协作 —— 每个组件都在做自己擅长的事,最后合力完成精准翻译。而这一切,都始于 2017 年那篇划时代的论文《Attention Is All You Need》~

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐