本文默认读者有一定的Transformer基础,如果没有,请先稍作学习Transormer以及BERT。

相信网上有很多方法可以生成BERT向量,最有代表性的一个就是bert as service,用几行代码就可以生成向量,但是这样生成的是句向量,也就是说,正确的做法是输入一句句子:

我是一个中国人,我热爱着中国的每一个城市。

输出的是这句句子的向量,一个768维的向量(google预训练是这么做的),这个向量是具有上下文信息的,详细参考Transformer结构。但是网上有一些做法是用bert as service来生成词级向量,例如输入[‘我’,‘是’,‘一个’, ‘中国’, ‘人’],得到5个768维的向量,用来作为词向量,但这样做是错误的!具体原因参照我前面的描述,既然思想是错误的,也就不奇怪效果不好了,所以在这种情况下,请先别着急说BERT预训练模型不work。

BERT生成token级别的向量,这两篇文章理解的比较准确(我的代码有一部分参考第二篇博客):

https://blog.csdn.net/u012526436/article/details/87697242

https://blog.csdn.net/shine19930820/article/details/85730536

为什么说是token级别的向量呢?因为Transformer结构所决定其输入和输出的长度相等的,而且对于中文预训练模型,做法是将中文拆成一个个的字来做学习的,因此每一个token就是一个字。对于一句话,我们会在其头上加[cls]在尾部加[SEP],并且BERT是一个多任务的预训练过程,现在假设text_a是我们需要获取向量的句子,text_b为空,那么,输入:

我是一个中国人,我热爱着中国的每一个城市。

处理后:

[CLS]我是一个中国人,我热爱着中国的每一个城市。[SEP]

通常我们会用第一个字符[CLS]的输出向量(768维)作为整个句子的向量表示,用来接全连接、softmax层做分类,现在我打算获取这样一个句子中每一个字符的向量表示,并存储下来,以备下游任务,如果我只用[CLS]的向量来做分类,那么就只取第一个向量,如果用所有token的向量来做卷积,那么就舍弃一头一尾的向量,取中间的向量来做卷积,这样下游任务不用改太多代码,把这样的信息存储在文件里,下游任务用起来比较灵活。

存储ndarray

要能够把词向量存储下来供下次使用,就肯定要知道怎么存储ndarray,因为拿到的词向量是shape为(N, seq_len, 768)的ndarray,N代表有多少个句子,seq_len代表句子的长度(加上头尾),768即向量的维度。这里我使用h5py存储ndarray,当然也有一些别的方法。

import h5py
# shape a: (3, 4, 5)
a = np.array([[[1,0.5,1,0.3,-1],[1,0.5,1,0.3,-1],[1,0.5,1,0.3,-1],[1,0.5,1,0.3,-1]],
[[1,0.5,1,0.3,-1],[1,0.5,1,0.3,-1],[1,0.5,1,0.3,-1],[1,0.5,1,0.3,-1]],
[[1,0.5,1,0.3,-1],[1,0.5,1,0.3,-1],[1,0.5,1,0.3,-1],[1,0.5,1,0.3,-1]]])
print(a.shape) save_file = h5py.File('../downstream/input_c_emb.h5', 'w')
save_file.create_dataset('train', data=a)
save_file.close() open_file = h5py.File('../downstream/input_c_emb.h5', 'r')
data = open_file['train'][:]
open_file.close()
print(data)
print(type(data))
print(data.shape)

  

字符级Token

因为我希望对中文字符进行一个字符一个字符的提取,而不是会把非中文字符拼在一起(这是google的逻辑),比如说”我出生于1996年“这句话,我希望提取为‘我’,‘出’,‘生’,‘于’,‘1’,‘9’,‘9’,‘6’,‘年’,因此需要自己写一个token类,在bert项目中tokenization.py文件中。

