干货|在OpenVINO 中使用 fine-tuning 的 BERT模型
大家好,我是 Tango,目前就职于 NTTDATA (中国)信息技术有限公司。看到公司名大家估计也能猜出来这是一家日企了,我也是日语专业毕业的,出于爱好进入了程序员的队伍。今天和我一起来看看我们强大的 OpenVINO 套件和 BERT 模型会擦出什么样的火花。
Part 1 关于环境
在本篇文章中,将从使用了 Tensorflow 的 BERT 的 fine-tuning 到 OpenVINO 的的转换、推理进行演示。在正式开始写代码前,和大家介绍一下我的环境。* Mac OS M1 版本
* OpenVINO 2021.4.689
* TF2
Part 2 安装OpenVINO
OpenVINO 安装教程在树莓派上体验Win11 + OpenVINO
Part 3 涉及技术
conda create -n py39_tf python=3.9
conda activate py39_tf
pip install tensorflow -i https://pypi.tuna.tsinghua.edu.cn/simple
我在安装完成后,运行时出现如下错误80859 illegal hardware instruction最后,通过下载whl文件进行安装可以解决。
随便运行点项目验证一下环境十分搭建成功
import tensorflow as tf
import time
mnist = tf.keras.dataset***nist
(x_train, y_train),(x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
model = tf.kera***odels.Sequential([
tf.keras.layers.Flatten(input_shape=(28, 28)),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.2),
tf.keras.layers.Dense(10, activation='softmax')
])
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
start = time.time()
model.fit(x_train, y_train, epochs=5)
end = time.time()
model.evaluate(x_test, y_test)
print(end - start)
可以看到项目可以运行。
Start 开始正式内容
环境搭建建好,我们就要开始尝试利用BERT来做点什么了。我们使用的数据集是SQuAD日语部分。
SQuAD:斯坦福问答数据集(SQuAD)是一个全新的阅读理解数据集,由众包人员基于一系列****文章的提问和对应的答案构成,其中每个问题的答案是相关文章中的文本片段或区间。SQuAD 包含关于 500 多篇文章的超过 100000 个问答对,规模远远超过其他阅读理解数据集。
1. BERT的fine-tuning
* 下载数据集
git clone https://github.com/google-research/bert.git
mv bert tf1_bert && cd tf1_bert
mkdir JaSQuAD_v2.0 && cd JaSQuAD_v2.0
# 将之前准备好的数据集放到JaSQuAD_v2.0文件夹下面
unzip -j squad-japanese.zip
rm squad-japanese.zip
cd ../
wget "http://nlp.ist.i.kyoto-u.ac.jp/DLcounter/lime.cgi?down=http://lotus.kuee.kyoto-u.ac.jp/nl-resource/JapaneseBertPretrainedModel/Japanese_L-12_H-768_A-12_E-30_BPE.zip&name=Japanese_L-12_H-768_A-12_E-30_BPE.zip" -O ./Japanese_L-12_H-768_A-12_E-30_BPE.zip
unzip Japanese_L-12_H-768_A-12_E-30_BPE.zip && rm Japanese_L-12_H-768_A-12_E-30_BPE.zip
准本好数据后,我们可以通过如下的Python代码读取文件内容
* 修改内置py文件及方法
def read_ja_squad_examples(input_file, is_training):
example********r> with tf.gfile.Open(input_file, "r") as reader:
json_lines = reader.readline*****r> #print(json_line****r> for line in json_lines:
data = json.loads(line)
if is_training:
start_position=data['start']
end_position=data['end']
orig_answer_text=' '.join(data['context'].split(' ')[data['start']:data['end']+1])
else:
start_position = -1
end_position = -1
orig_answer_text = ""
example = SquadExample(
qas_id=data['id'],
question_text=data['question'],
doc_tokens=data['context'].split(' '),
orig_answer_text=orig_answer_text,
start_position=start_position,
end_position=end_position,
is_impossible=False
)
examples.append(example)
return examples
我们将上面的代码添加到 run_squad.py 中:
同时顺手将该文件中的 read_squad_examples 方法修改为run_ja_squad_examples
2. 将单词 token 化
我们可以利用项目中的 tokenization.py 文件进行 token 化(将单词转换为数组)。我们将训练时需要用的单词保存在了 vocab.txt 文件中,如果不在这个里面的单词会被标记为 UNK。
3. 命名训练出来的文件
用 run_squad.py 里面的 create_model 输出的回答的开始和结束位置用 tf.variable_scope( ) 来命名。
with tf.variable_scope("cls/squad/output"):
final_hidden_matrix = tf.reshape(final_hidden,
[batch_size * seq_length, hidden_size])
logits = tf.matmul(final_hidden_matrix, output_weights, transpose_b=True)
logits = tf.nn.bias_add(logits, output_bia****r>
logits = tf.reshape(logit*****atch_size, seq_length, 2])
logits = tf.transpose(logits, [2, 0, 1])
unstacked_logits = tf.unstack(logits, axis=0)
开始训练
export SQUAD_DIR=JaSQuAD_v2.0
export BERT_DIR=Japanese_L-12_H-768_A-12_E-30_BPE
export OUTPUT_DIR=SQUAD_DIR/model/Japanese_L-12_H-768_A-12_E-30_BPE
python run_squad.py \
--vocab_file=BERT_DIR/vocab.txt \
--bert_config_file=BERT_DIR/bert_config.json \
--init_checkpoint=BERT_DIR/bert_model.ckpt \
--do_train=True \
--train_file=SQUAD_DIR/train.jsonl \
--do_predict=True \
--predict_file=SQUAD_DIR/valid.jsonl \
--train_batch_size=12 \
--learning_rate=3e-5 \
--num_train_epochs=3.0 \
--max_seq_length=384 \
--doc_stride=128 \
--output_dir=$OUTPUT_DIR \
--version_2_with_negative=True > ./train_jp.log
为了在 OpenVINO 中进行推理,需要将生成好的 model.ckpt 转换为 frozen graph 形式。
* run_squad.py
def model_fn_builder(...):
┆
(start_logits, end_logit************r>
### add export script
import os, sy***r> from tensorflow.python.framework import graph_io
with tf.Session(graph=tf.get_default_graph()) as sess:
(assignment_map, initialized_variable_names) = \
modeling.get_assignment_map_from_checkpoint(tf.trainable_variables(), init_checkpoint)
tf.train.init_from_checkpoint(init_checkpoint, assignment_map)
sess.run(tf.global_variables_initializer())
frozen = tf.graph_util.convert_variables_to_constants(sess, sess.graph_def, ["cls/squad/output/unstack"])
graph_io.write_graph(frozen, './', 'inference_graph.pb', as_text=False)
print('BERT frozen model path {}'.format(os.path.join(os.path.dirname(__file__), 'inference_graph.pb')))
sys.exit(0)
###
tvars = tf.trainable_variables()
* modeling.py (923-924行)
#if not non_static_indexes:
# return shape
在次运行 run_squad.py 生成 inference_graph.pb。
python run_squad.py \
--vocab_file=BERT_DIR/vocab.txt \
--bert_config_file=BERT_DIR/bert_config.json \
--init_checkpoint=OUTPUT_DIR/model.ckpt-XXXXX \
--do_train=False \
--do_predict=True \
--predict_file=SQUAD_DIR/valid.jsonl \
--max_seq_length=384 \
--doc_stride=128 \
--output_dir=./tmp \
--version_2_with_negative=True
使用 ModelOptimizer 转换 IR
export MO_PATH=/opt/intel/openvino_2021/deployment_tool***odel_optimizer
python MO_PATH/mo_tf.py \
--input_model ./inference_graph.pb \
--input "IteratorGetNext:0{i32}[1 384],IteratorGetNext:1{i32}[1 384],IteratorGetNext:2{i32}[1 384]" \
--disable_nhwc_tonchw \
--output_dirOUTPUT_DIR/i
推理
import collection***r>import logging
import json
import six
import tokenization
class SquadExample(object):⋯
class InputFeatures(object):⋯
def read_ja_squad_examples(⋯):⋯
def convert_examples_to_features(⋯):⋯
def _improce_answer_span(⋯):⋯
def check_i***ax_context(⋯):⋯
def export_feature(vocab_file, data_file, is_training, do_lower_case, max_seq_length, doc_stride, max_query_length, use_ja_squad):
tokenizer = tokenization.FullTokenizer(
vocab_file=vocab_file, do_lower_case=False)
examples = read_ja_squad_example****r> input_file=data_file, is_training=is_training)
logging.info("Load {} examples".format(len(example******r> features = convert_examples_to_features(examples, tokenizer, max_seq_length,doc_stride, max_query_length, is_training)
return features
tf.logging 修改为logging
tf.gfile.Gfile() 修改为open()
最后修改的代码为
import osimport argparseimport timeimport numpy as npimport logging as logformatter = '[%(levelname)s] %(asctime)*****essage)s'log.basicConfig(level=log.INFO, format=formatter)from openvino.inference_engine import IECoreimport tokenization_helper as TokenizationHelperparser = argparse.ArgumentParser()parser.add_argument("-d", "--device", default="CPU", type=str)parser.add_argument("-v", "--vocab", required=True, type=str)parser.add_argument("-m", "--model", required=True, type=str)parser.add_argument("-i", "--input-data", required=True, type=str)parser.add_argument("--is-training", action="store_true")parser.add_argument("--max-seq-length", type=int, default=384)parser.add_argument("--doc-stride", type=int, default=128)parser.add_argument("--max-query-length", type=int, default=64)parser.add_argument("--do-lower-case", action="store_true")parser.add_argument("--use-ja-squad", action="store_true")args = parser.parse_args()def main(): log.info("Initializing Inference Engine") ie = IECore() version = ie.get_versions(args.device)[args.device] version_str = "{}.{}.{}".format(version.major, version.minor, version.build_number) log.info("Plugin version is {}".format(version_str)) # read IR model_xml = arg***odel model_bin = os.path.splitext(model_xml)[0] + ".bin" log.info("Loading network files:\n\t{}\n\t{}".format(model_xml, model_bin)) ie_encoder = ie.read_network(model=model_xml, weight***odel_bin) # load model to the device log.info("Loading model to the {}".format(args.device)) ie_encoder_exec = ie.load_network(network=ie_encoder, device_name=args.device) # check input and output names input_names = list(ie_encoder.input_info.keys()) output_names = list(ie_encoder.outputs.keys()) input_info_text = "Inputs number: {}".format(len(ie_encoder.input_info.keys())) for input_key in ie_encoder.input_info: input_info_text += "\n\t- {} : {}".format(input_key, ie_encoder.input_info[input_key].input_data.shape) log.info(input_info_text) output_info_text = "Outputs number: {}".format(len(ie_encoder.outputs.keys())) for output_key in ie_encoder.outputs: output_info_text += "\n\t- {} : {}".format(output_key, ie_encoder.outputs[output_key].shape) log.info(output_info_text) #TokenizationHelper. log.info("Start tokenization") input_features = TokenizationHelper.tokenize( vocab_file = args.vocab, data_file = args.input_data, is_training = args.is_training, do_lower_case = args.do_lower_case, max_seq_length = arg***ax_seq_length, doc_stride = args.doc_stride, max_query_length = arg***ax_query_length, use_ja_squad = args.use_ja_squad ) log.info("Complete tokenization") log.info("Predict start") for infer_index, feature in enumerate(input_features): # create numpy inputs for IE inputs = { input_names[0]: np.array([feature.input_ids], dtype=np.int32), input_names[1]: np.array([feature.input_mask], dtype=np.int32), input_names[2]: np.array([feature.segment_ids], dtype=np.int32), } # infer by IE t_start = time.perf_counter() res = ie_encoder_exec.infer(inputs=inputs) t_end = time.perf_counter() log.info("Inference time : {:0.2} sec".format(t_end - t_start)) start_logits = res[output_names[0]].flatten() end_logits = res[output_names[1]].flatten() start_index = np.argmax(start_logits) end_index = np.argmax(end_logits) print(start_index, end_index) tok_tokens = feature.tokens[start_index:(end_index + 1)] print(" ".join(tok_tokens)) # to infer only one breakif __name__ == "__main__": main()
生成结果
$ python openvino_bert_infer.py \
-v Japanese_L-12_H-768_A-12_E-30_BPE/vocab.txt \
-m JaSQuAD_v2.0/model/Japanese_L-12_H-768_A-12_E-30_BPE/ir/inference_graph.xml \
-i JaSQuAD_v2.0/valid.jsonl
[INFO] 2021-11-20 07:53:25,332 Initializing Inference Engine
[INFO] 2021-11-20 07:53:25,338 Plugin version is 2.1.2021.2.0-1877-176bdf51370-releases/2021/2
[INFO] 2021-11-20 07:53:25,338 Loading network files:
JaSQuAD_v2.0/20201223_model/Japanese_L-12_H-768_A-12_E-30_BPE/ir/inference_graph.xml
JaSQuAD_v2.0/20201223_model/Japanese_L-12_H-768_A-12_E-30_BPE/ir/inference_graph.bin
[INFO] 2021-11-20 07:53:25,571 Loading model to the CPU
[INFO] 2021-11-20 07:53:26,320 Inputs number: 3
- IteratorGetNext/placeholder_out_port_0 : [1, 384]
- IteratorGetNext/placeholder_out_port_1 : [1, 384]
- IteratorGetNext/placeholder_out_port_2 : [1, 384]
[INFO] 2021-11-20 07:53:26,320 Outputs number: 2
- cls/squad/output/unstack/Squeeze_ : [1, 384]
- cls/squad/output/unstack/Squeeze_1725 : [1, 384]
[INFO] 2021-11-20 07:53:26,320 Start tokenization
[INFO] 2021-11-20 07:53:26,365 Load 1 example***r>[INFO] 2021-11-20 07:53:26,369 Complete tokenization
[INFO] 2021-11-20 07:53:26,369 Predict start
11 53
ノルマン ( ノルマン :ノルマン ##ド [UNK] フランス 語 :ノルマン ##ド [UNK] ラテン :ノルマン ##ニ )
は 、 [UNK] 世 紀 および [UNK] 世 紀 に フランス の 地 域 ノルマンディー に 名 前 を 与 えた 人 々 でした 。
从结果中可以看到,对于"ノルマンディーはどの国にありますか?"这个问题的答案是"ノルマン(ノルマン :ノルマンド;フランス語 :ノルマンド;ラテン:ノルマンニ)は、10世紀および11世紀にフランスの地域ノルマンディーに名前を与えた人々でした 。"推理时间根据环境问题可以维持0.15 sec/request 左右。
文章中还有很多需要修改的地方,该数据集来源一个日本作者,其实是英文版通过谷歌翻译机翻过来的。有时间我们那可以试试中文版的效果如何。好了,这次的内容就是这些,如果有疑问欢迎加流学习。
本文来源于公众号:OpenVINO 中文社区