文本挖掘从小白到精通(十六)--- 像使用scikit-learn一样玩转BERT

2020 年 8 月 12 日 AINLP

特别推荐|【文本挖掘系列教程】:


上一篇文章谈到如何简便的使用bert,好用是好用,但延展性、灵活性不足,主要是很难加入各种自定义特性(比如pipeline、和数值型特征混合使用等)。基于此,本篇文章就来谈谈,如何通过继承Sci-kit Learn中的两个基类 --- TransformerMixinBaseEstimator来实现一个高度定制化且易用的BERT特征提取器。




在NLP中获得最先进的结果曾经是一项艰巨的任务。为此,我们要不厌其烦的尝试各种文本特征抽取方法:TF-IDF、n-gram、word2vec、将其链接到外部知识库、词干化、归一化乃至分词。。。自从有了BERT,文本特征抽取的手工活就少多了!但是,总觉得少了些啥~
本教程的目的是用BERT和Sci-kit Learn建立一个最简单的句子级分类的例子。笔者不打算详细讨论BERT是什么,也不打算讨论它的内部机理,笔者只是想以最小的工作量向你展示如何利用Sci-kit Learn来“二次开发”bert特征抽取器,兼顾易用性和灵活性。
做好这个bert特征抽取器以后,笔者将用一个文本情绪分类数据集来检验一下经封装的Sci-kit Learn transformer的实际效果。
如无问题,我们可以在任何现有的Sci-kit Learn的pipeline上只需插入一行代码就可以玩转bert。

1 为什么是BERT?