class CharTokenizer(object):
"""Runs end-to-end tokenziation.""" def __init__(self, vocab_file, do_lower_case=True):
self.vocab = load_vocab(vocab_file)
self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case)
self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab) def tokenize(self, text):
split_tokens = []
for token in self.basic_tokenizer.tokenize(text):
for sub_token in token:
# 有的字符在预训练词典里没有
# 这部分字符替换成[UNK]符号
if not sub_token in self.vocab:
split_tokens.append('[UNK]')
else:
split_tokens.append(sub_token)
return split_tokens def convert_tokens_to_ids(self, tokens):
return convert_tokens_to_ids(self.vocab, tokens)

  

提取向量作为特征

这里对于输入输出稍作解释,我的输入有三个文件,train.txt,val.txt,test.txt,顾名思义了。每个文件中的一系列的句子,比如train.txt中有5000多行,代表5000多个句子,每一个句子是已经以空格分开的序列,比如”我 爱 中 国“。输出就是一个input_c_emb.h5,里面保存了所有的嵌入向量,以train,val,test标识为分隔。

代码注释还行,就不详细说了。

这段代码在项目中是token_features.py,项目地址后面会放。

# 获取token features,即每一个字符的向量,可以用cls作为句子向量,也可以用每一个字符的向量
import os
import sys
curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = os.path.split(curPath)[0]
sys.path.append(rootPath)
print(sys.path)
import tensorflow as tf
import tokenization
import modeling
import numpy as np
import h5py # 配置文件
# data_root是模型文件,可以用预训练的,也可以用在分类任务上微调过的模型
data_root = '../chinese_wwm_ext_L-12_H-768_A-12/'
bert_config_file = data_root + 'bert_config.json'
bert_config = modeling.BertConfig.from_json_file(bert_config_file)
init_checkpoint = data_root + 'bert_model.ckpt'
bert_vocab_file = data_root + 'vocab.txt' # 经过处理的输入文件路径
file_input_x_c_train = '../data/legal_domain/train_x_c.txt'
file_input_x_c_val = '../data/legal_domain/val_x_c.txt'
file_input_x_c_test = '../data/legal_domain/test_x_c.txt' # embedding存放路径
emb_file_dir = '../data/legal_domain/emb.h5' # graph
input_ids = tf.placeholder(tf.int32, shape=[None, None], name='input_ids')
input_mask = tf.placeholder(tf.int32, shape=[None, None], name='input_masks')
segment_ids = tf.placeholder(tf.int32, shape=[None, None], name='segment_ids') BATCH_SIZE = 16
SEQ_LEN = 510 def batch_iter(x, batch_size=64, shuffle=False):
"""生成批次数据,一个batch一个batch地产生句子向量"""
data_len = len(x)
num_batch = int((data_len - 1) / batch_size) + 1 if shuffle:
indices = np.random.permutation(np.arange(data_len))
x_shuffle = np.array(x)[indices]
else:
x_shuffle = x[:] word_mask = [[1] * (SEQ_LEN + 2) for i in range(data_len)]
word_segment_ids = [[0] * (SEQ_LEN + 2) for i in range(data_len)] for i in range(num_batch):
start_id = i * batch_size
end_id = min((i + 1) * batch_size, data_len)
yield x_shuffle[start_id:end_id], word_mask[start_id:end_id], word_segment_ids[start_id:end_id] def read_input(file_dir):
# 从文件中读到所有需要转化的句子
# 这里需要做统一长度为510
# input_list = []
with open(file_dir, 'r', encoding='utf-8') as f:
input_list = f.readlines() # input_list是输入list,每一个元素是一个str,代表输入文本
# 现在需要转化成id_list
word_id_list = []
for query in input_list:
split_tokens = token.tokenize(query)
if len(split_tokens) > SEQ_LEN:
split_tokens = split_tokens[:SEQ_LEN]
else:
while len(split_tokens) < SEQ_LEN:
split_tokens.append('[PAD]')
# ****************************************************
# 如果是需要用到句向量,需要用这个方法
# 加个CLS头,加个SEP尾
tokens = []
tokens.append("[CLS]")
for i_token in split_tokens:
tokens.append(i_token)
tokens.append("[SEP]")
# ****************************************************
word_ids = token.convert_tokens_to_ids(tokens)
word_id_list.append(word_ids)
return word_id_list # 初始化BERT
model = modeling.BertModel(
config=bert_config,
is_training=False,
input_ids=input_ids,
input_mask=input_mask,
token_type_ids=segment_ids,
use_one_hot_embeddings=False
) # 加载BERT模型
tvars = tf.trainable_variables()
(assignment, initialized_variable_names) = modeling.get_assignment_map_from_checkpoint(tvars, init_checkpoint)
tf.train.init_from_checkpoint(init_checkpoint, assignment)
# 获取最后一层和倒数第二层
encoder_last_layer = model.get_sequence_output()
encoder_last2_layer = model.all_encoder_layers[-2] # 读取数据
token = tokenization.CharTokenizer(vocab_file=bert_vocab_file) input_train_data = read_input(file_dir='../data/legal_domain/train_x_c.txt')
input_val_data = read_input(file_dir='../data/legal_domain/val_x_c.txt')
input_test_data = read_input(file_dir='../data/legal_domain/test_x_c.txt') with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
save_file = h5py.File('../downstream/input_c_emb.h5', 'w')
emb_train = []
train_batches = batch_iter(input_train_data, batch_size=BATCH_SIZE, shuffle=False)
for word_id, mask, segment in train_batches:
feed_data = {input_ids: word_id, input_mask: mask, segment_ids: segment}
last2 = sess.run(encoder_last2_layer, feed_dict=feed_data)
# print(last2.shape)
for sub_array in last2:
emb_train.append(sub_array)
# 可以保存了
emb_train_array = np.asarray(emb_train)
save_file.create_dataset('train', data=emb_train_array) # val
emb_val = []
val_batches = batch_iter(input_val_data, batch_size=BATCH_SIZE, shuffle=False)
for word_id, mask, segment in val_batches:
feed_data = {input_ids: word_id, input_mask: mask, segment_ids: segment}
last2 = sess.run(encoder_last2_layer, feed_dict=feed_data)
# print(last2.shape)
for sub_array in last2:
emb_val.append(sub_array)
# 可以保存了
emb_val_array = np.asarray(emb_val)
save_file.create_dataset('val', data=emb_val_array) # test
emb_test = []
test_batches = batch_iter(input_test_data, batch_size=BATCH_SIZE, shuffle=False)
for word_id, mask, segment in test_batches:
feed_data = {input_ids: word_id, input_mask: mask, segment_ids: segment}
last2 = sess.run(encoder_last2_layer, feed_dict=feed_data)
# print(last2.shape)
for sub_array in last2:
emb_test.append(sub_array)
# 可以保存了
emb_test_array = np.asarray(emb_test)
save_file.create_dataset('test', data=emb_test_array) save_file.close() print(emb_train_array.shape)
print(emb_val_array.shape)
print(emb_test_array.shape) # 这边目标是接下游CNN任务,因此先写入所有token的embedding,768维
# 写入shape直接是(N, max_seq_len + 2, 768)
# 下游需要选用的时候,如果卷积,则去掉头尾使用,如果全连接,则直接使用头部
# 这里直接设定max_seq_len=510,加上[cls]和[sep],得到512
# 写入(n, 512, 768) ndarray到文件,需要用的时候再读出来,就直接舍弃embedding层

  

