關於BERT模型的調用,這幾天基本上是摸得比較清楚了。
模型源碼在github,該項目的Readme.md文件中提供了9個模型的下載鏈接。前兩個是區分大小寫的英文模型,第三個是中文模型,4589沒有用過具體不太清楚,六七是不區分大小寫的英文模型(根據Readme.md中的描述,如果對大小寫不是很敏感的話用uncased已經完全足夠了,但是我覺得像GEC這種任務應該對大小寫還是相當敏感的)。文件夾名稱中的"bert_"前綴是我自己添加以區分其他模型的。
下載好所有模型分別解壓即為上圖所得,每個文件夾中的文件命名與數量都是相同的,
一共五個文件,bert_config.json是配置文件,vocab.txt是對應模型使用的token集合,其他三個ckpt文件即為模型。
調用分兩步,第一步先把文本轉化為BERT模型輸入的形式(一共三個輸入參數:input_ids,input_mask,token_type_ids),然后調用模型使用即可得到文本的向量表示,如下所示👇
# -*- coding: UTF-8 -*-
# Author: 囚生
# 調用BERT模型的工具函數
import os
import tensorflow as tf
from bert import modeling,tokenization
def text2input(text,tokenizer, # 接收三個參數: 超參數, 文本, 分詞器
maxlen=100, # 文本token最大數
return_tensor=True, # 是否返回tensor類型的結果: 否則返回list類型
): # 將文本轉化為BERT輸入
tokens = tokenizer.tokenize(text) # 分詞器分詞
if len(tokens)>maxlen-2: tokens = tokens[:maxlen-2] # 注意tokens的數量不能超過maxlen-2, 因為頭尾還需要加句首與分句標志
tokens_bert = ["[CLS]"] # 存放token的列表: 置入句首標志
token_type_ids = [0] # 標識token屬句類別的列表: 置入句首標志的標識
for token in tokens: # 添加token與對應標識
tokens_bert.append(token) # 添加token
token_type_ids.append(0) # 這個表示一般用0,1,2,...表示是第幾句話, 該函數一般只接收一個句子, 因此都是0
tokens_bert.append("[SEP]") # 置入分句標志
token_type_ids.append(0) # 置入分句標志的標識
input_ids = tokenizer.convert_tokens_to_ids(tokens_bert) # 將tokens轉化為input_ids
input_mask = [1]*len(input_ids) # 設置蒙布
while len(input_ids)
input_ids.append(0) # 零填充
input_mask.append(0) # 零填充
token_type_ids.append(0) # 零填充
if return_tensor: # 若返回tensor類型
input_ids = tf.convert_to_tensor([input_ids],dtype=tf.int32,name="input_ids")
input_mask = tf.convert_to_tensor([input_mask],dtype=tf.int32,name="input_mask")
token_type_ids = tf.convert_to_tensor([token_type_ids],dtype=tf.int32,name="token_type_ids")
return input_ids,input_mask,token_type_ids # 返回BERT輸入的三個參數
def load_model(input_ids,input_mask,token_type_ids,cpath,mpath,
): # 模型載入
config = modeling.BertConfig.from_json_file(cpath) # 載入配置文件
config_session = tf.ConfigProto() # 創建對象配置session運行參數
config_session.gpu_options.allow_growth = True # 動態申請顯存
with tf.Session(config=config_session).as_default() as session:
model = modeling.BertModel( # 載入模型
config=config, # BERT配置信息
is_training=True, # 訓練模式
input_ids=input_ids, # 輸入參數: 輸入token的索引
input_mask=input_mask, # 輸入參數: 蒙布
token_type_ids=token_type_ids, # 輸入參數:
use_one_hot_embeddings=False, # 不使用one-hot編碼
)
saver = tf.train.Saver() # 訓練保存器
session.run(tf.global_variables_initializer()) # 先初始化, 再加載參數,否則會把BERT的參數重新初始化
saver.restore(session,mpath) # 保存模型到ckpt文件
sequence_output = model.get_sequence_output() # 獲取每個token的輸出: shape(batch_size,sequence_length,embedding_size)
pooled_output = model.get_pooled_output() # 獲取每個分句的輸出: shape(batch_size,embedding_size)
layers = model.all_encoder_layers # 獲取所有層的輸出: shape(batch_size,sequence_length,embedding_size)
embedding_output = model.get_embedding_output()
embedding_table = model.get_embedding_table()
'''
with tf.Session() as session:
session.run(tf.global_variables_initializer())
sequence_output = session.run(sequence_output)
pooled_output = session.run(pooled_output)
embedding_output = session.run(embedding_output)
embedding_table = session.run(embedding_table)
print("sequence_output: {}".format(sequence_output.shape)) # (1,32,768)
print("pooled_output: {}".format(pooled_output.shape)) # (1,768)
print("embedding_output: {}".format(embedding_output.shape)) # (1,32,768)
print("embedding_table: {}".format(embedding_table.shape)) # (28996,768)
for layer in layers:
print(layer.shape) # (1,32,768)
'''
return layers,embedding_output,pooled_output,embedding_table
if __name__ == "__main__":
# 以下4個路徑變量請根據自己的實際情況修改
root = "otherdata/model/bert_cased_L-12_H-768_A-12"
vpath = os.path.join(root,"vocab.txt") # 詞匯表文件
cpath = os.path.join(root,"bert_config.json")
mpath = os.path.join(root,"bert_model.ckpt") # 這個文件其實不存在, 但是就得這么寫, 我也不知道為什么
tokenizer = tokenization.FullTokenizer(vpath) # 這個老版本中可能是CharTokenizer類, 目前源碼中不存在該類了
text = "This will , if not already , cause problems as there is very limited space for us ."
input_ids,input_mask,token_type_ids = text2input(hp,text,tokenizer,32)
load_model(input_ids,input_mask,token_type_ids,cpath,mpath)
注意代碼中調用到的bert庫,既可以直接從https://github.com/google-research/bert下載后放到同一目錄下,因為該項目本身就是bert庫的源碼,也可以直接pip install bert,其也是從github上下載源碼。
上述代碼中的text2input是針對單句文字的,多句文字可以參考其他人的博客,因為我自己查下來關於BERT模型調用還是有不少人在詳細寫過的。
我特別說一下模型輸出的問題,通過查看modeling.py中的源碼,可以看到BertModel類中定義了5個實例函數:
sequence_output = model.get_sequence_output() # 獲取每個token的輸出: shape(batch_size,sequence_length,embedding_size)
pooled_output = model.get_pooled_output() # 獲取每個分句的輸出: shape()
layers = model.all_encoder_layers # 獲取所有層的輸出: shape()
embedding_output = model.get_embedding_output()
embedding_table = model.get_embedding_table()
第三個輸出是12層encoders輸出的列表,該列表中最后一個元素即為sequence_output,其余不多做解釋。
我主要說一下在實際使用中的問題,當我在需要多次調用模型對多個句子進行模型輸出時,出現了如下的報錯:
NotFoundError (see above for traceback): Key bert_1/embeddings/LayerNorm/beta not found in checkpoint
[[Node: save_1/RestoreV2 = RestoreV2[dtypes=[DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, ..., DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT], _device="/job:localhost/replica:0/task:0/device:CPU:0"](_arg_save_1/Const_0_0, save_1/RestoreV2/tensor_names, save_1/RestoreV2/shape_and_slices)]]
[[Node: save_1/RestoreV2/_307 = _Recv[client_terminated=false, recv_device="/job:localhost/replica:0/task:0/device:GPU:0", send_device="/job:localhost/replica:0/task:0/device:CPU:0", send_device_incarnation=1, tensor_name="edge_312_save_1/RestoreV2", tensor_type=DT_FLOAT, _device="/job:localhost/replica:0/task:0/device:GPU:0"]()]]
事實上把上述模型調用的代碼僅需把最后一行復制一遍(即調用兩次模型),就會如上報錯
if __name__ == "__main__":
# 以下4個路徑變量請根據自己的實際情況修改
root = "otherdata/model/bert_cased_L-12_H-768_A-12"
vpath = os.path.join(root,"vocab.txt") # 詞匯表文件
cpath = os.path.join(root,"bert_config.json")
mpath = os.path.join(root,"bert_model.ckpt") # 這個文件其實不存在, 但是就得這么寫, 我也不知道為什么
tokenizer = tokenization.FullTokenizer(vpath) # 這個老版本中可能是CharTokenizer類, 目前源碼中不存在該類了
text = "This will , if not already , cause problems as there is very limited space for us ."
input_ids,input_mask,token_type_ids = text2input(hp,text,tokenizer,32)
load_model(input_ids,input_mask,token_type_ids,cpath,mpath)
load_model(input_ids,input_mask,token_type_ids,cpath,mpath) # 調用兩次
這個問題確實很讓人困擾,只能調用一次的模型未免太雞肋了。
通過檢查排錯,最后找到了問題所在:
在30行處添加tf.reset_default_graph(),作用是清除默認圖的堆棧,並設置全局圖為默認圖,
# -*- coding: UTF-8 -*-
# Author: 囚生
# 調用BERT模型的工具函數
import os
import tensorflow as tf
from bert import modeling,tokenization
def text2input(text,tokenizer, # 接收三個參數: 超參數, 文本, 分詞器
maxlen=100, # 文本token最大數
return_tensor=True, # 是否返回tensor類型的結果: 否則返回list類型
): # 將文本轉化為BERT輸入
tokens = tokenizer.tokenize(text) # 分詞器分詞
if len(tokens)>maxlen-2: tokens = tokens[:maxlen-2] # 注意tokens的數量不能超過maxlen-2, 因為頭尾還需要加句首與分句標志
tokens_bert = ["[CLS]"] # 存放token的列表: 置入句首標志
token_type_ids = [0] # 標識token屬句類別的列表: 置入句首標志的標識
for token in tokens: # 添加token與對應標識
tokens_bert.append(token) # 添加token
token_type_ids.append(0) # 這個表示一般用0,1,2,...表示是第幾句話, 該函數一般只接收一個句子, 因此都是0
tokens_bert.append("[SEP]") # 置入分句標志
token_type_ids.append(0) # 置入分句標志的標識
input_ids = tokenizer.convert_tokens_to_ids(tokens_bert) # 將tokens轉化為input_ids
input_mask = [1]*len(input_ids) # 設置蒙布
while len(input_ids)
input_ids.append(0) # 零填充
input_mask.append(0) # 零填充
token_type_ids.append(0) # 零填充
if return_tensor: # 若返回tensor類型
tf.reset_default_graph()
input_ids = tf.convert_to_tensor([input_ids],dtype=tf.int32,name="input_ids")
input_mask = tf.convert_to_tensor([input_mask],dtype=tf.int32,name="input_mask")
token_type_ids = tf.convert_to_tensor([token_type_ids],dtype=tf.int32,name="token_type_ids")
return input_ids,input_mask,token_type_ids # 返回BERT輸入的三個參數
def load_model(input_ids,input_mask,token_type_ids,cpath,mpath,
): # 模型載入鄭州婦科醫院哪家好 http://www.sptdfk.com/
config = modeling.BertConfig.from_json_file(cpath) # 載入配置文件
config_session = tf.ConfigProto() # 創建對象配置session運行參數
config_session.gpu_options.allow_growth = True # 動態申請顯存
with tf.Session(config=config_session).as_default() as session:
model = modeling.BertModel( # 載入模型
config=config, # BERT配置信息
is_training=True, # 訓練模式
input_ids=input_ids, # 輸入參數: 輸入token的索引
input_mask=input_mask, # 輸入參數: 蒙布
token_type_ids=token_type_ids, # 輸入參數:
use_one_hot_embeddings=False, # 不使用one-hot編碼
)
saver = tf.train.Saver() # 訓練保存器
session.run(tf.global_variables_initializer()) # 先初始化, 再加載參數,否則會把BERT的參數重新初始化
saver.restore(session,mpath) # 保存模型到ckpt文件
sequence_output = model.get_sequence_output() # 獲取每個token的輸出: shape(batch_size,sequence_length,embedding_size)
pooled_output = model.get_pooled_output() # 獲取每個分句的輸出: shape(batch_size,embedding_size)
layers = model.all_encoder_layers # 獲取所有層的輸出: shape(batch_size,sequence_length,embedding_size)
embedding_output = model.get_embedding_output()
embedding_table = model.get_embedding_table()
'''
with tf.Session() as session:
session.run(tf.global_variables_initializer())
sequence_output = session.run(sequence_output)
pooled_output = session.run(pooled_output)
embedding_output = session.run(embedding_output)
embedding_table = session.run(embedding_table)
print("sequence_output: {}".format(sequence_output.shape)) # (1,32,768)
print("pooled_output: {}".format(pooled_output.shape)) # (1,768)
print("embedding_output: {}".format(embedding_output.shape)) # (1,32,768)
print("embedding_table: {}".format(embedding_table.shape)) # (28996,768)
for layer in layers:
print(layer.shape) # (1,32,768)
'''
return layers,embedding_output,pooled_output,embedding_table
if __name__ == "__main__":
# 以下4個路徑變量請根據自己的實際情況修改
root = "otherdata/model/bert_cased_L-12_H-768_A-12"
vpath = os.path.join(root,"vocab.txt") # 詞匯表文件
cpath = os.path.join(root,"bert_config.json")
mpath = os.path.join(root,"bert_model.ckpt") # 這個文件其實不存在, 但是就得這么寫, 我也不知道為什么
tokenizer = tokenization.FullTokenizer(vpath) # 這個老版本中可能是CharTokenizer類, 目前源碼中不存在該類了
text = "This will , if not already , cause problems as there is very limited space for us ."
input_ids,input_mask,token_type_ids = text2input(hp,text,tokenizer,32)
load_model(input_ids,input_mask,token_type_ids,cpath,mpath)
load_model(input_ids,input_mask,token_type_ids,cpath,mpath) # 調用兩次
總之tensorflow用起來確實挺麻煩,我使用的版本是tensorflow-gpu v1.9.0,因為gpu版本再高一點的我的顯卡不支持,而我又不想卸了用cpu版本的了,建議如果使用tensorflow的話要么就用2.0版本以后的,要么就用v1.12.0這個長期更新的版本。總之tensorflow不同版本間的差異確實很大,別人的代碼很多時候不改都是跑不了的。
可能pytorch還是好一點吧,可惜現在主流的幾個模型transformer,bert源碼都是用的tensorflow架構的,也沒什么辦法了。