可视化教程开启BERT之旅

翻译自Jay Alammar 的文章:A Visual Guide to Using BERT for the First Time

近几年,处理语言的机器学习模型的发展迅速。并且驱动了一些数码产品的落地。

本篇文章是一个用 BERT的扩展版本来做句子分类的简单教程。同时提供了一个 notebok. 可以在 colab 或者 notebook 中去实践。

数据集:SST2

本次使用的数据集是 SST2,是一个电影评论的数据集。用标签 0/1 代表情感正负:

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 能力的同时,比 BERT 更小更快。
  • 第二个模型是一个基本的 Logistic Regression 模型,它将处理 DistilBERT 的输出结果并且将句子进行分类,输出0或1。

在这两个模型之间传递的数据是一个 768 维的向量。我们可以把这个向量看作是我们用来分类的句子的embedding向量。如果你阅读过之前的文章: Illustrated BERT ,这个向量实际上就是句向量([CLS]位置的向量)

模型的训练

虽然我们使用了两个模型,但是只需要训练我们的回归模型(Logistic Regression)即可。对于 DistilBERT 模型,我们使用该模型预训练的参数即可,这个模型没有被用来做句子分类任务的训练和微调。但是,从BERT的训练任务中,我们还是得到了一些句子分类能力,特别是使用 BERT 的第一个输出([CLS] token相关的输出)。我觉得可能是和 BERT 的第二个训练任务——NSP(Next Sentence Prediction)有关。 transformers 库提供了一个 DistilBERT 的实现和预训练模型。

教程概述

首先使用预训练的 distilBERT 模型为2000个句子生成句向量。

之后我们就不会再使用 distilBERT 模型了。剩下的就是使用 Scikit Learn 工具包进行操作。将整个数据集分成 train/test 数据集:

将 模型1 的输出分成 train/test 数据集,用于模型2:Logistic regression 的输入。注意,在实际使用时,sklearn 的 train/test 集应该在分割前进行 shuffles。而不是仅仅将数据集的前75作为训练集。

然后即可在 logistic regression 模型上使用训练集进行训练了:

如何计算单个预测

在我们讲解模型训练代码之前,先看看如何使用模型进行预测的。
比如,我们要对句子 “a visually stunning rumination on love” 进行分类,第一步就是用 BERT 的分词器(tokenizer)将句子分成 tokens;第二步,添加特殊的 tokens 用于句子分类任务(在句子开头加上 [CLS],在句子结尾加上 [SEP])。

第三步,分词器(tokenizer)会将每个 token 替换成 embedding 表中的ID,embedding 表是我们预训练模型自带的。想了解词嵌入的同学可以看这篇文章: The Illustrated Word2vec

分词器一行代码即可完成上述的步骤:

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

现在,我们输入的句子即可正确的传入 DistilBERT 模型中了。

如果你读过  Illustrated BERT ,这一步也可以用这种方式可视化:

DistilBERT 中的流程

输入的句向量流经 DistilBERT 的过程和 BERT 一样。输出也是每个token 用一个768维的向量表示。

由于这是一个句子分类任务,我们只取第一个向量(与 [CLS] token有关的向量)而忽略其他的 token 向量。我们将该向量作为 logistic regression模型的输入。

至此,剩下的就算 logistic regression模型基于该向量去训练的工作了。我们可以想象一个预测的流程:

训练过程就是我们接下来要讲的,参考代码进行讲解。

代码

在本节中,我们将重点介绍训练这个句子分类模型的代码。
所有的代码都可以在 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

导入 数据

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

导入预训练模型(译者注:如果在线下载模型慢的,可以去 HugginFace 官网下载,当然也可以找我要):

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

## 如果想使用 BERT,取消此行注释即可:
#model_class, tokenizer_class, pretrained_weights = (ppb.BertModel, ppb.BertTokenizer, 'bert-base-uncased')

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

现在我们可以将数据集的句子使用 tokenizer进行 token 化。注意,我们接下来要做的和上面讲的有一点点不同。上面的例子中仅用 tokenizer 处理一个句子。这里我们将会一个批次一个批次的处理。

Token化:

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

这行代码会将每个句子处理成 一个 ids list:

目前,我们的数据集还是一个list (或者是 pandas Series/DataFrame 结构)。在 DistilBERT 能处理之前,我们需要把所有的向量用 id 0 来填充较短的句子到一个相同的长度。你可以参考 notebook 中代码的 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 的输出。这是一个元组结构,shape 为:(batch_size, 句子最大token数,DistilBERT隐藏单元)。在我们的例子中:(2000, 66, 768)

BERT 的输出张量

将模型1的输出展开,首先看一下维度:

一个句子的完整旅程

整个句子的完整处理过程如下图所示:

重点部分

对于句子分类任务,我们只关心 BERT 模型的 [CLS] token 对于的输出,因此我们选择下图中立方体的切片部分,丢弃其他的部分:

下面就是切割三维张量来得到我们感兴趣的二维张量:

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

现在 features 就是一个包含了我们数据集中所有句子的句向量的2维 numpy 数组。

从 BERT 的输出切片后的张量

Logistic Regression 的数据集

现在我们有了 BERT 的输出,并且把数据组装成了训练 logistic regression 模型的格式。下面 768 列是特征,标签是我们的原始数据:

用于训练 logistic regression 模型的数据。features 是我们从 BERT 的 [CLS] token取到的句向量进行切片之后的特征向量。每一行对应我们数据集中的一个句子。每一列对应这 BERT/DistilBERT 模型的隐藏层的最后一层输出。

划分完数据集之后,我们就可以声明 Logistic Regression 模型进行训练。

labels = df[1]
train_features, test_features, train_labels, test_labels = train_test_split(features, labels)

上述代码即将数据集完成了划分:

下面,开始训练 Logistic Regression 模型:

lr_clf = LogisticRegression()
lr_clf.fit(train_features, train_labels)

训练完成之后可以在测试集上进行评估:

lr_clf.score(test_features, test_labels)

我这里得到了 81 的准确率。

得分基准

该数据集上最高的准确率是 96.8. 通过fine-tuning 更新 BERT 的参数权重, DistilBERT 模型在句子分类任务(称为下游任务)上可以提升我们得到的分数。通过对 DistilBERT 进行 fine-tuned 之后达到了 90.7 的准确率。 全量的 BERT 模型能达到 94.9 的分数。

Notebook

现在你可以深入 notebook 或者在 colab 中运行该项目。

发表评论

电子邮件地址不会被公开。 必填项已用*标注