HugginFace 中文填空(学习笔记) 电脑版发表于:2023/10/28 13:48 ![](https://img.tnblog.net/arcimg/hb/782c293bc3904ab0bb30af5ff454beae.png) >#HugginFace 中文填空(学习笔记) [TOC] ## 数据集介绍 tn2>本章使用的仍然是情感分类数据集,每条包括一句购物评价一集以及是不是好评的标识。 唯一不同的是将其中的句子字符串进行替换,然后通过模型进行训练后进行填空。 ## 实现代码 ### 安装环境 ```python %pip install -q transformers datasets torchtext ``` ### 准备数据集 #### 使用编码工具 tn2>首先需要加载编码工具。 ```python from transformers import BertTokenizer token = BertTokenizer.from_pretrained('bert-base-chinese') token ``` ![](https://img.tnblog.net/arcimg/hb/ec650e26ced24f2ca1c978ec0db7967d.png) tn2>进行试算。 ```python out = token.batch_encode_plus( batch_text_or_text_pairs=['轻轻的我走了,正如我轻轻地来。', '我轻轻的招手,作别西天的云彩。'], truncation=True, padding='max_length', max_length=18, return_tensors='pt', return_length=True) #查看编码输出 for k, v in out.items(): print(k, v.shape) #把编码还原为句子 print(token.decode(out['input_ids'][0])) ``` ![](https://img.tnblog.net/arcimg/hb/fbbb3239442b4997bf8891268833958b.png) #### 定义数据集 tn2>本次任务中,依然将使用`ChnSentiCorp`数据集,但需要对数据集进行一些操作,它变成一个填空任务数据集。 在开始处理之前,首先需要加载数据集,代码如下: ```python from datasets import load_dataset dataset = load_dataset('lansinuote/ChnSentiCorp') dataset ``` ![](https://img.tnblog.net/arcimg/hb/b1b95154121d43ef92a6ef2ee4737f2e.png) tn2>接下来对这些文本数据进行编码,便于后续的处理,代码如下: ```python def f(data): return token.batch_encode_plus(batch_text_or_text_pairs=data['text'], truncation=True, padding='max_length', max_length=30, return_length=True) # 丢掉'text', 'label'字段 dataset = dataset.map(function=f, batched=True, batch_size=1000, num_proc=4, remove_columns=['text', 'label']) dataset ``` tn2>`truncation=True`和`max_length=30`编码结果的长度不会长于30个词,超出30个词的部分会被截断。 `padding=max_length`表明不足会补充`[PAD]`到30个词为止。 `return_length=True`会让编码结果中多出一个`length`字段,表明这段数据的长度,由于PAD不会被计算在长度内,所以`length`一定小于或等于`30`,这个字段方便了后续数据过滤。 ![](https://img.tnblog.net/arcimg/hb/d666d72a08f44523a945fe0fa61ecf1c.png) tn2>接下来我们将丢弃句子长度小于30个词的句子给过滤掉。 ```python def f(data): return [i >= 30 for i in data['length']] dataset = dataset.filter(function=f, batched=True, batch_size=1000, num_proc=4) dataset ``` ![](https://img.tnblog.net/arcimg/hb/d02a1ef1a61c463c89ed900cfd56d3d6.png) tn2>训练集中少了314条数据,测试集合中少了47条数据。 #### 定义计算设备 ```python import torch device = 'cpu' if torch.cuda.is_available(): device = 'cuda' device ``` ![](https://img.tnblog.net/arcimg/hb/83aa5a8a7b3641ae87920a1e07538c14.png) #### 定义数据整理函数 tn2>我们把第15个词给挖出来,替换成`[MASK]`同时擦掉正确的答案。 模型通过`[MASK]`将其中进行预测出来。 ```python def collate_fn(data): #取出编码结果 input_ids = [i['input_ids'] for i in data] attention_mask = [i['attention_mask'] for i in data] token_type_ids = [i['token_type_ids'] for i in data] #转换为tensor格式 input_ids = torch.LongTensor(input_ids) attention_mask = torch.LongTensor(attention_mask) token_type_ids = torch.LongTensor(token_type_ids) #把第15个词替换为mask labels = input_ids[:, 15].reshape(-1).clone() input_ids[:, 15] = token.get_vocab()[token.mask_token] #移动到计算设备 input_ids = input_ids.to(device) attention_mask = attention_mask.to(device) token_type_ids = token_type_ids.to(device) labels = labels.to(device) return input_ids, attention_mask, token_type_ids, labels ``` tn2>试算。 ```python data = [{ 'input_ids': [ 101, 2769, 3221, 3791, 6427, 1159, 2110, 5442, 117, 2110, 749, 8409, 702, 6440, 3198, 4638, 1159, 5277, 4408, 119, 1728, 711, 2769, 3221, 5439, 2399, 782, 117, 3791, 102 ], 'token_type_ids': [0] * 30, 'attention_mask': [1] * 30 }, { 'input_ids': [ 101, 679, 7231, 8024, 2376, 3301, 1351, 6848, 4638, 8024, 3301, 1351, 3683, 6772, 4007, 2692, 8024, 2218, 3221, 100, 2970, 1366, 2208, 749, 8024, 5445, 684, 1059, 3221, 102 ], 'token_type_ids': [0] * 30, 'attention_mask': [1] * 30 }] #试算 input_ids, attention_mask, token_type_ids, labels = collate_fn(data) #把编码还原为句子 print(token.decode(input_ids[0])) print(token.decode(labels[0])) input_ids.shape, attention_mask.shape, token_type_ids.shape, labels ``` ![](https://img.tnblog.net/arcimg/hb/540473d1f9dd4826bfb494bbf136482c.png) #### 定义数据集加载器 ```python loader = torch.utils.data.DataLoader(dataset=dataset['train'], batch_size=16, collate_fn=collate_fn, shuffle=True, drop_last=True) len(loader) ``` ![](https://img.tnblog.net/arcimg/hb/d4b8d29ec1c9499493d8bb52e744b724.png) ```python for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(loader): break print(token.decode(input_ids[0])) print(token.decode(labels[0])) input_ids.shape, attention_mask.shape, token_type_ids.shape, labels ``` ![](https://img.tnblog.net/arcimg/hb/6a418998d7124e138c1113fb52435a45.png) ### 定义模型 #### 加载预训练模型 ```python # 加载预训练模型 from transformers import BertModel pretrained = BertModel.from_pretrained('bert-base-chinese') #统计参数量 sum(i.numel() for i in pretrained.parameters()) / 10000 ``` ![](https://img.tnblog.net/arcimg/hb/ca292854b1184be7869d8f79ec21759f.png) tn2>该模型有大约1个亿的参数量。 ```python # 不训练预训练模型,不需要计算梯度 for param in pretrained.parameters(): param.requires_grad_(False) ``` tn2>预训练的之后,可以进行一次试算。 ```python #预训练模型试算 #设定计算设备 pretrained.to(device) #模型试算 out = pretrained(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids) out.last_hidden_state.shape ``` ![](https://img.tnblog.net/arcimg/hb/844f0dff00a24d459be6e7bb182bd3f2.png) tn2>这里输出了16句话的结果,每句话包括了30个词,每个词被抽成了768维的向量。 #### 定义下游任务模型 ```python #定义下游任务模型 class Model(torch.nn.Module): def __init__(self): super().__init__() self.decoder = torch.nn.Linear(in_features=768, out_features=token.vocab_size, bias=False) #重新初始化decode中的bias参数为全0 self.bias = torch.nn.Parameter(data=torch.zeros(token.vocab_size)) self.decoder.bias = self.bias #定义Dropout层,防止过拟合 self.dropout = torch.nn.Dropout(p=0.5) def forward(self, input_ids, attention_mask, token_type_ids): #使用预训练模型抽取数据特征 with torch.no_grad(): out = pretrained(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids) #把第15个词的特征,投影到全字典范围内 out = self.dropout(out.last_hidden_state[:, 15]) out = self.decoder(out) return out model = Model() #设定计算设备 model.to(device) #试算 model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids).shape ``` tn2>在这段代码中,定义了下游任务模型,该模型只包括一个全连接的线性神经网络,权重矩阵为768x21128,所以它能够把一个768维度的向量转换到21128维空间中。 可以把backbone抽取的数据特征还原为字典中的任何一个字。型做分类下游任务模型的计算过程为,获取一批数据之后,使用backbone将这批数据抽取成特征矩阵,抽取的特征矩阵的形状应该是`16x30x768`,这在之前预训练模型的试算中已经看到。这3个维度分别代表了16句话、30个词、768 维度的特征向量。 是对baci 在本次的填空任务中,填空处固定出现在每句话的第15个词的位置,所以只取出每句来讲,需话的第15个词的特征,再尝试把这个词的特征投影到全体词表空间中,即还原为词典中的结果为数据 某个词。 在投影到全体词表空间中时,由于768x21128是一个很大的矩阵,如果直接计算,则很容易导致过拟合,所以对backbone抽取的数据特征要接入一个DropOut 网络,把其中的数据以一定的概率置为0,防止网络的过拟合。 在代码的最后对该模型进行了试算,运行结果如下: ![](https://img.tnblog.net/arcimg/hb/c629c4d03a1a4a1997b026b371be7e6d.png) tn2>可见,预测结果为16句的填空结果,如果在该结果上再套用`Softmax()`函数,则为在全体词表中每个词的概率。 ### 训练和测试 tn2>训练模型代码如下: ```python from transformers import AdamW from transformers.optimization import get_scheduler def train(): #定义优化器 optimizer = AdamW(model.parameters(), lr=5e-4, weight_decay=1.0) #定义loss函数 criterion = torch.nn.CrossEntropyLoss() #定义学习率调节器 scheduler = get_scheduler(name='linear', num_warmup_steps=0, num_training_steps=len(loader) * 5, optimizer=optimizer) #模型切换到训练模式 model.train() #共训练5个epoch for epoch in range(5): #按批次遍历训练集中的数据 for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(loader): #模型计算 out = model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids) #计算loss并使用梯度下降法优化模型参数 loss = criterion(out, labels) loss.backward() optimizer.step() scheduler.step() optimizer.zero_grad() #输出各项数据的情况,便于观察 if i % 50 == 0: out = out.argmax(dim=1) accuracy = (out == labels).sum().item() / len(labels) lr = optimizer.state_dict()['param_groups'][0]['lr'] print(epoch, i, loss.item(), lr, accuracy) train() ``` ![](https://img.tnblog.net/arcimg/hb/ee297cd974b6476caf3c4846a91edae9.png) #### 测试 ```python def test(): #定义测试数据集加载器 loader_test = torch.utils.data.DataLoader(dataset=dataset['test'], batch_size=32, collate_fn=collate_fn, shuffle=True, drop_last=True) #下游任务模型切换到运行模式 model.eval() correct = 0 total = 0 #按批次遍历测试集中的数据 for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(loader_test): #计算15个批次即可,不需要全部遍历 if i == 15: break print(i) #计算 with torch.no_grad(): out = model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids) #统计正确率 out = out.argmax(dim=1) correct += (out == labels).sum().item() total += len(labels) print(correct / total) test() ``` tn2>测试出的正确率如下: ![](https://img.tnblog.net/arcimg/hb/7f45520919134c4f9bc8325742a14520.png)