模型固化(深度神经网络+crf分词以及NER)

小孔同学推荐了两个基于循环神经网络以及CRF处理nlp问题的算法,分别是中分分词以及命名实体识别,都是github上开源的项目,之前也用过一些开源的分词工具以及产品级分词工具,比如波森nlp,发现github上这两个【BiLSTM+CRF】 【NeuroNER】,效果都非常不错,感谢koth 以及 Franck Dernoncourt。在这不分析模型,只说下把两个模型frozen一下,然后一块封装到nlp相关的接口中,通过flask可以非常方便的发布供项目其他模块使用~

分词模型

按照README中介绍的过程训练模型,训练完成后,想把分词作为工程的其他模块通过接口的形式发布出来,在这直接使用了作者固化出来的模型在Python中加载,最终脚本如下所示,指示单句子分词,如果是长文本,参照着作者github中 kcws/cc/seg_backend_api.cc 调用的分割函数,分割成短文本即可。

"""
分词主类,加载预先训练好并固化好的模型文件,加载训练生成的词-id映射表并进行分词
"""
import numpy as np
import tensorflow as tf
import math


class Node:
    """
    trie树中的节点, weight=0 表示该节点不是叶子节点
    """
    def __init__(self):
        self.weight = 0.0
        self.next = {}


class Trie:
    """
    trie 树实现,用来添加用户自定义词典
    每个节点包括一个权重以及指向下一个节点的连接
    权重初始化为0,当该节点是词的最终节点时,权重赋值为用户自定义词典的权重
    """
    def __init__(self):
        self.root = Node()

    def push_node(self, word, weight):
        """
        将词与权重压入构建trie数的节点以及路径
        :param word:  词
        :param weight: 权重
        :return: None
        """
        word_len = len(word)
        if word_len == 0: return
        temp = self.root
        for index, char in enumerate(word):
            if char in temp.next:
                temp = temp.next[char]
                if index + 1 == word_len: temp.weight = weight
                continue
            else:
                temp.next[char] = Node()
                temp = temp.next[char]
                if index + 1 == word_len: temp.weight = weight

    def search(self, sentence):
        """
        对输入的文本通过trie树做检索,查找文本中出现的用户自定义词,并返回该词所在位置以及该词权重
        :param sentence:  文本
        :return: eg: [([0, 1], 4.0), ([9, 10, 11], 4.0)]
            tuple 列表,其中[0, 1]表示该词出现在句子的第0,1索引出, 4.0表示用户对该词定义的权重
        """
        fake_list = []
        word_len = len(sentence)
        if word_len == 0: return fake_list
        point = self.root
        index = 0
        pre_match = -1
        word_range = None
        while index < word_len:
            word = sentence[index]
            if word in point.next:
                if pre_match < 0: pre_match = index
                point = point.next[word]
                index += 1
                if point.weight > 0: word_range = ([i for i in range(pre_match, index)], point.weight)
                if index == word_len: fake_list.append(word_range)
                continue
            else:
                if word_range: fake_list.append(word_range)
                word_range = None
                point = self.root
                index = pre_match + 1 if pre_match >= 0 else index + 1
                pre_match = -1
        return fake_list


