特别推荐|【文本挖掘系列教程】:
上一篇文章谈到如何简便的使用bert,好用是好用,但延展性、灵活性不足,主要是很难加入各种自定义特性(比如pipeline、和数值型特征混合使用等)。基于此,本篇文章就来谈谈,如何通过继承Sci-kit Learn中的两个基类 --- TransformerMixin和BaseEstimator来实现一个高度定制化且易用的BERT特征提取器。
下面,正式进入代码环节:
先载入必要的库:
import torch
import pandas as pd
import numpy as np
from sklearn.pipeline import Pipeline
from transformers import BertTokenizer,BertModel
from sklearn.linear_model import LogisticRegression,SGDClassifier
from sklearn import preprocessing
from sklearn import metrics
from sklearn import svm
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]
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])
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的环境中会更有意义。
我们只需要上述三个基本组件就可以得到一个语句嵌入。为了方便地与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 pd
from sklearn.base import TransformerMixin, BaseEstimator
import 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
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"] = split
x_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,))
训练模型很简单。我们只需要定义一个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 11Name: 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技术交流
推荐阅读
征稿启示| 200元稿费+5000DBC(价值20个小时GPU算力)
完结撒花!李宏毅老师深度学习与人类语言处理课程视频及课件(附下载)
模型压缩实践系列之——bert-of-theseus,一个非常亲民的bert压缩方法
文本自动摘要任务的“不完全”心得总结番外篇——submodular函数优化
斯坦福大学NLP组Python深度学习自然语言处理工具Stanza试用
关于AINLP
AINLP 是一个有趣有AI的自然语言处理社区,专注于 AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括文本摘要、智能问答、聊天机器人、机器翻译、自动生成、知识图谱、预训练模型、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLPer(id:ainlper),备注工作/研究方向+加群目的。
阅读至此了,分享、点赞、在看三选一吧🙏