第六章、从transformer到nlp大模型:编码器-解码器模型 (Encoder-Decoder)
0 前言
transformer实际上催生了大语言的三种框架的大模型:
模型架构 | 模型类型 | 代表模型 | 特点 |
---|---|---|---|
纯编码器模型 (Encoder-Only): 只使用 Transformer 的编码器部分 | 双向自注意力【双向模型 】 | BERT | 双向的。像一个可以同时扫视整个句子的分析者。常用来解决理解任务(分类、提取) |
纯解码器模型 (Decoder-Only): 只使用 Transformer 的解码器部分 | 掩码自注意力(单向)【自回归模型】 | GPT, LLaMA | 单向的(通常是从左到右)。像一个只能从左往右读的阅读器。常用来解决生成任务(创作、对话) |
编码器-解码器模型 (Encoder-Decoder) | 编码器双向注意力 + 解码器交叉注意力 | T5, BART | 序列到序列任务(如翻译、摘要) |
其实最主要的区别就是前一章所说的,没有掩码的多头注意力机制和有掩码的多头注意力机制的区别。无掩码的时候多头注意力机制是双向的,也就是说每个单词实际上结合了上下文的信息,有掩码的时候是单向的,即每个单词只结合了前面的单词的信息,它看不到后面的单词。
接下来我们拆开Transformer来看每部分能解决什么问题。
1 编码器-解码器模型 (Encoder-Decoder)
上一章中我们详细介绍了transformer模型的框架,相信读者已经可以自己搭建一个model了,本章我们将详细解释如何利用transformer模型训练一个英文到中文的翻译器en-zh
,并将整体代码和数据集放在github上,感兴趣的小伙伴可以clone一下走一下整个流程。
ps:求一个小星星~
https://github.com/Gaoxinyigithub/transformer
1.1 中英文数据集
我们有两个txt文件,文件中分别是英文句子,和中文句子。