class Segment:
    def __init__(self, frozen_model_path, vocab_path, user_dict_path=None):
        self.seq_max_len = 80
        self.graph = None
        self.frozen_model_path = frozen_model_path  
        self.base_vocab_path = vocab_path  
        self.user_dict_path = user_dict_path
        self.user_dict = None
        self.sess = None
        self.graph = None
        self.input = None
        self.transitions = None
        self.unary_score = None
        self.word_2_index = dict()
        self.load_model()

    def load_model(self):
        """
        加载深度神经网络模型,提取输入节点以及输出节点
        得到状态转移概率矩阵
        Returns: None
        """
        self.load_basic_vocab()
        self.load_user_dict()
        with tf.gfile.GFile(self.frozen_model_path, "rb") as f:
            graph_def = tf.GraphDef()
            graph_def.ParseFromString(f.read())
        with tf.Graph().as_default() as graph:
            tf.import_graph_def(graph_def, input_map=None, return_elements=None, name="prefix", op_dict=None,
                                producer_op_list=None)
        self.graph = graph
        self.sess = tf.Session(graph=self.graph)
        transitions_ = self.graph.get_tensor_by_name("prefix/transitions:0")
        self.unary_score = self.graph.get_tensor_by_name("prefix/Reshape_7:0")
        self.input = self.graph.get_tensor_by_name("prefix/input_placeholder:0")
        self.transitions = self.sess.run([transitions_])
        self.transitions = np.squeeze(np.array(self.transitions), axis=0)

    def load_basic_vocab(self):
        """
        加载词映射表  汉字:id
        Returns: None
        """
        with open(self.base_vocab_path, "r") as file_rd:
            for line in file_rd.readlines():
                line = line.strip()
                [word, index] = line.split("    ")
                try:
                    self.word_2_index[word] = int(index)
                except Exception: pass

    def load_user_dict(self):
        """
        加载用户自定义词典,生成trie树,viterbi解码前对unary code 进行修改
        :return: None
        """
        if not self.user_dict_path:
            self.user_dict = None
            return
        else:
            self.user_dict = Trie()
            with open(self.user_dict_path, "r") as file_rd:
                for line in file_rd.readlines():
                    line = line.strip()
                    if line and line == "": continue
                    try:
                        word, weight = line.split(" ")
                        self.user_dict.push_node(word, float(weight))
                    except Exception:
                        pass

    def fake_predication(self, unary_scores, sentence):
        """
        通过用户自定义词典检索句子,修改自定义词处的概率向量,用于后续的viterbi解码
        :param unary_scores: 句子的状态概率矩阵,shape=(sentence_len, state_len) 
        :param sentence: 原句子
        :return: None
        """
        fake_list = self.user_dict.search(sentence)
        sentence_len = unary_scores.shape[0]
        for item in fake_list:
            if max(item[0]) > sentence_len:
                return
            word_range = item[0]
            word_weight = item[1]
            word_len = len(word_range)
            for index in range(word_len):
                weight_total = 4.0 + word_weight
                if index == 0:
                    weights = [1.0, 1.0 + word_weight, 1.0, 1.0]
                elif index + 1 == word_len:
                    weights = [1.0, 1.0, 1.0, 1.0 + word_weight]
                else:
                    weights = [1.0, 1.0, 1.0 + word_weight, 1.0]
                weights = [math.log(i / weight_total) for i in weights]
                unary_scores[word_range[index]] = weights

    def predict(self, content):
        """
        分割主函数
        Args:
            content: 待分割的文本
            eg: 赵雅淇洒泪道歉和林丹没有任何经济关系
        Returns: 分割后的字符串
            eg: [{"tok": "赵雅琪"},{"tok": "洒泪"},{"tok": "道歉"},{"tok": "和"},
            {"tok": "林丹"},{"tok": "没有"},{"tok": "任何"},{"tok": "经济"},{"tok": "关系"}]
        """
        result = []
        word_index_seq = []
        for word in content:
            if word in self.word_2_index:
                word_index_seq.append(self.word_2_index[word])
            else:
                word_index_seq.append(1)
        word_index_seq.extend([0] * (self.seq_max_len - len(word_index_seq)))
        feed_input = np.expand_dims(np.array(word_index_seq), axis=0)
        unary_score_val = self.sess.run([self.unary_score], {self.input: feed_input})
        seq_len = sum([1 for item in word_index_seq if item > 0])
        tf_unary_scores_ = np.squeeze(unary_score_val[0], axis=0)
        tf_unary_scores_ = tf_unary_scores_[:seq_len]
        if self.user_dict:
            # 是否启用用户自定义词典进行优化分词结果
            self.fake_predication(tf_unary_scores_, content)
        tag_sequence, _ = tf.contrib.crf.viterbi_decode(tf_unary_scores_, self.transitions)
        pre_word = ""
        for tag, word in zip(tag_sequence, content):
            if tag == 0:
                pre_word = ""
                result.append({"tok": word})
            elif tag == 1 or tag == 2:
                pre_word += word
            elif tag == 3:
                pre_word += word
                result.append({"tok": pre_word})
                pre_word = ""
        return result

其中frozen_model_path是frozen之后生成的pb文件,vocab_path则是词-ID对照文件,user_dict_path是用户自定义词典,如图所示:


模型固化(深度神经网络+crf分词以及NER)_第1张图片

参照着koth的方法,网络输出的句子状态概率矩阵,在进行viterbi解码之前通过用户自定义词典以及权重进行调整,之后再解码,如果让我自己去想怎么实现,我可能会直接在viterbi解码后的状态向量,通过自定义词强行进行拆分合并。

命名实体识别模型

命名实体识别的项目也用了相似的,感兴趣的不妨也参考下,最终大致也是通过相同的方式对模型进行固化,然后封装到接口中。

