使用 BERT 实现情感分析
在 CV 问题中,目前已经有了很多成熟的模型供大家使用,只需要结合特定
的业务场景修改结尾的全连接层或添加 softmax 层即可满足需求,也就是我们常
说的迁移学习。那么在 NLP 领域是否有这样泛化能力很强的模型呢,答案是肯定
的。
2018 年 底 , Google 推 出 了 一 个 打 破 11 项 NLP 任 务 的 模 型 BERT
(Bidirectional Encoder Representation from Transformers),该模型一经
问世就火遍 AI 领域并受到了广大开发者的青睐,可以说是 NLP 领域中具有里程
碑意义的模型,目前 BERT 依旧是比赛中或者工业界首选的模型,各大公司也均
基于 BERT 进行了更多的升级与优化。
BERT 是一个预训练模型,该模型的训练阶段分为两个部分,预训练与微调,
预训练阶段 Google 已经处理好,如果要使用该模型,只需要针对特定场景进行
微调即可。在本章节中,我们会先介绍 BERT 的原理,再以一个实际的例子来讲
解如何微调。
1. Transformer 模型
在 NLP 任务中,常用的特种提取器有 RNN 及其变体、CNN 搭配池化层、
Transformer 等,RNN 类型的提取器有一个最大的优点能捕捉长依赖信息,但是
其速度很慢,CNN 搭配池化层能有效获取一些重要的特征并忽略没有意义的特征,
但是却无法捕捉长依赖信息,Transformer 兼具了 RNN 与 CNN 的优点,在保留长
依赖信息的同时速度也很快,其中的 attention 机制也使其具有了类似最大池化
层捕捉重要特征的能力。BERT 的特征提取器实际上就是采用的 Transformer 的
encoder 层,Google 提供了两个版本的 BERT,其中 base 版本的是由 12 层的
Transformer 的 encoder 堆叠在一起,large 版本的是由 24 层的 Transformer
的 encoder 堆叠在一起。
Transformer 模型是基于 encoder-decoder 结构的,如图 10.1-1 所示。其中
encoder 层由 6 层图中所示的结构堆叠而成,每一个层又包括了两个子层,第一
个子层是 multi-head self-attention,第二个子层是一个全连接层,这两个子
层 均 采 用 了 residual connection 来 进 行 连 接 , 并 且 还 有 一 层 layer
normalization 层。decoder 和 encoder 的结构类似,也是由 6 层图中所示的结
构堆叠而成,除了 encoder 提到的两层结构外,decoder 层还有一层额外的 masked
multi-head attention。
attention 是一种加权机制,针对候选值进行加权求和。Attention 能表述
为一个 query 与一个 key-value 集合的映射关系,其中 query,keys,values
都是向量,输出值是 values 的加权求和,这个权重是根据 query 与 key 来计算
出来的。
图 10.1-1 Transformer 结构
图 10.1-2 Scaled Dot-Product Attention
在 Transformer 中也运用了 attention 机制,并称为 Scaled Dot-Product
Attention,如图 10.1-2 所示。其输入值是由维度为的 queries、keys,维度
为的 values 构成,首先对 query 与 keys 做一个点积的运算再除以 ,然后
使用 softmax 计算出其权重并运用在 values 上。通常,计算 attention 值的时
候会采用以集合的形式来降低运算次数,多个 queries、keys 与 values 组合在
一起构成矩阵 Q、K、V,最终计算 attention 值的时候即可以如下公式来表示。
attentionQ,K,V =softmax()V
attention 的计算方法其实有很多种,最常用的有 additive attention 与
dot-product attention,Transformer 中提到的 Scaled Dot-Product Attention
其实就是 dot-product attention 的一个变体,添加了一个缩放因子 ,如果
的值很小,那么这两种 attention 操作得到的结果差不多,但是当的值很大
的时候,那么点积之后的结果某些值也会很大,softmax 之后就很容易出现一个
值全盘通吃的情况,而缩放因子就是为了解决这个问题而提出来的。
对于普通的 Attention 机制只能得到一个维度的加权求和值,transformer
采用了一种叫做 Multi-Head Attention 的方式,把 Q,K,V 做多种简单的线性变
换再用 Scaled Dot-Product Attention 计算 attention 的值,最终的结果则是
由多个 attention 的值拼接在一起并进行一个维度变换如图 10.1-3 所示。
图 10.1-3 Multi-Head Attention
Multi-head attention 让模型能从多个不同的角度去获取文本的信息,其计
算过程也可以用以下公式来表示:
MutilHeadQ,K,V =Concat(ℎ1…ℎℎ)
ℎ=Attention(Q,K,V)
其中,,对应的是 Q,K,V 的三个线性变换矩阵,的作用是用来对
拼接后的结果进行降维,head 的个数选择的是 8。
2. BERT 预训练
BERT 的 base 版本是由 12 个 transformer 的 encoder 层堆叠在一起,没有用
到 decoder 层,因此 transformer 的 decoder 这里就不再赘述,感兴趣的读者可
自己阅读 transformer 的论文。接下来我们就一起来看下 BERT 的训练阶段是怎
么做的。
BERT 的预训练阶段采用了两个独有的非监督任务,一个是 Masked Language
Model,还有一个是 Next Sentence Prediction。
Masked Language Model 可以理解为完形填空,随机 mask 掉训练预料中 15%
的词,用其上下文来做预测,例如:
my dog is hairy → my dog is [MASK]
此处将 hairy 进行了 mask 处理,再采用非监督学习的方法预测 mask 位置的
词是什么,但是该方法有一个问题,因为是 mask 掉 15%的词,其数量已经很高
了,这样就会导致某些词在微调阶段从未见过,为了解决这个问题,作者做了如
下的处理。
80%的时间是采用[mask],my dog is hairy → my dog is [MASK]
10%的时间是随机取一个词来代替 mask 的词,my dog is hairy -> my dog
is apple
10%的时间保持不变,my dog is hairy -> my dog is hairy
那么为啥要以一定的概率使用随机词呢?这是因为 transformer 要保持对每
个输入 token 分布式的表征,否则 Transformer 很可能会记住这个[MASK]就是
"hairy"。至于使用随机词带来的负面影响,Google 认为所有其他的 token(即非
"hairy"的 token)共享 15%*10% = 1.5%的概率,其影响是可以忽略不计的。
BERT 另一个预训练阶段的任务是 Next Sentence Prediction,选一些句子
对 A 与 B,其中 50%的数据 B 是 A 的下一条句子,剩余 50%的数据 B 是语料库中
随机选择的,学习其中的相关性,添加这样的预训练的目的是目前很多 NLP 的任
务比如 QA 和 NLI 都需要理解两个句子之间的关系,从而能让预训练的模型更好
的适应这样的任务。
3. 使用 BERT 进行文本分类
接下来我们以一个情感分析的例子来介绍如何在 keras 中使用对 bert 进行
微调。情感分析的应用场景很多,也是 nlp 领域一个最常见的任务,本文将会以
IMDB 数据集为例,该数据集是一个开源的电影评价数据集,我们的任务是根据
文本信息判断评价是正面情绪还是负面情绪。
在 keras 中,如果需要使用 BERT,只需要安装一个第三方库 keras-bert,
执行以下命令:pip install keras-bert
接下来需要下载 Google 官方的 BERT 预训练模型,下载好后解压到同级目录,
下载地址请参考以下页面:https://github.com/google-research/bert
数据集的下载地址为:http://ai.stanford.edu/~amaas/data/sentiment/
接下来就是编码部分,首先定义一些需要用到的数据,包括 BERT 的位置,
序列的最大长度,batch_size 的值。
1
2
3
4
5
config_path = 'uncased_L-12_H-768_A-12/bert_config.json'
checkpoint_path = 'uncased_L-12_H-768_A-12/bert_model.ckpt'
vocab_path = 'uncased_L-12_H-768_A-12/vocab.txt'
max_len = 32
batch_size = 64
def remove_html(text):
定义使用正则表达式去除一些异常的 HTML 标签的函数
1
2
3
r = re.compile(r'<[^>]+>')
return r.sub('', text)
定义读取数据的函数,该函数能根据数据的格式做一些处理并读取到内存中
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def read_file(filetype):
Path = './aclImdb/'
file_list = []
positive = path + filetype + '/pos/'
for f in os.listdir(positive):
file_list += [positive + f]
negative = path + filetype + '/neg/'
for f in os.listdir(negative):
file_list += [negative + f]
label = ([1] * 12500 + [0] * 12500)
text = []
for f_ in file_list:
with open(f_, encoding='utf8') as f:
text += [remove_html(''.join(f.readlines()))]
return label, text
将 BERT 官方提供的词典中的词添加到 keras-bert 的字典中,以便分词时使用。
19 token_dict = {}
20 with codecs.open(vocab_path, 'r', 'utf8') as reader:
21
22
23
24 tokenizer = Tokenizer(token_dict)
token = line.strip()
token_dict[token] = len(token_dict)
for line in reader:
为了保证序列的长度一样,需要对小于最大长度的序列补 0。
25 def seq_padding(text, padding=0):
26
27
max_len = max([len(t) for t in text])
return np.array([
np.concatenate([t,[padding]*(max_len
len(t) < max_len else t for t in text
-
len(t))])
if
])
while True:
idx = list(range(len(data[0])))
np.random.shuffle(idx)
X, S, Y = [], [], []
for i in idx:
因为数据量比较大,这里新建一个生成器,根据 batch_size 的大小来准备当前
显存可用的数据。
28 def data_generator(data, batch_size=64):
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
text = data[0][i][:max_len]
x, s = tokenizer.encode(first=text)
y = data[1][i]
X.append(x)
S.append(s)
Y.append([y])
if len(X) == batch_size or i == idx[-1]:
X = seq_padding(X)
S = seq_padding(S)
Y = seq_padding(Y)
yield [X, S], Y
[X, S, Y] = [], [], []
接下来调用 keras-bert 封装的方法,加载 BERT 模型到内存中,并把每一层设置
为可训练,这样在我们微调的过程中参数才会进行更新。
46 bert_model=load_trained_model_from_checkpoint(config_path,
checkpoint_path, seq_len=None)
47 for l in bert_model.layers:
48
l.trainable = True
然后开始构建模型,模型的输入包括两个输入值,其中 x 对应的是分词在词典中
对应的下标 index,s 表示的是 BERT 需要的段落向量,这里只要一个序列,因此
s 的值全为 0。然后把 x 与 s 作为 BERT 的输入值,得到 BERT 的输出,这里我们
只需要取句向量,也就是 BERT 中[CLS]对应的值,X[:0]表示的即是该值,最后
接上一个全连接层与一个 sigmoid 层做二分类即可。
49 x = Input(shape=(None,))
50 s = Input(shape=(None,))
51
52 out = bert_model([x, s])
53 out = Lambda(lambda x: x[:, 0])(out)
54 out = Dense(1, activation=’sigmoid’)(out)
55 model = Model([x, s], out)
模型定义完成后,即可定义其训练所需参数,其中损失函数采用的是交叉熵损失,
优化器采用的是 Adam 学习率为 1e-4,评估指标为 accuracy。然后调用上文定义
好的生成器来生成与 batch_size 一样长的训练数据与验证数据。最后执行
fit_generator 方法即可开始训练。
56 model.compile(
loss='binary_crossentropy',
optimizer=Adam(1e-4),
metrics=['accuracy']
)
57 model.summary()
58 train_data = read_file('train')
59 test_data = read_file('test')
60 train_gen = data_generator(train_data, batch_size)
test_gen = data_generator(test_data, batch_size)
25 model.fit_generator(
train_gen,
steps_per_epoch=len(train_data[0]) // batch_size,
epochs=5,
validation_data=test_gen,
validation_steps=len(test_data[0]) // batch_size
)
4. 小结
本节主要讲解了 BERT 模型的原理,并采用 keras 微调 BERT 实现了情感分析。
BERT 作为一个目前热门的预训练模型,其效果突出,在文本特征提取阶段均可
采用该模型,再根据具体的业务场景对损失函数进行修改即可实现对应的模型搭
建。当然在使用 keras-bert 之前建议读者务必弄清楚其原理,毕竟知其然还需
知其所以然。