项目地址

点击这里

使用BERT模型生成token级向量的更多相关文章

  1. 使用BERT模型生成句子序列向量

    之前我写过一篇文章,利用bert来生成token级向量(对于中文语料来说就是字级别向量),参考我的文章:<使用BERT模型生成token级向量>.但是这样做有一个致命的缺点就是字符序列长度 ...

  2. BERT模型图解

    转载于 腾讯Bugly 发表于 腾讯Bugly的专栏 原文链接:https://cloud.tencent.com/developer/article/1389555 本文首先介绍BERT模型要做什么 ...

  3. 图示详解BERT模型的输入与输出

    一.BERT整体结构 BERT主要用了Transformer的Encoder,而没有用其Decoder,我想是因为BERT是一个预训练模型,只要学到其中语义关系即可,不需要去解码完成具体的任务.整体架 ...

  4. BERT模型介绍

    前不久,谷歌AI团队新发布的BERT模型,在NLP业内引起巨大反响,认为是NLP领域里程碑式的进步.BERT模型在机器阅读理解顶级水平测试SQuAD1.1中表现出惊人的成绩:全部两个衡量指标上全面超越 ...

  5. [NLP自然语言处理]谷歌BERT模型深度解析

    我的机器学习教程「美团」算法工程师带你入门机器学习   已经开始更新了,欢迎大家订阅~ 任何关于算法.编程.AI行业知识或博客内容的问题,可以随时扫码关注公众号「图灵的猫」,加入”学习小组“,沙雕博主 ...

  6. BERT模型

    BERT模型是什么 BERT的全称是Bidirectional Encoder Representation from Transformers,即双向Transformer的Encoder,因为de ...

  7. Pytorch | BERT模型实现,提供转换脚本【横扫NLP】

    <谷歌终于开源BERT代码:3 亿参数量,机器之心全面解读>,上周推送的这篇文章,全面解读基于TensorFlow实现的BERT代码.现在,PyTorch用户的福利来了:一个名为Huggi ...

  8. BERT模型详解

    1 简介 BERT全称Bidirectional Enoceder Representations from Transformers,即双向的Transformers的Encoder.是谷歌于201 ...

  9. Bert模型实现垃圾邮件分类

    近日,对近些年在NLP领域很火的BERT模型进行了学习,并进行实践.今天在这里做一下笔记. 本篇博客包含下列内容: BERT模型简介 概览 BERT模型结构 BERT项目学习及代码走读 项目基本特性介 ...

