中文分词实现
1 问题描述
中文分词指的是将一个汉字序列切分成一个一个单独的词。中文分词是文本挖掘的基础,对于输入的一段中文,成功的进行中文分词,可以达到电脑自动识别语句含义的效果。它是信息提取、信息检索、机器翻译、文本分类、自动文摘、语音识别、文本语音转换、自然语言理解等中文信息处理领域的基础。
1.1 基础任务
- 实现基于词典的分词算法
实验一资料包下的“Dictionary_based”文件夹中提供了基础词典和分词算法的大致框架。分词算法的核心部分需要大家完成,实验中提供了若干测试样本用以帮助大家判断算法是否正确实现。
- 实现基于统计学习的分词算法
实验中给出Bi-LSTM+CRF模型的基础实现,相关代码及说明文档位于实验一资料包下的“Bi-LSTM+CRF”文件夹下。请根据给定的实验资料中README.md文件配置相应实验环境,说明:(1)提供源码PyTorch语言编写(可根据个人掌情况用其他语言编写),默认运行版本是CPU版本;(2)如希望运行NPU版本,大家可跟任课老师联系,申请华为云资源运行(需提前统计名单:姓名+学号+个人手机号码+邮箱);
1.2 选做任务
优化基础任务中实现的分词器,可考虑的优化方案有:
- 修改网络结构,例如引入BERT等预训练语言模型;
- 与命名实体识别算法相互配合,减少对命名实体的错误分割;
- 构造合适的词典集(可扩充+人工整理);
- 实现新词发现(登录)功能,识别测试集中的新词(未登录词);
- 调整、优化模型训练过程中的超参数。
完成优化后对测试文件“Bi-LSTM+CRF/data/test.txt”进行分词,分词结果保存到.txt文件中utf-8编码,词与词之间以空格分隔,每个测试样本占一行。文件“Bi-LSTM+CRF/cws_result.txt”中给出了输出示例。提交分词结果后,依据单词级别的F1-score进行评判,决定选做部分的实验分数。
单词级别的F1-score的计算方式如下:
Gold: 共同 创造 美好 的 新 世纪 —— 二○○一年 新年 贺词
Hypothesis: 共同 创造 美 好 的 新 世纪 —— 二○○一年 新年 贺词
Precision = 9 / 11 = 0.818
Recall = 9 / 10 = 0.9
F1-score = 2*Precision*Recall/(Precision+Recall)=0.857
2 模块设计-基于词典
2.1 前向最大匹配
从待分词句子的左边向右边搜索,寻找词的最大匹配。我们需要规定一个词的最大长度,每次扫描的时候寻找当前开始的这个长度的词来和字典中的词匹配,如果没有找到,就缩短长度继续寻找,直到找到字典中的词或者成为单字。
算法流程如下:
(1)从待分词子串中从前往后取出max_len个字,然后扫描分词字典,测试该max_len个字的子串是否在字典中;
(2)如果存在,则从待分词子串中删除掉该max_len个字的子串,重新按照规则取子串,重复(1);
(3)如果不存在于字典中,则减少该子串的最右一个字,之后重复(1)。
2.2 后向最大匹配
从待分词句子的右边向左边搜索,寻找词的最大匹配。同样,我们也需要规定一个词的最大长度,每次扫描的时候寻找当前开始的这个长度的词来和字典中的词匹配,如果没有找到,就缩短长度继续寻找,直到找到字典中的词或者成为单字。
算法流程如下:
(1)从待分词子串中从后往前取出max_len个字,然后扫描分词字典,测试该max_len个字的子串是否在字典中;
(2)如果存在,则从待分词子串中删除掉该max_len个字的子串,重新按照规则取子串,重复(1);
(3)如果不存在于字典中,则减少该子串的最左一个字,之后重复(1)。
2.3 双向最大匹配
将前向最大匹配算法和后向最大匹配算法进行比较,从而确定正确的分词方法。
算法流程如下:
(1)比较前向最大匹配和后向最大匹配结果;
(2)如果分词结果相同,返回其中任意一个;
(3)如果分词结果不同:
1、比较两者分词总数量,数量高者罚分;
2、比较两者分词中单字词数量,单字词多者罚分;
3、比较两者分词中非字典词数量,非字典词多者罚分;
4、选择罚分最少的作为最终结果。
3 模块设计-基于统计学习
3.1 data_u.py
(1) getist:单个分词转换成tag序列。按行读入数据,并分析各个字对应的标签,然后返回分析结果。
(2) handle_data:处理数据,并保存至save_path。按行读取对应文件中的数据,并做相应的处理,然后把处理的结果保存到data_save.pkl中。
3.2 dataloader.py
读取通过data_u.py处理完后的文件data_save.pkl,并将其向量化。
3.3 infer.py
通过已经训练好的模型,完成对测试文件的分析,并将分词结果保存到cws_result.txt文件中。
3.4 model.py
(1) init_hidden:通过torch.randn函数进行初始化操作。
(2) _get_lstm_features:获取LSTM框架。
(3) forward:预测每个标签的loss值,以减少无效预测。
(4) infer:采用Bi-LSTM+CRF的基础结构的分析结果。
3.5 run.py
采用小批量梯度下降法,对模型进行训练,使得loss值降低。
小批量梯度下降,是对批量梯度下降以及随机梯度下降的一个折中办法。其思想是:每次迭代 使用 batch_size个样本来对参数进行更新,每次使用一个batch可以大大减小收敛所需要的迭代次数,同时可以使收敛到的结果更加接近梯度下降的效果。
3 代码实现-基于词典
3.1 前向最大匹配
```python def forward_mm_split(self, fmm_text): """ 正向最大匹配分词算法 :param fmm_text: 待分词字符串 :return: 分词结果,以list形式存放,每个元素为分出的词 """ # 字词列表,存放分词结果 word_list = [] # 用于记录分词的起始位置 count = 0 # 字或词当前的长度 word_len = self.max_len while word_len > 0 and count < len(fmm_text): word = fmm_text[count:count + word_len] word_len = len(word) if (word in self.words) or (word in self.delimiter): word_list.append(word) count = count + word_len word_len = self.max_len else: word_len = word_len - 1 return word_list
```
3.2 后向最大匹配
```python def reverse_mm_split(self, rmm_text): """ 逆向最大匹配分词算法 :param rmm_text: 待分词字符串 :return: 分词结果,以list形式存放,每个元素为分出的词 """ # 字词列表,存放分词结果 word_list = [] # 用于记录分词的末尾位置 count = len(rmm_text) # 字或词当前的长度 word_len = self.max_len while word_len > 0 and count > 0: if count <= word_len: word = rmm_text[:count] else: word = rmm_text[(count - word_len):count] word_len = len(word) if (word in self.words) or (word in self.delimiter): word_list.insert(0, word) count = count - word_len word_len = self.max_len else: word_len = word_len - 1 return word_list
```
3.3 双向最大匹配
```python def bidirectional_mm_split(self, bi_text): """ 双向最大匹配分词算法 :param bi_text: 待分词字符串 :return: 分词结果,以list形式存放,每个元素为分出的词 """ # 前向最大匹配得到的分词结果 forward = self.forward_mm_split(bi_text) # 后向最大匹配得到的分词结果 reverse = self.reverse_mm_split(bi_text) # 总词数 forward_total_words = len(forward) reverse_total_words = len(reverse) # 单字词个数 forward_single_words = 0 reverse_single_words = 0 # 非字典词数 forward_illegal_words = 0 reverse_illegal_words = 0 # 罚分,分值越低,表明结果越好 forward_score = 0 reverse_score = 0 if forward == reverse: return forward else: # 统计前向匹配的各个词情况 for word in forward: if len(word) == 1: forward_single_words += 1 if word not in self.words: forward_illegal_words += 1 # 统计后向匹配的各个词情况 for word in reverse: if len(word) == 1: reverse_single_words += 1 if word not in self.words: reverse_illegal_words += 1 # 计算罚分 if forward_total_words < reverse_total_words: reverse_score += 1 else: forward_score += 1 if forward_illegal_words < reverse_illegal_words: reverse_score += 1 else: forward_score += 1 if forward_single_words < reverse_single_words: reverse_score += 1 else: forward_score += 1 # 比较罚分情况,罚分最小的选做最终结果 if forward_score < reverse_score: return forward else: return reverse
```
4 运行结果
利用实验包已给出的代码框架,实现完对应的匹配函数后,运行测试样例。
1) 基于词典的中文分词,得到输出结果如下:
图 1- SEQ 图 \* ARABIC \s 1 1 中文分词测试结果
1) 基于统计学习的中文分词,将输出的分词文件进行在线测评,结果如下:
图 1- SEQ 图 \* ARABIC \s 1 2 在线测评结果
5 实验小结
实验中总体的代码框架已给出,需要实现的部分为前向最大匹配、后向最大匹配和双向最大匹配三个核心函数。通过相关资料的参考以及对中文分词的个人理解,能够顺利实现各个匹配模式的实现。
基于统计学习的中文分词代码,由于已经给出了代码框架,所以只是在其基础上做了部分优化,并增加了一部分的训练数据。
实验中代码仅提供一种简单的 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.py
```python import pickle
from sklearn.model_selection import train_test_split
INPUT_DATA = "train.txt" SAVE_PATH = "./data_save.pkl" id2tag = ['B', 'M', 'E', 'S'] # B:分词头部 M:分词词中 E:分词词尾 S:独立成词 tag2id = {'B': 0, 'M': 1, 'E': 2, 'S': 3} word2id = {} id2word = []
def getlist(input_str): """ 单个分词转换为tag序列 :param input_str: 单个分词 :return: tag序列 """ out_str = [] if len(input_str) == 1: out_str.append(tag2id['S']) elif len(input_str) == 2: out_str = [tag2id['B'], tag2id['E']] else: m_num = len(input_str) - 2 m_list = [tag2id['M']] * m_num out_str.append(tag2id['B']) out_str.extend(m_list) out_str.append(tag2id['E']) return out_str
def handle_data(): """ 处理数据,并保存至save_path :return: """ x_data = [] y_data = [] word_num = 0 line_num = 0 with open(INPUT_DATA, 'r', encoding="utf-8") as ifp: for line in ifp: line_num = line_num + 1 line = line.strip() if not line: continue line_x = [] for i in range(len(line)): if line[i] == " ": continue if line[i] in id2word: line_x.append(word2id[line[i]]) else: id2word.append(line[i]) word2id[line[i]] = word_num line_x.append(word_num) word_num = word_num + 1 x_data.append(line_x)
line_arr = line.split()
line_y = []
for item in line_arr:
line_y.extend(getlist(item))
y_data.append(line_y)
print(x_data[0])
print([id2word[i] for i in x_data[0]])
print(y_data[0])
print([id2tag[i] for i in y_data[0]])
x_train, x_test, y_train, y_test = train_test_split(x_data, y_data, test_size=0.1, random_state=43)
with open(SAVE_PATH, 'wb') as output:
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_test, output)
pickle.dump(y_test, output)
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]
masks = [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)
masks = pad_sequence(masks, batch_first=True, padding_value=0)
return data_x, data_y, masks, data_length
if name == ' main ': # test with open('../data/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 input_data, label, mask, length in train_dataloader:
print(input_data, 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('cws_result.txt', 'w', encoding='utf-8')
with open('data/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)
with open('data/test_final.txt', 'r', encoding='utf-8') as f:
for test in f:
flag = False
test = test.strip()
x = torch.LongTensor(1, len(test))
mask = torch.ones_like(x, dtype=torch.uint8)
length = [len(test)]
for i in range(len(test)):
if test[i] in word2id:
x[0, i] = word2id[test[i]]
else:
x[0, i] = len(word2id)
predict = model.infer(x, mask, length)[0]
for i in range(len(test)):
print(test[i], end='', file=output)
if id2tag[predict[i]] in ['E', 'S']:
print(' ', end='', file=output)
print(file=output)
```
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(4, 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/data_save.pkl', 'rb') as inp:
word2id = pickle.load(inp)
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)
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
)
test_data = 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 = []
# test
entity_predict = set()
entity_label = set()
with torch.no_grad():
model.eval()
cur = 0
for sentence, label, mask, length in test_data:
if use_cuda:
sentence = sentence.cuda()
label = label.cuda()
mask = mask.cuda()
predict = model.infer(sentence, mask, length)
for i in range(len(length)):
entity_split(sentence[i, :length[i]], predict[i], id2tag, entity_predict, cur)
entity_split(sentence[i, :length[i]], label[i, :length[i]], id2tag, entity_label, cur)
cur += length[i]
right_predict = [i for i in entity_predict if i in entity_label]
if len(right_predict) != 0:
precision = float(len(right_predict)) / len(entity_predict)
recall = float(len(right_predict)) / len(entity_label)
logging.info("precision: %f" % precision)
logging.info("recall: %f" % recall)
logging.info("score: %f" % ((2 * precision * recall) / (precision + recall)))
else:
logging.info("precision: 0")
logging.info("recall: 0")
logging.info("score: 0")
model.train()
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())
```
参考文献
- 财经领域事件抽取技术的研究与应用(北京理工大学·陈贺)
- 文本综合处理平台的研究与实现(济南大学·王孟孟)
- 面向农业领域的智能问答系统设计与实现(南京林业大学·张文婕)
- 分布式全文检索系统中索引管理及文件预处理研究(中国科学技术大学·戴上静)
- 基于领域本体和模板逻辑的中英双语问答系统的研究(广西师范大学·毛俊青)
- 分布式全文检索系统中索引管理及文件预处理研究(中国科学技术大学·戴上静)
- 以就业为导向的中职计算机教学资源平台的设计与实现(河北科技大学·翟红丽)
- 融入中文分词信息的共指消解模型(华东师范大学·肖巧文)
- 基于自然语言理解的全文搜索研究(湖北大学·黄翠平)
- 面向中文产品评论的情感分析研究(西南大学·孙雪峰)
- 面向中文产品评论的情感分析研究(西南大学·孙雪峰)
- 文本分类方法及其四险一金领域应用研究(哈尔滨工程大学·吴汉瑜)
- 基于LEBERT和知识图谱的智能地址修正补全方法(暨南大学·林佳铎)
- 基于K-Means的分布式文本聚类系统的设计与实现(西安电子科技大学·马婵媛)
- Text Classification Based on Graph Convolutional Neural Network with Intimacy Matrix and Text Linking(华中师范大学·夏冰)
本文内容包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主题。发布者:代码海岸 ,原文地址:https://bishedaima.com/yuanma/35740.html