4. Transformer 模型基础:从直觉到架构

聊到分子特征编码,绕不开 Transformer。这个模型当年在 NLP 领域一炮而红,后来被化学信息学“借”过来,发现效果出奇的好。我最早接触它是在做分子性质预测项目时,当时试了一堆传统指纹和图神经网络,效果总差口气。换成 Transformer 之后,某些任务的精度直接跳了 5 个百分点——可以说印象深刻。

这一讲,我们先把 Transformer 的骨架搭起来。不扯太深的理论,重点是让你知道它“长什么样”、“每个零件干嘛用”。后面几讲再逐步填充血肉。

4.1 从全局看:编码器-解码器结构

Transformer 最初是为序列到序列任务设计的,比如机器翻译。所以它的整体架构分两半:编码器(Encoder)和 解码器(Decoder)。

我以前带新人时,常把这比作“做笔记”和“写作业”。编码器负责读懂输入序列,把它浓缩成一组“知识向量”;解码器则拿着这些向量,一步步生成输出序列。当然,在分子特征编码场景里,我们通常只取编码器部分——把分子的 SMILES 或图结构编码成特征向量,然后接下游任务(分类、回归等)。

核心观点:编码器把输入转成上下文相关的表示,解码器基于这些表示生成输出。分子编码任务主要用编码器。

具体来说,编码器和解码器由多个相同结构的层堆叠而成。每一层包含两个关键子层:多头自注意力和前馈神经网络。解码器则多一个掩码自注意力子层,用来防止看到未来信息。

为了让你看得更清楚,我画了张架构图。注意看两侧的结构差异:

Transformer 基础架构 编码器 ×N 多头自注意力 (Multi-Head Self-Attention) 残差连接 + 层归一化 前馈神经网络 (FFN) 残差连接 + 层归一化 输出:上下文向量 解码器 ×N 掩码多头自注意力 (Masked) 残差连接 + 层归一化 交叉注意力 (Cross-Attention) K, V 来自编码器 残差连接 + 层归一化 输出:生成序列 表示信息流动(K, V 传递)

你会发现,编码器的输出(上下文向量)会作为 Key 和 Value 传给解码器的交叉注意力层。这样解码器在生成每个新 token 时,都能“回顾”输入序列的全局信息。这是整个架构最巧妙的地方——信息不对称,但相互依赖。

实践提示:在分子编码场景,如果你只是做特征提取(比如把分子转成固定长度的向量),那么只取编码器部分就够了。解码器一般用于生成任务,比如分子从头生成或逆合成分析。

4.2 核心组件拆解

现在我们把编码器里的两个关键组件拆开来看。解码器的结构类似,只是多了一层交叉注意力,原理是相通的。

4.2.1 自注意力机制

说白了,自注意力的目的就是让每个位置“看”到序列里的其他所有位置,然后决定“我应该多关注谁”。你想想看,在句子 “The cat sat on the mat” 里,“sat” 和 “cat” 的关联显然比 “sat” 和 “mat” 更紧密——自注意力就是要学出这种关系。

具体计算分三步:

  1. 对每个输入,通过三个不同的线性变换得到 Query、Key、Value 向量。
  2. 计算 Query 和所有 Key 的点积,除以 sqrt(d_k) 做缩放,然后用 softmax 归一化成注意力权重。
  3. 将注意力权重与 Value 加权求和,得到当前位置的输出。

我之前用 Python 写过一个极简版,帮你理解核心运算逻辑:

import numpy as np

def simple_attention(Q, K, V):
    # Q, K, V: (seq_len, d_k)
    d_k = Q.shape[-1]
    scores = np.matmul(Q, K.T) / np.sqrt(d_k)   # 缩放点积
    weights = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True)  # softmax
    output = np.matmul(weights, V)
    return output, weights

实际项目中,我们绝不会手写这个——PyTorch 和 TensorFlow 都封装好了。但理解这个流程很重要,因为后面讲分子图结构时,你会看到同样的思想被用于“原子-原子”关系建模。

避坑指南:我曾经在开发生成模型时,忘记对解码器的自注意力加掩码,导致每个 token 都能看到未来的 token,训练时 loss 降得飞快但推理完全乱套。检查了半天才发现是掩码矩阵没传进去。嗯,这是个很傻但常见的错误。

4.2.2 多头注意力