随机推荐

  1. tomcat常见报错解决方法汇总

    报错一:内存泄漏,字眼This is very likely to create a memory leak. 解决方法:修改tomcat内存. 在tomcat/bin目录下,修改catalina.s ...

  2. golang高级用法总结

    sync.WaitGroup add()  done()  wait() 相当于计数器,done会减一,wait会等待所有线程都执行完才执行下面代码 sync.pool 用来封装连接池

  3. Java虚拟机栈(java stack)

    虚拟机栈(java stack) 百度图片搜索里的动图搜索功能不错,可以搜索一些动图,展示操作数栈的操作过程,比较形象.这点google差点意思 虚拟机栈(jvm stacks)是线程独占的 里面是多 ...

  4. C++全排列 next_permutation

    全排列函数  next_permutation 这是C++的STL中专门用来排列的函数(可以自动处理存在重复数据集的排列问题) 使用时要加上头文件 #include <algorithm> ...

  5. Maven 相关知识点解释

    在PC端上面关于Maven的安装等情况我这里就不再复述了,不懂的请自行百度谷歌. 今天聊一下Maven 里面的结构,及相关依赖解释. groupId,artfactId,version,type,cl ...

  6. springboot整合mybatis,mongodb,redis

    springboot整合常用的第三方框架,mybatis,mongodb,redis mybatis,采用xml编写sql语句 mongodb,对MongoTemplate进行了封装 redis,对r ...

  7. python大道——博客目录

      python基础 第一章 计算机基础 计算机基础 第二章 python基础语法 python入门 第三章 基础数据类型和文件操作 整型.布尔.字符串 列表.字典.集合 公共功能.小数据池 hash ...

  8. 永久修改 Linux pip国内源

    一些常用的国内源 清华大学:https://pypi.tuna.tsinghua.edu.cn/simple 阿里云:https://mirrors.aliyun.com/pypi/simple 中国 ...

  9. 网络 TCP三次握手,四次挥手详解

    三次握手,四次挥手可以说是炙手可热的面试题了,来看看它究竟长什么样子吧! 我们先把流程图贴上来 : 为什么这么复杂? 因为TCP是可靠性传输. 确认可靠传输的前提:  TCP连接管理机制 用TCP首部 ...

  10. centos jira wiki 开机自启

    启动jira 和 wiki 需要java环境变量 操作步骤: 在 /etc/rc.d/rc.local 文件中[ 注意要有可执行权限 ] export JAVA_HOME=xxxxxxxx expor ...