超级详细手把手讲解BiLSTM+CRF完成命名实体识别(一)

由于篇幅较大,打算分三篇博客来写,下面是第一部分,即用训练好的模型对句子进行预测。

全部代码先放上,然后将过程大体讲解一下:

data.py

import sys, pickle, os, random
import numpy as np


#第一步:数据处理
#pikle是一个将任意复杂的对象转成对象的文本或二进制表示的过程。
#同样,必须能够将对象经过序列化后的形式恢复到原有的对象。
#在 Python 中,这种序列化过程称为 pickle,
#可以将对象 pickle 成字符串、磁盘上的文件或者任何类似于文件的对象,
#也可以将这些字符串、文件或任何类似于文件的对象 unpickle 成原来的对象。


## tags, BIO
tag2label = {"O": 0,
             "B-PER": 1, "I-PER": 2,
             "B-LOC": 3, "I-LOC": 4,
             "B-ORG": 5, "I-ORG": 6
             }


#输入train_data文件的路径,读取训练集的语料,输出train_data
def read_corpus(corpus_path):
    """
    read corpus and return the list of samples
    :param corpus_path:
    :return: data
    """
    data = []
    with open(corpus_path, encoding='utf-8') as fr:
        '''lines的形状为['北\tB-LOC\n','京\tI-LOC\n','的\tO\n','...']总共有2220537个字及对应的tag'''
        lines = fr.readlines()
    sent_, tag_ = [], []
    for line in lines:
        if line != '\n':#每句话之间以换行符为区分
            # char 与 label之间有个空格
            # line.strip()的意思是去掉每句话句首句尾的空格
            # .split()的意思是根据空格来把整句话切割成一片片独立的字符串放到数组中,同时删除句子中的换行符号\n
            [char, label] = line.strip().split()
            # 把一个个的字放进sent_
            sent_.append(char)
            # 把字后面的tag放进tag_
            tag_.append(label)
        else:#一句话结束了,添加到data
            data.append((sent_, tag_))
            sent_, tag_ = [], []
    """ data的形状为[(['我',在'北','京'],['O','O','B-LOC','I-LOC'])...第一句话
                         (['我',在'天','安','门'],['O','O','B-LOC','I-LOC','I-LOC'])...第二句话  
                          ( 第三句话 )  ] 总共有50658句话"""
    return data


#由train_data来构造一个(统计非重复字)字典{'第一个字':[对应的id,该字出现的次数],'第二个字':[对应的id,该字出现的次数], , ,}
#去除低频词,生成一个word_id的字典并保存在输入的vocab_path的路径下,
#保存的方法是pickle模块自带的dump方法,保存后的文件格式是word2id.pkl文件
def vocab_build(vocab_path, corpus_path, min_count):
    """

    :param vocab_path:
    :param corpus_path:
    :param min_count:
    :return:
    """
    data = read_corpus(corpus_path)
    word2id = {}
    #sent_的形状为['我','在','北','京'],对应的tag_为['O','O','B-LOC','I-LOC']
    for sent_, tag_ in data:
        for word in sent_:
            # 如果字符串只包含数字则返回 True 否则返回 False。
            if word.isdigit():
                word = '<NUM>'
            #A-Z:(\u0041-\u005a)    a-z :\u0061-\u007a
            elif ('\u0041' <= word <='\u005a') or ('\u0061' <= word <='\u007a'):
                word = '<ENG>'
            if word not in word2id:#是新词
                # [len(word2id)+1, 1]用来统计[位置标签,出现次数],第一次出现定为1
                word2id[word] = [len(word2id)+1, 1]#[对应的id,出现的次数]
            else:#不是新词
                # word2id[word][1]实现对词频的统计,出现次数累加1
                word2id[word][1] += 1


    #其实前面统计词频的目的就是这里删除低频词,删除完之后也就不用统计词频了
    #用来统计低频词
    low_freq_words = []
    for word, [word_id, word_freq] in word2id.items():
        #寻找低于某个数字的低频词
        if word_freq < min_count and word != '<NUM>' and word != '<ENG>':
            low_freq_words.append(word)
    for word in low_freq_words:
        # 把这些低频词从字典中删除
        del word2id[word]

    # 删除低频词后为每个字重新建立id,而不再统计词频
    new_id = 1
    for word in word2id.keys():
        word2id[word] = new_id
        new_id += 1
    word2id['<UNK>'] = new_id
    word2id['<PAD>'] = 0

    print(len(word2id))
    with open(vocab_path, 'wb') as fw:
        # 序列化到名字为word2id.pkl文件
        pickle.dump(word2id, fw)


#输入一句话,生成一个 sentence_id
'''sentence_id的形状为[1,2,3,4,...]对应的sent为['当','希','望','工',程'...]'''
def sentence2id(sent, word2id):
    """

    :param sent:
    :param word2id:
    :return:
    """
    sentence_id = []
    for word in sent:
        if word.isdigit():
            word = '<NUM>'
        elif ('\u0041' <= word <= '\u005a') or ('\u0061' <= word <= '\u007a'):
            word = '<ENG>'
        if word not in word2id:
            word = '<UNK>'
        sentence_id.append(word2id[word])
    return sentence_id

#通过pickle模块自带的load方法(反序列化方法)加载输出word2id
def read_dictionary(vocab_path):
    """

    :param vocab_path:
    :return:
    """
    vocab_path = os.path.join(vocab_path)
    with open(vocab_path, 'rb') as fr:
        #反序列化方法加载输出
        word2id = pickle.load(fr)
    print('vocab_size:', len(word2id))
    return word2id
'''word2id的形状为{'当': 1, '希': 2, '望': 3, '工': 4, '程': 5,。。'<UNK>': 3904, '<PAD>': 0}
   总共3903个字'''


#输入vocab,vocab就是前面得到的word2id,embedding_dim=300
def random_embedding(vocab, embedding_dim):
    """

    :param vocab:
    :param embedding_dim:
    :return:
    """
    # 返回一个len(vocab)*embedding_dim=3905*300的矩阵(每个字投射到300维)作为初始值
    #numpy.random.uniform(low,high,size)功能:从一个均匀分布[low,high)中随机采样,注意定义域是左闭右开,即包含low,不包含high.
    # 参数介绍:
    #     
    #     low: 采样下界,float类型,默认值为0;
    #     high: 采样上界,float类型,默认值为1;
    #     size: 输出样本数目,为int或元组(tuple)
    # 类型,例如,size = (m, n, k), 则输出m * n * k个样本,缺省时输出1个值。
    #
    # 返回值:ndarray类型,其形状和参数size中描述一致。
    embedding_mat = np.random.uniform(-0.25, 0.25, (len(vocab), embedding_dim))
    embedding_mat = np.float32(embedding_mat)
    return embedding_mat


#padding,输入一句话,不够标准的样本用pad_mark来补齐
''' 
输入:seqs的形状为二维矩阵,形状为[[33,12,17,88,50]-第一句话
                                 [52,19,14,48,66,31,89]-第二句话
                                                    ] 
输出:seq_list为seqs经过padding后的序列
      seq_len_list保留了padding之前每条样本的真实长度
      seq_list和seq_len_list用来喂给feed_dict
'''
def pad_sequences(sequences, pad_mark=0):
    """

    :param sequences:
    :param pad_mark:
    :return:
    """
    # 返回一个序列中长度最长的那条样本的长度
    max_len = max(map(lambda x : len(x), sequences))
    seq_list, seq_len_list = [], []
    for seq in sequences:
        # 由元组格式()转化为列表格式[]
        seq = list(seq)
        # 不够最大长度的样本用0补上放到列表seq_list
        seq_ = seq[:max_len] + [pad_mark] * max(max_len - len(seq), 0)
        #seq_list为sequences经过padding后的序列
        seq_list.append(seq_)
        # seq_len_list用来统计每个样本的真实长度
        seq_len_list.append(min(len(seq), max_len))
    return seq_list, seq_len_list


#生成batch
''' seqs的形状为二维矩阵,形状为[[33,12,17,88,50....]...第一句话
                                [52,19,14,48,66....]...第二句话
                                                    ] 
   labels的形状为二维矩阵,形状为[[0, 0, 3, 4]....第一句话
                                 [0, 0, 3, 4]...第二句话
                                             ]
'''
def batch_yield(data, batch_size, vocab, tag2label, shuffle=False):
    """

    :param data:
    :param batch_size:
    :param vocab:
    :param tag2label:
    :param shuffle:
    :return:
    """
    if shuffle:
        random.shuffle(data)

    seqs, labels = [], []
    for (sent_, tag_) in data:#data形状[(['我',在'北','京'],['O','O','B-LOC','I-LOC']),...]
        # sent_的形状为[33,12,17,88,50....]句中的字在Wordid对应的位置标签
        # 如果tag_形状为['O','O','B-LOC','I-LOC'],对应的label_形状为[0, 0, 3, 4]
        # 返回tag2label字典中每个tag对应的value值

        '''sentence_id的形状为[1,2,3,4,...]对应的sent为['当','希','望','工',程'...]'''
        sent_ = sentence2id(sent_, vocab)#返回id如[1,2,3,4,...]
        # 如果tag_形状为['O','O','B-LOC','I-LOC'],对应的label_形状为[0, 0, 3, 4]
        label_ = [tag2label[tag] for tag in tag_]
        # 保证了seqs的长度为batch_size
        if len(seqs) == batch_size:
            yield seqs, labels
            seqs, labels = [], []

        seqs.append(sent_)#seqs如[[1,2,3,4],……]剧中词语的标号
        labels.append(label_)#abel_形状为[0, 0, 3, 4]#剧中词语的标签

    if len(seqs) != 0:
        yield seqs, labels

model.py 

