预训练模型已经火了这么久了,但作为菜本菜的我却还在用lstm。在生成任务上与同门学长用的预训练模型相比,效果差的比较明显。所以,我决定走上预训练的不归路。以下分享我的学习过程:
万事开头难,上视频:视频我喜欢看简短的
从零实现GPT-2,瞎写笑傲江湖外传,金庸直呼内行_哔哩哔哩_bilibili
这是一个非常简单的模型。他没有用huggingface的模型库,而是用pytorch自己搭建了一个模型。也没有用预训练的参数,而是从头开始训练。
1. 数据处理:数据用的是金庸的小说笑傲江湖。直接就是每次去128个字作为一个预料。输入x和输出y唯一的区别就在于移了一位。(根据transformer生成任务的特点)
2. tokenizer:对小说手动构建字典。利用set()函数获取整个小说的无序不重复的字,然后构建字典
chars = sorted(list(set(data)))
self.stoi = { ch:i for i,ch in enumerate(chars) }
self.itos = { i:ch for i,ch in enumerate(chars) }
3. 加载数据:
继承Dataset并修改__init__()、__len__() 和 __getitem__()
4. 模型:也是自己写的,结构包括词向量编码、位置编码、主体、输出层
class GPT(nn.Module):
""" the full GPT language model, with a context size of block_size """
def __init__(self, config):
super().__init__()
# input embedding stem
self.tok_emb = nn.Embedding(config.vocab_size, config.n_embd)
self.pos_emb = nn.Parameter(torch.zeros(1, config.block_size, config.n_embd))
self.drop = nn.Dropout(config.embd_pdrop)
# transformer 主体
self.blocks = nn.Sequential(*[Block(config) for _ in range(config.n_layer)])
# decoder head 输出线形层
self.ln_f = nn.LayerNorm(config.n_embd)
self.head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
主题结构呢,就是transformer的decoder部分,删掉中间的encoder-decoder注意力一块。那无非就是mask自注意力,和前馈神经网络两块。每块都包含曾归一化和残差链接两部分。
class Block(nn.Module):
""" an unassuming Transformer block """
def __init__(self, config):
super().__init__()
self.ln1 = nn.LayerNorm(config.n_embd)
self.ln2 = nn.LayerNorm(config.n_embd)
self.attn = CausalSelfAttention(config)
self.mlp = nn.Sequential( #前馈神经网络就是两个先行曾中间夹一个激活函数
nn.Linear(config.n_embd, 4 * config.n_embd),
nn.GELU(),
nn.Linear(4 * config.n_embd, config.n_embd),
nn.Dropout(config.resid_pdrop),
)
def forward(self, x):
x = x + self.attn(self.ln1(x)) #将词向量+位置向量输入,经过曾归一化进入mask注意力(self.attn)并做残差链接
x = x + self.mlp(self.ln2(x)) #将mask注意力输出结果层归一化,进入前馈神经网络,并残差链接
return x
以上是这个模型的主要内容了。从这个模型清晰的看到GPT2模型的结果,我也明白了重要的一点:预训练模型的大预料库基本是没有具体任务的标签的,那他是怎么训练的呢?就是通过移位操作,输入和输出之间搓开一位。训练的是模型对于语言整体的感知能力。也就是让你不断地看书,学习,掌握知识。对于具体的下游任务,再去fine-turning。
那么问题来了,我还是不会使用预训练模型
话不多说,上视频
GPT2中文闲聊对话系统代码讲解_哔哩哔哩_bilibili
1.数据处理,这里项目作者已经准备了大量的对话预料可供下载
2.tokenizer,作者有提供他们做好的中文字典
from transformers import BertTokenizerFast
tokenizer = BertTokenizerFast(vocab_file=args.vocab_path, sep_token="[SEP]", pad_token="[PAD]", cls_token="[CLS]")
input_ids = tokenizer.encode(utterance, add_special_tokens=False) #处理预料。
3. 加载数据,在这里通过dataloader的collect_fn对预料做了移位操作。dataset依然继承并修改三个函数即可。
4. 模型:这里模型通过transformers库的GPT2LMheadMoodel(config=model_fonfig)创建,因此不方便查看模型具体结构。
from transformers import GPT2LMHeadModel, GPT2Config
if args.pretrained_model: # 加载预训练模型
model = GPT2LMHeadModel.from_pretrained(args.pretrained_model)
else: # 初始化模型
model_config = GPT2Config.from_json_file(args.model_config)
model = GPT2LMHeadModel(config=model_config)
但是在debug过程中,进入模型可以看到。这里将在下一篇分享中介绍
class GPT2LMHeadModel(GPT2PreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.transformer =GPT2Model(config)
self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
其中GPT2Model的模型结构和上个项目的GPT模型结构是一毛一样的。
在这个项目中因为模型太大,没有看出什么,但可以为后面做一个铺垫。
以上是第二个项目的主要内容,和第一个没啥区别,只是训练出来一个会骂人的人工智障。
那我还是不会用预训练与微调啊。
经过前两个项目的铺垫,针对预训练与微调我想要明白以下几个问题:
Q1.字典要自己构建吗?如何处理数据啊?
Q2.如何加载预训练模型啊?网上的项目五花八门,有的下载了什么权重,有的说什么117M模型,345M模型参数已公开,有的说OpenAI,但他是tensorflow框架我不会,有的说huggingface,他太大了我没看懂。。。。
Q3.怎么将预训练模型用到下游任务啊,怎么在预训练模型上修改适用于我的任务啊?
Q4.微调怎么微调啊,什么是微调啊?怎么冻结原来模型参数啊
下面一一分析回答。
废话不多说,上视频HuggingFace简明教程,BERT中文模型实战示例.NLP预训练模型,Transformers类库,datasets类库快速入门._哔哩哔哩_bilibili
这个视频是bert预训练模型,和我要用的GPT2不是同一个。但是道理方法都是一样的。
主要有几个部分:
models:各种写好的预训练模型,每个模型包含一个model card和一个Files and versions。
datasets:常见的数据集
docs:相应模型的说明文档
之前也不知道模型卡是什么幺蛾子,现在明白,他相当于一个小tips,提示你这个模型怎么用
首先,还是来一遍项目顺序吧:
1.数据处理:咱还用闲聊对话项目里面的数据
2.tokenizer:这就是我们的Q1问题,要自己构建词典吗?
回答:可以自己构建,比如上个闲聊对话,就是自己构建字典,构建tokenizer,自己准备数据,自己从头训练。但是如果我们要用huggingface里面的预训练模型与参数的话,就不用自己构建了
比如我们想要用gpt2--chinese模型,找到该模型的模型卡,他告诉我们
from transformers import (
BertTokenizerFast,
AutoModel,
)
tokenizer = BertTokenizerFast.from_pretrained('bert-base-chinese')
model = AutoModel.from_pretrained('ckiplab/gpt2-base-chinese')
这就明白了,如果我们想要用预训练好的模型,那就直接加载他的tokenizer即可。
而处理数据也只需要如下,即可将文本转换成对应的id
data_ids = tokenizer.encode(data, add_speical_tokens=False)
3. 加载数据,在dataset的__getitem__() 和 dataloader的collect_fn()中修改数据处理成想要的样子即可。
4.加载模型,这就遇到了我们的Q2问题,想要加载预训练模型,直接从huggingface对应模型的model card里面复制即可
from transformers import AutoModel
model = AutoModel.from_pretrained('ckiplab/gpt2-base-chinese')
也就是说,不用下载什么权重文件(当然也可以),直接调用模型接口加载即可
5. 定义下游任务模型:这就是我们的Q3问题,如何将预训练模型用到下游任务,这对我来说是重点。但是明白之后原来也不难。上代码
class Model(torch.nn.Module):
def __init__(self):
super().__init__()
self.predmodel = AutoModel.from_pretrained('ckiplab/gpt2-base-chinese') #加载原始预训练模型
self.fc = torch.nn.Linear(768,vocab_size) # 为模型添加新的结构 输出层
def forward(self, input_ids, labels):
with torch.no_grad(): # 原来的与训练模型不用更新参数
out = self.predmodel(ipythonput_ids) # 原始预训练模型输出结果
out = self.fc(out.last_hidden_state[:,0]) # 放入下游具体任务的输出层中
out = out.softmax(dim = 1) # 并进行softmax操作
reture out # 才是最终的输出结果
model = Model() # 实例化
这里我们定义具体模型,就是先加载一个想要的预训练模型,这个模型实际上和最上面第一个项目 小说生成都是一样的。
它就是从输入向量到模型主体,这一大块,没有输出层。我们将预训练模型迁移到我们具体任务就是加上输出层也就是self.fc 。
这里如果是二分类任务,就加一个 self.fc = torch.nn.Linear(768,2)
如果是生成任务,就加一个 self.fc = torch.nn.Linear(768, tokenizer.vocab_size)
到此模型部分已经准备好了,就剩训练了‘
6.模型训练:优化器+损失函数
optimizer = Adamw(model.parameters,lr = 0.0001)
criterion = torch.nn.CrossEntropyLoss()
model.train() # 设置模型为训练模式
for i, data in enumerate(loader):
out = model(input_ids,..)
loss = criterion(out, labels)
loss.backward()
optimizer.step()
optimizer.zero_grad()
对于Q4,微调:微调,就是在自己的数据上将模型再训练一遍。那么如何冻结原模型参数,只训练新加模块呢?
两个方法:
a. 如步骤5中,在模型的forward部分,对于预训练模块,用 with torch.no_grad()不更新梯度。
def forward(self, input_ids, labels):
with torch.no_grad(): # 原来的与训练模型不用更新参数
out = self.predmodel(ipythonput_ids) # 原始预训练模型输出结果
out = self.fc(out.last_hidden_state[:,0]) # 放入下游具体任务的输出层中
b. 对预训练模型参数用param.requires_grad_(False)
def forward(self, input_ids, labels):
for param in self.predmodel.parameters():
param.requires_grad_(False) # 原来的与训练模型不用更新参数
out = self.predmodel(ipythonput_ids) # 原始预训练模型输出结果
out = self.fc(out.last_hidden_state[:,0]) # 放入下游具体任务的输出层中
两个方法的区别,什么时候该用with torch.no_grad()?什么时候该用.requires_grad ==False?_Y. F. Zhang的博客-CSDN博客
但是我没看懂。。
到此,简单的使用huggingface调用预训练模型并在下游任务上微调的基本过程就有了。至于写项目的话,还是得找大佬的项目,然后进行修改。