本篇文章参考:
Transformers for Sentiment Analysis.ipynb
部分细节可能会略作改动,代码注释尽数基于自己的理解。文章目的仅作个人领悟记录,并不完全是tutorial的翻译,可能并不适用所有初学者,但也可从中互相借鉴吸收参考。
接上篇:torchtext使用-- 单标签多分类任务TREC
这是第六篇,也是入门的最后一篇
这次我们将使用BERT来训练IMDB,虽然这听上去很不可思议。但是作为初学者学习BERT的使用,下游任务简单点也未尝不可
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchtext
from torchtext import data
from torchtext import datasetsimport spacy
import random
import math
import numpy as npuse_cuda=torch.cuda.is_available()
device=torch.device("cuda" if use_cuda else "cpu")SEED=1234
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
if use_cuda:
torch.cuda.manual_seed(SEED)torch.backends.cudnn.deterministic = True
由于BERT预训练有自己确定的分词方法和词表,因此对于IMDB数据的处理将使用transformer内部预置的BERT分词器
from transformers import BertTokenizer
tokenizer=BertTokenizer.from_pretrained('bert-base-uncased')
可以查看一下词表
len(tokenizer.vocab)
30522
对于一个句子的分词,只需要调用tokenizer的tokenize方法
token=tokenizer.tokenize("Don't Make sUch a fuss,get StUFf!")print(token)
['don', "'", 't', 'make', 'such', 'a', 'fuss', ',', 'get', 'stuff', '!']
可以调用内部的convert_tokens_to_ids方法,将token序列化
indexes=tokenizer.convert_tokens_to_ids(token)print(indexes)
[2123, 1005, 1056, 2191, 2107, 1037, 28554, 1010, 2131, 4933, 999]
由于我们最终还是要使用Field来处理IMDB的数据据,所以Field必须针对BERT的tokenizer做出调整,主要有以下两个调整 1.特殊token BERT在预训练的时候为了能够学到句子级别的特征,将输入改为一个个句对。针对句对,BERT采用了两个特殊的token:[CLS]、[SEP].后者用来表示句子的结束标志,而前者则是针对整个句对的抽象特征,它将作为最高隐层,用来表示句子级别的信息。对BERT以及预训练模型陌生的同学可以参考文章: 最火的几个全网络预训练模型梳理整合(BERT、ALBERT、XLNet详解)
因此,在Field中必须把默认的token改为与BERT一致
cls_token=tokenizer.cls_token
sep_token=tokenizer.sep_token
pad_token=tokenizer.pad_token
unk_token=tokenizer.unk_tokenprint(cls_token,sep_token,pad_token,unk_token)
[CLS] [SEP] [PAD] [UNK]
也可以直接得到idx
cls_token_id=tokenizer.cls_token_id
sep_token_id=tokenizer.sep_token_id
pad_token_id=tokenizer.pad_token_id
unk_token_id=tokenizer.unk_token_idprint(cls_token_id,sep_token_id, pad_token_id, unk_token_id)
101 102 0 100
2.输入长度限制 由于不同的BERT模型对于输入的最大长度要求可能不同,因此我们可以通过max_model_input_sizes来查看。同时在Field中也要指定最大长度的处理
max_model_input_sizes是一个字典,包含了各种BERT的最大长度限制
type(tokenizer.max_model_input_sizes)
dict
len(tokenizer.max_model_input_sizes)
18
而我们这里使用的是bert-base-uncased
max_input_length = tokenizer.max_model_input_sizes['bert-base-uncased']print(max_input_length)
512
所以对于我们训练数据的每一个句子,我们要保证喂给BERT前长度已经被cut过(当句子长度大于512)
要注意的是,由于BERT采用[CLS]、[SEP] 因此它对于这两个特有的句子级别token是敏感的,因为BERT会把它们视为句子的开始和结束。也就是说,如果我们对于训练的数据不加上这两个开始、结束符号,这对于BERT的施展是有影响的。因此,这里必须空出2个token留给[CLS]、[SEP]; 同时,在Field中也要指定init_token和eos_token为这两个标记
def tokenize_and_cut(sentence):
tokens=tokenizer.tokenize(sentence)
tokens=tokens[:max_input_length-2] #空出两个位置,让Field加上开始和结束的标记([CLS]、[SEP])
return tokens
接下来就是数据准备
1.Field 总结一下Field应该做出的调整:
1).更改tokenize.这里不再使用spacy,而是调用tokenize_and_cut
2).关闭vocab.由于我们已经有了tokenizer,因此在Field中我们需要指定use_vocab = False。也就是我们不需要Field来给分词之后的输入做序列化。
3).指定preprocessing为序列化.我们都知道preprocessing预处理是先于序列化的。因为关闭了vocab,Field将没办法自动对token进行序列化操作,所以我们指定preprocessing为tokenizer.convert_tokens_to_ids,这样在分词之后可以调用preprocessing代替Field原先的自动序列化。
4).指定特殊的token.这里需要指定四个token,即PAD\UNK\INIT\EOS。前面已经保证空出了两个位置留给INIT\EOS,所以这里可以放心地指定init_token = init_token_idx、eos_token = eos_token_idx,它会自动在sentence首位填充这两个token的idx。(要注意我们这里已经没有序列化的操作,而是preprocessing代替原先的序列化。而添加首位token这一步操作是在preprocessing和原先的序列化之间的。因此在这里,我们添加的token必须是token_idx,否则preprocessing将没办法把首尾token序列化)
5).设置batch_first = True.BERT要求输入的第一维是batch
TEXT=data.Field(batch_first=True,use_vocab=False,tokenize=tokenize_and_cut,
preprocessing=tokenizer.convert_tokens_to_ids,
init_token=cls_token_id,
eos_token=sep_token_id,
pad_token=pad_token_id,
unk_token=unk_token_id)LABEL=data.LabelField(dtype=torch.float)
2.datasets
train_data, test_data =https://www.it610.com/article/datasets.IMDB.splits(TEXT,LABEL)
train_data,valid_data=train_data.split(split_ratio=0.7)
print(f"Number of training examples: {len(train_data)}")
print(f"Number of validation examples: {len(valid_data)}")
print(f"Number of testing examples: {len(test_data)}")
Number of training examples: 17500
Number of validation examples: 7500
Number of testing examples: 25000
可以看到,经过这样的Field处理的IMDB数据将会被序列化为BERT“认识的”句子
print(vars(train_data.examples[0]))
{'text': [2023, 3185, 2038, 2288, 2000, 2022, 2028, 1997, 1996, 5409, 1045, 2031, 2412, 2464, 2191, 2009, 2000, 4966, 999, 999, 999, 1996, 2466, 2240, 2453, 2031, 13886, 2065, 1996, 2143, 2018, 2062, 4804, 1998, 4898, 2008, 2052, 2031, 3013, 1996, 14652, 1998, 5305, 2135, 5019, 2008, 1045, 3811, 14046, 3008, 2006, 1012, 1012, 1012, 1012, 2021, 1996, 2466, 2240, 2003, 2066, 1037, 6065, 8854, 1012, 2065, 2045, 2001, 2107, 1037, 2518, 2004, 1037, 3298, 27046, 3185, 9338, 1011, 2023, 2028, 2052, 2031, 22057, 2013, 2008, 1012, 2009, 6966, 2033, 1037, 2843, 1997, 1996, 4248, 2666, 3152, 2008, 2020, 2404, 2041, 1999, 1996, 3624, 1005, 1055, 1010, 3532, 5896, 3015, 1998, 7467, 1012, 1026, 7987, 1013, 1028, 1026, 7987, 1013, 1028, 1996, 2069, 21082, 3494, 1999, 1996, 2878, 3185, 2001, 1996, 15812, 1998, 13570, 1012, 1996, 2717, 1997, 1996, 2143, 1010, 2071, 2031, 4089, 2042, 2081, 2011, 2690, 2082, 2336, 1012, 1045, 2507, 2023, 2143, 1037, 5790, 1997, 1015, 2004, 2009, 2003, 5621, 9643, 1998, 2187, 2026, 2972, 2155, 2007, 1037, 3168, 1997, 2108, 22673, 1012, 2026, 6040, 1011, 2123, 1005, 1056, 3422, 2009, 999, 999, 999], 'label': 'neg'}
3.vocab 这里只需要再构造LABEL的词表
LABEL.build_vocab(train_data)
4.iterator
BATCH_SIZE = 128train_iterator, valid_iterator, test_iterator=data.BucketIterator.splits(
(train_data,valid_data,test_data),
batch_size=BATCH_SIZE,
device=device)
构建模型 这里会下载BERT的预训练模型,大概500MB不到。
from transformers import BertModel
bert=BertModel.from_pretrained('bert-base-uncased')
模型定义
对于BertModel来说,它的返回由四个部分组成:
1.last_hidden_state :(batch,seq,hidden_size)
整个输入的句子每一个token的隐层输出,也是我们这里将要用到的,可以将它作为embedding的替代
2.pooler_output:(batch,hidden_size)
输入句子第一个token的最高隐层。也就是[CLS]标记提取到的最终的句对级别的抽象信息。对于BERT的预训练来说,这个隐层信息将作为Next Sentence prediction任务的输入。然而我们这里将不会用到它,因为它对于情感分析来说效果不是很好。
This output is usually not a good summary of the semantic content of the input, you’re often better with averaging or pooling the sequence of hidden-states for the whole input sequence.
3.hidden_states:
一个元组,里面每个元素都是(batch,seq,hidden_size) 大小的FloatTensor,分别代表每一层的隐层和初始embedding的和
4.attentions:
一个元组,里面每个元素都是(batch,num_heads,seq,seq) 大小的FloatTensor,分别表示每一层的自注意力分数。
class BERTGRUSentiment(nn.Module):
def __init__(self,bert:BertModel,
hidden_dim:int,
output_dim:int,
n_layers:int,
bidirectional:bool,
dropout:float):
super(BERTGRUSentiment, self).__init__()self.bert=bert
embedding_dim=bert.config.to_dict()['hidden_size']self.rnn=nn.GRU(embedding_dim,hidden_dim,n_layers,
bidirectional=bidirectional,
batch_first=True,
dropout=0 if n_layers<2 else dropout)self.fc=nn.Linear(hidden_dim*2 if bidirectional else hidden_dim,output_dim)self.dropout=nn.Dropout(dropout)def forward(self,text):
with torch.no_grad():
embedding=self.bert(text)[0]
#embeddiing:(batch,seq,embedding_dim)_,hidden=self.rnn(embedding)
#(bi*num_layers,batch,hidden_size)hidden=self.dropout(torch.cat((hidden[-1,:,:],hidden[-2,:,:]),dim=1) if self.rnn.bidirectional else hidden[-1,:,:]) return self.fc(hidden)
#(batch,output_dim)
模型实例化
HIDDEN_DIM = 256
OUTPUT_DIM = 1
N_LAYERS = 2
BIDIRECTIONAL = True
DROPOUT = 0.25model = BERTGRUSentiment(bert,
HIDDEN_DIM,
OUTPUT_DIM,
N_LAYERS,
BIDIRECTIONAL,
DROPOUT)
注意!!在训练之前必须将模型中有关于BERT预训练的参数冻结
def freeze_parameters(model:nn.Module,demand:str):
for name, parameters in model.named_parameters():
if name.startswith(demand):
parameters.requires_grad = False
冻结:
freeze_parameters(model,'bert')
来数一下参与训练的参数:
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)print(f'The model has {count_parameters(model):,} trainable parameters')
The model has 2,759,169 trainable parameters
可以看到把BERT的参数冻结之后,整体模型参数还是比较少的。实际上要训练的就只是GRU和全连接层
训练模型
criterion=nn.BCEWithLogitsLoss()
criterion=criterion.to(device)
model=model.to(device)
optimizer=optim.Adam(model.parameters())
def binary_accuracy(preds, y):
"""
Returns accuracy per batch, i.e. if you get 8/10 right, this returns 0.8, NOT 8
"""#round predictions to the closest integer
rounded_preds = torch.round(torch.sigmoid(preds))
correct = rounded_preds.eq(y).float() #convert into float for division
acc = correct.sum() / len(correct)
return acc
def train(model:nn.Module, iterator:data.BucketIterator,
optimizer:optim.Adam, criterion:nn.BCEWithLogitsLoss):
epoch_loss = 0.
epoch_acc = 0.model.train()for batch in iterator:
preds=model(batch.text).squeeze(-1)
loss=criterion(preds,batch.label)
acc=binary_accuracy(preds,batch.label)optimizer.zero_grad()
loss.backward()
optimizer.step()epoch_loss+=loss.item()
epoch_acc+=acc.item()return epoch_loss/len(iterator),epoch_acc/len(iterator)def evaluate(model: nn.Module, iterator: data.BucketIterator,
criterion: nn.BCEWithLogitsLoss):
epoch_loss = 0.
epoch_acc = 0.model.eval()with torch.no_grad():
for batch in iterator:
preds = model(batch.text).squeeze(-1)
loss = criterion(preds, batch.label)
acc = binary_accuracy(preds, batch.label)epoch_loss += loss.item()
epoch_acc += acc.item()return epoch_loss / len(iterator), epoch_acc / len(iterator)
开始训练
import timedef epoch_time(start_time, end_time):
elapsed_time = end_time - start_time
elapsed_mins = int(elapsed_time / 60)
elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
return elapsed_mins, elapsed_secs
注意,如果你的机子显存不够,那么下面的这段代码基本上是会报错的。这主要是因为前面把model参数搬到了GPU,BERT参数有又多得让人发指,这时候基本上4、5个G的显存已经直接被占用了,再继续训练下去,显存就会支撑不了模型的迭代。
可以选择colab跑跑看,有钱人就忽略吧
N_EPOCHS = 5best_valid_loss = float('inf')for epoch in range(N_EPOCHS):start_time = time.time()train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)end_time = time.time()epoch_mins, epoch_secs = epoch_time(start_time, end_time)if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'tut6-model.pt')print(f'Epoch: {epoch+1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')
print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
【torchtext使用--Transformer的IMDB情感分析】下一篇补充篇:torchtext补充—利用torchtext读取自己的数据集
推荐阅读
- C语言学习|第十一届蓝桥杯省赛 大学B组 C/C++ 第一场
- paddle|动手从头实现LSTM
- pytorch|使用pytorch从头实现多层LSTM
- 推荐系统论文进阶|CTR预估 论文精读(十一)--Deep Interest Evolution Network(DIEN)
- pytorch|YOLOX 阅读笔记
- 前沿论文|论文精读(Neural Architecture Search without Training)
- 联邦学习|【阅读笔记】Towards Efficient and Privacy-preserving Federated Deep Learning
- OpenCV|OpenCV-Python实战(18)——深度学习简介与入门示例
- 深度学习|深度学习笔记总结
- 《繁凡的深度学习笔记》|一文绝对让你完全弄懂信息熵、相对熵、交叉熵的意义《繁凡的深度学习笔记》第 3 章 分类问题与信息论基础(中)(DL笔记整理