import numpy as np
import os, time, sys
import tensorflow as tf
from tensorflow.contrib.rnn import LSTMCell
from tensorflow.contrib.crf import crf_log_likelihood
from tensorflow.contrib.crf import viterbi_decode
from data import pad_sequences, batch_yield
from utils import get_logger
from eval import conlleval

#第二步:设置模型

class BiLSTM_CRF(object):
    def __init__(self, args, embeddings, tag2label, vocab, paths, config):
        #批次大小
        self.batch_size = args.batch_size
        self.epoch_num = args.epoch
        self.hidden_dim = args.hidden_dim
        self.embeddings = embeddings
        self.CRF = args.CRF#True
        self.update_embedding = args.update_embedding
        #drop操作参数
        self.dropout_keep_prob = args.dropout
        self.optimizer = args.optimizer#Adam
        self.lr = args.lr
        self.clip_grad = args.clip
        self.tag2label = tag2label
        self.num_tags = len(tag2label)
        #tag2label = {"O": 0,
             # "B-PER": 1, "I-PER": 2,
             # "B-LOC": 3, "I-LOC": 4,
             # "B-ORG": 5, "I-ORG": 6
             # }
        self.vocab = vocab
        self.shuffle = args.shuffle
        self.model_path = paths['model_path']
        self.summary_path = paths['summary_path']
        self.logger = get_logger(paths['log_path'])
        self.result_path = paths['result_path']
        self.config = config

    def build_graph(self):
        #占位符
        self.add_placeholders()
        self.lookup_layer_op()
        self.biLSTM_layer_op()
        self.softmax_pred_op()
        #损失函数
        self.loss_op()
        self.trainstep_op()
        #初始化所有变量
        self.init_op()

    def add_placeholders(self):
        self.word_ids = tf.placeholder(tf.int32, shape=[None, None], name="word_ids")
        #真实的标签序列
        self.labels = tf.placeholder(tf.int32, shape=[None, None], name="labels")
        #一个样本真实的序列长度
        self.sequence_lengths = tf.placeholder(tf.int32, shape=[None], name="sequence_lengths")
        #dropout
        self.dropout_pl = tf.placeholder(dtype=tf.float32, shape=[], name="dropout")
        #学习率
        self.lr_pl = tf.placeholder(dtype=tf.float32, shape=[], name="lr")

    def lookup_layer_op(self):
        with tf.variable_scope("words"):
            _word_embeddings = tf.Variable(self.embeddings,#3905*300的矩阵,矩阵元素均在-0.25到0.25之间
                                           dtype=tf.float32,
                                           trainable=self.update_embedding,#默认是True,如果为True,则会默认将变量添加到图形集合GraphKeys.TRAINABLE_VARIABLES中。此集合用于优化器Optimizer类优化的的默认变量列表【可为optimizer指定其他的变量集合】,可就是要训练的变量列表。这样的话在训练的过程中就会改变值
                                           name="_word_embeddings")
            word_embeddings = tf.nn.embedding_lookup(params=_word_embeddings,#
                                                     ids=self.word_ids,
                                                     name="word_embeddings")
            # params: 表示完整的嵌入张量,或者除了第一维度之外具有相同形状的P个张量的列表,表示经分割的嵌入张量
            #
            # ids: 一个类型为int32或int64的Tensor,包含要在params中查找的id
            #
            # partition_strategy: 指定分区策略的字符串,如果len(params) > 1,则相关。当前支持“div”和“mod”。 默认为“mod”
            #
            # name: 操作名称(可选)
            #
            # validate_indices:  是否验证收集索引
            #
            # max_norm: 如果不是None,嵌入值将被l2归一化为max_norm的值
            #
            #  
            #
            # tf.nn.embedding_lookup()
            # 函数的用法主要是选取一个张量里面索引对应的元素
            #
            # tf.nn.embedding_lookup(tensor, id):即tensor就是输入的张量,id
            # 就是张量对应的索引
            #更完整的信息  https://blog.csdn.net/yangfengling1023/article/details/82910951
        self.word_embeddings =  tf.nn.dropout(word_embeddings, self.dropout_pl)
        print('model 93行:')
        print(self.word_embeddings.shape)
        print('model 97:')
        print(self.word_ids)

    def biLSTM_layer_op(self):
        #关于tf.variable_scope和tf.get_variable:https://blog.csdn.net/zSean/article/details/75057806
        with tf.variable_scope("bi-lstm"):
            cell_fw = LSTMCell(self.hidden_dim)#隐藏层神经元,默认300
            cell_bw = LSTMCell(self.hidden_dim)

            # def bidirectional_dynamic_rnn(
            #         cell_fw,  # 前向RNN
            #         cell_bw,  # 后向RNN
            #         inputs,  # 输入
            #         sequence_length=None,  # 输入序列的实际长度(可选,默认为输入序列的最大长度)
            #         initial_state_fw=None,  # 前向的初始化状态(可选)
            #         initial_state_bw=None,  # 后向的初始化状态(可选)
            #         dtype=None,  # 初始化和输出的数据类型(可选)
            #         parallel_iterations=None,
            #         swap_memory=False,
            #         time_major=False,
            #         # 决定了输入输出tensor的格式:如果为true, 向量的形状必须为 `[max_time, batch_size, depth]`.
            #         # 如果为false, tensor的形状必须为`[batch_size, max_time, depth]`.
            #         scope=None
            # )
            #outputs为(output_fw, output_bw),是一个包含前向cell输出tensor和后向cell输出tensor组成的二元组。
            # 如果time_major == False(默认值), 则output_fw将是形状为[batch_size, max_time, cell_fw.output_size]
            # 的张量, 则output_bw将是形状为[batch_size, max_time, cell_bw.output_size]
            # 的张量;
            # 如果time_major == True, 则output_fw将是形状为[max_time, batch_size, cell_fw.output_size]
            # 的张量;output_bw将会是形状为[max_time, batch_size, cell_bw.output_size]
            # 的张量.
            # 与bidirectional_rnn不同, 它返回一个元组而不是单个连接的张量.如果优选连接的, 则正向和反向输出可以连接为tf.concat(outputs, 2).

            # output_states为(output_state_fw, output_state_bw),包含了前向和后向最后的隐藏状态的组成的二元组。 
            # output_state_fw和output_state_bw的类型为LSTMStateTuple。 
            # LSTMStateTuple由(c,h)组成,分别代表memory
            # cell和hidden(即c,h矩阵)
            # state。
            (output_fw_seq, output_bw_seq), _ = tf.nn.bidirectional_dynamic_rnn(
                cell_fw=cell_fw,
                cell_bw=cell_bw,
                inputs=self.word_embeddings,## 输入  [batch_szie, max_time, depth]depth=self.hidden_dim=300,max_time可以为句子的长度(一般以最长的句子为准,短句需要做padding),depth为输入句子词向量的维度
                sequence_length=self.sequence_lengths,# 输入序列的实际长度(可选,默认为输入序列的最大长度)
                dtype=tf.float32)
            print('model 137')
            print(self.word_embeddings)
            print('model 143')
            print(output_fw_seq.shape)
            print(output_bw_seq.shape)
            #则output_fw将是形状为[batch_size, max_time, cell_fw.output_size],
            # 的张量, 则output_bw将是形状为[batch_size, max_time, cell_bw.output_size]
            # 维持行数不变,后面的行接到前面的行后面  示例程序在tt.py

            output = tf.concat([output_fw_seq, output_bw_seq], axis=-1)#[batch_size, max_time, 600]-1是按行的意思
            print('model 151')
            print(output.shape)
            # model 143
            # (?, ?, 300)
            # (?, ?, 300)
            # model 151
            # (?, ?, 600)
            #经过droupput处理
            output = tf.nn.dropout(output, self.dropout_pl)

        with tf.variable_scope("proj"):
            W = tf.get_variable(name="W",
                                shape=[2 * self.hidden_dim, self.num_tags],#[600,7]
                                # 该函数返回一个用于初始化权重的初始化程序 “Xavier” 。
                                # 这个初始化器是用来保持每一层的梯度大小都差不多相同
                                initializer=tf.contrib.layers.xavier_initializer(),
                                dtype=tf.float32)

            b = tf.get_variable(name="b",
                                shape=[self.num_tags],#[7]
                                # tf.zeros_initializer(),也可以简写为tf.Zeros()
                                initializer=tf.zeros_initializer(),
                                dtype=tf.float32)
            # output的形状为[batch_size,steps,cell_num]批次大小,步长,神经元个数=600
            s = tf.shape(output)
            #print(output.shape)
            # reshape的目的是为了跟w做矩阵乘法
            output = tf.reshape(output, [-1, 2*self.hidden_dim])#-1就是未知值,是批次大小
            pred = tf.matmul(output, W) + b#[batch_size,self.num_tags]
            # s[1]=batch_size
            self.logits = tf.reshape(pred, [-1, s[1], self.num_tags])#[-1,batch_size,7]
            print("******************************************************************************")
            print(self.logits.shape)

    def loss_op(self):
        if self.CRF:
            # crf_log_likelihood作为损失函数
            # inputs:unary potentials,就是每个标签的预测概率值
            # tag_indices,这个就是真实的标签序列了
            # sequence_lengths,一个样本真实的序列长度,为了对齐长度会做些padding,但是可以把真实的长度放到这个参数里
            # transition_params,转移概率,可以没有,没有的话这个函数也会算出来
            # 输出:log_likelihood:标量;transition_params,转移概率,如果输入没输,它就自己算个给返回
            log_likelihood, self.transition_params = crf_log_likelihood(inputs=self.logits,
                                                                   tag_indices=self.labels,
                                                                   sequence_lengths=self.sequence_lengths)
            self.loss = -tf.reduce_mean(log_likelihood)

        else:
            # 交叉熵做损失函数
            losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=self.logits,
                                                                    labels=self.labels)
            #张量变换函数
            mask = tf.sequence_mask(self.sequence_lengths)
            #tf.boolean_mask     https://blog.csdn.net/qq_29444571/article/details/84574526
            losses = tf.boolean_mask(losses, mask)
            self.loss = tf.reduce_mean(losses)

        #添加标量统计结果
        tf.summary.scalar("loss", self.loss)

    def softmax_pred_op(self):
        if not self.CRF:
            self.labels_softmax_ = tf.argmax(self.logits, axis=-1)#-1表示按行取值最大的索引
            self.labels_softmax_ = tf.cast(self.labels_softmax_, tf.int32)

    def trainstep_op(self):
        with tf.variable_scope("train_step"):
            self.global_step = tf.Variable(0, name="global_step", trainable=False)
            if self.optimizer == 'Adam':
                optim = tf.train.AdamOptimizer(learning_rate=self.lr_pl)
            elif self.optimizer == 'Adadelta':
                optim = tf.train.AdadeltaOptimizer(learning_rate=self.lr_pl)
            elif self.optimizer == 'Adagrad':
                optim = tf.train.AdagradOptimizer(learning_rate=self.lr_pl)
            elif self.optimizer == 'RMSProp':
                optim = tf.train.RMSPropOptimizer(learning_rate=self.lr_pl)
            elif self.optimizer == 'Momentum':
                optim = tf.train.MomentumOptimizer(learning_rate=self.lr_pl, momentum=0.9)
            elif self.optimizer == 'SGD':
                optim = tf.train.GradientDescentOptimizer(learning_rate=self.lr_pl)
            else:
                optim = tf.train.GradientDescentOptimizer(learning_rate=self.lr_pl)
            #minimize()实际上包含了两个步骤,即compute_gradients和apply_gradients,前者用于计算梯度,后者用于使用计算得到的梯度来更新对应的variable
            grads_and_vars = optim.compute_gradients(self.loss)
            grads_and_vars_clip = [[tf.clip_by_value(g, -self.clip_grad, self.clip_grad), v] for g, v in grads_and_vars]
            self.train_op = optim.apply_gradients(grads_and_vars_clip, global_step=self.global_step)

    def init_op(self):
        self.init_op = tf.global_variables_initializer()

    def add_summary(self, sess):
        """

        :param sess:
        :return:
        """
        self.merged = tf.summary.merge_all()
        self.file_writer = tf.summary.FileWriter(self.summary_path, sess.graph)

    def train(self, train, dev):#下面的train=train_data, dev=test_data
        """ train_data的形状为[(['我',在'北','京'],['O','O','B-LOC','I-LOC'])...第一句话
                                 (['我',在'天','安','门'],['O','O','B-LOC','I-LOC','I-LOC'])...第二句话
                                  ( 第三句话 )  ] 总共有50658句话"""
        """

        :param train:
        :param dev:
        :return:
        """
        saver = tf.train.Saver(tf.global_variables())

        with tf.Session(config=self.config) as sess:
            sess.run(self.init_op)
            self.add_summary(sess)

            #epoch_num=40
            for epoch in range(self.epoch_num):
                self.run_one_epoch(sess, train, dev, self.tag2label, epoch, saver)

    def test(self, test):
        saver = tf.train.Saver()
        with tf.Session(config=self.config) as sess:
            self.logger.info('=========== testing ===========')
            saver.restore(sess, self.model_path)
            label_list, seq_len_list = self.dev_one_epoch(sess, test)
            self.evaluate(label_list, seq_len_list, test)
    #用模型测试一个句子
    def demo_one(self, sess, sent):
        """

        :param sess:
        :param sent: 
        :return:
        """
        label_list = []
        for seqs, labels in batch_yield(sent, self.batch_size, self.vocab, self.tag2label, shuffle=False):
            # print('model 268行:')
            # print(seqs)
            # print(labels)
            # model 268 行:以  小明的大学在北京的北京大学  为例
            # [[841, 37, 8, 55, 485, 73, 87, 74, 8, 87, 74, 55, 485]]可见batch_yield就是把输入的句子每个字的id返回,以及每个标签转化为对应的tag2label的值
            # [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
            label_list_, _ = self.predict_one_batch(sess, seqs)#得到了预测值
            # print('model 275行')
            # print(label_list_)
            # print(_)
            # model 275 行
            # [[1, 2, 0, 0, 0, 0, 3, 4, 0, 5, 6, 6, 6]]
            # [13]
            label_list.extend(label_list_)
        label2tag = {}
        for tag, label in self.tag2label.items():
            label2tag[label] = tag if label != 0 else label#
        print('model 304')
        print(label2tag)
        tag = [label2tag[label] for label in label_list[0]]
        print('model 307')
        print(tag)
        # model 304
        # {0: 0, 1: 'B-PER', 2: 'I-PER', 3: 'B-LOC', 4: 'I-LOC', 5: 'B-ORG', 6: 'I-ORG'}
        # model 307
        # ['B-PER', 'I-PER', 0, 0, 0, 0, 'B-LOC', 'I-LOC', 0, 'B-ORG', 'I-ORG', 'I-ORG', 'I-ORG']
        return tag

    def run_one_epoch(self, sess, train, dev, tag2label, epoch, saver):
        """

        :param sess:
        :param train:
        :param dev:
        :param tag2label:
        :param epoch:
        :param saver:
        :return:
        """
        # 计算出多少个batch,计算过程:(50658+64-1)//64=792
        num_batches = (len(train) + self.batch_size - 1) // self.batch_size
        # 记录开始训练的时间
        start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        # 产生每一个batch
        batches = batch_yield(train, self.batch_size, self.vocab, self.tag2label, shuffle=self.shuffle)
        for step, (seqs, labels) in enumerate(batches):
            # sys.stdout 是标准输出文件,write就是往这个文件写数据
            sys.stdout.write(' processing: {} batch / {} batches.'.format(step + 1, num_batches) + '\r')
            # step_num=epoch*792+step+1
            step_num = epoch * num_batches + step + 1
            feed_dict, _ = self.get_feed_dict(seqs, labels, self.lr, self.dropout_keep_prob)
            _, loss_train, summary, step_num_ = sess.run([self.train_op, self.loss, self.merged, self.global_step],
                                                         feed_dict=feed_dict)
            if step + 1 == 1 or (step + 1) % 300 == 0 or step + 1 == num_batches:#开头后每相隔300记录一次,最后再记录一次
                self.logger.info(
                    '{} epoch {}, step {}, loss: {:.4}, global_step: {}'.format(start_time, epoch + 1, step + 1,
                                                                                loss_train, step_num))
            #可视化
            self.file_writer.add_summary(summary, step_num)

            if step + 1 == num_batches:
                # 训练的最后一个batch保存模型
                saver.save(sess, self.model_path, global_step=step_num)

        self.logger.info('===========validation / test===========')
        label_list_dev, seq_len_list_dev = self.dev_one_epoch(sess, dev)#将test_data传过去
        self.evaluate(label_list_dev, seq_len_list_dev, dev, epoch)

    #占位符赋值
    def get_feed_dict(self, seqs, labels=None, lr=None, dropout=None):
        """

        :param seqs:
        :param labels:
        :param lr:
        :param dropout:
        :return: feed_dict
        """
        # seq_len_list用来统计每个样本的真实长度
        # word_ids就是seq_list,padding后的样本序列
        word_ids, seq_len_list = pad_sequences(seqs, pad_mark=0)

        feed_dict = {self.word_ids: word_ids,
                     self.sequence_lengths: seq_len_list}
        if labels is not None:
            # labels经过padding后,喂给feed_dict
            labels_, _ = pad_sequences(labels, pad_mark=0)
            feed_dict[self.labels] = labels_
        if lr is not None:
            feed_dict[self.lr_pl] = lr
        if dropout is not None:
            feed_dict[self.dropout_pl] = dropout
        # seq_len_list用来统计每个样本的真实长度
        return feed_dict, seq_len_list

    def dev_one_epoch(self, sess, dev):
        """

        :param sess:
        :param dev:
        :return:
        """
        label_list, seq_len_list = [], []
        #获取一个批次的句子中词的id以及标签
        for seqs, labels in batch_yield(dev, self.batch_size, self.vocab, self.tag2label, shuffle=False):

            label_list_, seq_len_list_ = self.predict_one_batch(sess, seqs)
            label_list.extend(label_list_)
            seq_len_list.extend(seq_len_list_)
        return label_list, seq_len_list

    def predict_one_batch(self, sess, seqs):
        """

        :param sess:
        :param seqs:
        :return: label_list
                 seq_len_list
        """
        # seq_len_list用来统计每个样本的真实长度
        feed_dict, seq_len_list = self.get_feed_dict(seqs, dropout=1.0)

        if self.CRF:
            # transition_params代表转移概率,由crf_log_likelihood方法计算出
            logits, transition_params = sess.run([self.logits, self.transition_params],
                                                 feed_dict=feed_dict)
            print('model 405')
            print(logits.shape)#1*13*7
            print(transition_params)#7*7矩阵
            label_list = []
            # 打包成元素形式为元组的列表[(logit,seq_len),(logit,seq_len),( ,),]
            #print(logits)
            print('model 411')
            print(seq_len_list)
            # model 411
            # [13] =小明的大学在北京的北京大学的长度
            for logit, seq_len in zip(logits, seq_len_list):#如果是demo情况下,输入句子,那么只有一个句子,所以只循环一次,训练模式下就不会
                #对logits解析得到一个数
                viterbi_seq, _ = viterbi_decode(logit[:seq_len], transition_params)
                label_list.append(viterbi_seq)
            print('*-*******************************************************')
            print(label_list)#对logit按行解析返回的值[[1, 2, 0, 0, 0, 0, 3, 4, 0, 5, 6, 6, 6]]#这就是预测结果,对应着tag2label里的值
            return label_list, seq_len_list

        else:#如果不用CRF,就是把self.logits每行取最大的
            label_list = sess.run(self.labels_softmax_, feed_dict=feed_dict)
            return label_list, seq_len_list

    def evaluate(self, label_list, seq_len_list, data, epoch=None):
        """

        :param label_list:
        :param seq_len_list:
        :param data:
        :param epoch:
        :return:
        """
        label2tag = {}
        for tag, label in self.tag2label.items():
            # tag2label = {"O": 0,
            #              "B-PER": 1, "I-PER": 2,
            #              "B-LOC": 3, "I-LOC": 4,
            #              "B-ORG": 5, "I-ORG": 6
            #              }
            label2tag[label] = tag if label != 0 else label

        model_predict = []
        for label_, (sent, tag) in zip(label_list, data):
            tag_ = [label2tag[label__] for label__ in label_]
            sent_res = []
            if  len(label_) != len(sent):
                print(sent)
                print(len(label_))
                print(tag)
            for i in range(len(sent)):
                sent_res.append([sent[i], tag[i], tag_[i]])
            model_predict.append(sent_res)
        epoch_num = str(epoch+1) if epoch != None else 'test'
        label_path = os.path.join(self.result_path, 'label_' + epoch_num)
        metric_path = os.path.join(self.result_path, 'result_metric_' + epoch_num)
        for _ in conlleval(model_predict, label_path, metric_path):
            self.logger.info(_)

