翻译自Jay Alammar 的文章:A Visual Guide to Using BERT for the First Time。
近几年,处理语言的机器学习模型的发展迅速。并且驱动了一些数码产品的落地。
本篇文章是一个用 BERT的扩展版本来做句子分类的简单教程。同时提供了一个 notebok. 可以在 colab 或者 notebook 中去实践。
数据集:SST2
本次使用的数据集是 SST2,是一个电影评论的数据集。用标签 0/1 代表情感正负:
sentence | label |
---|---|
a stirring , funny and finally transporting re imagining of beauty and the beast and 1930s horror films | 1 |
apparently reassembled from the cutting room floor of any given daytime soap | 0 |
they presume their audience won’t sit still for a sociology lesson | 0 |
this is a visually stunning rumination on love , memory , history and the war between art and commerce | 1 |
jonathan parker ‘s bartleby should have been the be all end all of the modern office anomie films | 1 |
模型: 句子情感分类
我们的目标是创建一个模型,输入 一个句子(就像数据集中的那些句子一样)并 输出 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
分词器一行代码即可完成上述的步骤:
代码语言:javascript复制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 上找到。 首先导入工具包:
代码语言:javascript复制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
导入 数据:
代码语言:javascript复制df = pd.read_csv('https://github.com/clairett/pytorch-sentiment-classification/raw/master/data/SST2/train.tsv', delimiter='t', header=None)
df.head()
导入预训练模型(译者注:如果在线下载模型慢的,可以去 HugginFace 官网下载,当然也可以找我要):
代码语言:javascript复制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化:
代码语言:javascript复制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
代码语言:javascript复制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 对于的输出,因此我们选择下图中立方体的切片部分,丢弃其他的部分:
下面就是切割三维张量来得到我们感兴趣的二维张量:
代码语言:javascript复制# 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 模型进行训练。
代码语言:javascript复制labels = df[1]
train_features, test_features, train_labels, test_labels = train_test_split(features, labels)
上述代码即将数据集完成了划分:
下面,开始训练 Logistic Regression 模型:
代码语言:javascript复制lr_clf = LogisticRegression()
lr_clf.fit(train_features, train_labels)
训练完成之后可以在测试集上进行评估:
代码语言:javascript复制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 中运行该项目。