参与王子嘉、Geek AI

BERT模型超酷炫,上手又太难?请查收这份BERT快速入门指南!

如果你是一名自然语言处理从业者,那你一定听说过最近大火的 BERT 模型。本文是一份使用简化版的 BERT 模型——DisTillBERT 完成句子情感分类任务的详细教程,是一份不可多得的 BERT 快速入门指南。


在过去的几年中,用于处理语言的机器学习模型取得了突飞猛进的进展。这些进展已经走出了实验室,开始为一些先进的数字产品赋能。最近成为 Google 搜索背后主要力量的 BERT 就是一个很好的例子。Google 认为这一进步(或者说在搜索中应用自然语言理解技术的进步)代表着「过去五年中最大的飞跃,也是搜索历史上最大的飞跃之一」。


本文是一篇简单的教程,介绍如何使用不同的 BERT 对句子进行分类。本文中的例子深入浅出,也足以展示 BERT 使用过程中所涉及的关键概念。

除了这篇博文,我还准备了一份对应的 notebook 代码,链接如下:

https://github.com/jalammar/jalammar.github.io/blob/master/notebooks/bert/A_Visual_Notebook_to_Using_BERT_for_the_First_Time.ipynb。

你也可以在 colab 中运行这份代码:

https://colab.research.google.com/github/jalammar/jalammar.github.io/blob/master/notebooks/bert/A_Visual_Notebook_to_Using_BERT_for_the_First_Time.ipynb


数据集:SST2


本文示例中使用的数据集为「SST2」,该数据集收集了一些影评中的句子,每个句子都有标注,好评被标注为 1,差评标注为 0。


sentencelabel
a stirring , funny and finally transporting re imagining of beauty and the beast and 1930s horror films1
apparently reassembled from the cutting room floor of any given daytime soap0
they presume their audience won't sit still for a sociology lesson0
this is a visually stunning rumination on love , memory , history and the war between art and commerce1
jonathan parker 's bartleby should have been the be all end all of the modern office anomie films1


模型:句子情感分类


我们的目标是创建一个分类器,它的输入是一句话(即类似于数据集中的句子),并输出一个 1(表示这句话体现出了积极的情感)或是 0(表示这句话体现出了消极的情感)。整体的框架如下图所示:

实际上,整个模型由两个子模型组成:
  • DistilBERT 先对句子进行处理,并将它提取到的信息传给下个模型。DistilBERT 是 BERT 的缩小版,它是由 HuggingFace 的一个团队开发并开源的。它是一种更轻量级并且运行速度更快的模型,同时基本达到了 BERT 原有的性能。

  • 另外一个模型,是 scikit learn 中的 Logistic 回归模型,它会接收 DistilBERT 处理后的结果,并将句子分类为积极或消极(0 或 1)。

我们在两个模型之间传递的数据是 768 维的向量。我们可以把这个向量看做能够用于分类任务的句子嵌入。


