热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

使用BERT生成句向量

转载请注明出处,原文地址在阅读本文之前如果您对BERT并不了解,请参阅我的其他博文BERT完全指南简介之前的文章介绍了BERT的原理、并用BERT

转载请注明出处,原文地址

在阅读本文之前如果您对BERT并不了解,请参阅我的其他博文BERT完全指南

简介

之前的文章介绍了BERT的原理、并用BERT做了文本分类与相似度计算,本文将会教大家用BERT来生成句向量,核心逻辑代码参考了hanxiao大神的bert-as-service,我的代码地址如下:

代码地址:BERT句向量

传统的句向量

对于传统的句向量生成方式,更多的是采用word embedding的方式取加权平均,该方法有一个最大的弊端,那就是无法理解上下文的语义,同一个词在不同的语境意思可能不一样,但是却会被表示成同样的word embedding,BERT生成句向量的优点在于可理解句意,并且排除了词向量加权引起的误差。

BERT句向量

BERT的包括两个版本,12层的transformer与24层的transformer,官方提供了12层的中文模型,下文也将会基于12层的模型来讲解。

每一层transformer的输出值,理论上来说都可以作为句向量,但是到底应该取哪一层呢,根据hanxiao大神的实验数据,最佳结果是取倒数第二层,最后一层的值太接近于目标,前面几层的值可能语义还未充分的学习到。

接下来我们从代码的角度来进详细讲解。

先看下args.py文件,该文件有几个句向量的重要参数,前几个都是路径,这里不再详细解释,这里主要说一下layer_indexes参数与max_seq_len参数,layer_indexes表示的是使用第几层的输出作为句向量,-2表示的是倒数第二层,max_seq_len表示的是序列的最大长度,因为输入的长度是不固定的,所以我们需要设置一个最大长度才能确保输出的维度是一样的,如果最大长度是20,当输入的序列长度小于20的时候,就会补0,如果大于20则会截取前面的部分 ,通常该值会取语料的长度的平均值+2,加2的原因是因为需要拼接两个占位符[CLS](表示序列的开始)与[SEP](表示序列的结束)

model_dir = os.path.join(file_path, 'chinese_L-12_H-768_A-12/')
config_name = os.path.join(model_dir, 'bert_config.json')
ckpt_name = os.path.join(model_dir, 'bert_model.ckpt')
output_dir = os.path.join(model_dir, '../tmp/result/')
vocab_file = os.path.join(model_dir, 'vocab.txt')
data_dir = os.path.join(model_dir, '../data/')num_train_epochs = 10
batch_size = 32
learning_rate = 0.00005
# gpu使用率
gpu_memory_fraction = 0.8
# 默认取倒数第二层的输出值作为句向量
layer_indexes = [-2]# 序列的最大程度,单文本建议把该值调小
max_seq_len = 20再来看graph.py文件,该代码的主要目的是把预训练好的模型加载进来,并修改输出层,我们一步一步来看。首先创建一个目录,该目录用于存放待输出的文件,定义bert的配置信息路径,根据路径读取配置信息转化为bert_config对象。tensorflow.python.tools.optimize_for_inference_lib import optimize_for_inference
tf.gfile.MakeDirs(args.output_dir)config_fp = args.config_name
logger.info('model config: %s' % config_fp)# 加载bert配置文件 with tf.gfile.GFile(config_fp, 'r') as f:bert_config = modeling.BertConfig.from_dict(json.load(f))

定义三个占位符,分别表示的是对应文本的index,mask与type_index,其中index表示的是在词典中的index,mask表示的是该位置是否有内容,举个例子,例如序列的最大长度是20,有效的字符只有10个字,加上[CLS]与[SEP]两个占位符,那有8个字符是空的,该8个位置设置为0其他位置设置为1,type_index表示的是是否是第一个句子,是第一个句子则设置为1,因为该项目只有一个句子,所以均为1。

logger.info('build graph...')
input_ids = tf.placeholder(tf.int32, (None, args.max_seq_len), 'input_ids')
input_mask = tf.placeholder(tf.int32, (None, args.max_seq_len), 'input_mask')
input_type_ids = tf.placeholder(tf.int32, (None, args.max_seq_len), 'input_type_ids')

根据上面定义的三个占位符,定义好输入的张量,实例化一个model对象,该对象就是预训练好的bert模型,然后从check_point文件中初始化权重

