本文首次介绍的Transformers模型。具体来说,将使用本文中的BERT(来自Transformers的双向编码器表示)模型。
Transformers模型比这些教程中涵盖的任何其他模型都大得多。因此将使用Transformers库来获取经过预训练的transformers,并将其用作嵌入层。 将冻结(而不是训练)Transformers,仅训练从Transformers产生的表示中学习的模型的其余部分。在这种情况下,将使用多层双向GRU,但是任何模型都可以从这些表示中学习。
介绍
“NLP’s ImageNet moment has arrived.”
– Sebastian Ruder
想象一下我们有能力构建支持谷歌翻译的自然语言处理(NLP)模型,并且在Python中仅需几行代码来完成,这听起来是不是让人非常兴奋。
而现在我们就可以坐在自己的机器前实现这个了!借助于被HuggingFace称为PyTorch-Transformers目前最先进的NLP工具。
https://github.com/huggingface/pytorch-transformers
我们可以简单地用Python导入它并进行实验。
我对现在NLP的研发速度感到非常惊讶,每一篇新论文、每一个框架和库都在推动着这个不可思议的强大领域的发展。由于围绕人工智能的研究的开放文化和大量免费可用的文本数据,几乎没有什么是我们今天不能做的。
无论我再怎么强调PyTorch-Transformers对研究社区和NLP行业的影响也不为过。我相信这有可能彻底改变我们所知的自然语言处理领域。
揭开NLP的神秘面纱
本质上,自然语言处理是教计算机理解人类语言的复杂性。
在讨论PyTorch-Transformers的技术细节之前,让我们快速回顾一下该库构建的概念——NLP。我们还将理解最先进的(state-of-the-art)意味着什么。
在我们开始PyTorch-Transformers的讲解之前,这里有一些你需要了解的东西:
注意 这篇文章将多处提及Transformers ,所以我强烈建议你阅读下面的指南,以对Transformers有个理解
https://www.analyticsvidhya.com/blog/2019/06/understanding-transformers-nlp-state-of-the-art-models/?utm_source=blog&utm_medium=pytorch-transformers-nlp-python
PyTorch-Transformers是什么?
PyTorch-Transformers是一个最先进的自然语言处理预训练模型库。
我从PyTorch-Transformers的文档中选取了这一部分。这个库目前包含PyTorch实现、预训练的模型权重、使用脚本和用于以下模型的转换工具:
上述所有模型都是适用于各种NLP任务的最佳模型。
大多数最先进的模型需要大量的训练数据和花费数天时间在昂贵的GPU硬件上进行训练,而这些只有大型技术公司和研究实验室才能负担得起。但随着PyTorch-Transformers的推出,现在任何人都可以利用这些最先进的模型!
在你的机器上安装PyTorch-Transformers
在Python中 Pytorch-Transformers非常简单。你可以只使用pip安装:
pip install pytorch-transformers
或者在Colab上使用以下命令:
!pip install pytorch-transformers
由于大多数这些模型都是GPU密集型的,因此我建议按照本文使用谷歌Colab。
准备资料
首先,像往常一样,为确定性结果设置随机种子。
import torch
import randomimport numpy as np
SEED = 1234
random.seed(SEED)np.random.seed(SEED)torch.manual_seed(SEED)torch.backends.cudnn.deterministic = True
transformer已经使用特定的词汇进行了训练,这意味着需要使用完全相同的词汇进行训练,并且还需要以与transformer最初训练时相同的方式标记数据。
幸运的是,transformer库为提供的每个transformer模型提供了标记器。在这种情况下,使用的是BERT模型,该模型会忽略大小写(即每个单词都小写)。通过加载预训练的基于bert-base-uncase的令牌生成器来获得此功能。
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')I1106 14:55:11.110527 139759243081536 file_utils.py:39] PyTorch version 1.3.0 available.I1106 14:55:11.917650 139759243081536 tokenization_utils.py:374] loading file https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-vocab.txt from cache at /home/ben/.cache/torch/transformers/26bc1ad6c0ac742e9b52263248f6d0f00068293b33709fae12320c0e35ccfbbb.542ce4285a40d23a559526243235df47c5f75c197f04f37d1a0c124c32c9a084
标记器具有vocab属性,其中包含将要使用的实际词汇。可以通过检查其长度来检查其中有多少个令牌。
len(tokenizer.vocab)
30522
使用令牌生成器就像在字符串上调用tokenizer.tokenize一样简单。这将以与预训练的transformer模型一致的方式标记和小写数据。
tokens = tokenizer.tokenize('Hello WORLD how ARE yoU?')
print(tokens)['hello', 'world', 'how', 'are', 'you', '?']
可以使用词汇表tokenizer.convert_tokens_to_ids对令牌进行数字化。
indexes = tokenizer.convert_tokens_to_ids(tokens)
print(indexes)
[7592, 2088, 2129, 2024, 2017, 1029]
transformer还接受了特殊标记的训练,以标记句子的开头和结尾,此处有详细说明。 以及标准填充和未知令牌。 也可以从令牌生成器中获取这些。
注意:分词器确实具有序列的开始和序列的结束属性(bos_token和eos_token),但未设置这些属性,因此不应将其用于此transformer。
init_token = tokenizer.cls_tokeneos_token = tokenizer.sep_tokenpad_token = tokenizer.pad_tokenunk_token = tokenizer.unk_token
print(init_token, eos_token, pad_token, unk_token)
[CLS] [SEP] [PAD] [UNK]
可以通过使用词汇表来转换特殊标记的索引...
init_token_idx = tokenizer.convert_tokens_to_ids(init_token)eos_token_idx = tokenizer.convert_tokens_to_ids(eos_token)pad_token_idx = tokenizer.convert_tokens_to_ids(pad_token)unk_token_idx = tokenizer.convert_tokens_to_ids(unk_token)
print(init_token_idx, eos_token_idx, pad_token_idx, unk_token_idx)
101 102 0 100
或通过从令牌生成器中明确获取它们。
init_token_idx = tokenizer.cls_token_ideos_token_idx = tokenizer.sep_token_idpad_token_idx = tokenizer.pad_token_idunk_token_idx = tokenizer.unk_token_id
print(init_token_idx, eos_token_idx, pad_token_idx, unk_token_idx)
101 102 0 100
需要处理的另一件事是,模型是在具有定义的最大长度的序列上进行训练的-它不知道如何处理序列的时间要比在其上进行训练的时间长。可以通过检查要使用的 transformer版本的max_model_input_sizes来获得这些输入大小的最大长度。在这种情况下,它是512个令牌。
max_input_length = tokenizer.max_model_input_sizes['bert-base-uncased']
print(max_input_length)
512
以前,已经使用spaCy标记器对示例进行标记。但是,现在需要定义一个函数,该函数将传递给TEXT字段,该函数将处理所有标记处理。它还将令牌的数量减少到最大长度。请注意,最大长度比实际的最大长度小2。这是因为需要向每个序列附加两个标记,一个标记添加到开始,一个标记添加到结束。
def tokenize_and_cut(sentence): tokens = tokenizer.tokenize(sentence) tokens = tokens[:max_input_length-2] return tokens
现在定义字段。 transforme期望批次尺寸为第一,因此将batch_first = True设置为True。因为已经有了由 transforme提供的文本词汇,所以将use_vocab = False设置为告诉torchtext将处理事物的词汇方面。将tokenize_and_cut函数作为令牌生成器传递。预处理参数是一个函数,该函数在标记了示例之后将其用于示例,这是我们将标记转换为其索引的地方。最后,定义特殊标记-请注意,将它们定义为它们的索引值而不是它们的字符串值,即100而不是[UNK],这是因为序列已经转换为索引。
像以前一样定义标签字段。
from torchtext import data
TEXT = data.Field(batch_first = True, use_vocab = False, tokenize = tokenize_and_cut, preprocessing = tokenizer.convert_tokens_to_ids, init_token = init_token_idx, eos_token = eos_token_idx, pad_token = pad_token_idx, unk_token = unk_token_idx)
LABEL = data.LabelField(dtype = torch.float)
像以前一样加载数据并创建验证拆分。
from torchtext import datasets
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
train_data, valid_data = train_data.split(random_state = random.seed(SEED))In [13]: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
可以检查一个示例,并确保文本已被数字化。
print(vars(train_data.examples[6]))
{'text': [5949, 1997, 2026, 2166, 1010, 1012, 1012, 1012, 1012, 1996, 2472, 2323, 2022, 10339, 1012, 2339, 2111, 2514, 2027, 2342, 2000, 2191, 22692, 5691, 2097, 2196, 2191, 3168, 2000, 2033, 1012, 2043, 2016, 2351, 2012, 1996, 2203, 1010, 2009, 2081, 2033, 4756, 1012, 1045, 2018, 2000, 2689, 1996, 3149, 2116, 2335, 2802, 1996, 2143, 2138, 1045, 2001, 2893, 10339, 3666, 2107, 3532, 3772, 1012, 11504, 1996, 3124, 2040, 2209, 9895, 2196, 4152, 2147, 2153, 1012, 2006, 2327, 1997, 2008, 1045, 3246, 1996, 2472, 2196, 4152, 2000, 2191, 2178, 2143, 1010, 1998, 2038, 2010, 3477, 5403, 3600, 2579, 2067, 2005, 2023, 10231, 1012, 1063, 1012, 6185, 2041, 1997, 2184, 1065], 'label': 'neg'}
可以使用convert_ids_to_tokens将这些索引转换回可读的令牌。
tokens = tokenizer.convert_ids_to_tokens(vars(train_data.examples[6])['text'])
print(tokens)
['waste', 'of', 'my', 'life', ',', '.', '.', '.', '.', 'the', 'director', 'should', 'be', 'embarrassed', '.', 'why', 'people', 'feel', 'they', 'need', 'to', 'make', 'worthless', 'movies', 'will', 'never', 'make', 'sense', 'to', 'me', '.', 'when', 'she', 'died', 'at', 'the', 'end', ',', 'it', 'made', 'me', 'laugh', '.', 'i', 'had', 'to', 'change', 'the', 'channel', 'many', 'times', 'throughout', 'the', 'film', 'because', 'i', 'was', 'getting', 'embarrassed', 'watching', 'such', 'poor', 'acting', '.', 'hopefully', 'the', 'guy', 'who', 'played', 'heath', 'never', 'gets', 'work', 'again', '.', 'on', 'top', 'of', 'that', 'i', 'hope', 'the', 'director', 'never', 'gets', 'to', 'make', 'another', 'film', ',', 'and', 'has', 'his', 'pay', '##che', '##ck', 'taken', 'back', 'for', 'this', 'crap', '.', '{', '.', '02', 'out', 'of', '10', '}']
尽管已经处理了文本的词汇表,但是仍然需要为标签构建词汇表。
LABEL.build_vocab(train_data)
print(LABEL.vocab.stoi)
defaultdict(None, {'neg': 0, 'pos': 1})
和以前一样,创建迭代器。 理想情况下,希望使用最大的批量,因为发现这可以为transformers提供最佳的结果。
BATCH_SIZE = 128
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits( (train_data, valid_data, test_data), batch_size = BATCH_SIZE, device = device)
建立模型
接下来,将加载预训练的模型,并确保加载与令牌化程序相同的模型。
from transformers import BertTokenizer, BertModel
bert = BertModel.from_pretrained('bert-base-uncased')
I1106 14:57:06.877642 139759243081536 configuration_utils.py:151] loading configuration file https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-config.json from cache at /home/ben/.cache/torch/transformers/4dad0251492946e18ac39290fcfe91b89d370fee250efe9521476438fe8ca185.bf3b9ea126d8c0001ee8a1e8b92229871d06d36d8808208cc2449280da87785c
I1106 14:57:06.878792 139759243081536 configuration_utils.py:168] Model config {
"attention_probs_dropout_prob": 0.1,
"finetuning_task": null,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"initializer_range": 0.02,
"intermediate_size": 3072,
"layer_norm_eps": 1e-12,
"max_position_embeddings": 512,
"num_attention_heads": 12,
"num_hidden_layers": 12,
"num_labels": 2,
"output_attentions": false,
"output_hidden_states": false,
"output_past": true,
"pruned_heads": {},
"torchscript": false,
"type_vocab_size": 2,
"use_bfloat16": false,
"vocab_size": 30522
}
I1106 14:57:07.421291 139759243081536 modeling_utils.py:337] loading weights file https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-pytorch_model.bin from cache at /home/ben/.cache/torch/transformers/aa1ef1aede4482d0dbcd4d52baad8ae300e60902e88fcb0bebdec09afd232066.36ca03ab34a1a5d5fa7bc3d03d55c4fa650fed07220e2eeebc06ce58d0e9a157
接下来,将定义实际模型。
将使用预训练的transformer模型,而不是使用嵌入层来获取文本的嵌入。然后,将这些嵌入内容输入到GRU中,以生成对输入句子的情感的预测。通过其config属性从transformer获取嵌入尺寸大小(称为hidden_size)。其余的初始化是标准的。
在前向遍历中,将transformers包装在no_grad中,以确保在模型的这一部分上没有计算出任何梯度。transformer实际上返回整个序列的嵌入以及合并的输出。文档指出,合并的输出“通常不是输入语义内容的良好总结,通常最好对整个输入序列的隐藏状态序列进行平均或合并”,因此将不使用它。正向传递的其余部分是递归模型的标准实现,在该模型中,我在最后的时间步中获取隐藏状态,然后将其通过线性层以进行预测。
import torch.nn as nn class BERTGRUSentiment(nn.Module): def __init__(self, bert, hidden_dim, output_dim, n_layers, bidirectional, dropout): super().__init__() self.bert = bert embedding_dim = bert.config.to_dict()['hidden_size'] self.rnn = nn.GRU(embedding_dim, hidden_dim, num_layers = n_layers, bidirectional = bidirectional, batch_first = True, dropout = 0 if n_layers < 2 else dropout) self.out = nn.Linear(hidden_dim * 2 if bidirectional else hidden_dim, output_dim) self.dropout = nn.Dropout(dropout) def forward(self, text): #text = [batch size, sent len] with torch.no_grad(): embedded = self.bert(text)[0] #embedded = [batch size, sent len, emb dim] _, hidden = self.rnn(embedded) #hidden = [n layers * n directions, batch size, emb dim] if self.rnn.bidirectional: hidden = self.dropout(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1)) else: hidden = self.dropout(hidden[-1,:,:]) #hidden = [batch size, hid dim] output = self.out(hidden) #output = [batch size, out dim] return output
接下来,使用标准超参数创建模型的实例。
HIDDEN_DIM = 256OUTPUT_DIM = 1N_LAYERS = 2BIDIRECTIONAL = TrueDROPOUT = 0.25 model = BERTGRUSentiment(bert, HIDDEN_DIM, OUTPUT_DIM, N_LAYERS, BIDIRECTIONAL, DROPOUT)
可以检查模型有多少个参数。 标准模型有5M以下,但这个有112M!幸运的是,这些参数中的110M来自transformer,将不再对其进行训练。
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')
该模型具有112,241,409个可训练参数
为了冻结参数(不训练它们),需要将其require_grad属性设置为False。为此,只需要遍历模型中的所有named_parameters,如果它们是berttransformer模型的一部分,则可以将设置为require_grad = False
for name, param in model.named_parameters(): if name.startswith('bert'): param.requires_grad = False
现在可以看到模型具有3M可训练的参数,几乎可以与FastText模型相提并论。但是,文本仍必须通过transformer传播,这会使训练花费更长的时间。
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')
该模型具有2,759,169个可训练参数
可以仔细检查可训练参数的名称,以确保它们有意义。 它们都是GRU(rnn)和线性层(out)的所有参数。
for name, param in model.named_parameters(): if param.requires_grad: print(name)rnn.weight_ih_l0rnn.weight_hh_l0rnn.bias_ih_l0rnn.bias_hh_l0rnn.weight_ih_l0_reversernn.weight_hh_l0_reversernn.bias_ih_l0_reversernn.bias_hh_l0_reversernn.weight_ih_l1rnn.weight_hh_l1rnn.bias_ih_l1rnn.bias_hh_l1rnn.weight_ih_l1_reversernn.weight_hh_l1_reversernn.bias_ih_l1_reversernn.bias_hh_l1_reverseout.weightout.bias
训练模型
按照标准,定义优化器和标准(损失函数)。
import torch.optim as optim optimizer = optim.Adam(model.parameters())
criterion = nn.BCEWithLogitsLoss()
将模型和标准放置到GPU上(如果可用)
model = model.to(device)criterion = criterion.to(device)
接下来,将定义以下功能:计算准确性,执行训练时间,执行评估时间以及计算训练/评估时间需要多长时间。
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 == y).float() #convert into float for division acc = correct.sum() / len(correct) return acc
def train(model, iterator, optimizer, criterion): epoch_loss = 0 epoch_acc = 0 model.train() for batch in iterator: optimizer.zero_grad() predictions = model(batch.text).squeeze(1) loss = criterion(predictions, batch.label) acc = binary_accuracy(predictions, batch.label) loss.backward() optimizer.step() epoch_loss += loss.item() epoch_acc += acc.item() return epoch_loss / len(iterator), epoch_acc / len(iterator)
def evaluate(model, iterator, criterion): epoch_loss = 0 epoch_acc = 0 model.eval() with torch.no_grad(): for batch in iterator: predictions = model(batch.text).squeeze(1) loss = criterion(predictions, batch.label) acc = binary_accuracy(predictions, batch.label) epoch_loss += loss.item() epoch_acc += acc.item() return epoch_loss / len(iterator), epoch_acc / len(iterator)
import time def 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
最后,将训练模型。 由于transformer的尺寸,与以前的任何型号相比,所需时间要长得多。即使没有训练任何变压器参数,仍然需要通过模型传递数据,这在标准GPU上花费了大量时间。
N_EPOCHS = 5 best_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}%') print(f'\t Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc*100:.2f}%')
Epoch: 01 | Epoch Time: 7m 27s
Train Loss: 0.286 | Train Acc: 88.16%
Val. Loss: 0.247 | Val. Acc: 90.26%
Epoch: 02 | Epoch Time: 7m 27s
Train Loss: 0.234 | Train Acc: 90.77%
Val. Loss: 0.229 | Val. Acc: 91.00%
Epoch: 03 | Epoch Time: 7m 27s
Train Loss: 0.209 | Train Acc: 91.83%
Val. Loss: 0.225 | Val. Acc: 91.10%
Epoch: 04 | Epoch Time: 7m 27s
Train Loss: 0.182 | Train Acc: 92.97%
Val. Loss: 0.217 | Val. Acc: 91.98%
Epoch: 05 | Epoch Time: 7m 27s
Train Loss: 0.156 | Train Acc: 94.17%
Val. Loss: 0.230 | Val. Acc: 91.76%
将加载带来最大验证损失的参数,并在测试集上进行尝试-到目前为止,带来了最好的结果!
model.load_state_dict(torch.load('tut6-model.pt')) test_loss, test_acc = evaluate(model, test_iterator, criterion) print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc*100:.2f}%')
Test Loss: 0.198 | Test Acc: 92.31%
推理
然后,将使用该模型来测试某些序列的情绪。 对输入序列进行标记化,将其修剪到最大长度,在任一侧添加特殊标记,将其转换为张量,添加伪造的批次尺寸,然后将其传递给模型。
def predict_sentiment(model, tokenizer, sentence): model.eval() tokens = tokenizer.tokenize(sentence) tokens = tokens[:max_input_length-2] indexed = [init_token_idx] + tokenizer.convert_tokens_to_ids(tokens) + [eos_token_idx] tensor = torch.LongTensor(indexed).to(device) tensor = tensor.unsqueeze(0) prediction = torch.sigmoid(model(tensor)) return prediction.item()
predict_sentiment(model, tokenizer, "This film is terrible")
0.02264496125280857
predict_sentiment(model, tokenizer, "This film is great")
0.9411056041717529