如果你读过我以前写的关于 Iluustrated BERT 的文章(https://jalammar.github.io/illustrated-bert/),这个向量其实就是以词 [CLS] 为输入的第一个位置上的结果。


模型训练


尽管整个模型包含了两个子模型,但是我们只需要训练 logistic 回归模型。至于 DistillBERT,我们会直接使用已经在英文上预训练好的模型。然而,这个模型不需要被训练,也不需要微调,就可以进行句子分类。BERT 训练时的一般目标让我们已经有一定的句子分类能力了,尤其是 BERT 对于第一个位置的输出(也就是与 [CLS] 对应的输出)。我觉得这主要得益于 BERT 的第二个训练目标——次句预测(Next sentence classification)。该目标似乎使得它第一个位置的输出封装了句子级的信息。Transformers 库给我们提供了 DistilBERT 的一种实现以及预训练好的模型。

教程概览


这篇教程的计划如下:我们首先用预训练好的 distilBERT 来生成 2,000 个句子的嵌入。

后面我们就不会再涉及 distilBERT 了。剩下的都是 Scikit Learn 的工作了,我们接下来要做的就是将数据集分成训练集和测试集。
将 distilBert(模型 #1)的输出划分为训练集和测试集后,就得到了我们训练和评估 logistic 回归(模型 #2)的数据集了。请注意,在现实中,sklearn 在将数据划分为训练集和测试集之前,会将样本打乱,而不是直接按照数据原来的顺序取前 75%。

接下来,我们要在训练集上训练逻辑 logistic 回归模型了:
每个预测值是怎么计算出来的?


在我们深入研究训练模型的代码前,让我们先看一下训练好的模型是如何计算出其预测值的。假设我们正在对句子「a visually stunning rumination on love」进行分类。第一步要做的就是用 BERT 分词器(tokenizer)将这些单词(word)划分成词(token)。然后,我们再加入句子分类所需的特殊词(在句子开始加入 [CLS],末端加入 [SEP])。
分词器要做的第三步就是查表,将这些词(token)替换为嵌入表中对应的编号,我们可以从预训练模型中得到这张嵌入表。如果对词嵌入不太了解,请参阅「The Illustrated Word2vec」:

https://jalammar.github.io/illustrated-word2vec/。


注意,只需要一行代码就可以完成分词器的所有工作:

tokenizer.encode("a visually stunning rumination on love", add_special_tokens=True)

现在,我们输入的句子是可以传递给 DistilBERT 的形式。

如果你读过「Illustrated BERT」(https://jalammar.github.io/illustrated-bert/),这一步也可以被可视化为下图:


DistilBERT 的数据流


将输入向量传递给 DistilBert 之后的工作方式就跟在 BERT 中一样,每个输入的词都会得到一个由 768 个浮点数组成的输出向量。
由于这是个句子分类任务,我们只关心第一个向量(与 [CLS] 对应的向量)。

该向量就是我们输入给 logistic 回归模型的向量。
从这一步开始,logistic 回归模型的任务就是:根据它在训练阶段学到的经验,对这个向量进行分类。我们可以把整个预测过程想象成这样:

我们将在下一节中讨论整个训练过程及其相关代码。


代码


在本节中,我们重点介绍训练这个句子分类模型的代码。读者可以从 colab 和 github 上获取完成本任务所需的所有 notebook 代码(链接见本文第二段)。

首先,我们需要导入相关的程序包。


import numpy as np
import pandas as pd
import torch
import transformers as ppb # pytorch transformers
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import train_test_split
数据集的链接如下:https://github.com/clairett/pytorch-sentiment-classification/。我们可以直接将其导入为一个 pandas 数据帧。


df = pd.read_csv('https://github.com/clairett/pytorch-sentiment-classification/raw/master/data/SST2/train.tsv', delimiter='\t', header=None)

我们可以通过「df.head()」查看前五行数据帧的内容:


df.head()

输出如下:


导入预训练好的 DistilBERT 模型与分词器


model_class, tokenizer_class, pretrained_weights = (ppb.DistilBertModel, ppb.DistilBertTokenizer, 'distilbert-base-uncased')

## Want BERT instead of distilBERT? Uncomment the following line:
#model_class, tokenizer_class, pretrained_weights = (ppb.BertModel, ppb.BertTokenizer, 'bert-base-uncased')# 

Load pretrained model/tokenizertokenizer = tokenizer_class.from_pretrained(pretrained_weights)
model = model_class.from_pretrained(pretrained_weights)

现在,我们可以对数据集进行分词操作了。请注意,我们这里要做的事情跟上文中的示例不太一样。上文中的例子只对一个句子进行了分词和处理。在这里,我们要将所有句子作为同一批进行分词和处理。在 notebook 中,考虑到算力的问题,我们只用了 2000 个句子。


分词


tokenized = df[0].apply((*lambda* x: tokenizer.encode(x, add_special_tokens=True)))

T

上面的代码将所有句子转化为了编号的列表。


现在,数据集变成了一个包含很多列表结构(或是 Pandas 的数组和数据帧)的列表。在 DistilBERT 可以将它们作为输入数据处理之前,我们需要把这些向量整理成相同的维度(在较短的句子后面填充上编号「0」)。你可以在 notebook 中看到「填充」操作对应的代码,它们就是 python 字符串和数组的简单操作。

完成「填充」操作后,我们就得到了 BERT 可以接收的矩阵/张量了。


DistilBERT 的处理


现在,我们根据填充后的词(token)矩阵创建一个输入张量,并将其传递给 DistilBERT


input_ids = torch.tensor(np.array(padded))

with torch.no_grad():

last_hidden_states = model(input_ids)

这一步完成后,会将 DistilBERT 的输出赋给「last_hidden_states」。这是一个维度为(句子数,序列中的最大词数,DistilBERT 模型中的隐藏层数)的元组。在本例中,这个维度就是(2000,66,768),因为我们有 2000 个句子,2000 个句子中最长的序列长度为 66,DistilBERT 模型中有 768 个隐藏层。


展开 BERT 的输出张量


接下来我们要把这个三维的输出张量展开。我们可以先分析一下它的维度:

回顾这些句子的「一生」


每一行代表数据集中的一个句子。第一个句子的处理过程如下图所示:

切片得到重要的部分


对于句子分类任务来说,我们只对 BERT 得到的 [CLS] 对应的输出感兴趣,所以我们只保留「立方体」中 [CLS] 对应的切片,删掉了其它内容。
这就是从三维张量中获取我们需要的二维张量的方法:

 # Slice the output for the first position for all the sequences, take all hidden unit outputs
 features = last_hidden_states[0][:,0,:].numpy()

现在,「features」是一个二维的 numpy 数组,它包含了我们数据集中所有句子的嵌入。

我们从 BERT 输出中切片得到的张量。

Logistic 回归中的数据集

现在我们已经得到了 BERT 的输出,并且把训练 logistic 回归模型的数据集组装好了。这 768 列是特征,我们还给出了原始数据集中的标签。

我们用于训练 logistic 回归的有标签数据集。这些特征就是 BERT 中对应 [CLS](位置 #0)的部分,也就是我们前一张图中切片得到的部分。每一行对应着数据集中的一个句子,每一列对应着 BERT/DistilBERT 模型中顶层 transformer 模块中前馈神经网络的隐藏单元。

在完成了机器学习传统的训练集/测试集划分操作后,我们就可以创建我们自己的 logistic 回归模型,并用我们的数据集进行训练了。
labels = df[1]
train_features, test_features, train_labels, test_labels = train_test_split(features, labels)

上面的代码把数据集分成了训练集和测试集:
接下来,我们要在训练集上训练 logistic 回归模型。
lr_clf = LogisticRegression()
lr_clf.fit(train_features, train_labels)

现在模型已经训练好了,我们可以用测试集对其进行评估。

lr_clf.score(test_features, test_labels)

上面一段代码的输出告诉我们这个模型获得了 81% 的准确率。

评分标准

作为参照,在这个数据集上所能达到的最高的准确率是 96.8。在这个任务中,我们同样可以训练 DistilBERT 来提高其得分,也就是通常所说的调优过程,该过程可以更新 BERT 的权重参数,并让其在句子分类任务中(通常被称为「下游任务」)获得更好的性能。经过调优后的 DistilBERT 可以达到 90.7 的准确率,而完整的 BERT 可以达到 94.9。

Notebook 代码

本文开头给出了相关 notebook 代码的链接,自己去探索一下吧!

这就是本文的全部内容了,这些内容对于第一次接触 BERT 的人来说应该是非常适用的。而下一步就是查看文档并试着进行调优了。你也可以回过头来把代码中的 DistilBERT 转换成 BERT,看看它又是如何工作的。
工程BERT
42
相关数据
逻辑技术

人工智能领域用逻辑来理解智能推理问题;它可以提供用于分析编程语言的技术,也可用作分析、表征知识或编程的工具。目前人们常用的逻辑分支有命题逻辑(Propositional Logic )以及一阶逻辑(FOL)等谓词逻辑。

前馈神经网络技术

前馈神经网络(FNN)是人工智能领域中最早发明的简单人工神经网络类型。在它内部,参数从输入层经过隐含层向输出层单向传播。与递归神经网络不同,在它内部不会构成有向环。FNN由一个输入层、一个(浅层网络)或多个(深层网络,因此叫作深度学习)隐藏层,和一个输出层构成。每个层(除输出层以外)与下一层连接。这种连接是 FNN 架构的关键,具有两个主要特征:加权平均值和激活函数。

word2vec技术

Word2vec,为一群用来产生词向量的相关模型。这些模型为浅而双层的神经网络,用来训练以重新建构语言学之词文本。网络以词表现,并且需猜测相邻位置的输入词,在word2vec中词袋模型假设下,词的顺序是不重要的。 训练完成之后,word2vec模型可用来映射每个词到一个向量,可用来表示词对词之间的关系。该向量为神经网络之隐藏层。 Word2vec依赖skip-grams或连续词袋(CBOW)来建立神经词嵌入。Word2vec为托马斯·米科洛夫(Tomas Mikolov)在Google带领的研究团队创造。该算法渐渐被其他人所分析和解释。

情感分类技术

情感分类是对带有感情色彩的主观性文本进行分析、推理的过程,即分析对说话人的态度,倾向正面,还是反面。

推荐文章
有些不明白的地方 1.为什么要扔掉那么多数据来变成2维? 2.三维的时候(2000,66,768),这里的2000代表2000个句子,66代表最多66个词,那么这个768是不是代表每个词的embedding? 3.如果真的是2中的那样,那是不是你只利用了句子的首单词来预测? 总之,我的疑问在于2,希望指点一下
第一个问题, logstics 回归对单个的word进行分类, 所以会将句子与词划分开,所以数据会从(2000,66,768)变为(2000,786), 此处的数据可以理解为该批次中(size=2000),第一个词(size=786)的在模型中的分类情况 第二个问题, 是的, 786代表每个词的embedding. 第三个问题, 显然我们不能只利用句首进行预测, 因为在任务中我们需要预测整句话的p/n, 所以我任务在logstic 回归后应该还有评分将句子中其他的词也进行预测最后求出总成绩