本文源码已经上传至 github.: https://github.com/HuBlanker/Keras-Chinese-NERgithub
本文主要理论依据论文:Bidirectional LSTM-CRF Models for Sequence Taggingjson
命名实体识别(Named Entity Recognition,简称 NER),是指识别文本中具备特定意义的实体,主要包括人名、地名、机构名、专有名词等。简单的讲,就是识别天然文本中的实体指称的边界和类别。后端
NER 是 NLP 领域的一个经典问题,在文本情感分析,意图识别等领域都有应用。它的实现方式也多种多样,从最先基于规则和词典,到传统机器学习到如今的深度学习。本文采用当前的经典解决方案,基于深度学习的 BiLSTM-CRF 模型
来解决 NER 问题。api
本文主要依据于 Bidirectional LSTM-CRF Models for Sequence Tagging 论文,并参考 github 上部分项目,实现了 基于 BilSTM-CRF 的中文文本命名实体识别,以用做 搜索中的意图识别。[]() 源码中包含完整的训练及部署代码,还有数据集的示例。数组
个人目的是,使用 中文样本训练模型,而后在线提供预测,用于线上的搜索服务。因此本文可能对原理的介绍比较少,主要集中于 实际操做。对于 用 BiLSTM-CRF 来实现 NER
概念尚不清楚的同窗,能够点击上方的论文了解一下,或者自行搜索了解。bash
训练过程分为如下几个部分:微信
那么让咱们来一步一步的解决这些问题。首先是样本数据部分。
咱们采用的格式是 字符-label. 也就是以下面这样,每一个字符和其标签一一对应,句子与句子之间用空行隔开。
这里数据中的全部标签是常见的 地名
, 人名
, 机构名
标签,其中 B-LOC
对应着一个地名的开始,O-LOC
对应着一个地名的中间部分。O
表明未识别部分,也就是Other
. 其余的以此类推。
经过这样的数据,咱们能够 拿到每个实体的边界,进行切分以后就能够拿到有效的实体识别数据。
6 O 月 O 油 O 印 O 的 O 《 O 北 B-LOC 京 I-LOC 文 O 物 O 保 O 存 O 保 O 管 O 状 O 态 O 之 O 调 O 查 O 报 O 告 O 》 O , O 调 O 查 O 范 O 围 O 涉 O 及 O 故 B-LOC 宫 I-LOC 、 O 历 B-LOC 博 I-LOC 、 O 古 B-ORG 研 I-ORG 所 I-ORG 、 O 北 B-LOC 大 I-LOC 清 I-LOC 华 I-LOC 图 I-LOC 书 I-LOC 馆 I-LOC 、 O 北 B-LOC 图 I-LOC 、 O 日 B-LOC 伪 O 资 O 料 O
我本人使用的样本是本身生成及标注的一部分,涉及到我的数据,不方便放到 github 中,所以 github 中仅有一个数据集的格式示例。
须要强调的是:对于 BiLSTM-CRF 模型解决 NER 问题来说,理论已经在论文中说的十分明白,模型搭建代码网上也是有不少不错的可使用的代码。
那么,重中之重就是样本的整理,固然这是一个逐步优化的过程,咱们可使用一部分样原本训练,以后逐步标注,或者用其余方式生成一些正确的样本。
在 github 仓库里,有完整的可用于训练的代码,我进行了脱敏,可是彻底不影响理解及执行。这里仅大体的贴一下核心代码。
首先是对数据进行编码的代码,经过对全部训练数据 char 级别的编码,来让模型能够"认识" 咱们的数据:
# 对传入目录下的训练和测试文件进行 char 级别的编码,以及加载已有的编码文件, # 只有在更换训练文件以后才须要 gen, 其余时间直接 load 便可。 class Word2Id: def __init__(self, file): self.file = file def gen_save(self): data_file = [args.train_data, args.test_data] all_char = [] for f in data_file: file = open(f, "rb") data = file.read().decode("utf-8") data = data.split("\n\n") data = [token.split("\n") for token in data] data = [[j.split() for j in i] for i in data] data.pop() all_char.extend([char[0] if char else 'unk' for sen in data for char in sen]) chars = set(all_char) word2id = {char: id_ + 1 for id_, char in enumerate(chars)} word2id["unk"] = 0 with open(self.file, "wb") as f: f.write(json.dumps(word2id, ensure_ascii=False).encode('utf-8')) def load(self): return json.load(open(self.file, 'r'))
2.1.4 版本的 keras,在 keras 版本里面已经包含 bilstm 模型,CRF 模型包含在 keras-contrib 中。
双向 LSTM 和单向 LSTM 的区别是用到 Bidirectional。
模型结构为一层 embedding 层+一层 BiLSTM+一层 CRF。
代码不难,且加了一些关键注释,以下:
# BILSTM-CRF 模型 class Ner: def __init__(self, vocab, labels_category, Embedding_dim=200): self.Embedding_dim = Embedding_dim self.vocab = vocab self.labels_category = labels_category self.model = self.build_model() # 构建模型 def build_model(self): model = Sequential() # embedding 层 model.add(Embedding(len(self.vocab), self.Embedding_dim, mask_zero=True)) # Random embedding # bilstm 层 model.add(Bidirectional(LSTM(100, return_sequences=True))) # crf 层 crf = CRF(len(self.labels_category), sparse_target=True) model.add(crf) model.summary() model.compile('adam', loss=crf.loss_function, metrics=[crf.accuracy]) return model # 训练方法 def train(self, data, label, EPOCHS): self.model.fit(data, label, batch_size=args.batch_size, callbacks=[CallBack()], epochs=EPOCHS) # 加载已有的模型进行训练 def retrain(self, model_path, data, label, epoch): model = self.load_model_fromfile(model_path) print("load model, evaluate it.") loss, accuracy = model.evaluate(data, label) print("load model, loss = %s, acc =%s ." % (loss, accuracy)) model.fit(data, label, batch_size=124, callbacks=[CallBack()], epochs=epoch) # 从给定的目录加载一个模型 def load_model_fromfile(self, model_path): crf = CRF(len(self.labels_category), sparse_target=True) return load_model(model_path, custom_objects={"CRF": CRF, 'crf_loss': crf.loss_function, 'crf_viterbi_accuracy': crf.accuracy}) # 预测,主要用于交互式的测试某些样本的预测结果。我我的习惯在训练完成以后手动测试一些常见的 case, def predict(self, model_path, data, maxlen): model = self.model char2id = [self.vocab.get(i) for i in data] input_data = pad_sequences([char2id], maxlen) model.load_weights(model_path) result = model.predict(input_data)[0][-len(data):] result_label = [np.argmax(i) for i in result] return result_label # 测试,能够用某个测试集跑一下模型,看看效果 def test(self, model_path, data, label): model = self.load_model_fromfile(model_path) loss, acc = model.evaluate(data, label) return loss, acc
在咱们用其余方式处理完数据以后,咱们拿到了咱们想要的格式,可是这个格式并非能够直接被模型接受的,所以咱们须要加载数据,而且进行一些处理,好比编码或者 padding.
# 处理数据集 class DataSet: def __init__(self, data_path, labels): with open(data_path, "rb") as f: self.data = f.read().decode("utf-8") self.process_data = self.process_data() self.labels = labels def process_data(self): # 读取样本并分割 train_data = self.data.split("\n\n") train_data = [token.split("\n") for token in train_data] train_data = [[j.split() for j in i] for i in train_data] train_data.pop() return train_data def generate_data(self, vocab, maxlen): char_data_sen = [[token[0] for token in i] for i in self.process_data] label_sen = [[token[1] for token in i] for i in self.process_data] # 对样本进行编码 sen2id = [[vocab.get(char, 0) for char in sen] for sen in char_data_sen] # 对样本中的标签进行编码 label2id = {label: id_ for id_, label in enumerate(self.labels)} lab_sen2id = [[label2id.get(lab, 0) for lab in sen] for sen in label_sen] # padding sen_pad = pad_sequences(sen2id, maxlen) lab_pad = pad_sequences(lab_sen2id, maxlen, value=-1) lab_pad = np.expand_dims(lab_pad, 2) return sen_pad, lab_pad
进行完上线的三个步骤以后,咱们基本上就能够进行训练了。
还有一部分的功能性代码,好比启动参数,模型保存格式等没有贴出来,使用的时候能够直接从 github 上看一下就好。
在 python3, keras 2.2.4 环境下,执行 python3 model.py --mode=train
, 便可开始训练,会将模型自动保存到 model 路径下,保存为 H5 和 SavedModel 两种格式。
模型运行期间及每一次 epoch 运行结束,会打印响应的 loss 及 accuracy. 以下图所示:
此外还能够运行python3 model.py --mode=predict --input_model_dir=model
来进行交互式的预测。
离线训练获得了效果让咱们满意的模型以后,就是在线预测的流程了。
tensorflow 模型如何部署到线上,一直是比较花里胡哨的,针对这种状况 Google 提供了 TensorFlow Servering,能够用一套标准化的流程,将训练好的模型直接上线并提供服务。
TensorFlow Serving 是一个用于机器学习模型 serving 的高性能开源库。它能够将训练好的机器学习模型部署到线上,使用 gRPC 做为接口接受外部调用。它支持模型热更新与自动模型版本管理。这意味着一旦部署 TensorFlow Serving 后,再也不须要为线上服务操心,只须要关心你的线下模型训练。
tensorflow serving 持续集成的大概流程以下:
基于 TF Serving 的持续集成框架仍是挺简明的,基本分三个步骤:
主要包括数据的收集和清洗、模型的训练、评测和优化。
将前一个步骤训练好的模型保存为指定的格式,以后在 TF Server 中上线;
客户端经过 gRPC 和 RESTfull API 两种方式同 TF Servering 端进行通讯,并获取服务,进行在线预测。
TF Serving 工做流程以下:
要想使用 tensorflow serving 来部署模型,须要将模型保存为特定的格式。
若是你是使用 keras models 构建的模型,那么直接tf.saved_model.save(self.model, save_dir)
便可。
若是你是使用 keras sequential 构建的模型,那么使用下面的方法,可让你将序列模型保存为 SavedModel 格式。
def export_saved_model(self, saved_dir, epoch): model_version = epoch model_signature = tf.saved_model.signature_def_utils.predict_signature_def( inputs={'input': self.model.input}, outputs={'output': self.model.output}) export_path = os.path.join(compat.as_bytes(saved_dir), compat.as_bytes(str(model_version))) builder = tf.saved_model.builder.SavedModelBuilder(export_path) builder.add_meta_graph_and_variables( sess=K.get_session(), tags=[tf.saved_model.tag_constants.SERVING], clear_devices=True, signature_def_map={ tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: model_signature }) builder.save()
将训练完毕的模型放到 serving 下对应的目录,让 serving 进行加载,模型文件树应该以下:
.
我在服务端启动 serving 的时候,使用了以下命令:
cmd="./tensorflow_model_server \ --port=4590 \ --rest_api_port=4591 \ --model_config_file=model/ \ --tensorflow_session_parallelism=40 \ --per_process_gpu_memory_fraction=0.2"
意味着我读取当前目录下 model 文件夹下的模型,加载而且对外提供了 RESTFUL 服务(在 4590 端口)以及 grpc 服务(在 4591 端口).
serving 对外提供了 RESTFUL 接口以及 GRPC 接口,足够咱们使用了。
RESTFUL
在命令行执行curl -d '{"inputs": [[348.0,3848.0,2557]]}' -X POST http://localhost:4591/v1/models/model:predict
, 其中,inputs 是在输出模型时定义的模型输入数据。也就是模型签名。
若是不肯定本身的模型定义,可使用 tensorflow 自带的saved_model_cli.py
文件来查看,首先运行find / -name="saved_model_cli.py"
, 找到本机上的对应文件,若是没有,能够去下载 TensorFlow 的源码,其中包括这个文件。
而后执行 python saved_model_cli.py show --dir model/15 --all
, 就能够看到下面这样的输出。
个人模型定义了:
名为"input"的输入,是一个二维的矩阵。
名为"output"的输出,是一个三维的矩阵。
模型返回的预测结果为一个三维数据,其中每个数组表明一个字符所在的标签。
以 "王强" 为例。
获得的结果为 shapre=(1,2,7) 的数组,其中 1 指的是咱们只输入了一个句子,2 指的是句子的长度,7 指的是咱们全部 tag 的长度。
[ [0,1,0,0,0,0,0] [0,0,1,0,0,0,0] ]
标签顺序是:[O, B-PER, I-PER, B-LOC, I-LOC, B-ORG, I-ORG]
用1
所在的下标对应到标签中,能够发现王强
的结果是B-PER, I-PER
, 也就是一我的名。
grpc
输入输出和 RESTFUL 是同样的,只是方式可能有点不同,这里简单的贴一下集成 GRPC 的那块代码。
public static void main(String[] args) { // 构造请求 ManagedChannel channel = ManagedChannelBuilder.forAddress("192.168.1.251", 7010).usePlaintext(true).build(); PredictionServiceGrpc.PredictionServiceBlockingStub stub = PredictionServiceGrpc.newBlockingStub(channel); Predict.PredictRequest.Builder predictRequestBuilder = Predict.PredictRequest.newBuilder(); Model.ModelSpec.Builder modelSpecBuilder = Model.ModelSpec.newBuilder(); // 你的模型的名字 modelSpecBuilder.setName("model"); modelSpecBuilder.setSignatureName(""); predictRequestBuilder.setModelSpec(modelSpecBuilder); TensorProto.Builder tensorProtoBuilder = TensorProto.newBuilder(); // 模型接受的数据类型 tensorProtoBuilder.setDtype(DataType.DT_FLOAT); TensorShapeProto.Builder tensorShapeBuilder = TensorShapeProto.newBuilder(); // 接受数据的 shape, 几维的数组,每一维多少个。个人测试数据是三个。 tensorShapeBuilder.addDim(TensorShapeProto.Dim.newBuilder().setSize(1)); tensorShapeBuilder.addDim(TensorShapeProto.Dim.newBuilder().setSize(3)); // 个人测试数据,这里须要把输入的字符串进行编码。好比在个人编码下,好比将 : 呼延十 编码成下面三个数字。 String s = "呼延十"; List<Float> ret = new ArrayList<>(); ret.add(348.0f); ret.add(3848.0f); ret.add(2557.0f); tensorProtoBuilder.setTensorShape(tensorShapeBuilder.build()); tensorProtoBuilder.addAllFloatVal(ret); predictRequestBuilder.putInputs("input", tensorProtoBuilder.build()); Predict.PredictResponse predictResponse = stub.predict(predictRequestBuilder.build()); // 这里拿到的是一个 (1,1,3) 的矩阵。因此咱们须要把他解码成咱们想要的 tag. 涉及到你的 tag 列表。 List<Float> output = predictResponse.getOutputsMap().get("output").getFloatValList(); List<String> tags = Arrays.asList("O", "B-PER", "I-PER", "B-LOC", "I-LOC", "B-ORG", "i-ORG"); List<String> rets = phraseFrom(s, output, tags); System.out.println(rets); } private static List<String> phraseFrom(String q, List<Float> output, List<String> tags) { List<List<Float>> partition = Lists.partition(output, tags.size()); List<Integer> idx = new ArrayList<>(); for (List<Float> floats : partition) { for (int j = 0; j < floats.size(); j++) { if (floats.get(j) == 1.0f) { idx.add(j); break; } } } assert q.length() != idx.size(); // 从 query 和每一个字的 tag 解析成词语的意图。 StringBuilder sb = new StringBuilder(); char[] chars = q.toCharArray(); List<String> rets = new ArrayList<>(); for (int i = 0; i < chars.length; i++) { Integer tag = idx.get(i); if ((tag & 1) == 1 && sb.length() != 0) { String item = sb.toString(); String ret = tags.get(idx.get(i - 1)); rets.add(ret); sb.setLength(0); sb.append(chars[i]); } else { sb.append(chars[i]); } } if (sb.length() != 0) { String ret = tags.get(idx.get(q.length() - 1)); rets.add(ret); } return rets; }
项目开发完成后,模型预测正确率 97%(训练了 30 个 epoch), 线上预测与 TensorFlow serving 交互耗时 20ms.
python 3.6.4
keras 2.2.4
tensorflow-gpu 1.14.0
JDK 1.8
Bidirectional LSTM-CRF Models for Sequence Tagging
最后,欢迎关注个人我的公众号【 呼延十 】,会不按期更新不少后端工程师的学习笔记。
也欢迎直接公众号私信或者邮箱联系我,必定知无不言,言无不尽。
完。
以上皆为我的所思所得,若有错误欢迎评论区指正。
欢迎转载,烦请署名并保留原文连接。
联系邮箱:huyanshi2580@gmail.com
更多学习笔记见我的博客或关注微信公众号 < 呼延十 >------>呼延十