作者:Chris McCormick
编译:ronghuaiyang
在本文中,我将深入研究谷歌的BERT生成的word embeddings,并向你展示如何通过BERT生成自己的word embeddings。
在本文中,我将深入研究谷歌的BERT生成的word embeddings,并向你展示如何通过BERT生成自己的word embeddings。
历史
2018年是NLP的突破之年。迁移学习,特别是像ELMO,Open-GPT,BERT之类的模型,允许研究人员针对特定的任务小小的微调一下(使用少量的数据和少量的计算),就可以得到一个很好的结果。不幸的是,对于许多刚开始学习NLP的人,甚至对于一些有经验的实践者,这些强大模型的理论和实际应用仍然没有得到很好的理解。
BERT是什么?
BERT(Bidirectional Encoder Representations from Transformers)于2018年末发布,是我们将在本教程中使用的模型,为读者更好地理解和指导在NLP中使用迁移学习模型提供了实用的指导。BERT是一种预训练语言表示的方法,用于创建NLP从业人员可以免费下载和使用的模型。你可以使用这些模型从文本数据中提取高质量的语言特征,也可以使用你自己的数据对这些模型进行微调,以完成特定的任务(分类、实体识别、问题回答等),从而生成最先进的预测。
为什么要使用BERT的嵌入?
在本教程中,我们将使用BERT从文本数据中提取特征,即单词和句子的嵌入向量。我们可以用这些词和句子的嵌入向量做什么?首先,这些嵌入对于关键字/搜索扩展、语义搜索和信息检索非常有用。例如,如果你希望将客户的问题或搜索与已经回答的问题或文档化的搜索相匹配,这些表示将帮助准确的检索匹配客户意图和上下文含义的结果,即使没有关键字或短语重叠。
其次,或许更重要的是,这些向量被用作下游模型的高质量特征输入。NLP模型(如LSTMs或CNNs)需要以数字向量的形式输入,这通常意味着需要将词汇表和部分语音等特征转换为数字表示。在过去,单词被表示为惟一索引值(one-hot编码),或者更有用的是作为神经单词嵌入,其中词汇与固定长度的特征嵌入进行匹配,这些特征嵌入是由Word2Vec或Fasttext等模型产生的。与Word2Vec之类的模型相比,BERT提供了一个优势,因为尽管Word2Vec下的每个单词都有一个固定的表示,而与单词出现的上下文无关,BERT生成的单词表示是由单词周围的单词动态通知的。例如,给定两句话:
“The man was accused of robbing a bank.” “The man went fishing by the bank of the river.”
Word2Vec将在两个句子中为单词“bank”生成相同的单词嵌入,而在BERT中为“bank”生成不同的单词嵌入。除了捕获一词多义之类的明显差异外,上下文相关的单词embeddings还捕获其他形式的信息,这些信息可以产生更精确的特征表示,从而提高模型性能。
从教育的角度看,仔细查看BERT的词嵌入的是一个深入学习BERT及其迁移学习模型的很好的方法,我们设置了一些实用知识和上下文,以便在后面的内容中更好地理解模型的内部细节。
安装和导入
使用Hugging Face的github仓库来安装pytorch接口。(这个库包含其他预训练语言模型的接口,比如OpenAI的GPT和GPT-2)我们之所以选择pytorch接口,是因为它在高级api(易于使用,但不能深入了解工作原理)和tensorflow代码(其中包含了很多细节,但通常会让我们忽略关于tensorflow的内容,此处的目的是BERT!)之间取得了很好的平衡。
!pip install pytorch-pretrained-bert
现在我们导入pytorch、预训练的BERT模型和BERT Tokenizer。我们将在后面的教程中详细解释BERT模型,但这是谷歌发布的预训练模型,它在Wikipedia和Book Corpus上运行了很多很多小时,Book Corpus是一个包含 10,000本不同类型书籍的数据集。这个模型(稍加修改)在一系列任务中击败了NLP基准测试。谷歌发布了一些BERT模型的变体,但是我们在这里使用的是两个可用尺寸(“base”和“large”)中较小的一个。
import torchfrom pytorch_pretrained_bert import BertTokenizer, BertModel, BertForMaskedLM# OPTIONAL: if you want to have more information on what's happening, activate the logger as followsimport logging#logging.basicConfig(level=logging.INFO)import matplotlib.pyplot as plt% matplotlib inline# Load pre-trained model tokenizer (vocabulary)tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')100%|██████████| 231508/231508 [00:00<00:00, 2386266.84B/s]
输入格式
因为BERT是一个预训练的模型,它期望以特定的格式输入数据,所以我们需要:
句子的开始([CLS])和分隔/结尾([SEP])的特别标记符合BERT中使用的固定词汇表的标记BERT‘s tokenizer中的token id掩码id,以指示序列中的哪些元素是令牌,哪些是填充元素段id用于区分不同的句子用于显示令牌在序列中的位置嵌入幸运的是,这个接口为我们处理了这些输入规范中的一些,因此我们只需要手动创建其中的一些(我们将在另一个教程中重新讨论其他输入)。
特殊的标记
BERT可以接受一到两句话作为输入,并希望每句话的开头和结尾都有特殊的标记:
2个句子的输入:
[CLS] the man went to the store [SEP] he bought a gallon of milk [SEP]
1个句子的输入:
[CLS] the man went to the store [SEP]
text = "Here is the sentence I want embeddings for."text = "After stealing money from the bank vault, the bank robber was seen fishing on the Mississippi river bank."marked_text = "[CLS] " text " [SEP]"print (marked_text)[CLS] After stealing money from the bank vault, the bank robber was seen fishing on the Mississippi river bank. [SEP]
我们导入了一个BERT-specific tokenizer,让我们看看输出:
Token初始化
tokenized_text = tokenizer.tokenize(marked_text)print (tokenized_text)['[CLS]', 'after', 'stealing', 'money', 'from', 'the', 'bank', 'vault', ',', 'the', 'bank', 'robber', 'was', 'seen', 'fishing', 'on', 'the', 'mississippi', 'river', 'bank', '.', '[SEP]']
注意“embeddings”一词是如何表示的:
[‘em’, ‘##bed’, ‘##ding’, ‘##s’]
原来的单词被分成更小的子单词和字符。这些子单词前面的两个#号只是我们的tokenizer用来表示这个子单词或字符是一个更大单词的一部分,并在其前面加上另一个子单词的方法。因此,例如,' ##bed ' token与' bed 'token是分开的,当一个较大的单词中出现子单词bed时,使用第一种方法,当一个独立的token “thing you sleep on”出现时,使用第二种方法。
为什么会这样?这是因为BERT tokenizer 是用Wordpiece模型创建的。这个模型使用贪心法创建了一个固定大小的词汇表,其中包含单个字符、子单词和最适合我们的语言数据的单词。由于我们的BERT tokenizer模型的词汇量限制大小为30,000,因此,用Wordpiece模型生成一个包含所有英语字符的词汇表,再加上该模型所训练的英语语料库中发现的~30,000个最常见的单词和子单词。这个词汇表包含个东西:
整个单词出现在单词前面或单独出现的子单词(“em”(如embeddings中的“em”)与“go get em”中的独立字符序列“em”分配相同的向量)不在单词前面的子单词,在前面加上“##”来表示这种情况单个字符要在此模型下对单词进行记号化,tokenizer首先检查整个单词是否在词汇表中。如果没有,则尝试将单词分解为词汇表中包含的尽可能大的子单词,最后将单词分解为单个字符。注意,由于这个原因,我们总是可以将一个单词表示为至少是它的单个字符的集合。
因此,不是将词汇表中的单词分配给诸如“OOV”或“UNK”之类的全集令牌,而是将词汇表中没有的单词分解为子单词和字符令牌,然后我们可以为它们生成嵌入。
因此,我们没有将“embeddings”和词汇表之外的每个单词分配给一个重载的未知词汇表标记,而是将其拆分为子单词标记[' em '、' ##bed '、' ##ding '、' ##s '],这些标记将保留原单词的一些上下文含义。我们甚至可以平均这些子单词的嵌入向量来为原始单词生成一个近似的向量。
下面是词汇表中包含的一些令牌示例。以两个#号开头的标记是子单词或单个字符。
list(tokenizer.vocab.keys())[5000:5020]['knight', 'lap', 'survey', 'ma', '##ow', 'noise', 'billy', '##ium', 'shooting', 'guide', 'bedroom', 'priest', 'resistance', 'motor', 'homes', 'sounded', 'giant', '##mer', '150', 'scenes']
我们需要调用tokenizer来匹配tokens在tokenizer词汇表中的索引:
indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text)for tup in zip(tokenized_text, indexed_tokens): print (tup)('[CLS]', 101)('after', 2044)('stealing', 11065)('money', 2769)('from', 2013)('the', 1996)('bank', 2924)('vault', 11632)(',', 1010)('the', 1996)('bank', 2924)('robber', 27307)('was', 2001)('seen', 2464)('fishing', 5645)('on', 2006)('the', 1996)('mississippi', 5900)('river', 2314)('bank', 2924)('.', 1012)('[SEP]', 102)
Segment ID
BERT接受了句子对的训练,并期望使用1和0来区分这两个句子。也就是说,对于“tokenized_text”中的每个标记,我们必须指定它属于哪个句子:句子0(一系列0)或句子1(一系列1)。对于我们的目的,单句输入只需要一系列的1,所以我们将为输入语句中的每个标记创建一个1向量。
如果你想处理两个句子,请将第一个句子中的每个单词加上“[SEP]”token赋值为0,第二个句子中的所有token赋值为1。
segments_ids = [1] * len(tokenized_text)print (segments_ids)[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
运行一下我们的例子
我们需要将数据转换为torch张量并调用BERT模型。BERT PyTorch接口要求数据使用torch张量而不是Python列表,所以我们在这里转换列表——这不会改变形状或数据。
eval()将我们的模型置于评估模式,而不是训练模式。在这种情况下,评估模式关闭了训练中使用的dropout正则化。
调用 from_pretrained 将从网上获取模型。当我们加载 bert-base-uncased时,我们会在日志中看到打印的模型定义。该模型是一个12层的深度神经网络!
# Convert inputs to PyTorch tensorstokens_tensor = torch.tensor([indexed_tokens])segments_tensors = torch.tensor([segments_ids])# Load pre-trained model (weights)model = BertModel.from_pretrained('bert-base-uncased')# Put the model in "evaluation" mode, meaning feed-forward operation.model.eval()
让我们获取网络的隐藏状态。
torch.no_grad禁用梯度计算,节省内存,并加快计算速度(我们不需要梯度或反向传播,因为我们只是运行向前传播)。
# Predict hidden states features for each layerwith torch.no_grad(): encoded_layers, _ = model(tokens_tensor, segments_tensors)
输出
这个模型的全部隐藏状态存储在对象“encoded_layers”中,有点令人眼花缭乱。这个对象有四个维度,顺序如下:
层数(12层)batch号(1句)单词/令牌号(在我们的句子中有22个令牌)隐藏单元/特征号(768个特征)这是202,752个值来唯一表示我们的一句话!
第二维度,是批处理大小,用于同时向模型提交多个句子,这里,我们只有一个句子。
print ("Number of layers:", len(encoded_layers))layer_i = 0print ("Number of batches:", len(encoded_layers[layer_i]))batch_i = 0print ("Number of tokens:", len(encoded_layers[layer_i][batch_i]))token_i = 0print ("Number of hidden units:", len(encoded_layers[layer_i][batch_i][token_i]))Number of layers: 12Number of batches: 1Number of tokens: 22Number of hidden units: 768
让我们快速查看一下给定层和token的值范围。
你将发现,所有层和token的范围都非常相似,大多数值位于[- 2,2]之间,少量值位于-10左右。
import torchfrom pytorch_pretrained_bert import BertTokenizer, BertModel, BertForMaskedLM# OPTIONAL: if you want to have more information on what's happening, activate the logger as followsimport logging#logging.basicConfig(level=logging.INFO)import matplotlib.pyplot as plt% matplotlib inline# Load pre-trained model tokenizer (vocabulary)tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')100%|██████████| 231508/231508 [00:00<00:00, 2386266.84B/s]04
正加财富网内容推荐 | ||
OK交易所下载 | USDT钱包下载 | 比特币平台下载 |
新手交易教程 | 平台提币指南 | 挖矿方法讲解 |