BERT 在 NLP 方向中是一个十分具有里程碑的模型,那么如何通过 BERT 提取一个句子的句向量呢?
网络上的资料还是很多的,由于比赛要求只使用 Pytorch 框架,所以很多基于 TF 的教程和库就没办法用了。在查询了部分资料后,我终于总结出了一个提取的方法:
这一篇总结我会尽力写得通俗易懂,一读就明白。
算法解释
import torch
from pytorch_pretrained_bert import BertTokenizer, BertModel, BertForMaskedLM
导入 torch
和 pytorch_pretrained_bert
库,站在巨人的肩膀上,直接缩短大量的训练时间。
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased') # 导入预训练模型
tokenized_text = tokenizer.tokenize(text) # tokenize 输入的文本
indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text) # 向量化
这一部分的代码主要是将输入的文本转化为 tokenized 的文本,然后再把该文本给向量化。
问题来了,什么是 tokenized 和向量化?
tokenized
即标记化,把输入的句子进行标记划的操作,举个小例子:
tokenized_text = tokenizer.tokenize('Hello world !')
>>> ['hello', 'world', '!']
大家可能注意到了,为什么字母全部为小写了?这是因为我们使用的模型 bert_base_uncased
是不分大小写的,所以输出的 tokenized_text
会全由小写表示。
其实 BERT 还支持输入两个句子,但是需要放在同一个字符串中,并通过一个标记来表示一个句子的开始和结束,再举个小例子:
tokenized_text = tokenizer.tokenize('[CLS] Hello world! [SEP] I\'m Xiao Ming. [SEP]')
>>> ['[CLS]', 'hello', 'world', '!', '[SEP]', 'i', "'", 'm', 'xiao', 'ming', '.', '[SEP]']
其中的 [CLS]
是句子的开始符,[SEP]
是句子的结束符,同时也可以作为下一个句子的开始符,所以第二句的 [CLS]
就可以省去。
向量化
向量化便是把生成的 tokenized_text
变为矩阵向量表示,从而便于后续的网络运算,举个例子:
tokenized_text = tokenizer.tokenize('[CLS] Hello world world [SEP] I\'m Xiao Ming. [SEP]')
indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text)
>>> [101, 7592, 2088, 2088, 102, 1045, 1005, 1049, 19523, 11861, 1012, 102]
通过这个例子可以发现,两个 world 生成的向量是相同的,即每一个单词或者标记都有对应的词向量,储存在一个预训练好的表中;
让我们继续
OK ,在解释完原理后,继续一通操作:
segments_ids = [1] * len(tokenized_text)
tokens_tensor = torch.tensor([indexed_tokens])
segments_tensors = torch.tensor([segments_ids])
segments_ids
是用来标记分句的,用 0 和 1 分开一个 tokenized_text
中的两个句子——一个句子为 0,另一个句子为 1。因为我一次只输入一个句子,所以就全设为 1,长度与 tokenized_text
相同(不相同会报错,这个需要注意)。
而下面的 tokens_tensor
和 segments_tensors
是把生成出来的两个向量矩阵给张量化。
model = BertModel.from_pretrained('bert-base-uncased')
初始化预训练模型 bert-base-uncased
。
tokens_tensor = tokens_tensor.cuda()
segments_tensors = segments_tensors.cuda()
model.cuda()
将矩阵和模型全部放入 cuda
中,即用 GPU 运算,加快矩阵运算的速度。
encoded_layers, pooled_output= model(tokens_tensor, segments_tensors)
将 tokens_tensor
和 segments_tensors
放入模型前向传播,计算出最后的输出。其中 encoded_layers
为每一层产生的输出,pooled_output
为最后输出的句向量,而句向量为我们最终所需要的矩阵向量。
至此,提取句向量的工作已经完成了,接下来就需要输入下层网络王城后续工作了,代码放在了下面。
代码
import torch
from pytorch_pretrained_bert import BertTokenizer, BertModel, BertForMaskedLM
def encode(text):
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
tokenized_text = tokenizer.tokenize(text)
indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text)
print(indexed_tokens)
segments_ids = [1] * len(tokenized_text)
tokens_tensor = torch.tensor([indexed_tokens])
segments_tensors = torch.tensor([segments_ids])
model = BertModel.from_pretrained('bert-base-uncased')
tokens_tensor = tokens_tensor.cuda()
segments_tensors = segments_tensors.cuda()
model.cuda()
encoded_layers, pooled_output= model(tokens_tensor, segments_tensors)
return pooled_output
encode('[CLS] Hello world world [SEP] I\'m Xiao Ming. [SEP]')