1.2 数据预处理
首先先来看这部分的代码
src_file = 'data/en.txt' # 英文txt地址
trg_file = 'data/zh.txt' # 中文txt地址
src_lang = 'en_core_web_sm'
# 'en_core_web_sm' 是spaCy库中的预训练的英语语言模型
# 具体解释放在下面
trg_lang = 'zh_core_web_sm'
# 'zh_core_web_sm' 是spaCy库中的预训练的中文语言模型
max_strlen = 80
batchsize = 1500
src_data, trg_data = read_data(src_file, trg_file)
EN_TEXT, FR_TEXT = create_fields(src_lang, trg_lang)
train_iter, src_pad, trg_pad = create_dataset(src_data, trg_data, EN_TEXT, FR_TEXT, max_strlen, batchsize)
en_core_web_sm
及zh_core_web_sm
模型可以通过下面的连接搜索下载,注以版本号的对应关系,选择合适的下载即可。
https://github.com/explosion/spacy-models/releases
-
spaCy 是一个流行的开源自然语言处理(NLP)库,用于处理和分析文本数据。它提供了:
- 分词(Tokenization)
- 词性标注(Part-of-speech tagging)
- 命名实体识别(Named Entity Recognition)
- 依存句法分析(Dependency parsing)
- 文本分类等功能
-
en_core_web_sm 是一个预训练的英语语言模型名称,由三部分组成:
- en:表示英语(English)
- core:表示核心功能(包含基本的NLP功能)
- web:表示模型是在网络文本上训练的
- sm:表示小型(small)版本
模型 | 大小 | 包含内容 | 用途 |
---|---|---|---|
en_core_web_sm | ~12MB | 词汇、词向量、语法、实体 | 快速处理,资源有限环境 |
en_core_web_md | ~40MB | 小型版+词向量 | 平衡性能与资源 |
en_core_web_lg | ~560MB | 中型版+更大的词向量 | 最高准确性,资源充足 |
中文模型也是类似的
Token分词
接下来我们来看一下spaCy
库的用法,还是以代码案例来看
import spacy
import re
from torchtext.legacy import data# 设置要使用的模型
src_lang = 'en_core_web_sm'
# 加载模型
nlp = spacy.load(src_lang)
# 使用模型处理文本
doc = nlp("This is a sample English sentence.")
# 访问处理结果
for token in doc:print(token.text, token.pos_, token.dep_)
下图为上面这段代码的输出结果,实际上就是对句子进行分解,分解的标准就是我们常说的token,并且标明了词性。
接下来再来看中文
import spacy
import re
from torchtext.legacy import data# 设置要使用的模型
src_lang = 'zh_core_web_sm'
# 加载模型
nlp = spacy.load(src_lang)
# 使用模型处理文本
doc = nlp("今天是周一,我又要上班了。")
# 访问处理结果
for token in doc:print(token.text, token.pos_, token.dep_)
结果如下图所示,实际上每个token是文本的一个最小的有意义的单位。
对大量分词做处理形成一个字典
想像一下最早的活字印刷术,我们先要印刷一段文本,首先需要刻字,再按照文稿,将一个个泥活字捡出来,排在一块带有框的铁板上。在排好的字版上洒上松脂、蜡等粘合剂,加热铁板使其熔化,然后用一块平板将字面压平,冷却后字模就固定住了,成为一块完整的印版。
有没有觉得这个过程很熟悉,nlp 某种程度上像是一个电子板的活字印刷,为什么这么说?活字印刷本质上是人来一个个选块,那nlp呢?聪明的你一定想到了,实际上就是根据概率哪个token的概率高就选哪个,是不是很合理~
那么这里的字典实际上就相当与大堆大堆的泥活字。
接下来我们来利用data.TabularDataset(path, format, fields)
函数来生成字典
data.TabularDataset(path, format, fields)
'''
path(str):数据文件的地址
format(str):数据文件的格式,可选【CSV,TSV,JSON】格式
fields((list(tuple(str,Field)))or dict[str: tuple(str, Field)):如果使用list,格式必须为 CSV 或 TSV,并且列表的值应为(name, field)的元组。
'''
接下来再来看一下field是什么,Field 类是深度学习框架(如 PyTorch)中用于定义如何预处理文本数据的一个“说明书”。将原始的人类可读的文本(比如一句话),转换成一个模型可读的、数值化的张量(Tensor)。
class Field(RawField):"""定义一种数据类型及将其转换为张量的相关指令。Field类模拟了常见的文本处理数据类型,这些类型可以用张量表示。它包含一个Vocab(词表)对象,该对象定义了字段元素的可能取值集合及其对应的数值表示。Field对象还包含其他与数据数值化方式相关的参数,例如分词方法以及应生成的张量类型。如果一个Field在数据集的两列之间共享(例如,QA数据集中的问题和答案),那么它们将共享一个词表。属性:sequential: 该数据类型是否代表序列数据。如果为False,则不进行分词。默认值: True。use_vocab: 是否使用Vocab对象。如果为False,则该字段中的数据应已完成数值化。默认值: True。init_token: 一个将被预加到使用此字段的每个样本开头的标记(token),如果不需要起始标记则为None。默认值: None。eos_token: 一个将被追加到使用此字段的每个样本末尾的标记(EOS,句子结束标记),如果不需要结束标记则为None。默认值: None。fix_length: 使用此字段的所有样本将被填充到的固定长度,如果为None则表示序列长度可变。默认值: None。dtype: 代表此类数据批量样本的torch.dtype类型。默认值: torch.long。preprocessing: 一个在分词之后、数值化之前应用于样本的预处理流水线(Pipeline)。许多数据集会使用自定义的预处理器替换此属性。默认值: None。postprocessing: 一个在数值化之后、数值被转换为张量之前应用于样本的后处理流水线(Pipeline)。该流水线函数将批量数据作为列表接收,同时接收该字段的Vocab。默认值: None。lower: 是否将该字段中的文本转换为小写。默认值: False。tokenize: 用于将此字段中的字符串分词为序列样本的函数。如果为"spacy",则使用SpaCy分词器。如果传入不可序列化的函数,则该字段将无法被序列化。默认值: string.split (使用字符串的split方法)。tokenizer_language: 要构建的分词器的语言。目前各种语言仅支持SpaCy。include_lengths: 是返回一个填充后的迷你批次和一个包含各样本长度的元组,还是仅返回填充后的迷你批次。默认值: False。batch_first: 是否生成批次维度在第一位的张量。默认值: False。pad_token: 用作填充(padding)的字符串标记。默认值: "<pad>"。unk_token: 用于表示超出词表词汇(OOV)的字符串标记。默认值: "<unk>"。pad_first: 在序列的开头进行填充。默认值: False。truncate_first: 在序列的开头进行截断。默认值: False。stop_words: 在预处理步骤中要丢弃的标记(停用词)。默认值: None。is_target: 此字段是否为目标变量(target variable)。会影响对批次的迭代。默认值: False。"""
具体生成字典代码
# 设置要使用的模型
src_lang = 'en_core_web_sm'
# 加载模型
nlp = spacy.load(src_lang)
# 首先我们将上面分词的代码写成一个函数分词器
def tokenizer(sentence):# 使用模型处理文本doc = nlp(sentence)tokens=[]# 访问处理结果for token in doc:tokens.append(token.text)return tokens
# 利用Feild定义如何分词
# lower=True:全部转成小写字符
# tokenize=tokenizer:加载分词器
feild = data.Field(lower=True, tokenize=tokenizer)
# list[(name, field)]
data_fields=[('en',feild )]
# 对en_1.csv文件进行分层(该csv文件具体内容在下面的图中展示)
en=data.TabularDataset('./en_1.csv', format='csv', fields=data_fields)
feild.build_vocab(en)
# 查看结果(也放在下面的图中)
feild.vocab.stoi
print(feild.vocab.stoi.values())
print(feild.vocab.stoi.keys())
en_1.csv
内容如下
对应生成的字典,可以看到How已经从大写变成了小写how,同时多个相同的词也变成了单个。
最后就是对数据做批处理,此处省略该部分的叙述,有机会给补上。
1.3 模型训练
模型训练实际上包含以下几个步骤:
- 模型参数设定
- 模型类创建
- 模型设置为训练模式
- 获取目标,模型推理,计算损失迭代参数
'''模型训练'''
# 模型参数定义
d_model = 512
heads = 8
N = 6
dropout = 0.1
src_vocab = len(EN_TEXT.vocab) # 英文的数据集中的token的个数
trg_vocab = len(ZH_TEXT.vocab) # 中文的数据集中的token的个数
# device = 'cuda' if torch.cuda.is_available() else 'cpu'
device = 'cpu' # 由于torchtext的版本需要的比较低,而我的cuda版本比较高,所以torch无法使用cuda如果你可以可以将这一行注释掉,并取消上一行的注释
model = Transformer(src_vocab, trg_vocab, d_model, N, heads, dropout,device=device)
optim = torch.optim.Adam(model.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)for p in model.parameters():if p.dim() > 1:nn.init.xavier_uniform_(p)# 模型训练
def train_model(epochs, print_every=10):model.train() # 将模型设置为训练模式start = time.time() # 记录训练开始时间temp = start # 临时时间变量,用于计算间隔时间total_loss = 0 #累积损失for epoch in range(epochs): # 外层循环:遍历所有训练轮次for i, batch in enumerate(train_iter): # 内层循环:遍历训练数据中的所有批次src = batch.src.transpose(0, 1) # 转置源语言序列维度trg = batch.trg.transpose(0, 1) # 转置目标语言序列维度trg_input = trg[:, :-1] # 目标序列输入(去掉最后一个token)# 记录目标,方便后续求loss functiontargets = trg[:, 1:].contiguous().view(-1) # 目标序列标签(去掉第一个token)并展平# 使用掩码代码创建函数来制作掩码src_mask, trg_mask = create_masks(src, trg_input, src_pad, trg_pad)preds = model(src, trg_input, src_mask, trg_mask) # 向前传播预测结果optim.zero_grad() # 清零梯度loss = F.cross_entropy(preds.view(-1, preds.size(-1)),targets, ignore_index=trg_pad) # 计算交叉熵损失loss.backward() # 反向传播计算梯度optim.step() # 跟新模型参数total_loss += loss.item() # 累积损失if (i + 1) % print_every == 0: # 每隔一定迭代次数打印进度loss_avg = total_loss / print_everyprint("time = %dm, epoch %d, iter = %d, loss = %.3f, %ds per %d iters" %((time.time() - start) // 60, epoch + 1, i + 1, loss_avg,time.time() - temp, print_every))total_loss = 0temp = time.time()# 训练结束后保存最终模型final_checkpoint = {'model_state_dict': model.state_dict(),'optimizer_state_dict': optim.state_dict(),'epoch': epochs,'ZH_TEXT': ZH_TEXT,'EN_TEXT': EN_TEXT,'src_pad':src_pad,'trg_pad':trg_pad}torch.save(final_checkpoint, 'model_final.pth')print("最终模型已保存为: model_final.pth")train_model(200)
1.4 模型推理
- 读取训练好的模型及其参数
- 设置模型为推理模式
- 基于编码器编码后,基于解码器依次生成token。
# 加载保存的检查点
checkpoint_path = 'transformer/model_final.pth'
checkpoint = torch.load(checkpoint_path, map_location=torch.device('cpu')) # 使用CPU加载,或指定GPU# 恢复词汇表
ZH_TEXT = checkpoint['ZH_TEXT']
EN_TEXT = checkpoint['EN_TEXT']
# 恢复部分参数
src_pad=checkpoint['src_pad']
trg_pad=checkpoint['trg_pad']
# 模型参数定义
d_model = 512
heads = 8
N = 6
dropout = 0.1
src_vocab = len(EN_TEXT.vocab) # 英文的数据集中的token的个数
trg_vocab = len(ZH_TEXT.vocab) # 中文的数据集中的token的个数
# device = 'cuda' if torch.cuda.is_available() else 'cpu'
device = 'cpu' # 由于torchtext的版本需要的比较低,而我的cuda版本比较高,所以torch无法使用cuda如果你可以可以将这一行注释掉,并取消上一行的注释
model = Transformer(src_vocab, trg_vocab, d_model, N, heads, dropout,device=device)# 恢复模型参数
model.load_state_dict(checkpoint['model_state_dict'])
# 恢复优化器状态 继续训练的时候才用的上
# optim = torch.optim.Adam(model.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)
# optim.load_state_dict(checkpoint['optimizer_state_dict'])
# 获取训练轮次信息
trained_epochs = checkpoint['epoch']print(f"模型已从 {checkpoint_path} 加载")
print(f"模型已训练 {trained_epochs} 个轮次")'''
基于编码器-解码器架构的神经机器翻译模型的推理过程,用于将源语言句子翻译成目标语言。
在该代码中想要确定模型的输入和输出
'''
def translate(src, max_len=80, custom_string=False):"""用于推理:param src: 输入源,可以是与处理好的张量或原始字符串:param max_len: 生成翻译的最大长度限制:param custom_string: 标志位,只是输入是否为原始字符串:return:"""model.eval() # 设置为推理模式if custom_string == True: # 输入是否为原始字符src = tokenize_en(src, EN_TEXT) # 从Let me see. 变成向量 [89,21,95,2]src = torch.LongTensor(src) # 从向量 [89,21,95,2] 变成torch张量 tensor([89, 21, 95, 2])src_mask = (src != src_pad).unsqueeze(-2) # 张量中都不是1,新的张量tensor([[True, True, True, True]])e_outputs = model.encoder(src.unsqueeze(0), src_mask) # 计算编码器的输出# 4个数分别展开成512的向量,而且实际上已经是融合上下文信息的向量结果。outputs = torch.zeros(max_len).type_as(src.data) # 这里实际上相当于初始化一个输出outputs[0] = torch.LongTensor([ZH_TEXT.vocab.stoi['<sos>']])# ZH_TEXT.vocab.stoi是一个字典,是字符和数字的对应为了形成向量# <unk>:未知单词(out-of-vocabulary words)# <pad>:填充标记(用于使序列长度一致)# <sos>:序列开始标记 <eos>:序列结束标记for i in range(1, max_len): # 循环从1开始到max_len-1,i表示当前已生成的序列长度trg_mask = np.triu(np.ones((1, i, i)).astype('uint8'))trg_mask = Variable(torch.from_numpy(trg_mask) == 0)out = model.out(model.decoder(outputs[:i].unsqueeze(0), # 依次生成下一个输出e_outputs, trg_mask, src_mask))out = F.softmax(out, dim=-1) # 转为概率val, ix = out[:, -1].data.topk(1) # 获取概率和idoutputs[i] = ix[0][0]if ix[0][0] == ZH_TEXT.vocab.stoi['<eos>']:breakreturn ' '.join([ZH_TEXT.vocab.itos[ix] for ix in outputs[:i]] # 最终结果)words = 'Let me see.'
a=translate(words, custom_string=True)
print(a)