import logging, sys, argparse

#第三步
def str2bool(v):
    # copy from StackOverflow
    if v.lower() in ('yes', 'true', 't', 'y', '1'):
        return True
    elif v.lower() in ('no', 'false', 'f', 'n', '0'):
        return False
    else:
        # 首先被内层IOError异常捕获,打印“inner exception”, 然后把相同的异常再抛出,
        # 被外层的except捕获,打印"outter exception"
        raise argparse.ArgumentTypeError('Boolean value expected.')


#根据输入的tag返回对应的字符
def get_entity(tag_seq, char_seq): #['B-PER', 'I-PER', 0, 0, 0, 0, 'B-LOC', 'I-LOC', 0, 'B-ORG', 'I-ORG', 'I-ORG', 'I-ORG'],['小', '明', '的', '大', '学', '在', '北', '京', '的', '北', '京', '大', '学']
    PER = get_PER_entity(tag_seq, char_seq)
    LOC = get_LOC_entity(tag_seq, char_seq)
    ORG = get_ORG_entity(tag_seq, char_seq)
    return PER, LOC, ORG


#输出PER对应的字符
def get_PER_entity(tag_seq, char_seq):
    length = len(char_seq)#句子长度
    PER = []
    # 构成一个zip对象,形状类似[( 1, ),( 1, ),( 2, ),( 2, )]
    # zip函数可以接受一系列的可迭代对象作为参数,将对象中对应的元素打包成一个个tuple(元组),
    # 在zip函数的括号里面加上*号,则是zip函数的逆操作
    for i, (char, tag) in enumerate(zip(char_seq, tag_seq)):#如小 B-PER
        # tag里包含了O,B-PER,I-PER,B-LOCI-PER,B-ORG,I-PER
        if tag == 'B-PER':
            if 'per' in locals().keys():#把上一个名字加进去,这样才能继续往里面添加
                PER.append(per)
                del per
            per = char
            if i+1 == length:#说明这个名字就一个字
                PER.append(per)
        if tag == 'I-PER':
            per += char
            if i+1 == length:
                PER.append(per)
        if tag not in ['I-PER', 'B-PER']:
            if 'per' in locals().keys():
                PER.append(per)
                del per
            continue
    return PER


