通过本文你将了解如何训练一个人名, 地址, 组织, 公司, 产品, 时间, 共 6 个实体的命名实体识别模型.
准备训练样本
下面的链接中提供了已经用 brat 标注好的数据文件以及 brat 的配置文件, 因为标注内容较多放到 brat 里加载会比较慢, 所以拆分成了 10 份, 每份包括 3000 多条样本数据, 将这 10 份文件和相应的配置文件放到 brat 目录 / data/project 路径下, 然后就可以从浏览器访问文件内容以及相应的标注情况了.
链接: https://pan.baidu.com/s/1-wjQnvCSrbhor9x3GD6WSA
提取码: 99z3
如果你还不知道什么是 brat, 或还不清楚如何使用 brat, 强烈建议先阅读前两篇文章《用深度学习做命名实体识别 (二): 文本标注工具 brat》,《用深度学习做命名实体识别 (三): 文本数据标注过程》.
标注数据虽然有了, 但是还不能满足我们的训练要求, 因为我们需要根据 ann 和 txt, 将其转成训练所需的数据格式, 格式如下:
可以看到, 每一行一个字符, 字符后面跟上空格, 然后跟上该字符的标注, 每个样本之间用空行分隔.
另外, 也可以看到这里采用的是 BIO 的标注方式:
B, 即 Begin, 表示开始
I, 即 Intermediate, 表示中间
O, 即 Other, 表示其他, 用于标记无关字符
转换代码如下:
- # -*- coding: utf-8 -*-
- """
- 数据格式转化
- """
- import codecs
- import os
- __author__ = '程序员一一涤生'
- tag_dic = {"时间": "TIME",
- "地点": "LOCATION",
- "人名": "PERSON_NAME",
- "组织名": "ORG_NAME",
- "公司名": "COMPANY_NAME",
- "产品名": "PRODUCT_NAME"}
- # 转换成可训练的格式, 最后以 "END O" 结尾
- def from_ann2dic(r_ann_path, r_txt_path, w_path):
- q_dic = {}
- print("开始读取文件:%s" % r_ann_path)
- with codecs.open(r_ann_path, "r", encoding="utf-8") as f:
- line = f.readline()
- line = line.strip("\n\r")
- while line != "":
- line_arr = line.split()
- print(line_arr)
- cls = tag_dic[line_arr[1]]
- start_index = int(line_arr[2])
- end_index = int(line_arr[3])
- length = end_index - start_index
- for r in range(length):
- if r == 0:
- q_dic[start_index] = ("B-%s" % cls)
- else:
- q_dic[start_index + r] = ("I-%s" % cls)
- line = f.readline()
- line = line.strip("\n\r")
- print("开始读取文件:%s" % r_txt_path)
- with codecs.open(r_txt_path, "r", encoding="utf-8") as f:
- content_str = f.read()
- # content_str = content_str.replace("\n", "").replace("\r","").replace("//////", "\n")
- print("开始写入文本 %s" % w_path)
- with codecs.open(w_path, "w", encoding="utf-8") as w:
- for i, str in enumerate(content_str):
- if str is "" or str =="" or str == "\n" or str == "\r":
- print("===============")
- elif str == "/":
- if i == len(content_str) - len("//////") + 1: # 表示到达末尾
- # w.write("\n")
- break
- # 连续六个字符首尾都是 /, 则表示换一行
- elif content_str[i + len("//////") - 1] == "/" and content_str[i + len("//////") - 2] == "/" and \
- content_str[i + len("//////") - 3] == "/" and content_str[i + len("//////") - 4] == "/" and \
- content_str[i + len("//////") - 5] == "/":
- w.write("\n")
- i += len("//////")
- else:
- if i in q_dic:
- tag = q_dic[i]
- else:
- tag = "O" # 大写字母 O
- w.write('%s %s\n' % (str, tag))
- w.write('%s\n' % "END O")
- # 去除空行
- def drop_null_row(r_path, w_path):
- q_list = []
- with codecs.open(r_path, "r", encoding="utf-8") as f:
- line = f.readline()
- line = line.strip("\n\r")
- while line != "END O":
- if line != "":
- q_list.append(line)
- line = f.readline()
- line = line.strip("\n\r")
- with codecs.open(w_path, "w", encoding="utf-8") as w:
- for i, line in enumerate(q_list):
- w.write('%s\n' % line)
- # 生成 train.txt,dev.txt,test.txt
- # 除 8,9-new.txt 分别用于 dev 和 test 外, 剩下的合并成 train.txt
- def rw0(data_root_dir, w_path):
- if os.path.exists(w_path):
- os.remove(w_path)
- for file in os.listdir(data_root_dir):
- path = os.path.join(data_root_dir, file)
- if file.endswith("8-new.txt"):
- # 重命名为 dev.txt
- os.rename(path, os.path.join(data_root_dir, "dev.txt"))
- continue
- if file.endswith("9-new.txt"):
- # 重命名为 test.txt
- os.rename(path, os.path.join(data_root_dir, "test.txt"))
- continue
- q_list = []
- print("开始读取文件:%s" % file)
- with codecs.open(path, "r", encoding="utf-8") as f:
- line = f.readline()
- line = line.strip("\n\r")
- while line != "END O":
- q_list.append(line)
- line = f.readline()
- line = line.strip("\n\r")
- print("开始写入文本 %s" % w_path)
- with codecs.open(w_path, "a", encoding="utf-8") as f:
- for item in q_list:
- if item.__contains__('\ufeff1'):
- print("===============")
- f.write('%s\n' % item)
- if __name__ == '__main__':
- data_dir = "datas"
- for file in os.listdir(data_dir):
- if file.find(".") == -1:
- continue
- file_name = file[0:file.find(".")]
- r_ann_path = os.path.join(data_dir, "%s.ann" % file_name)
- r_txt_path = os.path.join(data_dir, "%s.txt" % file_name)
- w_path = "%s/new/%s-new.txt" % (data_dir, file_name)
- from_ann2dic(r_ann_path, r_txt_path, w_path)
- # 生成 train.txt,dev.txt,test.txt
- rw0("%s/new" % data_dir, "%s/new/train.txt" % data_dir)
注意把该代码文件和 datas 目录放在一级, 然后把从云盘下载的 10 个标注数据文件放在 datas 目录下, 然后再执行上面的代码, 执行完成后, 会在 datas/new 目录下生成一些文件, 我们要的是其中的 train,dev,test 三个 txt 文件.
ok, 到此我们的训练数据就准备好了, 接下来我们需要准备预训练模型.
准备预训练模型
使用预训练模型做微调的训练方式称为迁移学习, 不太明白什么意思也没关系, 只要知道这样做可以让我们的训练收敛的更快, 并且可以使得在较少的训练样本上训练也能得到不错的效果. 这里我们将使用目前最好的自然语言表征模型之一的 bert 的中文预训练模型. 如果你还不清楚 bert, 也没关系, 这里你只要知道使用 bert 可以得到比 word2vec(词向量) 更好的表征即可.
bert 在中文维基百科上预训练的模型下载地址:
下载下来, 解压后会看到如下几个文件:
这里我们已经将 bert_model.ckpt.data-00000-of-00001 文件复制一份, 命名为 bert_model.ckpt, 所以多了一个 bert_model.ckpt 文件. 因为不这样做的话, 后续的训练会报错, 找不到 ckpt.
以上工作都完成后, 就可以进入训练环节了.
准备训练环境
强烈建议使用 GPU 来训练, 否则你会疯的. 关于 GPU 环境的搭建可以参考这篇文章《如何在阿里云租一台 GPU 服务器做深度学习?》.
训练
本文的模型训练参考的是 GitHub 上一个开源的项目, 该项目是基于 bert+crf 算法来训练命名实体模型的, 比基于 lstm+crf 的项目的效果要好, 下面是该项目的地址:
https://github.com/macanv/BERT-BiLSTM-CRF-NER
笔者基于该项目做了一些代码修改, 修改的目的如下:
原来的项目是采用 install 的方式直接将项目安装到你的 python 虚拟环境下, 然后通过命令行执行训练, 笔者直接调整了源代码, 为了可以基于源代码执行一些调试;
原来的项目训练的时候几乎没有日志信息, 修改后的项目可以看到训练日志;
原来的项目只能在训练结束后输出评估结果, 修改后的项目可以让评估脱离训练过程独立进行.
修改后的项目地址:
链接: https://pan.baidu.com/s/1Bht_-K9i-7WUbXdvG4sdpg
提取码: sibq
修改后的项目下载下来解压后, 需要做 3 件事情:
将之前下载的 bert 预训练模型 chinese_L-12_H-768_A-12 目录以及目录中的文件放到项目的 models 目录下.
将之前准备的 train,dev,test 三个文件放到 person_data 目录下.
为该项目新建一个 python 的虚拟环境, 然后安装所需要的依赖包, 关于需要哪些依赖包, 项目中的 requirement.txt 是这么描述的:
tensorflow 的安装, 因为我们是在 GPU 上训练, 所以只需要安装 tensorflow-gpu, 笔者安装的是 tensorflow1.13.1 版本, 因为笔者的 CUDA 版本是 10.0.
接下来, 执行以下命令进行训练:
nohup python bert_lstm_ner.py -max_seq_length 500 -batch_size 2 -learning_rate 2e-5 -num_train_epochs 3.0 -filter_adam_var True -verbose -data_dir person_data -output_dir output -init_checkpoint models/chinese_L-12_H-768_A-12/bert_model.ckpt -bert_config_file models/chinese_L-12_H-768_A-12/bert_config.JSON -vocab_file models/chinese_L-12_H-768_A-12/vocab.txt>log.out 2>&1 &
让我们对命令中的参数做一些解释:
nohup
使用 nohup 命令, 可以保证在命令窗口被关闭, 或远程链接中断的情况下, 不影响远端 python 程序的执行. python 程序执行过程中的日志信息会保存在当前文件夹下的 log.out 文件中.
max_seq_length
每个样本的最大长度, 不能超过 512. 如果你的某些样本超过了这个长度, 需要截断. 截断代码可以使用项目根路径下的 data_process.py 文件.
batch_size
每次送到模型进行训练的样本数量. 一般是 2 幂次方. 如果你的 GPU 显存够大, 可以尝试增大 batch_size.
learning_rate
初始学习率, 用于调整模型的学习速度, 过大过小都不好. 刚开始训练时: 学习率以 0.01 ~ 0.001 为宜. 接近训练结束: 学习速率的衰减应该在 100 倍以上. 这里因为我们采用的是迁移学习, 由于预模型本身已经在原始数据集上收敛, 此时学习率应该设置的较小, 所以这里设置成 0.00002.
num_train_epochs
每次用完所有样本后, 记为一个 epoch. 这里是指设置多少个 epoch 后训练结束.
filter_adam_var
保存训练模型的时候是否过滤掉 Adam 的参数, 默认为 False. 设置为 True 可以减小模型的大小.
verbose
加上该参数就会打开 tensorflow 的日志.
data_dir
train,dev,test 数据所在的目录.
output_dir
模型输出目录.
init_checkpoint
预训练模型的路径, 这里我们使用了 bert 的中文预训练模型.
bert_config_file
bert 模型的配置文件所在路径.
vocab_file
bert 的词汇表文件路径.
开始训练后, 通过以下命令查看训练过程的日志信息:
tail -f log.out
下图截取自训练结束后的部分输出日志:
可以看到评估损失值降到了 0.04862.
训练会持续 3 个多小时 (在一块 Nvidia Geforce RTX2060 GPU 上), 结束后, 会看到对 test.txt 样本进行测试的结果:
测试
每训练 500 步, 程序会在 output 目录下保存一个模型文件, 我们可以通过修改 output 目录下的 checkpoint 文件来指定要用来测试的模型文件.
然后执行如下命令来对 test.txt 中的内容进行测试 (注意 bert_lstm_ner-test.py 中的配置要和训练时指定的参数配置一致):
python bert_lstm_ner-test.py
测试输出的结果和上面训练完成后输出的结果的格式是一样的. 如果你按照本文的步骤, 完整的走到这里了, 那么你已经有了一个可以识别 人名, 地址, 组织, 公司, 产品, 时间, 共 6 个实体的命名实体识别模型, 下一篇文章《用深度学习做命名实体识别 (五): 模型使用》将介绍如何使用这个模型来提供一个 REST 风格的实体识别接口, 对该接口传入一个句子参数, 接口会返回句子中的人名, 地址, 组织, 公司, 产品, 时间信息.
来源: https://www.cnblogs.com/anai/p/11492956.html