经典神经网络(15)GLM模型原理详解及其微调(文本摘要)
- 2024年01月16日,智谱推出新一代基座大模型 GLM-4。新一代基座大模型 GLM-4 的整体性能相比上一代大幅提升,十余项指标逼近或达到 GPT-4;支持更长上下文;更强的多模态;支持更快推理速度,更多并发,大大降低推理成本;同时 GLM-4 增强了智能体能力。
- GLM-4 实现自主根据用户意图,自动理解、规划复杂指令,自由调用网页浏览器、Code Interpreter 代码解释器和多模态文生图大模型,以完成复杂任务。
- 今天,我们了解下由清华大学研发的初代GLM模型。
1 GLM模型简介
作者在论文中表示,现有的预训练框架大体上可以分为三类:
- 自回归模型。例如 GPT,学习的是从左到右的语言模型。虽然在长文本生成方面取得了成功,并且在参数达到数十亿时展示了少样本学习能力,但其固有的缺点是单向注意机制,不能完全捕捉自然语言理解(NLU)任务中上下文词之间的依赖关系。
- 自编码模型。例如 BERT,通过去噪目标(例如掩码语言模型,MLM)学习双向上下文编码器。编码器生成适合自然语言理解任务的上下文化表示,但不能直接用于文本生成。
- 编码器-解码器模型。在编码器中采用双向注意机制,在解码器中采用单向注意机制,并在二者之间采用交叉注意机制。这些模型通常用于条件生成任务,例如文本摘要和响应生成。
- 基于编码器-解码器架构的T5模型统一了自然语言理解(NLU)和条件生成,但需要更多的参数才能与基于 BERT 的模型的性能相匹配。
- 这些预训练框架都不够灵活,无法在所有自然语言处理(NLP)任务中竞争。
- 之前的研究尝试通过多任务学习来统一不同的框架,然而,由于自编码和自回归目标在本质上存在差异,简单的统一无法充分继承这两种框架的优点。
- 基于以上的原因,清华研究团队提出了一种基于
自回归空白填充(Autoregressive Blank Infilling)
的预训练模型,名为 GLM(General Language Model)。- GLM 通过添加
二维位置编码
并允许以任意顺序预测片段
,提高了空白填充预训练的效果,这在 NLU 任务中带来了比 BERT 和 T5 更好的性能提升。 - 同时,通过改变空白的数量和长度,GLM 可以针对不同类型的任务进行预训练。
- 在涵盖 NLU、条件生成和无条件生成的广泛任务中,GLM 在相同模型规模和数据量下,性能优于 BERT、T5 和 GPT。
- GLM 通过添加
1.1 模型介绍
1.1.1 预训练目标—自回归空白填充
作者从 λ=3 的泊松分布中随机采样 span
长度,反复采样新的 span,直到至少有 15% 的原始 token被掩码。根据经验,作者发现 15%`的比例对于下游 NLU 任务的良好表现至关重要。
为了开启 autoregressive generation
,每个 span
都被填充了 special token
,即 [START]
(记做 [S]
)和 [END]
(记做[E]
),分别用于输入和输出。通过这种方式,在一个统一的模型中自动学习双向编码器(用于 Part A
)和单向解码器(用于 Part B
)。下图说明了 GLM
的实现。
-
如上图左部分所示:
- (a)原始文本为 [ x 1 , x 2 , x 3 , x 4 , x 5 , x 6 ] [x_1, x_2, x_3, x_4, x_5, x_6] [x1,x2,x3,x4,x5,x6],对原始文本随机进行连续mask。这里假设mask掉2个text span [ x 3 ] [x_3] [x3] 和 [ x 5 , x 6 ] [x_5, x_6] [x5,x6] 。
- (b)将 [ x 3 ] [x_3] [x3] 和 [ x 5 , x 6 ] [x_5, x_6] [x5,x6]替换为 [ M ] [M] [M] 标志,并打乱Part B的顺序。随机交换span的顺序,这是为了捕捉span之间的内在联系。
-
如上图中间部分所示:
- GLM 自回归地生成 Part B,每个span前面都加上 [S] 作为输入,后面加上 [E] 作为输出。
二维位置编码
表示不同片段之间和片段内部的位置关系:- Position 1:表示片段在原始文本中的相对位置。如上图所示, [ x 3 ] \left[ x_3 \right] [x3]和 [ x 5 , x 6 ] \left[ x_5,x_6 \right] [x5,x6]被挖去。那么,被挖去的片段在第一个维度上的位置编码就是它们在原始文本中的索引,即 [ x 3 ] \left[ x_3 \right] [x3]来自片段 3, [ x 5 , x 6 ] \left[ x_5,x_6 \right] [x5,x6]来自片段 5。
- Position 2:表示片段内部的相对位置。对于 Part A 的词,它们的第二个位置 id 都是0。对于 Part B 的词,它们的范围是从 1 到区域的长度。
- GLM 的二维位置编码方法确保了模型在重建被遮盖的跨度时不知道它们的长度,这与其他模型相比是一个重要的区别。
- 例如,XLNet 编码了原始位置,以便它可以感知缺失的 token 的数量;
- 而 SpanBERT 用多个 [MASK] 标记替换了跨度,并保持了长度不变。
- GLM 的设计适合下游任务,因为通常生成的文本的长度是事先未知的。
- 这两个
positional id
通过learnable embedding table
被投影到两个embedding
向量中,这两个embedding
向量然后都被添加到input token embedding
中。
-
如上图右部分所示:
自注意力掩码
。 灰色区域表示被掩盖,Part A 的词语可以自我看到(蓝色框),但不能看到 Part B。- Part B的词语可以看到Part A 和Part B 中的前面的词语(黄色和绿色框对应两个片段)。
1.1.2 多任务预训练
-
上图所示的预训练目标中,GLM 遮盖了短的文本区域,这适合于 NLU 任务。
-
作者更感兴趣的是预训练一个能够同时处理 NLU 和文本生成的单一模型。因此,作者研究了一种多任务预训练设置,其中一个生成更长文本的目标与空白填充目标共同优化。
-
GLM 考虑了以下两种目标:
- 文档级别。采样一个单一的区域,其长度从原始长度的 50% 到100% 之间的均匀分布中采样。该目标旨在进行长文本生成。
- 句子级别。限制遮盖的区域必须是完整的句子。多个区域(句子)被采样,覆盖原始文本的 15% 的词数。该目标旨在进行 seq2seq 任务,其预测结果通常是完整的句子或段落。
- 这两个新目标的定义与
blank infilling objective
相同,唯一的区别是span的数量和span的长度
。
blank infilling objective 采样多个 span,平均 span`长度最短;
document-level 采样一个 span,span 长度最长;
sentence-level采样多个 span,平均 span长度适中。
1.1.3 模型结构
GLM 使用了一个单一的 Transformer,并对架构做了一些修改:
(1)重新排列了层归一化和残差连接的顺序(将Post-LayerNorm改为Pre-LayerNorm,现在大模型的标配),这对于大规模的语言模型来避免数值错误是非常关键。
(2)在模型最后一个自注意力层之后,额外增加一个层归一化。
blocks of:
layer norm # 前置归一化
self attention # 自注意力
residual connection # 残差连接
layer norm # 前置归一化
mlp # 前馈神经网络
residual connection # 残差连接
followed by a final layer norm. # 额外增加一个层归一化
(3)使用了一个单一的线性层来进行输出词的预测。
(4)用 GeLU 替换了 ReLU 激活函数。
# glm-large-chinese模型结构如下所示
GLMForConditionalGeneration(
(glm): GLMModel(
# 1、词嵌入
(word_embeddings): VocabEmbedding()
(transformer): GLMStack(
(embedding_dropout): Dropout(p=0.1, inplace=False)
# 2、二维位置编码
(position_embeddings): Embedding(1025, 1024) # position 1
(block_position_embeddings): Embedding(1025, 1024) # position 2
# 3、堆叠GLMBlock
(layers): ModuleList(
(0-23): 24 x GLMBlock(
(input_layernorm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
(attention): SelfAttention(
(query_key_value): Linear(in_features=1024, out_features=3072, bias=True)
(attention_dropout): Dropout(p=0.1, inplace=False)
(dense): Linear(in_features=1024, out_features=1024, bias=True)
(output_dropout): Dropout(p=0.1, inplace=False)
)
(post_attention_layernorm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
(mlp): MLP(
(dense_h_to_4h): Linear(in_features=1024, out_features=4096, bias=True)
(dense_4h_to_h): Linear(in_features=4096, out_features=1024, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
)
)
)
# 额外增加一个层归一化
(final_layernorm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
)
)
)
训练超参数如下:
1.2 GLM模型的微调
- 通常,对于下游的自然语言理解任务,一个线性分类器将预训练模型产生的序列或 token 的 embedding 作为输入,并预测正确的标签。
- 这些做法与生成式预训练任务不同,导致了预训练和微调之间的不一致。
1.2.1 文本分类任务
- GLM 将 NLU 分类任务重新制定为填空生成任务。如下图所示,情感分类任务可以表述为
{SENTENCE}。这真的是 [MASK]”
。候选标签 y 也映射到填空的答案,例如标签 “positive” 和“negative” 分别映射到单词 “good” 和 “bad”。 - 因此,句子是正面或负面的概率与在空白处预测“好”或“坏”成正比,然后我们用交叉熵损失来微调 GLM。
1.2.2 文本生成任务
- 对于文本生成任务,如下图所示,给定的上下文构成了输入的 Part A,末尾附加了一个 mask 符号。模型自回归地生成 Part B 的文本。
- 可以直接应用预训练的 GLM 进行无条件的生成,或者在下游的条件生成任务上对其进行微调。
1.2.3 与其他模型的比较分析
-
与
BERT
的比较:- 正如
XLNet
所指出的,由于MLM
的独立性假设,BERT
无法捕获到masked token
之间的相互依赖性。 BERT
的另一个缺点是,它不能正确填充multiple token
的blank
。为了推断长度为 l 的答案的概率,BERT
需要连续进行 l 次预测。如果长度 l 未知,我们可能需要列举所有可能的长度,因为BERT
需要根据长度来改变[MASK] token
的数量。
- 正如
-
与
XLNet
的比较:GLM
和XLNet
都是用自回归目标进行预训练的,但它们之间有两个区别:- 首先,
XLNet
在corruption
前使用original position encoding
。在推理过程中,我们需要知道或枚举出答案的长度,这与BERT
的问题相同。 - 第二,
XLNet
使用了双流的自注意力机制,而不是right-shift
,以避免Transformer
内部的信息泄露。这使预训练的时间成本增加了一倍。
- 首先,
-
与
T5
的比较:T5
提出了一个类似的blank infilling objective
来预训练一个encoder-decoder Transformer
。T5
对编码器和解码器使用独立的positional encoding
,并依靠多个哨兵token
来区分masked span
。在下游任务中,只使用其中一个哨兵token
,导致模型容量的浪费、以及预训练与微调之间的不一致。UniLM 和 GLM在编码器和解码器之间共享了同一组 positional embedding 。
此外,
T5
总是以固定的从左到右的顺序(即,单向地)预测span
。因此,GLM
可以在参数和数据较少的情况下在NLU
和seq2seq
任务中明显优于T5
。相比之下,GLM采用了类似于XLNet的随机混洗策略,混洗了多个span的顺序。
-
与
UniLM
的比较:UniLM
在自编码框架下,通过在双向注意力、单向注意力、以及交叉注意力中改变attention mask
,结合了不同的预训练目标。然而,UniLM
总是用[MASK] tokens
替换masked span
,这限制了它针对masked span
及其上下文之间的依赖关系的建模能力。GLM
输入前一个token
并自动生成next token
。
2 微调GLM模型做文本摘要
"""
微调清华大学开源的glm-large-chinese做摘要生成
数据集(只取5000条):nlpcc_2017: https://huggingface.co/datasets/supremezxc/nlpcc_2017
model link:THUDM/glm-large-chinese
link: https://huggingface.co/THUDM/glm-large-chinese
新版本transformers报错:
AttributeError: 'GLMChineseTokenizer' object has no attribute 'sp_model'
# 原始
def __init__(self, vocab_file, **kwargs):
super().__init__(**kwargs) # 置后
self.vocab_file = vocab_file
self.sp_model = spm.SentencePieceProcessor()
self.sp_model.Load(vocab_file)
# 修改
def __init__(self, vocab_file, **kwargs):
self.vocab_file = vocab_file
self.sp_model = spm.SentencePieceProcessor()
self.sp_model.Load(vocab_file)
super().__init__(**kwargs) # 置后
"""
import torch
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, Seq2SeqTrainer, Seq2SeqTrainingArguments
device = "cuda" if torch.cuda.is_available() else 'cpu' # the device to load the model onto
# model_dir = r'D:\python\models\model-download\ZhipuAI\glm-large-chinese'
model_dir = r'/root/autodl-fs/models/zhipu/glm-large-chinese'
def predict(model, input_text):
inputs = tokenizer("摘要生成: \n" + input_text + tokenizer.mask_token, return_tensors="pt")
inputs = tokenizer.build_inputs_for_generation(inputs, max_gen_length=64)
inputs = inputs.to(device)
output = model.generate(**inputs
, max_new_tokens=64
, eos_token_id=tokenizer.eop_token_id
, do_sample=True
, pad_token_id=tokenizer.eop_token_id
)
return tokenizer.decode(output[0].tolist())
def model_eval(model):
from rouge import Rouge
rouge = Rouge()
model = model.eval()
predict = []
with torch.inference_mode():
for d in ds["test"]:
inputs = tokenizer("摘要生成: \n" + d["content"] + tokenizer.mask_token, return_tensors="pt")
inputs = tokenizer.build_inputs_for_generation(inputs, max_gen_length=64)
inputs = inputs.to("cuda")
output = model.generate(**inputs, max_new_tokens=64, eos_token_id=tokenizer.eop_token_id,
do_sample=True, pad_token_id=tokenizer.eop_token_id)
predict.append(
tokenizer.decode(output[0].tolist()).split("<|startofpiece|>")[1].replace("<|endofpiece|>", "").strip())
print("curID:", len(predict))
docode_preds = []
decode_labels = []
for p, q in zip(predict, ds["test"]["title"]):
if len(p) > 0:
docode_pred = " ".join(p)
decode_label = " ".join(q)
docode_preds.append(docode_pred)
decode_labels.append(decode_label)
scores = rouge.get_scores(docode_preds, decode_labels, avg=True)
r = {
"rouge-1": scores["rouge-1"]["f"],
"rouge-2": scores["rouge-2"]["f"],
"rouge-l": scores["rouge-l"]["f"],
}
print(r)
return r
if __name__ == '__main__':
# 1、加载tokenizer及模型
tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
model = AutoModelForSeq2SeqLM.from_pretrained(model_dir, trust_remote_code=True).to(device)
# 2、加载数据集
ds = Dataset.load_from_disk("./nlpcc_2017/")
ds = ds.train_test_split(100, seed=42)
# 3、数据集预处理
def process_func(exmaples):
# Note:添加任务前缀 + [MASK]
contents = ["摘要生成: \n" + e + tokenizer.mask_token for e in exmaples["content"]]
# 这里pad_token为<|endoftext|>
inputs = tokenizer(contents, max_length=384, truncation=True, padding="max_length", return_tensors="pt")
# inputs = (['input_ids', 'position_ids', 'attention_mask', 'labels'])
# 将inputs和targets合并为一个句子,其中:position_ids为二维位置编码,分别为:position_ids和block_position_ids
# input_ids shape = (bs, inputs_max_length + max_gen_length)
# position_ids shape = (bs, 2, inputs_max_length + max_gen_length)
# attention_mask shape = (bs, 1, inputs_max_length + max_gen_length, inputs_max_length + max_gen_length)
# labels shape = (bs, inputs_max_length + max_gen_length)
inputs = tokenizer.build_inputs_for_generation(inputs, targets=exmaples['title'], padding=True, max_gen_length=64)
return inputs
tokenized_ds = ds.map(process_func, batched=True, remove_columns=ds["train"].column_names)
# 4、配置训练参数以及创建训练器
args = Seq2SeqTrainingArguments(
output_dir="./summary_glm",
per_device_train_batch_size=1,
per_device_eval_batch_size=4,
gradient_accumulation_steps=8,
logging_steps=8,
num_train_epochs=1
)
trainer = Seq2SeqTrainer(
args=args,
model=model,
train_dataset=tokenized_ds["train"],
tokenizer=tokenizer,
)
trainer.train()
# 5、模型评估
model_eval(model)
# 6、模型预测
input_text = """
“足球从未高于生死”,这是3年前欧洲杯赛场上丹麦球员埃里克森心脏骤停时,各路媒体报道该事件用的最多的表达。
而在经历了那段惊心动魄但又充满人情味的艰难时刻后,32岁的埃里克森时隔1100天再次为国征战欧洲杯,而且奉献了进球。
17日凌晨的欧洲杯小组赛,埃里克森进球的那一刻,感动和欣慰扑面而来。最终丹麦和斯洛文尼亚队1比1战平,各取1分。
丹麦队对垒斯洛文尼亚,这场热度并不算高的小组赛首轮争夺因为一个人的出现得到了外界的关注,他就是埃里克森。
曼联中场在在第17分钟的进球帮助祖国球队取得了领先,他也在经历上届欧洲杯的心脏骤停的遭遇之后,实现了“王者归来”。
尽管这次破门遗憾没能帮助丹麦队最终获得胜利,但绰号“爱神”的埃里克森依然得到了全场乃至全世界球迷的祝福。
"""
print(predict(model, input_text))