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

这一次我们讲,当args.mode=‘train’时的情况

这次会多执行一个函数

## 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)

很简单,就是获取训练数据和测试数据。

原始的训练数据长这个模样:

有二百多万行,可见 read_corpus函数将其转化成了句子的形式,我们看一下这个函数

#输入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

代码十分简单,作用就是刚才我们说的,以换行为分隔将其转化为句子。

我们继续往下面看,因为args.mode='train',所以执行这个函数:

## 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

上一篇我们已经详细的解释了下面两行代码的内容

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

然后我们看看这一句干了啥

#训练
model.train(train=train_data, dev=test_data)  # use test_data as the dev_data to see overfitting phenomena

点进去

    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)

代码量不是很多,我们一步步来分析一下

epoch_num=40,迭代了40次,for循环每次迭代都调用了这个函数

self.run_one_epoch(sess, train, dev, self.tag2label, epoch, saver)

传入了训练数据,测试数据,tag2label,迭代索引,saver用于模型操作,参数都很简单。

    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)

批次大小是64,一共50658个句子,所以计算一下一共需要多少个批次,记为num_batches,然后记录一下训练开始的时间,然后每for一次产生一个批次的数据,调用的是

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

之前已经看过这个函数,返回的两个值也在注释中写清楚了,是很简单的函数。

紧接着是一个for循环,循环一个批次的数据,sys.stdout.write对于初学者来说和print是一个意思,

step_num = epoch * num_batches + step + 1

这一句,epoch是迭代索引,在这里step_num起到一个id的作用,然后初始化feed_dict,然后进行训练,然后记录,可视化,保存模型等。

然后需要测试

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)

看看dev_one_epoch这个函数,每for一次,产生一个批次的测试数据

    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

调用predict_one_batch就是要进行预测了,

    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

这个函数首先对一个批次的数据进行了feed_dict,其中有padding操作,然后利用bilstm计算logits,然后通过viterbi_decode函数来解析logits,如果不用CRF则用求最大索引值的方式解析logits,这就是predict_ont_batch的作用。

然后返回一整个批次的预测结果。然后运行下面

self.evaluate(label_list_dev, seq_len_list_dev, dev, epoch)
    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(_)

首先利用for循环做了个label2tag的字典,就是tag2label倒过来。

data就是测试数据,和训练数据形状一样,训练数据形状为

""" train_data的形状为[(['我',在'北','京'],['O','O','B-LOC','I-LOC'])...第一句话
                                 (['我',在'天','安','门'],['O','O','B-LOC','I-LOC','I-LOC'])...第二句话
                                  ( 第三句话 )  ] 总共有50658句话"""

label_list形状为

[[1, 2, 0, 0, 0, 0, 3, 4, 0, 5, 6, 6, 6],
 [……………………………………],
 [……………………………………],
 ………………
]

每行对应每个句子的预测label

tag_形状为一个句子中所有字的label对应的tag组成的列表,比如

[O,O,B_PER,……]

sent_res形状是

[
  ['我','O','O']#字,正确标签,预测标签
]

然后一个for循环,是使用conlleval.pl对CRF测试结果进行评价的方法,这里不做过多描述。

  • 9
    点赞
  • 72
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

CtrlZ1

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

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

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

打赏作者

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

抵扣说明:

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

余额充值