ChatGLM背后的秘密武器:GLM自回归填空技术详解(附代码实战)
深入ChatGLM的基石:GLM自回归填空技术实战与架构解析
如果你最近在关注开源大语言模型的进展,尤其是那些在中文语境下表现亮眼的选手,那么ChatGLM系列模型的名字你一定不会陌生。它让很多开发者和研究者第一次能在消费级显卡上,相对流畅地体验到一个具备对话能力的“类ChatGPT”模型。但当我们赞叹其对话的连贯性与对中文的友好时,是否曾好奇,支撑起ChatGLM如此能力的底层核心技术究竟是什么?答案就藏在它的前身——GLM(General Language Model)模型之中。GLM并非又一个简单的“微调版GPT”,它提出了一套名为“自回归填空”的独特预训练范式,试图用一个统一的架构,优雅地解决自然语言理解、条件生成和无条件生成这三类看似迥异的任务。今天,我们就抛开论文综述式的泛泛而谈,直接深入到技术实现的腹地,结合代码层面的思考,来拆解这套“秘密武器”究竟是如何工作的。
1. 为何需要GLM:统一架构的野心与挑战
在GLM出现之前,预训练语言模型的江湖大致由三大门派瓜分,它们各自为政,擅长不同的领域。
- 自编码模型(如BERT):通过“完形填空”(掩码语言模型,MLM)的方式学习文本的双向上下文表示。它就像一个严谨的文本分析师,能深刻理解句子中每个词与上下文的关系,因此在情感分析、命名实体识别等自然语言理解任务上表现卓越。但它的“填空”是独立预测每个掩码位置,缺乏生成文本所需的序列依赖关系,让它难以胜任流畅的文本生成工作。
- 自回归模型(如GPT系列):严格遵循从左到右的生成模式,根据上文预测下一个词。它是一位才华横溢的作家,擅长续写故事、生成文章。然而,其单向的注意力机制意味着在预测时无法“看到”下文,这在需要全局理解的任务(如文本分类)上存在天然劣势。
- 编码器-解码器模型(如T5、BART):采用两段式架构,编码器理解输入,解码器生成输出。它是专业的翻译官和总结者,在机器翻译、文本摘要等条件生成任务上得心应手。但这种架构通常参数量更大,且编码器和解码器的能力割裂,在纯理解或纯生成任务上可能并非最优。
这就带来了一个现实的困境:在实际应用中,我们往往希望一个模型能“文武双全”——既能准确理解用户意图(NLU),又能根据指令生成高质量的回答或内容(NLG)。传统的做法是维护多个模型,或者在一个庞大的编码器-解码器模型上进行多任务学习,但这无疑增加了部署和优化的复杂度。
GLM的核心动机,正是要打破这种藩篱。它提出了一个大胆的设想:能否只用一个Transformer架构(具体来说是编码器),通过设计巧妙的预训练任务,让它同时掌握双向理解能力和自回归生成能力? 自回归填空技术,就是实现这一野心的关键钥匙。它不像BERT那样独立预测离散的掩码,也不像GPT那样严格单向生成,而是将文本生成过程巧妙地融入到一个可控的“填空”框架中。
2. 自回归填空:核心机制深度拆解
自回归填空听起来有些抽象,我们可以将其想象成一个更智能、更具挑战性的“完形填空”。传统的完形填空是给你一篇文章,其中挖掉几个孤立的词让你猜。而GLM的填空,则是挖掉一个或多个连续的文本片段(span),然后要求模型按照被打乱的顺序,以自回归(逐个token生成) 的方式,把这些片段重新“写”回去。
2.1 任务构建流程:从原始文本到模型输入
让我们用一个具体的例子,一步步拆解这个过程。假设原始句子是: [x1, x2, x3, x4, x5, x6]。
第一步:随机掩码连续片段 GLM不会像BERT那样随机掩码15%的单个token。它会从泊松分布(λ=3)中采样片段的长度,随机选择文本中的连续区间进行掩码,直到累计被掩码的token数达到文本总长度的约15%。比如,它可能选中了两个片段:[x3] 和 [x5, x6]。掩码后,原句变为: [x1, x2, [M], x4, [M]],这里的 [M] 代表一个被挖空的片段占位符。
第二步:打乱片段顺序并构造输入 这是GLM区别于T5等模型的关键一步。模型并非按原顺序(从左到右)预测片段,而是将选中的片段顺序随机打乱。假设打乱后顺序为:先预测 [x5, x6],再预测 [x3]。然后,我们将打乱后的片段序列附在掩码后文本的后面,并用特殊的 [S] token作为每个片段的起始标记。最终,模型的输入序列构造为: [x1, x2, [M], x4, [M], [S], x5, x6, [S], x3]
这个序列被分为两部分:
- Part A:
[x1, x2, [M], x4, [M]]— 包含原始上下文和被掩码的位置。 - Part B:
[ [S], x5, x6, [S], x3 ]— 包含被打乱顺序的答案片段,用于让模型学习预测。
注意:在预训练时,Part B中的答案(
x5, x6, x3)对模型是可见的,模型的任务是学习如何根据Part A的上下文,以自回归的方式生成Part B。在推理时,Part B的答案部分会被替换为[MASK]token,由模型生成。
2.2 二维位置编码:空间与顺序的智慧
传统的Transformer使用一维位置编码来标记token在序列中的顺序。但在GLM的填空任务中,一个token同时具有两种身份:它在原始句子中的位置,以及它在被预测的片段内部的位置。GLM引入了二维位置编码来同时编码这两种信息。
- 位置1(原始位置):编码每个token在原始输入文本(即Part A中未被掩码的部分)中的绝对或相对位置。这有助于模型保持对原始句子结构的理解。
- 位置2(片段内位置):对于Part A中的token,其片段内位置被设为0(表示不属于待预测片段)。对于Part B中每个片段内的token,则从1开始重新编号。这明确告知了模型当前正在生成的是哪个片段,以及生成到了该片段的第几个词。
这种设计使得模型能够清晰地区分“上下文token”和“待生成token”,并理解生成任务中的层次结构,是统一理解和生成能力的关键。
2.3 注意力掩码:可控的信息流
为了实现自回归生成,同时又能利用双向上下文信息,GLM设计了一种分区的注意力掩码机制。这可能是整个架构中最精妙的部分。
我们可以通过一个简化的注意力掩码矩阵来理解:
| 能否看到 | x1 | x2 | [M] | x4 | [M] | [S] | x5 | x6 | [S] | x3 |
|---|---|---|---|---|---|---|---|---|---|---|
| x1 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| x2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| [M] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| x4 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| [M] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| [S] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| x5 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| x6 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| [S] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| x3 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
规则解读:
- Part A 内部:所有token(包括
[M])之间是双向可见的。这意味着在预测片段时,模型可以充分考虑到整个原文的上下文信息,这是其拥有强大NLU能力的基础。 - Part B 内部:遵循标准的自回归因果掩码。每个token只能看到它自己以及它之前的token(包括Part A的所有token)。这确保了生成过程的因果性,使其能够进行序列生成。
- 从Part B 看 Part A:允许。Part B中的token可以充分“回顾”Part A的全部信息。
- 从Part A 看 Part B(答案部分):禁止。Part A中的token不能“偷看”Part B的答案,否则预训练任务就失去了意义。
这种设计创造了一种“混合注意力”模式:在编码上下文时是双向的,在生成答案时是单向自回归的。一个单一的Transformer编码器,通过不同的注意力掩码模式,动态地切换了它的“工作模式”。
3. 从预训练到微调:GLM的任务适配哲学
GLM的野心不止于一个新颖的预训练任务,更在于一套统一的“任务框架”。其核心理念是:将所有下游任务都转化为“填空”问题。这种思想与T5的“Text-to-Text”有异曲同工之妙,但实现方式更贴合其自回归填空的架构。
3.1 多粒度预训练目标
为了应对不同性质的下游任务,GLM在预训练阶段就设计了三种不同粒度的填空目标,并交替进行训练:
| 预训练目标 | 掩码策略 | 片段长度 | 主要针对任务 |
|---|---|---|---|
| Token-Level (词汇级) | 掩码短片段,总计覆盖原文~15%token | 泊松分布采样(λ=3),多为短span | 自然语言理解(NLU),如分类、NER |
| Sentence-Level (句子级) | 掩码完整句子,覆盖原文~15%token | 片段为整个句子 | 条件生成(Seq2Seq),如摘要、翻译 |
| Document-Level (文档级) | 掩码一个很长的连续片段 | 长度为原文的50%-100% | 无条件生成(Language Modeling) |
这种多目标预训练确保了模型既能处理细粒度的理解,也能驾驭长文本的生成。
3.2 下游任务微调范式
在微调阶段,GLM通过设计“提示模板”(Prompt Template),将各类任务都规整到填空框架下。
分类任务示例(情感分析):
- 原始样本:
(文本="这部电影太精彩了!", 标签="积极") - 转化为填空:设计模板
“这部电影太精彩了!它的情感是[MASK]的。” - 标签映射:将“积极”、“消极”等标签映射为具体的填空词,如
v(“积极”) = “好”,v(“消极”) = “糟”。 - 模型操作:模型预测
[MASK]位置为“好”的概率,即对应“积极”类别的概率。通过一个线性层将词汇表概率映射到类别概率。
生成任务示例(文本摘要):
- 原始样本:
(长文档=“...”, 摘要=“...”) - 转化为填空:将长文档作为Part A,在末尾添加一个
[MASK]token,然后将摘要作为Part B(在训练时可见,推理时由模型生成)。 - 模型操作:模型基于长文档的完整上下文(双向注意力),以自回归的方式(单向生成)生成
[MASK]对应的摘要内容。
这种统一的微调方式极大地简化了任务适配流程,你不需要为分类任务额外添加一个分类头,也不需要为生成任务切换成编码器-解码器结构。同一个模型,同一种前向传播方式,通过不同的输入构造,就能应对多种任务。
4. 代码实战:窥探GLM实现的关键片段
理论固然重要,但代码能让我们看得更真切。我们无法在此重现完整的GLM训练代码,但可以剖析几个关键实现环节,来加深理解。以下示例基于对GLM开源代码的解读和简化。
4.1 构造掩码与片段打乱
这是数据预处理的核心步骤。假设我们有一个已经token化的文本 input_ids。
import torch
import numpy as np
def create_autoregressive_blank_infill(input_ids, mask_ratio=0.15, poisson_lambda=3):
"""
简化版的GLM自回归填空样本构造函数。
Args:
input_ids: 原始token id序列 [seq_len]
mask_ratio: 目标掩码比例
poisson_lambda: 泊松分布参数,控制片段平均长度
Returns:
processed_ids: 处理后的输入ID
target_ids: 需要被预测的target ID(对应Part B的答案部分)
attention_mask: 二维注意力掩码矩阵
position_ids_1d: 原始位置ID(简化,实际为2D)
"""
seq_len = len(input_ids)
# 1. 确定要掩码的token总数
num_to_mask = max(1, int(seq_len * mask_ratio))
masked_spans = []
total_masked = 0
# 2. 使用泊松分布采样片段长度,直到掩码足够数量的token
while total_masked < num_to_mask:
span_length = np.random.poisson(lam=poisson_lambda)
span_length = max(1, span_length) # 至少为1
if total_masked + span_length > seq_len:
span_length = seq_len - total_masked
start = np.random.randint(0, seq_len - span_length + 1)
# 简单起见,避免重叠,实际代码更复杂
masked_spans.append((start, start+span_length))
total_masked += span_length
# 3. 打乱片段顺序
np.random.shuffle(masked_spans)
# 4. 构建Part A (上下文+掩码占位符) 和 Part B (打乱的答案)
# 这里大幅简化了[M]和[S] token的插入逻辑,仅示意流程
# 实际中,Part A的[M]是一个特殊token,Part B的每个片段前有[S]
part_a_tokens = []
part_b_tokens = []
last_end = 0
for start, end in masked_spans:
# 添加未被掩码的上下文
part_a_tokens.extend(input_ids[last_end:start])
# 添加掩码占位符 [MASK]
part_a_tokens.append(MASK_TOKEN_ID) # 假设MASK_TOKEN_ID已定义
# 将被掩码的片段加入Part B
part_b_tokens.append(S_TOKEN_ID) # 片段开始符
part_b_tokens.extend(input_ids[start:end])
last_end = end
# 添加剩余未被掩码的上下文
part_a_tokens.extend(input_ids[last_end:])
# 5. 拼接最终输入
processed_ids = part_a_tokens + part_b_tokens
# target_ids 对应 part_b_tokens 中答案部分(不含S_TOKEN_ID)
target_ids = [tid for tid in part_b_tokens if tid != S_TOKEN_ID]
return processed_ids, target_ids
4.2 注意力掩码矩阵的实现
在Transformer的前向传播中,我们需要根据输入构造对应的注意力掩码矩阵。
def get_glm_attention_mask(processed_ids, mask_token_id=MASK_TOKEN_ID, s_token_id=S_TOKEN_ID):
"""
生成GLM分区注意力掩码。
这是一个概念性实现,展示了分区逻辑。
"""
seq_len = len(processed_ids)
# 初始化一个全0矩阵(表示不可见)
attention_mask = torch.zeros((seq_len, seq_len), dtype=torch.bool)
# 找到Part A和Part B的分界点(第一个[S] token的位置)
try:
split_idx = processed_ids.index(s_token_id)
except ValueError:
split_idx = seq_len # 如果没有Part B(某些任务模式)
# Part A (索引 0 到 split_idx-1)
# Part A内部全连通(双向)
attention_mask[:split_idx, :split_idx] = 1
# Part B (索引 split_idx 到 seq_len-1)
for i in range(split_idx, seq_len):
# Part B的token i 可以看到所有Part A的token
attention_mask[i, :split_idx] = 1
# Part B的token i 可以看到Part B中它自己及之前的token(自回归)
attention_mask[i, split_idx:i+1] = 1
# 注意:实际实现中,mask_token_id和s_token_id可能用于更精细的控制,
# 例如确保Part A的[MASK]位置在计算loss时被忽略等。
return attention_mask
4.3 模型前向传播的简化视图
在GLM的Transformer层中,前向传播需要结合2D位置编码和分区注意力掩码。
class GLMTransformerLayer(nn.Module):
# ... 初始化部分省略 ...
def forward(self, hidden_states, attention_mask, position_ids):
"""
hidden_states: 输入嵌入 [batch, seq_len, hidden_size]
attention_mask: 上述函数生成的掩码矩阵 [batch, 1, seq_len, seq_len] 或 [batch, seq_len, seq_len]
position_ids: 二维位置编码,形状为 [batch, seq_len, 2]
"""
# 1. 应用2D位置编码(通常是可学习的嵌入层)
pos_emb_1 = self.position_embeddings_1(position_ids[:, :, 0]) # 原始位置
pos_emb_2 = self.position_embeddings_2(position_ids[:, :, 1]) # 片段内位置
hidden_states = hidden_states + pos_emb_1 + pos_emb_2
# 2. 自注意力层,使用GLM特有的注意力掩码
# 注意力掩码确保了Part A的双向和Part B的自回归属性
attention_output = self.attention(
hidden_states,
attention_mask=attention_mask
)
# 3. 前馈网络等后续层...
# GLM通常采用Pre-LN (LayerNorm在残差连接之前) 的变体
return output
运行这些代码片段需要完整的模型上下文,但它们清晰地勾勒出了GLM实现的核心骨架:动态的样本构造、分区的注意力控制、以及融合了两种位置信息的编码方式。在实际的ChatGLM中,这些机制被高度优化,并集成了旋转位置编码(RoPE)等更先进的技术,但其灵魂依然是这套自回归填空的范式。
理解GLM,不仅是为了读懂一篇论文,更是为了掌握一种构建通用语言模型的思维方式。它告诉我们,通过精心设计预训练任务和注意力控制,模型的潜力可以被更灵活地激发。当你下次与ChatGLM对话时,或许可以想象,它正在内部进行着无数个巧妙的“填空”游戏,将你的问题转化为它最擅长的生成任务,从而给出那些令人惊喜的回答。这种统一而优雅的架构思想,正是其能够在众多开源模型中脱颖而出的深层原因。
更多推荐



所有评论(0)