0. BERT简介
Bert 全称为 Bidirectional Encoder Representations from Transformers(Bert)。和 ELMo 不同,BERT 通过在所有层联合调节左右两个上下文来预训练深层双向表示,此外还通过组装长句作为输入增强了对长程语义的理解。Bert 可以被微调以广泛用于各类任务,仅需额外添加一个输出层,无需进行针对任务的模型结构调整,就在文本分类,语义理解等一些任务上取得了 state-of-the-art 的成绩。
Bert 的论文中对预训练好的 Bert 模型设计了两种应用于具体领域任务的用法,一种是 fine-tune(微调) 方法,一种是 feature extract(特征抽取) 方法。
- fine tune(微调)方法指的是加载预训练好的 Bert 模型,其实就是一堆网络权重的值,把具体领域任务的数据集喂给该模型,在网络上继续反向传播训练,不断调整原有模型的权重,获得一个适用于新的特定任务的模型。这很好理解,就相当于利用 Bert 模型帮我们初始化了一个网络的初始权重,是一种常见的迁移学习手段。
- feature extract(特征抽取)方法指的是调用预训练好的 Bert 模型,对新任务的句子做句子编码,将任意长度的句子编码成定长的向量。编码后,作为你自己设计的某种模型(例如 LSTM、SVM 等都由你自己定)的输入,等于说将 Bert 作为一个句子特征编码器,这种方法没有反向传播过程发生,至于如果后续把定长句子向量输入到 LSTM 种继续反向传播训练,那就不关 Bert 的事了。这也是一种常见的语言模型用法,同类的类似 ELMo。
我们首先来看下如何用特征抽取方法进行文本分类。
1. 背景
本博客将会记录使用transformer BERT模型进行文本分类过程,该模型以句子为输入(影评),输出为1(句子带有积极情感)或者0(句子带有消极情感);模型大致结构如下图所示,这里就用的是上述所说的feature extract特征抽取方法,使用BERT的生成的句子向量。
2. 加载数据集与预训练模型
首先引入需要使用的lib以及数据集,这里使用的是SST影评数据集
代码语言:txt复制import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import cross_val_score
import torch
import transformers as ppb
import warnings
warnings.filterwarnings('ignore')
df = pd.read_csv('https://github.com/clairett/pytorch-sentiment-classification/raw/master/data/SST2/train.tsv', delimiter='t', header=None)
接下来使用transformer加载预训练模型
代码语言:txt复制# For DistilBERT:
model_class, tokenizer_class, pretrained_weights = (ppb.DistilBertModel, ppb.DistilBertTokenizer, 'distilbert-base-uncased')
## Want BERT instead of distilBERT? Uncomment the following line:
#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)
3. 模型输入
在深入代码理解如何训练模型之前,我们先来看看一个训练好的模型是如何计算出预测结果的。
先来尝试对句子a visually stunning rumination on love进行分类。
如上图所示,句子输入至模型之前会进行tokenize
第一步,使用BERT 分词器将英文单词转化为标准词(token),如果是中文将进行分词;
第二步,加上句子分类所需的特殊标准词(special token,如在首位的CLS和句子结尾的SEP);
第三步,分词器会用嵌入表中的id替换每一个标准词(嵌入表是从训练好的模型中得到)
tokenize完成之后,将会把tokenize数组转换为二维数组,每次将一批数据输入至BERT模型,可以处理更快。
代码语言:javascript复制tokenized = batch_1[0].apply((lambda x: tokenizer.encode(x, add_special_tokens=True)))
max_len = 0
for i in tokenized.values:
if len(i) > max_len:
max_len = len(i)
padded = np.array([i [0]*(max_len-len(i)) for i in tokenized.values])
attention_mask = np.where(padded != 0, 1, 0)
因为上述生成的padded模型无法识别出来那些是有词语,哪些是无词语(空)。所以这里会生成一个attention_mask ,1表示是有词语,0表示无词语。
4. 使用BERT预训练模型
现在,我们需要从填充好的标记词矩阵中获得一个张量,作为DistilBERT的输入。
代码语言:javascript复制input_ids = torch.tensor(padded)
attention_mask = torch.tensor(attention_mask)
with torch.no_grad():
last_hidden_states = model(input_ids, attention_mask=attention_mask)
运行此步骤后,last_hidden_states保存DistilBERT的输出。它是一个具有多维度的元组:
对于句子分类问题,我们仅对[CLS]标记的BERT输出感兴趣,因此我们只选择该三维数据集的一个切片作为后续分类模型的特征输入。代码与解释如下图所示
代码语言:javascript复制features = last_hidden_states[0][:,0,:].numpy()
labels = batch_1[1]
5. 分类模型训练
后续将划分训练集与测试集,并使用LR模型进行分类
代码语言:javascript复制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。
6. 附录 尝试fine tune
fine tune 的使用是具有一定限制的。预训练模型的模型结构是为预训练任务设计的,所以显然的,如果我们要在预训练模型的基础上进行再次的反向传播,那么我们做的具体领域任务对网络的设计要求必然得和预训练任务是一致的。那么 Bert 预训练过程究竟在做什么任务呢?Bert 一共设计了两个任务。
任务一:屏蔽语言模型(Masked LM)
该任务类似于高中生做的英语完形填空,将语料中句子的部分单词进行遮盖,使用 [MASK]
作为屏蔽符号,然后预测被遮盖词是什么。例如对于原始句子 my dog is hairy
,屏蔽后 my dog is [MASK]
。该任务中,隐层最后一层的 [MASK]
标记对应的向量会被喂给一个对应词汇表的 softmax 层,进行单词分类预测。当然具体实现还有很多问题,比如 [MASK]
会在训练集的上下文里出现,而测试集里永远没有,参见论文,此处不做详细介绍。
任务二:相邻句子判断(Next Sentence Prediction)
语料中的句子都是有序邻接的,我们使用 [SEP]
作为句子的分隔符号,[CLS]
作为句子的分类符号,现在对语料中的部分句子进行打乱并拼接,形成如下这样的训练样本:
Input = [CLS] the man went to [MASK] store [SEP] he bought a gallon [MASK] milk [SEP]
Label = IsNext
Input = [CLS] the man [MASK] to the store [SEP] penguin [MASK] are flight ##less birds [SEP]
Label = NotNext
输入网络后,针对隐层最后一层 [CLS]
符号的词嵌入做 softmax 二分类,做一个预测两个句子是否是相邻的二分类任务。
可以看出,这两种任务都在训练过程中学习输入标记符号的 embedding,再基于最后一层的 embedding 仅添加一个输出层即可完成任务。该过程还可能引入一些特殊词汇符号,通过学习特殊符号譬如 [CLS]
的 embedding 来完成具体任务。
所以,如果我们要来 fine-tune 做任务,也是这个思路:首先对原有模型做适当构造,一般仅需加一输出层完成任务。
图 a 和 b 是序列级别的任务,c 和 d 是词级别的任务。a 做句子对分类任务,b 做单句分类任务,构造非常简单,将图中红色箭头指的 [CLS]
对应的隐层输出接一个 softmax 输出层。c 做的是阅读理解问题,d 做的是命名实体识别(NER),模型构造也类似,取图中箭头指出的部分词对应的隐层输出分别接一个分类输出层完成任务。
类似以上这些任务的设计,可以将预训练模型 fine-tuning 到各类任务上,但也不是总是适用的,有些 NLP 任务并不适合被 Transformer encoder 架构表示,而是需要适合特定任务的模型架构。因此基于特征的方法就有用武之地了。
如果使用HuggingFace进行FineTune也很方便,代码如下
代码语言:javascript复制from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained("bert-base-cased", num_labels=2)
from transformers import TrainingArguments
training_args = TrainingArguments("test_trainer")
from transformers import Trainer
trainer = Trainer(
model=model, args=training_args, train_dataset=small_train_dataset, eval_dataset=small_eval_dataset
)
trainer.train()
Ref
- https://colab.research.google.com/github/jalammar/jalammar.github.io/blob/master/notebooks/bert/A_Visual_Notebook_to_Using_BERT_for_the_First_Time.ipynb
- https://nlp.stanford.edu/sentiment/index.html SST数据集
- https://cloud.tencent.com/developer/article/1555590
- https://work.padeoe.com/notes/bert.html
- https://huggingface.co/transformers/training.html huggingface BERT fine tune