在过去几年里,处理语言的机器学习模型法展迅速加快。这种进展已经从实验室毕业,开始助力一些领先的数字产品。一个很好的例子是 Google 的文章 recent announcement of how the BERT model is now a major force behind Google Search 。谷歌认为这一步(或者说应用于搜索的自然语言理解的进步)代表了 "过去五年中最大的飞跃,也是搜索历史上最大的飞跃之一"。
the biggest leap forward in the past five years, and one of the biggest leaps forward in the history of Search
这篇文章是一个简单的教程,介绍如何使用 BERT 的变体来对句子进行分类。这是一个足够基础的例子,作为第一个入门,但又足够深入,可以展示其中的一些关键概念。
在这篇文章的同时,我还准备了一个 Jupyter Notebook。你可以在这里看到Jupyter Notebook或在colab上运行。
SST2 数据集
在这个例子中,我们将使用的数据集是SST2,它包含了电影评论中的句子,每个句子都被标记为正面的(数值为1)或负面的(数值为0)。
模型:句子情感分类
我们的目标是创建一个模型,将一个句子(就像我们数据集中的句子一样),并产生1(表示该句子带有正面的情绪)或0(表示该句子带有负面的情绪)。我们可以把这个过程想象成这样。
- DistilBERT对句子进行处理,并将它从中提取的一些信息传递给下一个模型。DistilBERT是由 HuggingFace 团队开发并开源的一个较小版本的BERT。它是BERT的一个更轻更快的版本,其性能与BERT大致相当。
- 下一个模型是 scikit learn 的逻辑回归模型将接收 DistilBERT 的处理结果,并将句子分为正向或负向(分别为1或0)。
我们在两个模型之间传递的数据是一个大小为768的向量。我们可以把这个向量看作是句嵌入,用它来进行分类。
模型训练
虽然我们会使用两个模型,但只会训练逻辑回归模型。对于 DistillBERT,我们会使用一个预训练模型。不过这个模型没有对橘子分类作过训练或者微调。但是,我们从 BERT 训练的总体目标中,得到一些句子分类的能力。尤其是 BERT 的第一个位置的输出(与 [CLS] 这一 token 有关)。我相信这是由于 BERT 的第二个训练对象--下一句话分类。这个目标似乎是训练模型对第一个位置的输出进行句子意义上的封装。transformers 库为我们提供了 DistilBERT 的实现以及模型的预训练版本。
教程概述
本教程的计划是这样的:我们将首先使用训练好的 DistilBERT 来生成2000个句子的句嵌入。
在此之后,我们不会再用 DistilBERT。从这里开始都是 Scikit Learn。我们在这个数据集上做通常的训练/测试集拆分。
对 DistilBERT(模型#1)的输出进行训练/测试集拆分,可以创建在其上进行逻辑回归的训练和评估集(模型#2)。请注意,在现实中,在进行训练/测试集拆分之前 sklearn 会对例子进行随机排列,它不会只取数据集中出现的前75%的例子。
然后我们在训练集上训练逻辑回归模型。
单次预测如何计算
在深入代码并解释如何训练模型之前,让我们看看训练好的模型是如何计算其预测的。
我们尝试对 "a visually stunning rumination on love" 这个句子进行分类。第一步是使用 BERT 的 tokenizer 首先将单词分割成 tokens。然后,我们添加句子分类所需的特殊 token(分别是句首的 [CLS]和句尾的 [SEP])。
tokenizer 做的第三步是将每个 token 从 embedding 中替换成它的 id,这是我们通过训练模型得到的一个组件。阅读 The Illustrated Word2vec 了解词嵌入的背景。
请注意,tokenizer 只需一行代码就能完成所有这些步骤。
tokenizer.encode("a visually stunning rumination on love", add_special_tokens=True)
我们的输入可以传递给 DistilBERT 了。
如果你读过 Illustrated BERT ,这一步也可以这样可视化。
流经DistilBERT
通过DistilBERT传递输入向量可以像BERT一样。每个向量由768个浮点数组成。
因为这是一个句子分类任务,所以除了第一个向量(与[CLS] token 相关的那个)之外,我们忽略所有的向量。我们将这一个向量作为逻辑回归模型的输入。
从这里开始,逻辑回归模型的工作就是根据它从训练阶段学到的对这个向量进行分类。我们可以认为预测是这样的。
训练是我们下一节要讨论的内容。
代码
在本节中,我们将重点介绍训练这个句子分类模型的代码。包含所有这些代码的 Jupyter Notebook 可以在 colab 和 github 上找到。
让我们从导入开始吧:
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
数据集在 github 上以文件的形式存在,所以我们直接将其导入 pandas dataframe
中即可。
df = pd.read_csv('https://github.com/clairett/pytorch-sentiment-classification/raw/master/data/SST2/train.tsv', delimiter='/t', header=None)
我们可以使用 df.head()
来查看 dataframe 的前五行。
df.head()
导入预训练的 DistilBERT 模型和 tokenizer。
model_class, tokenizer_class, pretrained_weights = (ppb.DistilBERTModel, ppb.DistilBERTTokenizer, 'DistilBERT-base-uncased')
# 想要用 BERT 代替 DistilBERT?使用下面这行
# model_class, tokenizer_class, pretrained_weights = (ppb.BertModel, ppb.BertTokenizer, 'bert-base-uncased')
# 加载预训练模型/tokenizer
tokenizer = tokenizer_class.from_pretrained(pretrained_weights)
model = model_class.from_pretrained(pretrained_weights)
现在我们可以对数据集进行 token 切分。请注意,我们要做的事情与上面有些不同。上面的例子只标记并处理了一个句子。这里,我们将对所有句子进行切分,并将其作为一个批处理(考虑到资源问题,Notebook 将处理一组较小的例子,比如2000个句子)。
Tokenization
tokenized = df[0].apply((lambda x: tokenizer.encode(x, add_special_tokens=True)))
这就把每个句子变成了 id 的列表。
数据集目前是一个嵌套的列表(或 pandas Series/DataFrame )。在 DistilBERT 将其作为输入进行处理之前,我们需要通过将较短的句子用 token id 0 进行 padding,使所有的向量长度相同,你可以参考笔记本上的步骤,这是基本的 python 字符串和数组操作。
Padding 之后,我们就有一个矩阵/张量,可以传给BERT了。
用 DistilBERT 处理
现在我们从填充的 token 矩阵中创建一个输入张量,并将其发送给 DistilBERT
input_ids = torch.tensor(np.array(padded))
with torch.no_grad():
last_hidden_states = model(input_ids)
运行这一步后,last_hidden_states
持有 DistilBERT 的输出。它是一个 tuple,形状为(例子数、序列中的最大标记数、DistilBERT 模型中的隐藏单元数)。在我们的例子中,这将是2000(因为我们只限制了2000个例子),66(这是2000个例子中最长序列中的 tokens 数),768(DistilBERT 模型中的隐藏单元数)。
解开 BERT 的输出张量。
让我们来解开这个三维输出张量。我们可以先从它的尺寸开始。
重复一个句子的处理过程
每一行都与我们数据集中的一个句子相关联。回顾一下第一句话的处理步骤,我们可以认为它是这样的。
切开重要的部分
对于句子分类,我们只对 BERT 输出中的 [CLS] 这一 token 感兴趣,所以我们选择了这一切片,并丢弃其他所有的东西。
这就是我们如何对三维张量进行切分,得到我们感兴趣的二维张量。
# 切分所有序列的第一个位置的输出,取所有隐藏单位输出
features = last_hidden_states[0][:,0,:].numpy()
现在features
是一个2维的 numpy 数组,包含了我们数据集中所有句子的句嵌入。
逻辑回归的数据集
现在我们已经有了 BERT 的输出,我们已经组装好了训练逻辑回归模型所需要的数据集。768列都是特征,以及我们刚刚从初始数据集中得到的标签。
我们用来训练逻辑回归的标注数据集。特征是 BERT 的输出向量的切片[CLS] 这一 token(位置#0)。每一行对应于我们数据集中的一个句子,每一列对应 Bert/DistilBERT 模型顶部 trnsformer 的前馈神经网络的一个隐藏单元输出。
拆分完训练/测试集后,我们可以用逻辑回归模型对数据集进行训练。
labels = df[1]
train_features, test_features, train_labels, test_labels = train_test_split(features, labels)
它将数据集分割成训练/测试集。
接下来,我们在训练集上训练逻辑回归模型。
lr_clf = LogisticRegression()
lr_clf.fit(train_features, train_labels)
现在模型已经被训练好了,我们可以根据测试集对其进行评分。
lr_clf. score(test_features, test_labels)
分数基准
作为参考,目前这个数据集的最高准确率分数是96.8。DistilBERT 可以被训练来提高它在这个任务上的得分--这个过程叫做微调,它更新 BERT 的权重,使它在句子分类中取得更好的表现(我们可以称之为下游任务)。微调后的 DistilBERT 原来的准确率达到了90.7。全尺寸的 BERT 模型达到了94.9。
Notebook
直接进入 Jupyter Notebook 或在 colab 上运行。
就这样了! 这是与 BERT 的第一次接触。下一步就是到文档中去尝试一下微调。你也可以回过头来从 DistilBERT 切换到 BERT,看看效果如何。
感谢Clément Delangue、Victor Sanh 和 Huggingface 团队为本教程的早期版本提供的反馈。