# -*- coding:utf-8 -*-
"""
命名实体识别主类
加载预先训练好并固化好的模型文件,加载词向量模型文件
"""
import numpy as np
import tensorflow as tf
import dataset_predict as ds


class NERPredict:
    def __init__(self, frozen_model_path, word_2_vec_path, trained_model_path):
        self.graph = None
        self.frozen_model_path = frozen_model_path
        self.word_vec_path = word_2_vec_path
        self.pre_trained_model_path = trained_model_path
        with tf.gfile.GFile(self.frozen_model_path, "rb") as f:
            graph_def = tf.GraphDef()
            graph_def.ParseFromString(f.read())
        with tf.Graph().as_default() as graph:
            tf.import_graph_def(graph_def, input_map=None, return_elements=None, name="prefix", op_dict=None,
                                producer_op_list=None)
        self.graph = graph
        self.transition_params_trained = self.graph.get_tensor_by_name('prefix/crf/transitions:0')
        self.input_token_indices = self.graph.get_tensor_by_name('prefix/input_token_indices:0')
        self.dropout_keep_prob = self.graph.get_tensor_by_name('prefix/dropout_keep_prob:0')
        self.unary_scores = self.graph.get_tensor_by_name('prefix/feedforward_before_crf/scores:0')
        self.predictions = self.graph.get_tensor_by_name('prefix/feedforward_before_crf/predictions:0')
        self.sess = tf.Session(graph=self.graph)
        self.dataset = ds.Dataset(verbose=False, debug=False)
        self.parameters = {
            'token_pretrained_embedding_filepath': self.word_vec_path,
            'pretrained_model_folder': self.pre_trained_model_path,
            'remap_unknown_tokens_to_unk': True
        }
        self.dataset.load_dataset(self.parameters)
        transition_temp = self.sess.run([self.transition_params_trained])
        transition_temp = np.array(transition_temp)
        self.transition = np.squeeze(transition_temp, axis=0)

    def predict(self, tokens_of_sentence):
        """
        实体识别主函数
        Args:
            content: 词序列
        Returns: 实体识别结果
        """
        small_score = -1000.0
        large_score = 0.0
        if isinstance(tokens_of_sentence, str):
            tokens_of_sentence = tokens_of_sentence.split(" ")
            token_indices, label_indices, character_indices_padded, character_indices, token_lengths, \
            characters, label_vector_indices = self.dataset.convert_to_indices_for_sentence(
                tokens_of_sentence)
            feed_dict = {
                self.input_token_indices: token_indices["deploy"][0],
                self.dropout_keep_prob: 1.
            }
            unary_scores, predictions = self.sess.run([self.unary_scores, self.predictions], feed_dict)
            unary_scores = np.squeeze(np.array(unary_scores))
            concate_array = np.ones(shape=(unary_scores.shape[0], 2), dtype=np.float32) * small_score
            unary_scores = np.concatenate((unary_scores, concate_array), axis=1)
            start_unary_scores = np.array([[small_score] * self.dataset.number_of_classes + [large_score, small_score]])
            end_unary_scores = np.array([[small_score] * self.dataset.number_of_classes + [small_score, large_score]])
            unary_scores = np.concatenate((start_unary_scores, unary_scores, end_unary_scores), axis=0)
            predictions, _ = tf.contrib.crf.viterbi_decode(unary_scores, self.transition)
            predictions = predictions[1:-1]
            assert (len(predictions) == len(tokens_of_sentence))
            predictions = [self.dataset.index_to_label[index] for index in predictions]
            return predictions

dataset_predict 是工程自带的一个类,偷懒直接纠过来用了,至于怎样frozen原模型生成pb文件,在这不在介绍。从代码中可以看到循环神经网络结合CRF 做分词以及命名实体识别的模型结构几乎是一致的,最终也都用到了viterbi解码,有兴趣的可以参考下。

模型固化部分(针对ner项目)

"""
frozen the model test
"""
graph = tf.get_default_graph()
input_graph_def = graph.as_graph_def()
output_nodes = ['crf/transitions', 'feedforward_before_crf/scores', 'feedforward_before_crf/predictions']
output_graph_def = graph_util.convert_variables_to_constants(sess, input_graph_def, output_nodes)
with tf.gfile.GFile("frozen_model.pb", "wb") as f:
f.write(output_graph_def.SerializeToString())
print("frozen the ner model")
"""
frozen model test finish
"""

欢迎一块交流讨论,文中有错误的地方,还请指正,谢谢~
email: [email protected]

你可能感兴趣的:(模型固化(深度神经网络+crf分词以及NER))