自注意力一次只能学一种“关系模式”。但实际任务中,分子里的原子可能同时有“局部成键关系”、“全局环结构关系”、“官能团归属关系”——多个头正好解决这个问题。

多头注意力的做法很简单:把 Query、Key、Value 切分成 h 份(h 是头数),每份分别做自注意力,最后把结果拼起来再过一次线性层。我见过初学者把“多头”想象成多个独立的注意力模块,其实不是——它们共享输入,只是投影到不同的子空间。

头数 h 和每个头的维度 d_k 满足关系:d_model = h * d_k。比如说,d_model=512,h=8,那么每个头的维度就是 64。这个配比我个人习惯在开新项目时先用默认值,跑通后再调。

经验之谈:并不是头越多越好。我在分子性质预测任务里试过 h=4、8、16,结果 h=8 和 h=16 差别很小,但显存占用翻倍。建议你从 8 头起步,根据结果再决定是否增加。

4.2.3 位置编码

自注意力是置换不变的——它不考虑顺序。这对分子来说很要命,因为 SMILES 字符串的顺序(原子出现的先后)包含了重要信息,分子图的拓扑结构更是跟顺序强相关。所以我们必须加位置编码。

原版 Transformer 使用的是正弦/余弦位置编码,公式如下:

PE(pos, 2i) = sin(pos / 10000^{2i / d_model})
PE(pos, 2i+1) = cos(pos / 10000^{2i / d_model})

这个方法的好处是:不用额外学习参数,而且理论上能处理任意长度的序列。我实际用下来,它在 SMILES 编码任务上表现不错,但如果你用图数据,可能需要考虑其他位置编码方式(比如拉普拉斯位置编码),这个我们后面会专门讲。

注意:分子 SMILES 是变长字符串,最大长度可能到几百。原版正弦位置编码在长度外推性上很友好,但如果你用可学习位置编码,一定要确保训练时见过足够长的序列,否则推理时遇到超长序列会崩。

4.2.4 前馈神经网络 (FFN)

自注意力层后面接一个两层的全连接网络,中间带一个 ReLU 激活:

FFN(x) = max(0, xW1 + b1) W2 + b2

通常 W1 的维度是 d_model × d_ff,d_ff 一般是 d_model 的 4 倍。这个 FFN 本质上在做“逐位置的特征变换”——每个位置的输出只依赖该位置的信息,和自注意力的“跨位置”特性互补。

我常用的配置:d_model=256,d_ff=1024,h=8,层数 4~6。这个配置在大部分中小型分子数据集上都能兼顾速度和效果。如果数据量特别大(比如几百万分子),可以升到 d_model=512,层数 8。

4.3 残差连接与层归一化

两个重要的小技巧,但少了它们 Transformer 根本训不动。

  • 残差连接:把子层的输入加到输出上,即 output = x + sublayer(x)。这解决了深层网络梯度消失的问题。我第一次训 6 层 Transformer 时没加残差,loss 直接不动,加了之后立刻收敛。
  • 层归一化:对每个样本的特征维度做归一化(均值为 0,方差为 1)。它能让训练更稳定,而且不受 batch size 影响——这一点比批归一化更适合序列模型。

原版架构用的是“后归一化”(子层后才做归一化),但后来很多变体改用“前归一化”(子层前就归一化),训练更稳定。我的经验是:小模型用后归一化差别不大,大模型推荐前归一化。

4.4 关键超参数速查

以下是我做分子编码项目时最常调的参数,整理成了一张表:

参数含义常见范围我的建议
d_model词嵌入维度 / 特征维度128 ~ 1024256 起步,看数据量决定
d_ff前馈网络隐藏层维度d_model × 4保持 4 倍,调太大除了费显存没好处
h注意力头数4 ~ 168 是安全牌
N编码器/解码器层数2 ~ 12分子任务 4~6 层够用
dropout丢弃率0.1 ~ 0.3小数据集用 0.2,大数据集 0.1
pos_encoding位置编码方式sin/cos / 可学习 / RoPESMILES 用 sin/cos,图结构用 RoPE

嗯,这部分信息量有点大。但别担心,后面几讲我们会逐一展开这些组件在分子场景下的具体用法。你现在只要记住 Transformer 的基本骨架:自注意力捕捉全局依赖,FFN 做逐位置变换,残差+归一化保证训练稳定——就够了。