#输出LOC对应的字符
def get_LOC_entity(tag_seq, char_seq):
    length = len(char_seq)
    LOC = []
    for i, (char, tag) in enumerate(zip(char_seq, tag_seq)):
        if tag == 'B-LOC':
            if 'loc' in locals().keys():
                LOC.append(loc)
                del loc
            loc = char
            if i+1 == length:
                LOC.append(loc)
        if tag == 'I-LOC':
            loc += char
            if i+1 == length:
                LOC.append(loc)
        if tag not in ['I-LOC', 'B-LOC']:
            if 'loc' in locals().keys():
                LOC.append(loc)
                del loc
            continue
    return LOC


#输出ORG对应的字符
def get_ORG_entity(tag_seq, char_seq):
    length = len(char_seq)
    ORG = []
    for i, (char, tag) in enumerate(zip(char_seq, tag_seq)):
        if tag == 'B-ORG':
            if 'org' in locals().keys():
                ORG.append(org)
                del org
            org = char
            if i+1 == length:
                ORG.append(org)
        if tag == 'I-ORG':
            org += char
            if i+1 == length:
                ORG.append(org)
        if tag not in ['I-ORG', 'B-ORG']:
            if 'org' in locals().keys():
                ORG.append(org)
                del org
            continue
    return ORG

#记录日志
def get_logger(filename):
    logger = logging.getLogger('logger')
    logger.setLevel(logging.DEBUG)
    logging.basicConfig(format='%(message)s', level=logging.DEBUG)
    handler = logging.FileHandler(filename)
    handler.setLevel(logging.DEBUG)
    handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s: %(message)s'))
    logging.getLogger().addHandler(handler)
    return logger

 eval.py

import os

#第四步

#使用conlleval.pl对CRF测试结果进行评价的方法
def conlleval(label_predict, label_path, metric_path):
    """

    :param label_predict:
    :param label_path:
    :par am metric_path:
    :return:
    """
    eval_perl = "./conlleval_rev.pl"
    with open(label_path, "w") as fw:
        line = []
        for sent_result in label_predict:
            for char, tag, tag_ in sent_result:
                tag = '0' if tag == 'O' else tag
                char = char.encode("utf-8")
                line.append("{} {} {}\n".format(char, tag, tag_))
            line.append("\n")
        fw.writelines(line)
    os.system("perl {} < {} > {}".format(eval_perl, label_path, metric_path))
    with open(metric_path) as fr:
        metrics = [line.strip() for line in fr]
    return metrics
    

 main.py

import tensorflow as tf
import numpy as np
import os, argparse, time, random
from model import BiLSTM_CRF
from utils import str2bool, get_logger, get_entity
from data import read_corpus, read_dictionary, tag2label, random_embedding

#第五步运行

## Session configuration
#在python代码中设置使用的GPU
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'  # default: 0
#记录设备指派情况:tf.ConfigProto(log_device_placement=True)
#设置tf.ConfigProto()中参数log_device_placement = True ,
#可以获取到 operations 和 Tensor 被指派到哪个设备(几号CPU或几号GPU)上运行,
#会在终端打印出各项操作是在哪个设备上运行的。
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
config.gpu_options.per_process_gpu_memory_fraction = 0.2  # need ~700MB GPU memory



## hyperparameters超参数设置
#使用argparse的第一步就是创建一个解析器对象,并告诉它将会有些什么参数。
#那么当你的程序运行时,该解析器就可以用于处理命令行参数
parser = argparse.ArgumentParser(description='BiLSTM-CRF for Chinese NER task')
#方法add_argument(name or flags...[, action][, nargs][, const][, default][, type][, choices][, required][, help][, metavar][, dest])
# 其中:
# name or flags:命令行参数名或者选项,如上面的address或者-p,--port.​
# 其中命令行参数如果没给定,且没有设置defualt,则出错。但是如果是选项的话,则设置为None
# nargs:命令行参数的个数,
# 一般使用通配符表示,其中,'?'表示只用一个,'*'表示0到多个,'+'表示至少一个
# default:默认值
# type:参数的类型,默认是字符串string类型,还有float、int等类型
# help:和ArgumentParser方法中的参数作用相似,出现的场合也一致
# 最常用的地方就是这些,其他的可以参考官方文档。
parser.add_argument('--train_data', type=str, default='data_path', help='train data source')
parser.add_argument('--test_data', type=str, default='data_path', help='test data source')
parser.add_argument('--batch_size', type=int, default=64, help='#sample of each minibatch')
parser.add_argument('--epoch', type=int, default=40, help='#epoch of training')
parser.add_argument('--hidden_dim', type=int, default=300, help='#dim of hidden state')
parser.add_argument('--optimizer', type=str, default='Adam', help='Adam/Adadelta/Adagrad/RMSProp/Momentum/SGD')
parser.add_argument('--CRF', type=str2bool, default=True, help='use CRF at the top layer. if False, use Softmax')
parser.add_argument('--lr', type=float, default=0.001, help='learning rate')
parser.add_argument('--clip', type=float, default=5.0, help='gradient clipping')
parser.add_argument('--dropout', type=float, default=0.5, help='dropout keep_prob')
parser.add_argument('--update_embedding', type=str2bool, default=True, help='update embedding during training')
parser.add_argument('--pretrain_embedding', type=str, default='random', help='use pretrained char embedding or init it randomly')
parser.add_argument('--embedding_dim', type=int, default=300, help='random init char embedding_dim')
parser.add_argument('--shuffle', type=str2bool, default=True, help='shuffle training data before each epoch')
parser.add_argument('--mode', type=str, default='demo', help='train/test/demo')
parser.add_argument('--demo_model', type=str, default='1521112368', help='model for test and demo')
#传递参数送入模型中解析
args = parser.parse_args()


## get char embeddings

'''word2id的形状为{'当': 1, '希': 2, '望': 3, '工': 4, '程': 5,。。'<UNK>': 3904, '<PAD>': 0}
   train_data总共3903个去重后的字'''
