【导读】BERT是目前非常流行的NLP基础组件之一,基于BERT可以构建许多效果优秀的高层NLP应用。由于BERT的训练需要消耗大量的计算资源,大部分普通用户会使用预训练的BERT模型。本文介绍可使用官方预训练模型的Keras版BERT。
Github项目CyberZHG/keras-bert是基于Keras的BERT实现,其提供了中文文档。文档内容如下:
BERT的非官方实现,可以加载官方的预训练模型进行特征提取和预测。
安装
pip install keras-bert
特征提取展示中使用官方预训练好的chinese_L-12_H-768_A-12
可以得到和官方工具一样的结果。
预测展示中可以填补出缺失词并预测是否是上下文。
特征提取示例中展示了如何在TPU上进行特征提取。
分类示例中在IMDB数据集上对模型进行了微调以适应新的分类任务。
Tokenizer
类可以用来进行分词工作,包括归一化和英文部分的最大贪心匹配等,在CJK字符集内的中文会以单字分隔。
from keras_bert import Tokenizer
token_dict = {
'[CLS]': 0,
'[SEP]': 1,
'un': 2,
'##aff': 3,
'##able': 4,
'[UNK]': 5,
}
tokenizer = Tokenizer(token_dict)
print(tokenizer.tokenize('unaffable')) # 分词结果是:`['[CLS]', 'un', '##aff', '##able', '[SEP]']`
indices, segments = tokenizer.encode('unaffable')
print(indices) # 词对应的下标:`[0, 2, 3, 4, 1]`
print(segments) # 段落对应下标:`[0, 0, 0, 0, 0]`
print(tokenizer.tokenize(first='unaffable', second='钢'))
# 分词结果是:`['[CLS]', 'un', '##aff', '##able', '[SEP]', '钢', '[SEP]']`
indices, segments = tokenizer.encode(first='unaffable', second='钢', max_len=10)
print(indices) # 词对应的下标:`[0, 2, 3, 4, 1, 5, 1, 0, 0, 0]`
print(segments) # 段落对应下标:`[0, 0, 0, 0, 0, 1, 1, 1, 1, 1]`
训练和使用
训练过程推荐使用官方的代码。这个代码库内包含一个的训练过程,training
为True
的情况下使用的是带warmup的Adam优化器:
import keras
from keras_bert import get_base_dict, get_model, gen_batch_inputs
# 随便的输入样例:
sentence_pairs = [
[['all', 'work', 'and', 'no', 'play'], ['makes', 'jack', 'a', 'dull', 'boy']],
[['from', 'the', 'day', 'forth'], ['my', 'arm', 'changed']],
[['and', 'a', 'voice', 'echoed'], ['power', 'give', 'me', 'more', 'power']],
]
# 构建自定义词典
token_dict = get_base_dict() # 初始化特殊符号,如`[CLS]`
for pairs in sentence_pairs:
for token in pairs[0] + pairs[1]:
if token not in token_dict:
token_dict[token] = len(token_dict)
token_list = list(token_dict.keys()) # Used for selecting a random word
# 构建和训练模型
model = get_model(
token_num=len(token_dict),
head_num=5,
transformer_num=12,
embed_dim=25,
feed_forward_dim=100,
seq_len=20,
pos_num=20,
dropout_rate=0.05,
)
model.summary()
def _generator():
while True:
yield gen_batch_inputs(
sentence_pairs,
token_dict,
token_list,
seq_len=20,
mask_rate=0.3,
swap_sentence_rate=1.0,
)
model.fit_generator(
generator=_generator(),
steps_per_epoch=1000,
epochs=100,
validation_data=_generator(),
validation_steps=100,
callbacks=[
keras.callbacks.EarlyStopping(monitor='val_loss', patience=5)
],
)
# 使用训练好的模型
inputs, output_layer = get_model(
token_num=len(token_dict),
head_num=5,
transformer_num=12,
embed_dim=25,
feed_forward_dim=100,
seq_len=20,
pos_num=20,
dropout_rate=0.05,
training=False, # 当`training`是`False`,返回值是输入和输出
trainable=False, # 模型是否可训练,默认值和`training`相同
output_layer_num=4, # 最后几层的输出将合并在一起作为最终的输出,只有当`training`是`False`有效
)
虽然看起来相似,但这两个参数是不相关的。training
表示是否在训练BERT语言模型,当为True
时完整的BERT模型会被返回,当为False
时没有MLM和NSP相关计算的结构,返回输入层和根据output_layer_num
合并最后几层的输出。加载的层是否可训练只跟trainable
有关。
此外,trainable
可以是一个包含字符串的列表,如果某一层的前缀出现在列表中,则当前层是可训练的。在使用预训练模型时,如果不想再训练嵌入层,可以传入trainable=['Encoder']
来只对编码层进行调整。
AdamWarmup
优化器可用于学习率的「热身」与「衰减」。学习率将在warmpup_steps
步线性增长到lr
,并在总共decay_steps
步后线性减少到min_lr
。辅助函数calc_train_steps
可用于计算这两个步数:
import numpy as np
from keras_bert import AdamWarmup, calc_train_steps
train_x = np.random.standard_normal((1024, 100))
total_steps, warmup_steps = calc_train_steps(
num_example=train_x.shape[0],
batch_size=32,
epochs=10,
warmup_proportion=0.1,
)
optimizer = AdamWarmup(total_steps, warmup_steps, lr=1e-3, min_lr=1e-5)
在training
为True
的情况下,输入包含三项:token下标、segment下标、被masked的词的模版。当training
为False
时输入只包含前两项。位置下标由于是固定的,会在模型内部生成,不需要手动再输入一遍。被masked的词的模版在输入被masked的词是值为1,否则为0。
库中记录了一些预训练模型的下载地址,可以通过如下方式获得解压后的checkpoint的路径:
from keras_bert import get_pretrained, PretrainedList, get_checkpoint_paths
model_path = get_pretrained(PretrainedList.multi_cased_base)
paths = get_checkpoint_paths(model_path)
print(paths.config, paths.checkpoint, paths.vocab)
提取特征
如果不需要微调,只想提取词/句子的特征,则可以使用extract_embeddings
来简化流程。如提取每个句子对应的全部词的特征:
from keras_bert import extract_embeddings
model_path = 'xxx/yyy/uncased_L-12_H-768_A-12'
texts = ['all work and no play', 'makes jack a dull boy~']
embeddings = extract_embeddings(model_path, texts)
返回的结果是一个list,长度和输入文本的个数相同,每个元素都是numpy的数组,默认会根据输出的长度进行裁剪,所以在这个例子中输出的大小分别为(8, 768)
和(9, 768)
。
如果输入是成对的句子,想使用最后4层特征,且提取NSP
位输出和max-pooling的结果,则可以用:
from keras_bert import extract_embeddings, POOL_NSP, POOL_MAX
model_path = 'xxx/yyy/uncased_L-12_H-768_A-12'
texts = [
('all work and no play', 'makes jack a dull boy'),
('makes jack a dull boy', 'all work and no play'),
]
embeddings = extract_embeddings(model_path, texts, output_layer_num=4, poolings=[POOL_NSP, POOL_MAX])
输出结果中不再包含词的特征,NSP
和max-pooling的输出会拼接在一起,每个numpy数组的大小为(768 x 4 x 2,)
。
第二个参数接受的是一个generator,如果想读取文件并生成特征,可以用下面的方法:
import codecs
from keras_bert import extract_embeddings
model_path = 'xxx/yyy/uncased_L-12_H-768_A-12'
with codecs.open('xxx.txt', 'r', 'utf8') as reader:
texts = map(lambda x: x.strip(), reader)
embeddings = extract_embeddings(model_path, texts)
在环境变量里加入TF_KERAS=1
可以启用tensorflow.python.keras
。加入TF_EAGER=1
可以启用eager execution。在Keras本身没去支持之前,如果想使用tensorflow 2.0则必须使用TF_KERAS=1
。
在环境变量中加入KERAS_BACKEND=theano
来启用theano
后端。
参考链接:
https://github.com/CyberZHG/keras-bert
-END-
专 · 知
专知,专业可信的人工智能知识分发,让认知协作更快更好!欢迎登录www.zhuanzhi.ai,注册登录专知,获取更多AI知识资料!
欢迎微信扫一扫加入专知人工智能知识星球群,获取最新AI专业干货知识教程视频资料和与专家交流咨询!
请加专知小助手微信(扫一扫如下二维码添加),加入专知人工智能主题群,咨询技术商务合作~
专知《深度学习:算法到实战》课程全部完成!550+位同学在学习,现在报名,限时优惠!网易云课堂人工智能畅销榜首位!
点击“阅读原文”,了解报名专知《深度学习:算法到实战》课程