本文源碼已經上傳至 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
更多學習筆記見我的博客或關注微信公衆號 < 呼延十 >------>呼延十