word2id = read_dictionary(os.path.join('.', args.train_data, 'word2id.pkl'))#.\args.train_data\word2id.pkl

#通过调用random_embedding函数返回一个len(vocab)*embedding_dim=3905*300的矩阵(矩阵元素均在-0.25到0.25之间)作为初始值
if args.pretrain_embedding == 'random':
    embeddings = random_embedding(word2id, args.embedding_dim)
else:
    embedding_path = 'pretrain_embedding.npy'
    embeddings = np.array(np.load(embedding_path), dtype='float32')


## read corpus and get training data
if args.mode != 'demo':
    # 设置train_path的路径为data_path下的train_data文件
    train_path = os.path.join('.', args.train_data, 'train_data')#.\args.train_data(默认值data_path)\train_data
    # 设置test_path的路径为data_path下的test_path文件
    test_path = os.path.join('.', args.test_data, 'test_data')#.\args.train_data(默认值data_path)\test_data
    # 通过read_corpus函数读取出train_data
    """ train_data的形状为[(['我',在'北','京'],['O','O','B-LOC','I-LOC'])...第一句话
                         (['我',在'天','安','门'],['O','O','B-LOC','I-LOC','I-LOC'])...第二句话  
                          ( 第三句话 )  ] 总共有50658句话"""
    train_data = read_corpus(train_path)
    test_data = read_corpus(test_path)
    test_size = len(test_data)


## paths setting
paths = {}
# 时间戳就是一个时间点,一般就是为了在同步更新的情况下提高效率之用。
#就比如一个文件,如果他没有被更改,那么他的时间戳就不会改变,那么就没有必要写回,以提高效率,
#如果不论有没有被更改都重新写回的话,很显然效率会有所下降。

#如果是训练就获取最新时间,否则就=args.demo_model
timestamp = str(int(time.time())) if args.mode == 'train' else args.demo_model
#输出地址,默认是./data_path_save/时间戳
output_path = os.path.join('.', args.train_data+"_save", timestamp)
#如果地址不存在就新建
if not os.path.exists(output_path): os.makedirs(output_path)
#./data_path_save/时间戳/summaries
summary_path = os.path.join(output_path, "summaries")
paths['summary_path'] = summary_path
#如果地址不存在就新建
if not os.path.exists(summary_path): os.makedirs(summary_path)
#./data_path_save/时间戳/checkpoints/
model_path = os.path.join(output_path, "checkpoints/")
if not os.path.exists(model_path): os.makedirs(model_path)
#./data_path_save/时间戳/checkpoints/model
ckpt_prefix = os.path.join(model_path, "model")
paths['model_path'] = ckpt_prefix
#./data_path_save/时间戳/results
result_path = os.path.join(output_path, "results")
paths['result_path'] = result_path
#如果不存在就新建
if not os.path.exists(result_path): os.makedirs(result_path)
log_path = os.path.join(result_path, "log.txt")
paths['log_path'] = log_path
#把调用的函数及各个参数写入日志文件
#2019-07-26 08:45:40,081:INFO: Namespace(CRF=True, batch_size=64, clip=5.0, demo_model='1521112368', dropout=0.5, embedding_dim=300, epoch=40, hidden_dim=300, lr=0.001, mode='demo', optimizer='Adam', pretrain_embedding='random', shuffle=True, test_data='data_path', train_data='data_path', update_embedding=True)
get_logger(log_path).info(str(args))


## training model
if args.mode == 'train':
    #引入第二步建立的模型
    model = BiLSTM_CRF(args, embeddings, tag2label, word2id, paths, config=config)
    # 创建节点,无返回值
    model.build_graph()

    ## hyperparameters-tuning, split train/dev
    # dev_data = train_data[:5000]; dev_size = len(dev_data)
    # train_data = train_data[5000:]; train_size = len(train_data)
    # print("train data: {0}\ndev data: {1}".format(train_size, dev_size))
    # model.train(train=train_data, dev=dev_data)

    ## train model on the whole training data
    print("train data: {}".format(len(train_data)))
    #训练
    model.train(train=train_data, dev=test_data)  # use test_data as the dev_data to see overfitting phenomena

## testing model
elif args.mode == 'test':
    ckpt_file = tf.train.latest_checkpoint(model_path)
    print(ckpt_file)
    paths['model_path'] = ckpt_file
    model = BiLSTM_CRF(args, embeddings, tag2label, word2id, paths, config=config)
    model.build_graph()
    print("test data: {}".format(test_size))
    #测试
    model.test(test_data)

## demo
elif args.mode == 'demo':
    ckpt_file = tf.train.latest_checkpoint(model_path)
    print(ckpt_file)
    paths['model_path'] = ckpt_file
    model = BiLSTM_CRF(args, embeddings, tag2label, word2id, paths, config=config)
    model.build_graph()
    saver = tf.train.Saver()
    with tf.Session(config=config) as sess:
        print('============= demo =============')
        saver.restore(sess, ckpt_file)#读入已经训练好的模型
        while(1):
            print('Please input your sentence:')
            demo_sent = input()
            if demo_sent == '' or demo_sent.isspace():
                print('See you next time!')
                break
            else:
                demo_sent = list(demo_sent.strip())#['小', '明', '的', '大', '学', '在', '北', '京', '的', '北', '京', '大', '学']
                demo_data = [(demo_sent, ['O'] * len(demo_sent))]#如[(['小', '明', '的', '大', '学', '在', '北', '京', '的', '北', '京', '大', '学'], ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O'])]
                #用模型测试一个句子
                print('main 172行:')
                print(demo_data)
                #main 172行:
                #[(['小', '明', '的', '大', '学', '在', '北', '京', '的', '北', '京', '大', '学'], ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O'])]
                tag = model.demo_one(sess, demo_data)## ['B-PER', 'I-PER', 0, 0, 0, 0, 'B-LOC', 'I-LOC', 0, 'B-ORG', 'I-ORG', 'I-ORG', 'I-ORG']
                PER, LOC, ORG = get_entity(tag, demo_sent)
                print('PER: {}\nLOC: {}\nORG: {}'.format(PER, LOC, ORG))

在main.py运行一下。输入你要进行实体识别的句子,本例例句是‘小明的大学是北京的北京大学’

一、使用训练好的模型进行实体识别,即args.mode=demo

一直到main.py的120行,都是在定义一些参数。我们从122行开始讲解具体的代码,这个模型其实已经是训练好了的,如果咱们不修改参数,那么默认就是输入句子然后进行实体识别,走的是第三个if,即152行处,咱们先讲这一部分代码,即使用已经训练好的模型进行实体识别。

## demo
elif args.mode == 'demo':
    ckpt_file = tf.train.latest_checkpoint(model_path)
    print(ckpt_file)
    paths['model_path'] = ckpt_file
    model = BiLSTM_CRF(args, embeddings, tag2label, word2id, paths, config=config)
    model.build_graph()
    saver = tf.train.Saver()
    with tf.Session(config=config) as sess:
        print('============= demo =============')
        saver.restore(sess, ckpt_file)#读入已经训练好的模型
        while(1):
            print('Please input your sentence:')
            demo_sent = input()
            if demo_sent == '' or demo_sent.isspace():
                print('See you next time!')
                break
            else:
                demo_sent = list(demo_sent.strip())#['小', '明', '的', '大', '学', '在', '北', '京', '的', '北', '京', '大', '学']
                demo_data = [(demo_sent, ['O'] * len(demo_sent))]#如[(['小', '明', '的', '大', '学', '在', '北', '京', '的', '北', '京', '大', '学'], ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O'])]
                #用模型测试一个句子
                print('main 172行:')
                print(demo_data)
                #main 172行:
                #[(['小', '明', '的', '大', '学', '在', '北', '京', '的', '北', '京', '大', '学'], ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O'])]
                tag = model.demo_one(sess, demo_data)## ['B-PER', 'I-PER', 0, 0, 0, 0, 'B-LOC', 'I-LOC', 0, 'B-ORG', 'I-ORG', 'I-ORG', 'I-ORG']
                PER, LOC, ORG = get_entity(tag, demo_sent)
                print('PER: {}\nLOC: {}\nORG: {}'.format(PER, LOC, ORG))

咱们看一些这几行代码做了个什么事情。

首先它使用tf.train.latest_checkpoint()来自动获取最后一次保存的模型。然后我们看这两行

model = BiLSTM_CRF(args, embeddings, tag2label, word2id, paths, config=config)
model.build_graph()

定义了一个BiLSTM_CRF对象,然后调用了他的build_graph函数。在初始化对象的时候传入了这么几个参数:我们可以Ctrl+左键找到他们定义的位置,看看它们分别是什么。

args,

embeddings:3905*300的矩阵,矩阵元素均在-0.25到0.25之间

tag2label: {"O": 0, "B-PER": 1, "I-PER": 2, "B-LOC": 3, "I-LOC": 4, "B-ORG": 5, "I-ORG": 6 }

word2id:
word2id = read_dictionary(os.path.join('.', args.train_data, 'word2id.pkl'))

def read_dictionary(vocab_path):
    """

    :param vocab_path:
    :return:
    """
    vocab_path = os.path.join(vocab_path)
    with open(vocab_path, 'rb') as fr:
        #反序列化方法加载输出
        word2id = pickle.load(fr)
    print('vocab_size:', len(word2id))
    return word2id
'''word2id的形状为{'当': 1, '希': 2, '望': 3, '工': 4, '程': 5,。。'<UNK>': 3904, '<PAD>': 0}
   总共3903个字'''

这是用反序列化的方式输出词与id的对应文件,这个文件是进行第一步数据处理的时候存入的,调用的是data.py里的vocab_build函数,过程很简单,这里就不讲了。