机器学习的一个重要部分,就是给特定任务找到好的特征表示(Feature Representation)。对于NLP领域的特征抽取来说,特征抽取特值文本表示(Text Representation)。简而言之,文本表示就是不将文本视为字符串,而视为在数学上处理起来更为方便的向量。
如何将字符串变为包含语义关联性的向量,就是文本表示的核心问题。
如果找到了任务相关的良好文本表示,NLP任务算是向前推进了一大半。困难的是,要为一个语言任务创建好的特征是很难的,至少传统上是很难的。
BERT是一个在海量文本上训练的深度转化模型。海量的预训练
结合模型架构和一些巧妙的训练技巧,使BERT能够学习到NLP任务中"比较好"的特征。在这里,我们将利用所有这些优秀的工作,使用基于PyTorch的transformers(https://github.com/huggingface/transformers )来创建一个可复用的特征提取器。
同时,我们可以将封装好的bert特征提取器嵌入到任何Sci-kit Learn流程中。更多关于BERT工作原理的信息,可以阅读Jay Alamar关于BERT原理(http://jalammar.github.io/illustrated-bert ) 和如何使用BERT(http://jalammar.github.io/a-visual-guide-to-using-bert-for-the-first-time/ )的2篇优秀博客文章。

2 创建一个基于Sci-kit Learn的BERT特征提取器

对于笔者的大多数项目而言,尝试着从简单开始,看看能用Sci-kit Learn能走多远。笔者特别喜欢使用Sci-kit Learn中的流水线(pipeline )API,因为任何给定的模型都是由transformer和estimator组成的,使用fit、predict就能轻松进行训练和预测。
每当像BERT这样的新方法出现时,我就会为它建立一个transformer和estimator。然后我就可以很容易地将其纳入到我现有的任何管道中,而不需要太多工作。
因此,让我们为BERT创建一个基于Sci-kit Learn的berttransformer,我们可以将其与任何estimator相连接。这个transformer应该将一个字符串的列表映射到与该字符串相关联的 BERT 特征。所以我们的类型签名应该是List[str]→torch.FloatTensor 。
一般的,使用hugging face的transformers库会有以下三个主要步骤:
  • 将字符串分解成整型编码的token 
  • 在编码后的token上运行BERT,得到词汇和句子的BERT表示
  • 将提取到的词汇特征或者语句特征用于分类/序列模型训练

下面,正式进入代码环节:

先载入必要的库:

import torchimport pandas as pdimport numpy as npfrom sklearn.pipeline import Pipelinefrom transformers import BertTokenizer,BertModelfrom sklearn.linear_model import LogisticRegression,SGDClassifierfrom sklearn import preprocessingfrom sklearn import metricsfrom sklearn import svm
2.1 Tokenization
Tokeniza tion只需要两行代码。 我们定义了我们想要的 tokenizer,然后运行 encode_plus 方法,它可以让我们设置各类参数,比如 maximum size(语句最大长度)和special characters(是否包含特殊字符)等。
tokenizer = BertTokenizer.from_pretrained(r"E:\2020.04.05_pytorch_protrained_models\bert-base-uncased")tokenized_dict = tokenizer.encode_plus(    "hi my name is nicolas",    add_special_tokens=True,    max_length=5    )

检视Tokenization的结果:

tokenized_dict['input_ids']
[101, 7632, 2026, 2171, 102]


你会发现,我们将最大序列长度设置为5,在我提供的输入中只有5个字,但它有两个被截断的token(truncated tokens)。这是因为我们将add_special_tokens设置为True。对于BERT模型来说,这意味着添加一个[CLS]"类 "token和一个[SEP]"分隔符 "token。这两个token对maximum size值为5会有影响,所以我们最终会丢掉两个词汇,这是需要着重注意的地方。在返回的结果字典中,我们只需要 input_ids 字段,这个字段持有我们将传递给 BERT 模型的令牌化单词的整型编码。CLS token表征整个语句嵌入,separator toekn用来告诉BERT下一个新的句子将出现。对于我们的基本句子分类任务,我们将使用CLS嵌入作为特征集合(the set of features)。

2.2 Model

下一步是使用 BERT 模型生成语句嵌入。同样, transformers 库为我们完成了大部分工作。我们可以创建一个简单的BERT模型,然后在我们的tokenized 输出上运行预测。
bert_model = BertModel.from_pretrained(r"E:\2020.04.05_pytorch_protrained_models\bert-base-uncased")tokenized_text = torch.tensor(tokenized_dict["input_ids"])with torch.no_grad():    embeddings = bert_model(torch.tensor(tokenized_text.unsqueeze(0)))

检视嵌入shape。

embeddings[0][:, 0, :].shape
torch.Size([1, 768])
请注意,BERT模型需要接受一个张量,其形式为 [batch_size , sentence_length],这意味着我们需要unsqueeze 一维矩阵。
另外注意这里我们如何使用torch.no_grad()。第一次处理大批量的样本时,如果忘了这个操作,会导致电脑的内存被占用完,电脑由此也会变得奇卡无比~所以在运行预测之前记得关闭梯度,否则你会保存太多的梯度信息,运行速度会变得极度糟糕!
embeddings返回的tuple默认有两个字段,第一个是矩阵,形如:
批量大小×句子长度×嵌入维度
对于基本的BERT模型和我们的例子,最终是[1, 5, 768]。第一个张量持有我们感兴趣的分类所需的嵌入。第二个张量是集合输出。池输出是在训练下一个句子时,经过线性层和Tanh激活函数后的[CLS]嵌入。在本文中,可以对第二个忽略不计。

2.3 抽取embeddings

为了完成我们的 BERT 特征提取器,我们最后需要的是将最终的嵌入物组合成一个单一的向量,用于分类。
对于大多数分类任务,你只需要抓取[CLS] token 对应的嵌入就可以做得很好。我们可以用下面这个函数来操作:
 
   
get_cls = lambda x: x[0][:, 0, :]

这将会取出[CLS] token对应的嵌入( embeddings )和池化输出(pooled outputs)的Tuple,抓取嵌入的所有批次,只是第一个CLS token,以及所有的嵌入神经元。但是,也许你想更“花哨”地使用其他功能。比方说你想用所有的内嵌神经元来做预测,我们可以用不同的函数把它们串联起来:

 
   
flatten_embed = lambda x: torch.flatten(x[0])

这将返回一个大向量,由序列中的每一个token的嵌入组成。通过定义在最终层上操作的函数,我们可以更灵活地使用下游分类的特征。这在Sci-kit Learn transformer的环境中会更有意义。

2.4 建立一个基于Sci-kit Learn的Bert文本特征抽取器 --- BertTransformer

我们只需要上述三个基本组件就可以得到一个语句嵌入。为了方便地与Sci-kit Learn中的方法衔接上,我们希望在一个大的句子列表上进行操作。我们可以通过建立一个Sci-kit Learn畛域来实现这个目的(我们要做一个Sci-kit Learn transformer transformer!)。这样我们只需将一个文本列表传递给它,调用transform函数,我们的分类器就可以开始学习了~
因此,创建一个新类,叫做BertTransformer,它继承自BaseEstimator和TransformerMixin,然后把我们上面工作过的代码作为tokenization步骤和prediction步骤放进去。

 
   
from typing import Callable, List, Optional, Tuple
import pandas as pdfrom sklearn.base import TransformerMixin, BaseEstimatorimport torch

class BertTransformer(BaseEstimator, TransformerMixin): def __init__( self, bert_tokenizer, bert_model, max_length: int = 60, device ='cpu', #可选用gpu,此时将cpu改为cuda embedding_func: Optional[Callable[[torch.tensor], torch.tensor]] = None, ): self.tokenizer = bert_tokenizer self.device = device self.model = bert_model.to(self.device) self.model.eval() self.max_length = max_length self.embedding_func = embedding_func
if self.embedding_func is None: self.embedding_func = lambda x: x[0][:, 0, :]
def _tokenize(self, text: str) -> Tuple[torch.tensor, torch.tensor]: # 使用预设的tokenizer对输入文本进行Tokenize tokenized_text = self.tokenizer.encode_plus(text, add_special_tokens=True, max_length=self.max_length )["input_ids"]
# 创建attention mask,“告知”bert使用所有的词汇 attention_mask = [1] * len(tokenized_text)
# bert需要批量读取数据,所以我们需要unsqueeze每一行数据 return ( torch.tensor(tokenized_text).unsqueeze(0).to( self.device), torch.tensor(attention_mask).unsqueeze(0).to( self.device), )
def _tokenize_and_predict(self, text: str) -> torch.tensor: tokenized, attention_mask = self._tokenize(text)
embeddings = self.model(tokenized, attention_mask) return self.embedding_func(embeddings)
def transform(self, text: List[str]): if isinstance(text, pd.Series): text = text.tolist()
with torch.no_grad(): #释放内存 return torch.stack([self._tokenize_and_predict(string) for string in text])
def fit(self, X, y=None): """不需要拟合数据,返回自身即可""" return self
这个transformer 使用了我们之前在第30-33行写的所有tokenization代码,以及第45-48行的prediction和extraction代码。
我们唯一要做的其他事情就是把它全部链接到一个transform方法中,这个transform方法使用一个单一的列表解析来进行 tokenize,然后把所有的句子都嵌入到一个列表中,这在第51-52行发生。
现在我们可以用BERT的所有功能和经典的Sci-kit Learn模型的所有简单性来制作一个超级简单的“分类器流水线”!

3 在情绪分类数据集上测试效果

对于测试数据,这里将使用Figure-Eight Sentiment Analysis:Emotion in Text数据集。这个数据集有40K条推文,分为13种不同的情绪状态。
笔者将数据加载到pandas数据框中,并将数据随机分成70%的训练集、15%的验证集和15%的测试集。
3.1 载入数据和数据预处理
figure8_df = pd.read_csv("text_emotion.csv")figure8_df.head(15)

检视所有的类别。

np.unique(figure8_df["sentiment"].tolist())
array(['anger', 'boredom', 'empty', 'enthusiasm', 'fun', 'happiness',
'hate', 'love', 'neutral', 'relief', 'sadness', 'surprise',

'worry'], dtype='<U10')

将标签映射为整形数值,便于分类器读入。

le = preprocessing.LabelEncoder() le.fit(np.unique(figure8_df["sentiment"].tolist()))

LabelEncoder()

对整个数据集的标签进行离散化处理。

figure8_df["sentiment"] =figure8_df["sentiment"].apply(lambda x:le.transform([x])[0])

figure8_df.head(15)

split = np.random.choice(    ["train", "val", "test"],    size=figure8_df.shape[0],    p=[0.30, 0.50, 0.20])

print('标签值标准化:%s' % le.transform(["empty", "enthusiasm", "neutral","sadness",'worry']))print('标准化标签值反转:%s' % le.inverse_transform([0, 2 ,0 ,1 ,2]))
标签值标准化:[ 2  3  8 10 12]
标准化标签值反转:['anger' 'empty' 'anger' 'boredom' 'empty']
figure8_df["split"] = splitx_train = figure8_df[figure8_df["split"] == "train"]y_train = x_train["sentiment"]x_test = figure8_df[figure8_df["split"] == "test"]y_test = x_test["sentiment"]x_val = figure8_df[figure8_df["split"] == "val"]y_val = x_val["sentiment"]

检视训练集和测试集的shape。

x_train.shape,y_train.shape,x_test.shape,y_test.shape,x_val.shape,y_val.shape
((12084, 5), (12084,), (7973, 5), (7973,), (19943, 5), (19943,))


3.2 训练模型

训练模型很简单。我们只需要定义一个pipeline,用一个transformer和一个estimator就可以了。

x_train["content"][:5]
7                  Hmmm. http://www.djhero.com/ is down
9 @kelcouch I'm sorry at least it's Friday?
11 Choked on her retainers
13 @BrodyJenner if u watch the hills in london u ...
14 Got the news
Name: content, dtype: object
y_train[:5]
7     12
9 10
11 12
13 10
14 11

Name: sentiment, dtype: int64


bert_transformer = BertTransformer(tokenizer, bert_model)
classifier = SGDClassifier()
model = Pipeline( [ ("vectorizer", bert_transformer), ("classifier", classifier), ])model.fit(x_train["content"].tolist(), y_train.tolist())

3.3 检视模型预测效果

predictions = model.predict(x_test["content"].tolist())print("Confusion Matrix:")print(metrics.confusion_matrix(y_test,predictions))


在运行上述模型后,我们的验证集得到了相当好的结果。

验证集中,有一些类的分类几乎是完美的。这个模型只使用了BERT变换器的CLS嵌入和一个SVM,它在所有主要标签上都得到了几乎完美的预测!

这些结果出乎意料的好,所以我看了一下困惑矩阵,似乎“热情(enthusiasm)”和“快乐 (fun)”都被归类为“幸福(happiness)”,这一点我百分百没问题。看起来真正有问题的孩子是“空虚(empty)”和“超脱(relief )”,但如果我说实话,我甚至不知道这些情绪是什么🤷♂,所以我要把这个标记为成功~


但如果我们也想要一些经典的TF-IDF特征呢?

这也很简单!我们只需要做一个特征联合(feature union),然后传递给分类器。

from sklearn.feature_extraction.text import (   CountVectorizer, TfidfTransformer)
tf_idf = Pipeline([ ("vect", CountVectorizer()), ("tfidf", TfidfTransformer()) ])
model = Pipeline([ ("union", FeatureUnion(transformer_list=[ ("bert", bert_transformer), ("tf_idf", tf_idf) ])), ("classifier", classifier), ])
model.fit(x_train["content"], y_train)
笔者喜欢使用pipeline,主要是它们的使用是如此的灵活~
笔者可以创建这些可塑性很强的组件,并且可以很容易地组合在一起。
现在,我们可以用一行代码就能把BERT的功能添加到任何Sci-kit Learn模型中。

Sci-kit Learn transformers的使用超级方便。现在我们可以轻松地将基于BERT的功能插入任何Sci-kit Learn流程中。只需定义我们的BERT模型,并将其作为一个“特征化(faturization)”步骤添加到pipeline中即可。Sci-kit Learn会处理剩下的事情。试着把这些特征整合到你的旧模型中,看看是否能提高性能。

结语

Sci-kit Learn transformers的使用超级方便。现在我们可以轻松地将基于BERT的功能插入任何Sci-kit Learn模型中。只需定义我们的BERT模型,并将其作为一个“特征化(faturization)”步骤添加到pipeline中即可,Sci-kit Learn会处理剩下的事情。

 



欢迎加入AINLP技术交流群
进群请添加AINLP小助手微信 AINLPer(id: ainlper),备注NLP技术交流

推荐阅读

这个NLP工具,玩得根本停不下来

征稿启示| 200元稿费+5000DBC(价值20个小时GPU算力)

完结撒花!李宏毅老师深度学习与人类语言处理课程视频及课件(附下载)

从数据到模型,你可能需要1篇详实的pytorch踩坑指南

如何让Bert在finetune小数据集时更“稳”一点

模型压缩实践系列之——bert-of-theseus,一个非常亲民的bert压缩方法

文本自动摘要任务的“不完全”心得总结番外篇——submodular函数优化

Node2Vec 论文+代码笔记

模型压缩实践收尾篇——模型蒸馏以及其他一些技巧实践小结

中文命名实体识别工具(NER)哪家强?

学自然语言处理,其实更应该学好英语

斯坦福大学NLP组Python深度学习自然语言处理工具Stanza试用

关于AINLP

AINLP 是一个有趣有AI的自然语言处理社区,专注于 AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括文本摘要、智能问答、聊天机器人、机器翻译、自动生成、知识图谱、预训练模型、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLPer(id:ainlper),备注工作/研究方向+加群目的。


阅读至此了,分享、点赞、在看三选一吧🙏

登录查看更多
0

相关内容

BERT全称Bidirectional Encoder Representations from Transformers,是预训练语言表示的方法,可以在大型文本语料库(如维基百科)上训练通用的“语言理解”模型,然后将该模型用于下游NLP任务,比如机器翻译、问答。
【干货书】Python数据科学入门,464页pdf
专知会员服务
73+阅读 · 2020年9月20日
【干货书】Python语音计算导论,408页pdf
专知会员服务
103+阅读 · 2020年7月12日
【实用书】学习用Python编写代码进行数据分析,103页pdf
专知会员服务
198+阅读 · 2020年6月29日
Python导论,476页pdf,现代Python计算
专知会员服务
263+阅读 · 2020年5月17日
专知会员服务
118+阅读 · 2019年12月24日
【机器学习课程】Google机器学习速成课程
专知会员服务
168+阅读 · 2019年12月2日
【书籍】深度学习框架:PyTorch入门与实践(附代码)
专知会员服务
167+阅读 · 2019年10月28日
听说你还没读过 Bert 源码?
AINLP
7+阅读 · 2019年8月7日
教你用Python进行自然语言处理(附代码)
数据派THU
6+阅读 · 2018年3月28日
机器学习实战:Python信用卡欺诈检测
引力空间站
6+阅读 · 2017年9月6日
搭建LSTM(深度学习模型)做文本情感分类的代码
数据挖掘入门与实战
4+阅读 · 2017年9月3日
可怕,40 行代码的人脸识别实践
51CTO博客
3+阅读 · 2017年7月22日
如何用Python做舆情时间序列可视化?
CocoaChina
11+阅读 · 2017年7月21日
A Modern Introduction to Online Learning
Arxiv
21+阅读 · 2019年12月31日
How to Fine-Tune BERT for Text Classification?
Arxiv
13+阅读 · 2019年5月14日
Dynamic Transfer Learning for Named Entity Recognition
Arxiv
3+阅读 · 2018年12月13日
Angular-Based Word Meta-Embedding Learning
Arxiv
3+阅读 · 2018年8月13日
Arxiv
15+阅读 · 2018年2月4日
VIP会员
相关VIP内容
【干货书】Python数据科学入门,464页pdf
专知会员服务
73+阅读 · 2020年9月20日
【干货书】Python语音计算导论,408页pdf
专知会员服务
103+阅读 · 2020年7月12日
【实用书】学习用Python编写代码进行数据分析,103页pdf
专知会员服务
198+阅读 · 2020年6月29日
Python导论,476页pdf,现代Python计算
专知会员服务
263+阅读 · 2020年5月17日
专知会员服务
118+阅读 · 2019年12月24日
【机器学习课程】Google机器学习速成课程
专知会员服务
168+阅读 · 2019年12月2日
【书籍】深度学习框架:PyTorch入门与实践(附代码)
专知会员服务
167+阅读 · 2019年10月28日
相关资讯
听说你还没读过 Bert 源码?
AINLP
7+阅读 · 2019年8月7日
教你用Python进行自然语言处理(附代码)
数据派THU
6+阅读 · 2018年3月28日
机器学习实战:Python信用卡欺诈检测
引力空间站
6+阅读 · 2017年9月6日
搭建LSTM(深度学习模型)做文本情感分类的代码
数据挖掘入门与实战
4+阅读 · 2017年9月3日
可怕,40 行代码的人脸识别实践
51CTO博客
3+阅读 · 2017年7月22日
如何用Python做舆情时间序列可视化?
CocoaChina
11+阅读 · 2017年7月21日
相关论文
Top
微信扫码咨询专知VIP会员