input_tensors = [input_ids, input_mask, input_type_ids]model = modeling.BertModel(config=bert_config,is_training=False,input_ids=input_ids,input_mask=input_mask,token_type_ids=input_type_ids,use_one_hot_embeddings=False)tvars = tf.trainable_variables()init_checkpoint = args.ckpt_name
(assignment_map, initialized_variable_names) = modeling.get_assignment_map_from_checkpoint(tvars, init_checkpoint)tf.train.init_from_checkpoint(init_checkpoint, assignment_map)

接下来判断一下args.index_layeres参数的长度,如果长度为1,则只取改层的输出,否则遍历需要取的层,把所有层的weight取出来并拼接成一个768*层数的张量

with tf.variable_scope("pooling"):if len(args.layer_indexes) == 1:encoder_layer = model.all_encoder_layers[args.layer_indexes[0]]else:all_layers = [model.all_encoder_layers[l] for l in args.layer_indexes]encoder_layer = tf.concat(all_layers, -1)

接下来是句向量生成的核心代码,这里定义了两个方法,一个mul_mask 和一个masked_reduce_mean,我们先看masked_reduce_mean(encoder_layer, input_mask)这里调用方法时传入的是encoder_layer即输出值,与input_mask即是否有有效文本,masked_reduce_mean方法中又调用了mul_mask方法,即先把input_mask进行了一个维度扩展,然后与encoder_layer相乘,为什么要维度扩展呢,我们看下两个值的维度,我们还是假设序列的最大长度是20,那么encoder_layer的维度为[20,768],为了把无效的位置的内容置为0,input_mask的维度为[20],扩充之后变成了[20,1],两个值相乘,便把input_mask为0的位置的encoder_layer的值改为了0, 然后把相乘得到的值在axis=1的位置进行相加最后除以input_mask在axis=1的维度的和,然后把得到的结果添加一个别名final_encodes

mul_mask = lambda x, m: x * tf.expand_dims(m, axis=-1)
masked_reduce_mean = lambda x, m: tf.reduce_sum(mul_mask(x, m), axis=1) / (tf.reduce_sum(m, axis=1, keepdims=True) + 1e-10)input_mask = tf.cast(input_mask, tf.float32)
pooled = masked_reduce_mean(encoder_layer, input_mask)
pooled = tf.identity(pooled, 'final_encodes')output_tensors = [pooled]
tmp_g = tf.get_default_graph().as_graph_def()

最后把得到的句向量重新添加进graph中,并返回graph的路径。

config = tf.ConfigProto(allow_soft_placement=True)
with tf.Session(config=config) as sess:logger.info('load parameters from checkpoint...')sess.run(tf.global_variables_initializer())logger.info('freeze...')tmp_g = tf.graph_util.convert_variables_to_constants(sess, tmp_g, [n.name[:-2] for n in output_tensors])dtypes = [n.dtype for n in input_tensors]logger.info('optimize...')tmp_g = optimize_for_inference(tmp_g,[n.name[:-2] for n in input_tensors],[n.name[:-2] for n in output_tensors],[dtype.as_datatype_enum for dtype in dtypes],False)
tmp_file = tempfile.NamedTemporaryFile('w', delete=False, dir=args.output_dir).name
logger.info('write graph to a tmp file: %s' % tmp_file)
with tf.gfile.GFile(tmp_file, 'wb') as f:f.write(tmp_g.SerializeToString())
return tmp_file

实际的使用和BERT做文本分类的方法类似,只是在返回的EstimatorSpec不太一样,具体细节不在详解,可参考我的具体代码。

with tf.gfile.GFile(self.graph_path, 'rb') as f:graph_def = tf.GraphDef()graph_def.ParseFromString(f.read())input_names = ['input_ids', 'input_mask', 'input_type_ids']output = tf.import_graph_def(graph_def,input_map={k + ':0': features[k] for k in input_names},return_elements=['final_encodes:0'])return EstimatorSpec(mode=mode, predictions={'encodes': output[0]
})

最后再贴一下代码地址

BERT生成句向量
————————————————
版权声明:本文为CSDN博主「爱编程真是太好了」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012526436/article/details/87697242


推荐阅读
author-avatar
Rianbow_小渊渊设
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有