paths:{'summary_path':……,'model_path':......,'result_path':......,'log_path':......}

这些就是传入的参数,然后我们看看这个类的初始化干了些什么

class BiLSTM_CRF(object):
    def __init__(self, args, embeddings, tag2label, vocab, paths, config):
        #批次大小
        self.batch_size = args.batch_size#64
        self.epoch_num = args.epoch#40
        self.hidden_dim = args.hidden_dim#300
        self.embeddings = embeddings#3905*300的矩阵
        self.CRF = args.CRF#True
        self.update_embedding = args.update_embedding#True
        #drop操作参数
        self.dropout_keep_prob = args.dropout#0.5
        self.optimizer = args.optimizer#Adam
        self.lr = args.lr#学习率0.001
        self.clip_grad = args.clip#5.0
        self.tag2label = tag2label
        self.num_tags = len(tag2label)#7
        #tag2label = {"O": 0,
             # "B-PER": 1, "I-PER": 2,
             # "B-LOC": 3, "I-LOC": 4,
             # "B-ORG": 5, "I-ORG": 6
             # }
        self.vocab = vocab#word2id
        '''word2id的形状为{'当': 1, '希': 2, '望': 3, '工': 4, '程': 5,。。'<UNK>': 3904, '<PAD>': 0}
           总共3903个字,为什么只有4000个字,是因为经过了删除低频词操作'''
        self.shuffle = args.shuffle#True
        self.model_path = paths['model_path']#懒得写了
        self.summary_path = paths['summary_path']
        self.logger = get_logger(paths['log_path'])
        self.result_path = paths['result_path']
        self.config = config

是对一些变量的初始化。

然后调用model.build_graph()函数。

    def build_graph(self):
        #占位符
        self.add_placeholders()
        self.lookup_layer_op()
        self.biLSTM_layer_op()
        self.softmax_pred_op()
        #损失函数
        self.loss_op()
        self.trainstep_op()
        #初始化所有变量
        self.init_op()

里面调用的第一个函数是设置占位符,不做过多解释。

 我们看第二个函数

    def lookup_layer_op(self):
        with tf.variable_scope("words"):
            _word_embeddings = tf.Variable(self.embeddings,#3905*300的矩阵,矩阵元素均在-0.25到0.25之间
                                           dtype=tf.float32,
                                           trainable=self.update_embedding,#默认是True,如果为True,则会默认将变量添加到图形集合GraphKeys.TRAINABLE_VARIABLES中。此集合用于优化器Optimizer类优化的的默认变量列表【可为optimizer指定其他的变量集合】,可就是要训练的变量列表。这样的话在训练的过程中就会改变值
                                           name="_word_embeddings")
            word_embeddings = tf.nn.embedding_lookup(params=_word_embeddings,#
                                                     ids=self.word_ids,
                                                     name="word_embeddings")
            # params: 表示完整的嵌入张量,或者除了第一维度之外具有相同形状的P个张量的列表,表示经分割的嵌入张量
            #
            # ids: 一个类型为int32或int64的Tensor,包含要在params中查找的id
            #
            # partition_strategy: 指定分区策略的字符串,如果len(params) > 1,则相关。当前支持“div”和“mod”。 默认为“mod”
            #
            # name: 操作名称(可选)
            #
            # validate_indices:  是否验证收集索引
            #
            # max_norm: 如果不是None,嵌入值将被l2归一化为max_norm的值
            #
            #  
            #
            # tf.nn.embedding_lookup()
            # 函数的用法主要是选取一个张量里面索引对应的元素
            #
            # tf.nn.embedding_lookup(tensor, id):即tensor就是输入的张量,id
            # 就是张量对应的索引
            #更完整的信息  https://blog.csdn.net/yangfengling1023/article/details/82910951
            #牛逼的网站   https://blog.csdn.net/candy134834/article/details/81074417
            #所以返回的矩阵是1*13*300的矩阵
        self.word_embeddings =  tf.nn.dropout(word_embeddings, self.dropout_pl)
        print('model 93行:')
        print(self.word_embeddings.shape)
        print('model 97:')
        print(self.word_ids)
我们知道
_word_embeddings是3905*300的矩阵,根据embedding_lookup函数的用法,(具体可以参考网站https://blog.csdn.net/candy134834/article/details/81074417),我们需要知道self.word_ids是什么尺寸的矩阵。因为self.word_ids是个占位符,所以我们定位到设置占位符的函数get_feed_dict,
word_ids, seq_len_list = pad_sequences(seqs, pad_mark=0)

关于seqs,我们定位到model.py中的函数demo_one:(get_feed_dict函数多处都有调用,为什么偏偏找到这个函数?因为我们待会会运行到这个函数,正是通过demo_one调用的get_feed_dict)

 for seqs, labels in batch_yield(sent, self.batch_size, self.vocab, self.tag2label, shuffle=False):
            # print('model 268行:')
            # print(seqs)
            # print(labels)
            # model 268 行:以  小明的大学在北京的北京大学  为例
            # [[841, 37, 8, 55, 485, 73, 87, 74, 8, 87, 74, 55, 485]]可见batch_yield就是把输入的句子每个字的id返回,以及每个标签转化为对应的tag2label的值
            # [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
            label_list_, _ = self.predict_one_batch(sess, seqs)#得到了预测值
            ……………………………………

很明显,seqs就是输入的句子每个字的id,以  小明的大学在北京的北京大学  为例 ,则seqs= [[841, 37, 8, 55, 485, 73, 87, 74, 8, 87, 74, 55, 485]]

然后我们再言归正传,回到pad_sequences(seqs,pad_mark=0)函数,

def pad_sequences(sequences, pad_mark=0):
    """

    :param sequences:
    :param pad_mark:
    :return:
    """
    # 返回一个序列中长度最长的那条样本的长度
    max_len = max(map(lambda x : len(x), sequences))
    seq_list, seq_len_list = [], []
    for seq in sequences:
        # 由元组格式()转化为列表格式[]
        seq = list(seq)
        # 不够最大长度的样本用0补上放到列表seq_list
        seq_ = seq[:max_len] + [pad_mark] * max(max_len - len(seq), 0)
        #seq_list为sequences经过padding后的序列
        seq_list.append(seq_)
        # seq_len_list用来统计每个样本的真实长度
        seq_len_list.append(min(len(seq), max_len))
    return seq_list, seq_len_list

原来是执行了个padding操作,pad_mark=0意思就是默认补0,不过很明显,我们每次使用模型预测,只有一句话,所以padding不会有什么作用,并不会补0。

所以word_ids, seq_len_list = pad_sequences(seqs, pad_mark=0)这段代码返回的word_ids其实就是seqs的值,即[[841, 37, 8, 55, 485, 73, 87, 74, 8, 87, 74, 55, 485]],当然如果有多个句子,就会有补0的情况。(训练的时候就是多个句子训练的)

我们再看tf.nn.embedding_lookup函数:

我们看word_ids是一个1*13即句子个数*最大字数的矩阵,而输入的矩阵是3905*300,这样的话,经过tf.nn.embedding_lookup函数之后返回的矩阵是1*13*300的矩阵。

看完了tf.nn.embedding_lookup然后再往下看

self.word_embeddings =  tf.nn.dropout(word_embeddings, self.dropout_pl)

额,很简单了,就是进行dropout处理,去掉一些不用。上面是第二个函数的全部解释

再看第三个函数self.biLSTM_layer_op()

    def biLSTM_layer_op(self):
        #关于tf.variable_scope和tf.get_variable:https://blog.csdn.net/zSean/article/details/75057806
        with tf.variable_scope("bi-lstm"):
            cell_fw = LSTMCell(self.hidden_dim)#隐藏层神经元,默认300
            cell_bw = LSTMCell(self.hidden_dim)

            # def bidirectional_dynamic_rnn(
            #         cell_fw,  # 前向RNN
            #         cell_bw,  # 后向RNN
            #         inputs,  # 输入
            #         sequence_length=None,  # 输入序列的实际长度(可选,默认为输入序列的最大长度)
            #         initial_state_fw=None,  # 前向的初始化状态(可选)
            #         initial_state_bw=None,  # 后向的初始化状态(可选)
            #         dtype=None,  # 初始化和输出的数据类型(可选)
            #         parallel_iterations=None,
            #         swap_memory=False,
            #         time_major=False,
            #         # 决定了输入输出tensor的格式:如果为true, 向量的形状必须为 `[max_time, batch_size, depth]`.
            #         # 如果为false, tensor的形状必须为`[batch_size, max_time, depth]`.
            #         scope=None
            # )
            #outputs为(output_fw, output_bw),是一个包含前向cell输出tensor和后向cell输出tensor组成的二元组。
            # 如果time_major == False(默认值), 则output_fw将是形状为[batch_size, max_time, cell_fw.output_size]
            # 的张量, 则output_bw将是形状为[batch_size, max_time, cell_bw.output_size]
            # 的张量;
            # 如果time_major == True, 则output_fw将是形状为[max_time, batch_size, cell_fw.output_size]
            # 的张量;output_bw将会是形状为[max_time, batch_size, cell_bw.output_size]
            # 的张量.
            # 与bidirectional_rnn不同, 它返回一个元组而不是单个连接的张量.如果优选连接的, 则正向和反向输出可以连接为tf.concat(outputs, 2).

            # output_states为(output_state_fw, output_state_bw),包含了前向和后向最后的隐藏状态的组成的二元组。 
            # output_state_fw和output_state_bw的类型为LSTMStateTuple。 
            # LSTMStateTuple由(c,h)组成,分别代表memory
            # cell和hidden(即c,h矩阵)
            # state。
            (output_fw_seq, output_bw_seq), _ = tf.nn.bidirectional_dynamic_rnn(
                cell_fw=cell_fw,
                cell_bw=cell_bw,
                inputs=self.word_embeddings,## 输入  [batch_szie, max_time, depth]depth=self.hidden_dim=300,max_time可以为句子的长度(一般以最长的句子为准,短句需要做padding),depth为输入句子词向量的维度
                sequence_length=self.sequence_lengths,# 输入序列的实际长度(可选,默认为输入序列的最大长度)
                dtype=tf.float32)
            print('model 137')
            print(self.word_embeddings)
            print('model 143')
            print(output_fw_seq.shape)
            print(output_bw_seq.shape)
            #则output_fw将是形状为[batch_size, max_time, cell_fw.output_size],
            # 的张量, 则output_bw将是形状为[batch_size, max_time, cell_bw.output_size]
            # 维持行数不变,后面的行接到前面的行后面  示例程序在tt.py

            output = tf.concat([output_fw_seq, output_bw_seq], axis=-1)#[batch_size, max_time, 600]-1是按行的意思
            print('model 151')
            print(output.shape)
            # model 143
            # (?, ?, 300)
            # (?, ?, 300)
            # model 151
            # (?, ?, 600)
            #经过droupput处理
            output = tf.nn.dropout(output, self.dropout_pl)

        with tf.variable_scope("proj"):
            W = tf.get_variable(name="W",
                                shape=[2 * self.hidden_dim, self.num_tags],#[600,7]
                                # 该函数返回一个用于初始化权重的初始化程序 “Xavier” 。
                                # 这个初始化器是用来保持每一层的梯度大小都差不多相同
                                initializer=tf.contrib.layers.xavier_initializer(),
                                dtype=tf.float32)

            b = tf.get_variable(name="b",
                                shape=[self.num_tags],#[7]
                                # tf.zeros_initializer(),也可以简写为tf.Zeros()
                                initializer=tf.zeros_initializer(),
                                dtype=tf.float32)
            # output的形状为[batch_size,steps,cell_num]批次大小,步长,神经元个数=600
            s = tf.shape(output)
            #print(output.shape)
            # reshape的目的是为了跟w做矩阵乘法
            output = tf.reshape(output, [-1, 2*self.hidden_dim])#-1就是未知值,是批次大小
            pred = tf.matmul(output, W) + b#[batch_size,self.num_tags]
            # s[1]=batch_size
            self.logits = tf.reshape(pred, [-1, s[1], self.num_tags])#[-1,batch_size,7]
            print("******************************************************************************")
            print(self.logits.shape)

先定义前后向RNN,再用tf.nn.bidirectional_dynamic_rnn来创建双向递归神经网络的动态版本,再dropout处理,然后老套路,定义权值w和b做矩阵乘法,再reshape一下。这个函数相对来说比较简单。

再看第四个函数self.softmax_pred_op()

    def softmax_pred_op(self):
        if not self.CRF:
            self.labels_softmax_ = tf.argmax(self.logits, axis=-1)#-1表示按行取值最大的索引
            self.labels_softmax_ = tf.cast(self.labels_softmax_, tf.int32)

如果不是用CRF方法的话,用argmax获取行中最大值的索引,然后用cast转化为tf.int32

再看第五个函数self.loss_op(),损失函数。

    def loss_op(self):
        if self.CRF:
            # crf_log_likelihood作为损失函数
            # inputs:unary potentials,就是每个标签的预测概率值
            # tag_indices,这个就是真实的标签序列了
            # sequence_lengths,一个样本真实的序列长度,为了对齐长度会做些padding,但是可以把真实的长度放到这个参数里
            # transition_params,转移概率,可以没有,没有的话这个函数也会算出来
            # 输出:log_likelihood:标量;transition_params,转移概率,如果输入没输,它就自己算个给返回
            log_likelihood, self.transition_params = crf_log_likelihood(inputs=self.logits,
                                                                   tag_indices=self.labels,
                                                                   sequence_lengths=self.sequence_lengths)
            self.loss = -tf.reduce_mean(log_likelihood)
         else:
            # 交叉熵做损失函数
            losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=self.logits,
                                                                    labels=self.labels)
            #张量变换函数
            mask = tf.sequence_mask(self.sequence_lengths)
            #tf.boolean_mask     https://blog.csdn.net/qq_29444571/article/details/84574526
            losses = tf.boolean_mask(losses, mask)
            self.loss = tf.reduce_mean(losses)

        #添加标量统计结果
        tf.summary.scalar("loss", self.loss)

