訓練BERT模型加入到深度學習網絡層中——keras_bert庫使用指南


  1 前言

  BERT模型的使用可以分為兩種形式:第一種使用方法直接將語句序列輸入BERT模型獲取特征表示,BERT模型一共提供十二層不同的特征向量輸出,隨層數的遞進,特征表示從專於詞義表示到專於語義表示而有所區別,此時BERT模型相當於靜態的word2vector模型,僅用於特征表示,關於如何獲取BERT預訓練模型及如何使用第一種方法,可以參考前一篇博客。

  第二種則是更為常用的將BERT模型作為深度學習網絡的一部分繼續訓練,以達到個性化需求適應的目的,此時BERT模型相當於為深度學習網絡層中預設了一個較優的初始參數值,有利於模型訓練在優化目標損失值時有一個較好的初始點,這有利於優化算法能夠更快更精確地尋找到可行域中的最優解。

  本文主要就如何使用keras_bert庫來實現第二種方法,以及當中可能存在的問題做闡述

  2 keras_bert 庫使用示例

  keras_bert庫是利用keras框架封裝BERT模型訓練使用的python包,源碼來自於https://github.com/CyberZHG/keras-bert,可以使用pip直接安裝👇

  pip install keras_bert

  官方給出的keras_bert使用示例是這樣的👇

  #! -*- coding:utf-8 -*-

  import json

  import numpy as np

  import pandas as pd

  from random import choice

  from keras_bert import load_trained_model_from_checkpoint, Tokenizer

  import re, os

  import codecs

  maxlen = 100

  config_path = '../bert/chinese_L-12_H-768_A-12/bert_config.json'

  checkpoint_path = '../bert/chinese_L-12_H-768_A-12/bert_model.ckpt'

  dict_path = '../bert/chinese_L-12_H-768_A-12/vocab.txt'

  token_dict = {}

  with codecs.open(dict_path, 'r', 'utf8') as reader:

  for line in reader:

  token = line.strip()

  token_dict[token] = len(token_dict)

  class OurTokenizer(Tokenizer):

  def _tokenize(self, text):

  R = []

  for c in text:

  if c in self._token_dict:

  R.append(c)

  elif self._is_space(c):

  R.append('[unused1]') # space類用未經訓練的[unused1]表示

  else:

  R.append('[UNK]') # 剩余的字符是[UNK]

  return R

  tokenizer = OurTokenizer(token_dict)

  neg = pd.read_excel('neg.xls', header=None)

  pos = pd.read_excel('pos.xls', header=None)

  data = []

  for d in neg[0]:

  data.append((d, 0))

  for d in pos[0]:

  data.append((d, 1))

  # 按照9:1的比例划分訓練集和驗證集

  random_order = range(len(data))

  np.random.shuffle(random_order)

  train_data = [data[j] for i, j in enumerate(random_order) if i % 10 != 0]

  valid_data = [data[j] for i, j in enumerate(random_order) if i % 10 == 0]

  def seq_padding(X, padding=0):

  L = [len(x) for x in X]

  ML = max(L)

  return np.array([

  np.concatenate([x, [padding] * (ML - len(x))]) if len(x) < ML else x for x in X

  ])

  class data_generator:

  def __init__(self, data, batch_size=32):

  self.data = data

  self.batch_size = batch_size

  self.steps = len(self.data) // self.batch_size

  if len(self.data) % self.batch_size != 0:

  self.steps += 1

  def __len__(self):

  return self.steps

  def __iter__(self):

  while True:

  idxs = range(len(self.data))

  np.random.shuffle(idxs)

  X1, X2, Y = [], [], []

  for i in idxs:

  d = self.data[i]

  text = d[0][:maxlen]

  x1, x2 = tokenizer.encode(first=text)

  y = d[1]

  X1.append(x1)

  X2.append(x2)

  Y.append([y])

  if len(X1) == self.batch_size or i == idxs[-1]:

  X1 = seq_padding(X1)

  X2 = seq_padding(X2)

  Y = seq_padding(Y)

  yield [X1, X2], Y

  [X1, X2, Y] = [], [], []

  from keras.layers import *

  from keras.models import Model

  import keras.backend as K

  from keras.optimizers import Adam

  bert_model = load_trained_model_from_checkpoint(config_path, checkpoint_path, seq_len=None)

  for l in bert_model.layers:

  l.trainable = True

  x1_in = Input(shape=(None,))

  x2_in = Input(shape=(None,))

  x = bert_model([x1_in, x2_in])

  x = Lambda(lambda x: x[:, 0])(x)

  p = Dense(1, activation='sigmoid')(x)

  model = Model([x1_in, x2_in], p)

  model.compile(

  loss='binary_crossentropy',

  optimizer=Adam(1e-5), # 用足夠小的學習率

  metrics=['accuracy']

  )

  model.summary()

  train_D = data_generator(train_data)

  valid_D = data_generator(valid_data)

  model.fit_generator(

  train_D.__iter__(),

  steps_per_epoch=len(train_D),

  epochs=5,

  validation_data=valid_D.__iter__(),

  validation_steps=len(valid_D)

  )

  上面這段代碼的模型構建的核心部分如下所示👇

  bert_model = load_trained_model_from_checkpoint(config_path, checkpoint_path, seq_len=None)

  for l in bert_model.layers:

  l.trainable = True

  x1_in = Input(shape=(None,))

  x2_in = Input(shape=(None,))

  x = bert_model([x1_in, x2_in])

  x = Lambda(lambda x: x[:, 0])(x)

  p = Dense(1, activation='sigmoid')(x)

  model = Model([x1_in, x2_in], p)

  model.compile(

  loss='binary_crossentropy',

  optimizer=Adam(1e-5), # 用足夠小的學習率

  metrics=['accuracy']

  )

  model.summary()

  我們首先需要導入bert_model,兩個參數config_path與checkpoint_path,在下載好BERT模型后是顯而易見的👇

  

 

  

 

  然后我們看到bert_model的是需要兩個輸入x1_in與x2_in,這兩個輸入怎么獲得?事實上這兩個輸入就是第一種使用BERT模型方法需要輸入的參數,即下面這段代碼中text2input函數返回的前兩個值(這段代碼即為BERT模型的第一種使用方法示例)👇

  ​

  # -*- 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()

  '''鄭州較好的婦科醫院 http://www.kd0371.com/

  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)

  知悉了keras_bert 的輸入變量的格式基本上問題就迎刃而解了。事實上僅一個bert_model 模型就有10億以上的參數數量,如果你將bert_model 的輸出結果繼續添加到復雜的神經網絡中,模型參數數量是極為可怕,基本上只有幾G內存或顯存的個人計算機連一個epoch 都跑不動,代碼是很難在本地調試的,因此筆者將就keras_bert可能發生的問題做一個匯總。

  3 keras_bert 庫使用問題填坑

  這部分筆者限於水平與經驗,舉幾個初次使用keras_bert常常會遇到的問題做解釋。

  3.1 使用已導出的帶有BERT模型的模型時發生未知層錯誤:Unknown Layers

  事實上如果在模型網絡層中存在自定義的層,如Attention層、Capsule層時,模型導出后再次導入就會出現未知層錯誤。此時在加載模型時需要將自定義的layer類作為custom_objects的參數以字典形式傳入👇

  model = load_model("model_weight.h5", custom_objects={'Attention':Attention,'Capsule':Capsule})

  當然我們並不能簡單得知BERT模型中到底使用了多少自定義層,因此需要使用get_custom_objects返回所有自定義的Layer信息,如下所示👇

  from keras_bert import get_custom_objects

  model = load_model("model_weight.h5",custom_objects=get_custom_objects())

  這里說一個小插曲,因為如果不將model導出,直接將訓練后的model用於預測是可以使用的,筆者一開始不知道如何解決這個問題就想,索性不使用model.save("model_weight.h5")來導出模型,直接用pickle庫把model以二進制數據導出:pickle.dump(model,open("model_weight.p","wb")),之后再以二進制數據輸入pickle.load(open("model_weight.p","rb"))就完事了,結果仍然會報同樣的錯誤。而且筆者發現這兩種導出模型的方法獲得的本地文件大小竟然是出奇的一致,看來model.save()函數就是基本上以二進制輸出流存儲了模型參數。

  3.2 使用已導出的帶有BERT模型的模型時發生值錯誤:ValueError: Tensor Tensor("dense_1/truediv:0", shape=(?, 100, 2015), dtype=float32) is not an element of this graph.

  將導出到外部存儲的模型重新載入后用於測試集標簽預測時,還會發生如上的ValueError,這個問題的解決方法需要用到如下的結構👇

  import tensorflow as tf

  global graph

  graph = tf.get_default_graph()

  with graph.as_default():

  ...

  這里的關鍵在於with graph.as_default()的位置應該放在哪里,如過你把整個函數的內容都框在里面又會報錯讓你不要做這種愚蠢的事情(tensorflow真的會報這種錯),筆者目前也沒有太弄清楚這個位置應該怎么放,經驗告訴我只要涉及測試集輸入數據的地方,就需要放在這個scope里,比如:

  with graph.as_default():

  input1 = numpy.array(inputs[0])

  input2 = numpy.array(inputs[1])

  input3 = numpy.array(input_pos_onehot)

  print(input1.shape) #

  print(input2.shape) #

  print(input3.shape) #

  y_pred = model.predict([input1,input2,input3])

  print(y_pred.shape)

  pickle.dump(y_pred,open("y_pred.p","wb"))

  這里原先的inputs與input_pos_onehot變量都是list類型的,如果在它們還是list時就放在這個scope里就會報錯(比如在上面這段代碼的前面是生成input1,input2,input3的流程,就別放在這個scope里了),但是寫成這種形式就不會再報錯了。筆者確實沒有太弄清楚這個機制是什么樣的,總之盡量少放點代碼在里面為好。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM