中文命名实体识别实现
一、问题描述
命名实体识别的任务被定义为识别出文本中出现的专有名称和有意义的数量短语并加以归类。命名实体是文本中基本的信息元素,是正确理解文本的基础。狭义地讲,命名实体是指现实世界中的具体的或抽象的实体,如人、地点、机构等,通常用唯一的标志符(专有名称〉表示,如人名、地名、机构名等。广义地讲,命名实体还可以包含时间、数量表达式等。至于命名实体的确切含义,只能根据具体应用来确定。比如,在具体应用中,可能需要把住址、电子信箱地址、电话号码、会议名称等作为命名实体。
二、基础任务
1.实现基于Bi-LSTM+CRF的命名实体识别算法
实验二资料包下的“RMRB_NER_CORPUS.txt”文件中提供了基于人民日报的NER标注数据,需要对数据集进行合理比例的划分,使其可以用于训练命名实体识别模型。
分词实验与命名实体识别实验所采用的模型有一定交集,因此除了自主实现模型以外,还可以参考实验1必做项中给出的Bi-LSTM+CRF标准实现并对其进行部分修改。若选择对实验1必做项中的Bi-LSTM+CRF模型进行修改,主要需要修改的部分包括数据预处理、模型的输入输出层。
2.尝试用命名实体识别算法提升分词模型的性能
命名实体识别结果将对特定名词的识别产生提升效果,请你尝试利用NER模型结果优化实验一中的分词结果。请自行设计融合策略,并在实验报告中进行说明。
三、选做任务
为了进一步优化实验一的分词结果,可以从以下角度进行改进:
- 优化命名实体识别模型,可考虑的优化方案有:
- 修改网络结构,例如引入BERT等预训练语言模型;
- 调整、优化模型训练过程中的超参数。
- 数据增强
实验二提供的人民日报语料与分词所采用的语料并不一定是同分布的,你可以自行搜集更为合适的数据集进行训练。
调整融合策略
1. 基础模块
循环神经网络(RNN)被广泛运用于序列任务或时序任务,且具有良好的基线效果。在命名实体识别任务上,此类神经网络通常能够兼顾预测准确率与运行速度,得到了广泛运用。
RNN的基本结构可由下图表示:
xt是第t层的输入,它可以是一个词的one-hot向量,也可以是概率分布表示;
st是第t层的隐藏状态,它负贵整个神经网络的记忆功能。由上一层的隐藏状态和本层输入共同决定,st=f(Uxt+Wst-1),f通常是一个非线性的激活函数,比如tanh或ReLU。由于每一层的St都会向后一直传递,所以理论上st能够捕获到前面每一层发生的事情(但实际中太长的依赖很难训练)。
ot是第t层的输出,比如我有预测下一个词是什么时,ot就是一个长度为V的向量,V是所有词的总数,ot[i]表示下一个词是w_i的概率。最后用softmax对这些概率进行归一化ot=softmax(Vst)
每一层的参数U,W,V是共享的,这样极大地缩小了参数空间;每一层并不一定都得有输入和输出,比如对句子进行情感分析时只需要最后一层给一个输出即可,核心在于隐藏层的传递。
LSTM作为RNN的变种之一,通过引入门机制缓解了可能出现的梯度消失与梯度爆炸问题,其单元结构如图2.2所示。
图 2- SEQ 图 \* ARABIC \s 1 1 LSTM单元结构
2.系统实现
2.1 data_u_ner.py
(1) getist:单个分词转换成tag序列。按行读入数据,并分析各个字对应的标签,然后返回分析结果。
(2) handle_data:处理数据,并保存至save_path。按行读取对应文件中的数据,并做相应的处理,然后把处理的结果保存到data_save.pkl中。
2.2 dataloader.py
读取通过data_u.py处理完后的文件data_save.pkl,并将其向量化。
2.3 infer.py
通过已经训练好的模型,完成对测试文件的分析,并将分词结果保存到cws_result.txt文件中。
2.4 model.py
(1) init_hidden:通过torch.randn函数进行初始化操作。
(2) _get_lstm_features:获取LSTM框架。
(3) forward:预测每个标签的loss值,以减少无效预测。
(4) infer:采用Bi-LSTM+CRF的基础结构的分析结果。
2.5 run.py
采用小批量梯度下降法,对模型进行训练,使得loss值降低。
小批量梯度下降,是对批量梯度下降以及随机梯度下降的一个折中办法。其思想是:每次迭代 使用 batch_size个样本来对参数进行更新,每次使用一个batch可以大大减小收敛所需要的迭代次数,同时可以使收敛到的结果更加接近梯度下降的效果。
2.6 split.py
将数据集划分为训练集和测试集。
3. 实验小结
本次实验是基于实验一的依次扩充,实现了对于中文实体的识别,大部分实验的代码老师都已经给出,所以实现起来没有技术上的压力。主要是看明白代码花费了一些时间,再就是对代码进行部分调整已达到优化的目的。
总体上实验不困难,加深了对于自然语言处理相关知识的掌握,对人工智能识别人类语言的过程有了浅层的认知。
实验中代码仅提供一种简单的 Bi-LSTM+CRF PyTorch 实现方案。
更优实现可参见:https://github.com/bamtercelboo/pytorch_NER_BiLSTM_CNN_CRF/
环境搭建
- 安装 Anaconda
官方文档: anaconda install
- 搭建虚拟环境并安装 PyTorch
```shell # 创建虚拟环境 conda create -n nlplab python=3.7 # 创建名为 nlplab 的虚拟环境
# 虚拟环境相关命令 conda activate nlplab # 激活虚拟环境nlplab,成功执行后应看到命令行首部由 (base) 变为 (nlplab) conda deactivate # 退出当前虚拟环境 conda info -e # 查看所有虚拟环境,*指示当前所处环境
# 安装 Pytorch 1.6.0 CPU 版本 # 注意:先激活 nlplab 虚拟环境,再进行安装 conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/ conda install pytorch==1.6.0 cpuonly ```
运行方式
- 配置 PyCharm
安装 PyCharm,并在 PyCharm 中使用 Anaconda 虚拟环境 ( 参考 )
- 安装其他依赖
sh
# 在 nlplab 虚拟环境中安装
pip install -r requirements.txt
- 训练
```shell # save 目录下存放了一个粗略训练过的模型,可先跳过训练过程直接进行推断
# 数据准备,data 目录下运行 python 0.split.py python 1.data_u_ner.py # 模型训练,项目根目录下运行 # 若安装并配置了 GPU 相关运行环境可添加命令行参数 --cuda 来使用 GPU 训练 python run.py ```
- 推断
shell
python infer.py
附录 命名实体识别实现的源程序
data_u_ner.py
```python import pickle
INPUT_DATA = "train.txt" TRAIN_DATA = "ner_train.txt" VALID_DATA = "ner_valid.txt" SAVE_PATH = "ner_data_save.pkl"
unique = set() with open('ner_train.txt', 'r')as f: for line in f: try: unique.update([line.strip('\n').split(' ')[1]]) except[]: pass id2tag = list(unique) print(id2tag) tag2id = {} for i, label in enumerate(id2tag): tag2id[label] = i
word2id = {} id2word = []
def getlist(input_str): """ 单个分词转换为tag序列 :param input_str: 单个分词 :return: tag序列 """ output_str = [] if len(input_str) == 1: output_str.append(tag2id['S']) elif len(input_str) == 2: output_str = [tag2id['B'], tag2id['E']] else: m_num = len(input_str) - 2 m_list = [tag2id['M']] * m_num output_str.append(tag2id['B']) output_str.extend(m_list) output_str.append(tag2id['E']) return output_str
def handle_data(): """ 处理数据,并保存至save_path :return: """ output = open(SAVE_PATH, 'wb') x_train = [] y_train = [] x_valid = [] y_valid = []
word_num = 0
with open(TRAIN_DATA, 'r', encoding="gbk") as ifp:
file_set(x_train, y_train, ifp, word_num)
with open(VALID_DATA, 'r', encoding="GBK") as ifp:
file_set(x_valid, y_valid, ifp, word_num)
print(x_train[0])
print([id2word[temp] for temp in x_train[0]])
print(y_train[0])
print([id2tag[temp] for temp in y_train[0]])
pickle.dump(word2id, output)
pickle.dump(id2word, output)
pickle.dump(tag2id, output)
pickle.dump(id2tag, output)
pickle.dump(x_train, output)
pickle.dump(y_train, output)
pickle.dump(x_valid, output)
pickle.dump(y_valid, output)
output.close()
def file_set(x, y, ifp, word_num): line_x = [] line_y = [] for file_line in ifp: file_line = file_line.strip() if not file_line: x.append(line_x) y.append(line_y) line_x = [] line_y = [] continue file_line = file_line.split(' ') if file_line[0] in id2word: line_x.append(word2id[file_line[0]]) else: id2word.append(file_line[0]) word2id[file_line[0]] = word_num line_x.append(word_num) word_num = word_num + 1 line_y.append(tag2id[file_line[1]])
if name == " main ": handle_data()
```
dataloader.py
```python import torch import pickle from torch.utils.data import Dataset, DataLoader from torch.nn.utils.rnn import pad_sequence
class Sentence(Dataset): def init (self, x, y, batch_size=10): self.x = x self.y = y self.batch_size = batch_size
def __len__(self):
return len(self.x)
def __getitem__(self, idx):
assert len(self.x[idx]) == len(self.y[idx])
return self.x[idx], self.y[idx]
@staticmethod
def collate_fn(train_data):
train_data.sort(key=lambda data: len(data[0]), reverse=True)
data_length = [len(data[0]) for data in train_data]
data_x = [torch.LongTensor(data[0]) for data in train_data]
data_y = [torch.LongTensor(data[1]) for data in train_data]
data_mask = [torch.ones(i, dtype=torch.uint8) for i in data_length]
data_x = pad_sequence(data_x, batch_first=True, padding_value=0)
data_y = pad_sequence(data_y, batch_first=True, padding_value=0)
data_mask = pad_sequence(data_mask, batch_first=True, padding_value=0)
return data_x, data_y, data_mask, data_length
if name == ' main ': # test with open('../data/ner_data_save.pkl', 'rb') as inp: word2id = pickle.load(inp) id2word = pickle.load(inp) tag2id = pickle.load(inp) id2tag = pickle.load(inp) x_train = pickle.load(inp) y_train = pickle.load(inp) x_test = pickle.load(inp) y_test = pickle.load(inp)
train_dataloader = DataLoader(Sentence(x_train, y_train), batch_size=10, shuffle=True,
collate_fn=Sentence.collate_fn)
for in_put, label, mask, length in train_dataloader:
print(in_put, label)
break
```
infer.py
```python import torch import pickle
if name == ' main ': model = torch.load('save/model_epoch9.pkl', map_location=torch.device('cpu')) output = open('ner_result.txt', 'w', encoding='gbk')
with open('data/ner_data_save.pkl', 'rb') as fnp:
word2id = pickle.load(fnp)
id2word = pickle.load(fnp)
tag2id = pickle.load(fnp)
id2tag = pickle.load(fnp)
x_train = pickle.load(fnp)
y_train = pickle.load(fnp)
x_test = pickle.load(fnp)
y_test = pickle.load(fnp)
with open('data/ner_test.txt', 'r', encoding='gbk') as f:
line_test = ''
for test in f:
flag = False
test = test.strip()
if not test:
test = test.split(' ')
x = torch.LongTensor(1, len(line_test))
mask = torch.ones_like(x, dtype=torch.uint8)
length = [len(line_test)]
for i in range(len(line_test)):
if line_test[i] in word2id:
x[0, i] = word2id[line_test[i]]
else:
x[0, i] = len(word2id)
predict = model.infer(x, mask, length)[0]
for i in range(len(line_test)):
print(line_test[i], id2tag[predict[i]], file=output)
print(file=output)
line_test = ''
else:
test = test.split(' ')
line_test += test[0]
```
model.py
```python import torch import torch.nn as nn from torchcrf import CRF from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
class CWS(nn.Module): def init (self, vocab_size, tag2id, embedding_dim, hidden_dim): super(CWS, self). init () self.embedding_dim = embedding_dim self.hidden_dim = hidden_dim self.vocab_size = vocab_size self.tag2id = tag2id self.tag_set_size = len(tag2id)
self.word_embeds = nn.Embedding(vocab_size + 1, embedding_dim)
self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2, num_layers=1,
bidirectional=True, batch_first=True)
self.hidden2tag = nn.Linear(hidden_dim, self.tag_set_size)
self.crf = CRF(21, batch_first=True)
def init_hidden(self, batch_size, device):
return (torch.randn(2, batch_size, self.hidden_dim // 2, device=device),
torch.randn(2, batch_size, self.hidden_dim // 2, device=device))
def _get_lstm_features(self, sentence, length):
batch_size, seq_len = sentence.size(0), sentence.size(1)
# idx->embedding
embeds = self.word_embeds(sentence.view(-1)).reshape(batch_size, seq_len, -1)
embeds = pack_padded_sequence(embeds, length, batch_first=True)
# LSTM forward
self.hidden = self.init_hidden(batch_size, sentence.device)
lstm_out, self.hidden = self.lstm(embeds, self.hidden)
lstm_out, _ = pad_packed_sequence(lstm_out, batch_first=True)
lstm_feats = self.hidden2tag(lstm_out)
return lstm_feats
def forward(self, sentence, tags, mask, length):
emissions = self._get_lstm_features(sentence, length)
loss = -self.crf(emissions, tags, mask, reduction='mean')
return loss
def infer(self, sentence, mask, length):
emissions = self._get_lstm_features(sentence, length)
return self.crf.decode(emissions, mask)
```
run.py
```python import pickle import logging import argparse import os import torch from torch.utils.data import DataLoader from torch.optim import Adam from model import CWS from dataloader import Sentence
def get_param(): parser = argparse.ArgumentParser() parser.add_argument('--embedding_dim', type=int, default=100) parser.add_argument('--lr', type=float, default=0.005) parser.add_argument('--max_epoch', type=int, default=10) parser.add_argument('--batch_size', type=int, default=128) parser.add_argument('--hidden_dim', type=int, default=200) parser.add_argument('--cuda', action='store_true', default=False) return parser.parse_args()
def set_logger(): log_file = os.path.join('save', 'log.txt') logging.basicConfig( format='%(asctime)s %(levelname)-8s %(message)s', level=logging.DEBUG, datefmt='%Y-%m%d %H:%M:%S', filename=log_file, filemode='w', )
console = logging.StreamHandler()
console.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
console.setFormatter(formatter)
logging.getLogger('').addHandler(console)
def entity_split(x, y, id2tag, entities, cur): start, end = -1, -1 for j in range(len(x)): if id2tag[y[j]] == 'B': start = cur + j elif id2tag[y[j]] == 'M' and start != -1: continue elif id2tag[y[j]] == 'E' and start != -1: end = cur + j entities.add((start, end)) start, end = -1, -1 elif id2tag[y[j]] == 'S': entities.add((cur + j, cur + j)) start, end = -1, -1 else: start, end = -1, -1
def main(args): use_cuda = args.cuda and torch.cuda.is_available()
with open('data/ner_data_save.pkl', 'rb') as inp:
word2id = pickle.load(inp)
pickle.load(inp)
tag2id = pickle.load(inp)
pickle.load(inp)
x_train = pickle.load(inp)
y_train = pickle.load(inp)
x_test = pickle.load(inp)
y_test = pickle.load(inp)
model = CWS(len(word2id), tag2id, args.embedding_dim, args.hidden_dim)
if use_cuda:
model = model.cuda()
for name, param in model.named_parameters():
logging.debug('%s: %s, require_grad=%s' % (name, str(param.shape), str(param.requires_grad)))
optimizer = Adam(model.parameters(), lr=args.lr)
train_data = DataLoader(
dataset=Sentence(x_train, y_train),
shuffle=True,
batch_size=args.batch_size,
collate_fn=Sentence.collate_fn,
drop_last=False,
num_workers=6
)
DataLoader(
dataset=Sentence(x_test[:1000], y_test[:1000]),
shuffle=False,
batch_size=args.batch_size,
collate_fn=Sentence.collate_fn,
drop_last=False,
num_workers=6
)
for epoch in range(args.max_epoch):
step = 0
log = []
for sentence, label, mask, length in train_data:
if use_cuda:
sentence = sentence.cuda()
label = label.cuda()
mask = mask.cuda()
# forward
loss = model(sentence, label, mask, length)
log.append(loss.item())
# backward
optimizer.zero_grad()
loss.backward()
optimizer.step()
step += 1
if step % 100 == 0:
logging.debug('epoch %d-step %d loss: %f' % (epoch, step, sum(log) / len(log)))
log = []
path_name = "./save/model_epoch" + str(epoch) + ".pkl"
torch.save(model, path_name)
logging.info("model has been saved in %s" % path_name)
if name == ' main ': set_logger() main(get_param())
```
split.py
```python import random
corpus_file = 'RMRB_NER_CORPUS.txt' corpus = [] with open(corpus_file, 'r', encoding='utf-8')as f: record = [] for line in f: if line != '\n': record.append(line.strip('\n').split(' ')) else: corpus.append(record) record = []
random.seed(43) random.shuffle(corpus)
fullLen = len(corpus) splitLen = len(corpus) // 10
train = corpus[:splitLen * 8] valid = corpus[splitLen * 8:splitLen * 9] test = corpus[splitLen * 9:]
train_file = 'ner_train.txt' valid_file = 'ner_valid.txt' test_file = 'ner_test.txt'
for split_file, split_corpus in zip([train_file, valid_file, test_file], [train, valid, test]): with open(split_file, 'w')as f: for sentence in split_corpus: for word, label in sentence: f.write(word) f.write(' ') f.write(label) f.write('\n') f.write('\n')
```
参考文献
- 基于词汇增强的中文命名实体识别(重庆邮电大学·王猛旗)
- 基于深度学习的中文组织机构名分级识别(北京理工大学·樊英)
- 基于深度学习的中文命名实体识别技术研究(国防科技大学·龚成)
- 基于深度学习的中文实体识别及关系抽取研究(兰州交通大学·罗平)
- 基于BERT的中文实体识别研究(西南财经大学·鲍嘉业)
- 基于次最优路径的中文嵌套命名实体识别(北方工业大学·朱奕霏)
- 面向旅游领域的命名实体识别研究(新疆大学·崔丽平)
- 基于注意力机制的实体识别及关系联合抽取方法研究(上海师范大学·刘克昊)
- 基于中文短文本的命名实体识别和实体链接方法研究(浙江科技学院·高蕾)
- 基于深度学习的实体识别与关系抽取方法的设计与实现(广西大学·洪龙翔)
- 基于自监督学习和图神经网络的实体链接方法研究(哈尔滨工业大学·何长鸿)
- 基于词汇增强的中文命名实体识别(重庆邮电大学·王猛旗)
- 命名实体识别与关系抽取研究及应用(湖南工业大学·李飞)
- 基于深度学习的金融领域命名实体识别方法(中南财经政法大学·焦樵)
- 基于混合神经网络的中文命名实体识别研究(青岛科技大学·刘雨婷)
本文内容包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主题。发布者:源码客栈 ,原文地址:https://bishedaima.com/yuanma/35741.html