只说一下如果不用CRF的话,就是使用交叉熵做损失函数,然后看下面的三句话:

首先是张量变换函数,这个函数很有意思,给大家看个示例就明白了:

import tensorflow as tf

a = tf.sequence_mask([13])
b = tf.sequence_mask([[1, 2], [7, 8]])

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    print(sess.run(a))
    print(sess.run(b))

输出:
[[ True  True  True  True  True  True  True  True  True  True  True  True
   True]]
[[[ True False False False False False False False]
  [ True  True False False False False False False]]
 [[ True  True  True  True  True  True  True False]
  [ True  True  True  True  True  True  True  True]]]

所以mask:[[ True  True  True  True  True  True  True  True  True  True  True  True  True]]

再看boolean_mask(a,b) 将使a (m维)矩阵仅保留与b中“True”元素同下标的部分的值,然后求平均值

第六个函数trainstep_op:其实就是优化器。默认是Adam

    def trainstep_op(self):
        with tf.variable_scope("train_step"):
            self.global_step = tf.Variable(0, name="global_step", trainable=False)
            if self.optimizer == 'Adam':
                optim = tf.train.AdamOptimizer(learning_rate=self.lr_pl)
            elif self.optimizer == 'Adadelta':
                optim = tf.train.AdadeltaOptimizer(learning_rate=self.lr_pl)
            elif self.optimizer == 'Adagrad':
                optim = tf.train.AdagradOptimizer(learning_rate=self.lr_pl)
            elif self.optimizer == 'RMSProp':
                optim = tf.train.RMSPropOptimizer(learning_rate=self.lr_pl)
            elif self.optimizer == 'Momentum':
                optim = tf.train.MomentumOptimizer(learning_rate=self.lr_pl, momentum=0.9)
            elif self.optimizer == 'SGD':
                optim = tf.train.GradientDescentOptimizer(learning_rate=self.lr_pl)
            else:
                optim = tf.train.GradientDescentOptimizer(learning_rate=self.lr_pl)
            #minimize()实际上包含了两个步骤,即compute_gradients和apply_gradients,前者用于计算梯度,后者用于使用计算得到的梯度来更新对应的variable
            grads_and_vars = optim.compute_gradients(self.loss)
            grads_and_vars_clip = [[tf.clip_by_value(g, -self.clip_grad, self.clip_grad), v] for g, v in grads_and_vars]
            self.train_op = optim.apply_gradients(grads_and_vars_clip, global_step=self.global_step)

最后一个函数init_op用于初始化:

    def init_op(self):
        self.init_op = tf.global_variables_initializer()

好了到现在我们已经处理完了这两句:

model = BiLSTM_CRF(args, embeddings, tag2label, word2id, paths, config=config)
model.build_graph()

继续往下看: 

saver = tf.train.Saver()
    with tf.Session(config=config) as sess:
        print('============= demo =============')
        saver.restore(sess, ckpt_file)#读入已经训练好的模型
        while(1):
            print('Please input your sentence:')
            demo_sent = input()
            if demo_sent == '' or demo_sent.isspace():
                print('See you next time!')
                break
            else:
                demo_sent = list(demo_sent.strip())#['小', '明', '的', '大', '学', '在', '北', '京', '的', '北', '京', '大', '学']
                demo_data = [(demo_sent, ['O'] * len(demo_sent))]#如[(['小', '明', '的', '大', '学', '在', '北', '京', '的', '北', '京', '大', '学'], ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O'])]
                #用模型测试一个句子
                print('main 172行:')
                print(demo_data)
                #main 172行:
                #[(['小', '明', '的', '大', '学', '在', '北', '京', '的', '北', '京', '大', '学'], ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O'])]
                tag = model.demo_one(sess, demo_data)## ['B-PER', 'I-PER', 0, 0, 0, 0, 'B-LOC', 'I-LOC', 0, 'B-ORG', 'I-ORG', 'I-ORG', 'I-ORG']
                PER, LOC, ORG = get_entity(tag, demo_sent)
                print('PER: {}\nLOC: {}\nORG: {}'.format(PER, LOC, ORG))

saver引入已经训练好的模型,然后输入句子,如果句子合法,首先以字为单位划分,然后初始化标签为O,然后重点在下面两句话:

tag = model.demo_one(sess, demo_data)## ['B-PER', 'I-PER', 0, 0, 0, 0, 'B-LOC', 'I-LOC', 0,'B-ORG', 'I-ORG', 'I-ORG', 'I-ORG']
PER, LOC, ORG = get_entity(tag, demo_sent)

看看这个demo_one函数是干嘛的

    def demo_one(self, sess, sent):
        """

        :param sess:
        :param sent: 
        :return:
        """
        label_list = []
        for seqs, labels in batch_yield(sent, self.batch_size, self.vocab, self.tag2label, shuffle=False):
            # print('model 268行:')
            # print(seqs)
            # print(labels)
            # model 268 行:以  小明的大学在北京的北京大学  为例
            # [[841, 37, 8, 55, 485, 73, 87, 74, 8, 87, 74, 55, 485]]可见batch_yield就是把输入的句子每个字的id返回,以及每个标签转化为对应的tag2label的值
            # [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
            label_list_, _ = self.predict_one_batch(sess, seqs)#得到了预测值
            print('model 275行')
            print(label_list_)
            print(_)
            # model 275 行
            # [[1, 2, 0, 0, 0, 0, 3, 4, 0, 5, 6, 6, 6]]
            # [13]
            label_list.extend(label_list_)
        label2tag = {}
        for tag, label in self.tag2label.items():
            label2tag[label] = tag if label != 0 else label#
        print('model 304')
        print(label2tag)
        tag = [label2tag[label] for label in label_list[0]]
        print('model 307')
        print(tag)
        # model 304
        # {0: 0, 1: 'B-PER', 2: 'I-PER', 3: 'B-LOC', 4: 'I-LOC', 5: 'B-ORG', 6: 'I-ORG'}
        # model 307
        # ['B-PER', 'I-PER', 0, 0, 0, 0, 'B-LOC', 'I-LOC', 0, 'B-ORG', 'I-ORG', 'I-ORG', 'I-ORG']
        return tag

她只干了两件事,第一得到预测标签id,第二转化为label,转化为label很简单,关键我们看一下如何从每个字的id预测得到标签的。我们注意到有一句

label_list_, _ = self.predict_one_batch(sess, seqs)#得到了预测值

看一下这个函数

    def predict_one_batch(self, sess, seqs):
        """

        :param sess:
        :param seqs:
        :return: label_list
                 seq_len_list
        """
        # seq_len_list用来统计每个样本的真实长度
        feed_dict, seq_len_list = self.get_feed_dict(seqs, dropout=1.0)

        if self.CRF:
            # transition_params代表转移概率,由crf_log_likelihood方法计算出
            logits, transition_params = sess.run([self.logits, self.transition_params],
                                                 feed_dict=feed_dict)
            print('model 405')
            print(logits.shape)#1*13*7
            print(transition_params)#7*7矩阵
            label_list = []
            # 打包成元素形式为元组的列表[(logit,seq_len),(logit,seq_len),( ,),]
            #print(logits)
            print('model 411')
            print(seq_len_list)
            # model 411
            # [13] =小明的大学在北京的北京大学的长度
            for logit, seq_len in zip(logits, seq_len_list):#如果是demo情况下,输入句子,那么只有一个句子,所以只循环一次,训练模式下就不会
                #对logits解析得到一个数
                viterbi_seq, _ = viterbi_decode(logit[:seq_len], transition_params)#logit[:seq_len]  13*7
                print('model 431')
                print(logit[:seq_len].shape)
                #viterbi_decode([batch_size,time_step,num_tabs][0]#是个二维[time_step,num_tabs], [num_tabs,num_tabs])
                label_list.append(viterbi_seq)#[time_step,num_tabs]
            print('*-*******************************************************')
            print(label_list)#对logit按行解析返回的值[[1, 2, 0, 0, 0, 0, 3, 4, 0, 5, 6, 6, 6]]#这就是预测结果,对应着tag2label里的值
            return label_list, seq_len_list

        else:#如果不用CRF,就是把self.logits每行取最大的
            label_list = sess.run(self.labels_softmax_, feed_dict=feed_dict)
            return label_list, seq_len_list

我们注意到分了两部分,我们应该先看else部分,因为我们可以推测这两个是实现了同一个功能的不同方式,所以我们可以从简单的入手就知道他们实现的是什么功能,进而更方便地独麻烦的那部分的代码了。

else里面调用了self.labels_softmax_,我们一看便知道,就是前面讲过的一个函数的内容

    def softmax_pred_op(self):
        if not self.CRF:
            self.labels_softmax_ = tf.argmax(self.logits, axis=-1)#-1表示按行取值最大的索引
            self.labels_softmax_ = tf.cast(self.labels_softmax_, tf.int32)

他对logits采用了按行取最大值索引的方式来得到预测值,这样的话我们就知道了,if里面肯定也是对logits进行了一系列的处理从而得到了预测值。于是我们再看if里面

它使用了viterbi_decode方法来解析logits,结果是一样的,得到预测结果。

这样demo_one函数就讲完了

PER, LOC, ORG = get_entity(tag, demo_sent)

这一句还是比较简单的,就是根据标签分别输出了。看代码注释就能看懂。

  • 71
    点赞
  • 395
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
### 回答1: bilstm是一种双向长短期记忆网络,CRF是条件随机场。它们通常被结合起来用于命名实体识别任务。双向网络可以同时考虑前后文信息,从而提高识别准确性,而条件随机场可以将标记解码过程建模为一个整体,进一步提高效率。因此,将bilstm和CRF结合可以实现更准确和高效的实体识别。 ### 回答2: 命名实体识别(Named Entity Recognition,简称NER)是自然语言处理中的重要任务之一,其目标是在文本中识别出特定类别的实体(如人名、地名、组织、时间等)。在该领域中,很多成果是基于机器学习算法,目前在深度学习领域,双向长短时记忆网络和条件随机场的结合(BiLSTM-CRF)是目前比较常用的算法之一。下面将详细手把手讲解BiLSTM-CRF完成命名实体识别的过程。 1. 数据预处理 首先,需要对待处理的文本数据进行预处理。预处理的步骤包括分词、标注、文本清洗等,并且应保证数据的质量以得到更好的实验效果。另外,预处理也需要将文本转化为数字表示,例如利用独热编码(One-hot Encoding)技术将文本转化为稀疏矩阵表示。 2. 建立BiLSTM神经网络 BiLSTM是一种循环神经网络,它通过能够在训练中自适应调整权重,来对输入的序列数据进行建模。以输入序列$x_1,x_2,\cdots,x_T$为例,BiLSTM网络首先将输入序列的每个元素$x_t$表示为一个向量,然后将这些向量输入到LSTM中进行建模,得到一个序列$h_1,h_2,\cdots,h_T$。其中,$h_t$表示输入序列$x_t$与之前所有元素$x_1,x_2,\cdots,x_{t-1}$的隐状态信息。 3. 添加CRF层 在建立了BiLSTM神经网络之后,需要添加一个CRF层来对NER任务进行处理,并将NER任务转变为一个序列标注问题。CRF层的作用是利用全局上下文信息来对输出序列进行优化,从而提高NER的准确性。 4. 模型训练 在添加了CRF层之后,需要对模型进行训练。训练的目标是最小化模型对训练集的误差。在训练过程中,需要将输入数据和输出数据一起作为模型的输入,根据模型输出和真实标注输出的误差来更新模型的权重。 5. 模型预测 模型训练完成之后,可以利用测试集对模型进行测试,获取对新数据的预测结果。预测的过程就是将待预测的文本输入到模型中,根据模型输出的概率值来确定每个序列元素的标签,进而得到NER任务的输出结果。 总结:BiLSTM-CRF是目前比较常用的NER算法之一,其主要思想是利用双向LSTM网络对文本进行建模,再利用CRF层对序列标注进行全局优化,从而提高NER准确性。BiLSTM-CRF能够整合文本信息和上下文信息,能够识别较长的实体,具有很强的实用性和应用价值。 ### 回答3: 命名实体识别是自然语言处理中的一个重要任务,目的是从文本中识别出命名实体(如人名、地名、组织名等),对于许多应用领域都有着重要的作用。而BiLSTM-CRF是目前较为流行的命名实体识别模型,本文将从数据预处理、模型结构、训练和预测等方面手把手讲解如何完成基于BiLSTM-CRF命名实体识别任务。 一、数据预处理 1. 设计标注方案 定义好需要识别的实体类别,比如人名、地名、组织名等,以及如何将这些实体标注在文本中(采用BIOES、BILOU等标注方式),务必要对数据集的标注方案和标注质量进行反复检查和优化。 2. 数据清洗和切分 对于原始数据,需要进行清洗和格式化,去除无用信息、空格、标点符号等。接着进行数据集的切分,一般按照8:1:1的比例划分训练集、验证集和测试集。 3. 数据向量化和特征提取 将原始语料转化为模型所需的向量形式,一般采用词向量的方式来表示。具体实现可选用Word2Vec、Glove等预训练的词向量模型,或者使用深度学习模型自己学习得到词向量。此外,还可以提取一些基于词性、上下文等的特征,对模型的性能有所提升。 二、模型结构 1. BiLSTM-CRF框架简介 BiLSTM-CRF是一个经典的序列标注模型,它将上下文信息通过双向LSTM网络进行编码,然后基于CRF模型进行标注预测。其优点是既考虑了单词级别的信息,又考虑了句子层面的信息,具有较强的语义理解能力和上下文感知能力。 2. BiLSTM-CRF模型细节 该模型共包含四个部分:输入层、BiLSTM层、CRF层和输出层。 2.1 输入层:将每个词的向量表示输入给BiLSTM层。 2.2 BiLSTM层:通过双向LSTM网络对输入序列进行编码,将每个词的向量表示进行拼接、隐藏层计算和激活操作。 2.3 CRF层:基于转移矩阵和状态矩阵来学习不同实体类别之间的转移规律,确保预测标签序列的一致性和合理性。 2.4 输出层:对模型进行训练并预测标签序列。 三、训练和预测 1. 模型训练 在训练模型前,需要设置一些超参数,比如学习率、L2正则化参数、dropout率等。接着使用交叉熵损失函数对模型进行训练,采用优化算法(如Adam、SGD等)对模型参数进行更新。在训练过程中,还需要进行验证和调参,以达到最佳性能。 2. 模型预测 模型训练完成后,使用测试集对模型进行评估。在预测时,对输入的测试文本进行分词和词向量化处理,然后使用训练得到的模型对文本进行序列标注。输出文本中的实体类别和位置信息。 以上,便是一份详尽的、手把手讲解BiLSTM-CRF模型完成命名实体识别的流程。当然,实际应用中还需要继续优化和调整模型,以达到更好的性能和效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CtrlZ1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值