自然语言处理之Python实现中文分词

中文分词实现 1 问题描述 中文分词指的是将一个汉字序列切分成一个一个单独的词,中文分词是文本挖掘的基础,对于输入的一段中文,成功的进行中文分词

本文包含相关资料包-----> 点击直达获取<-------

中文分词实现

1 问题描述

中文分词指的是将一个汉字序列切分成一个一个单独的词。中文分词是文本挖掘的基础,对于输入的一段中文,成功的进行中文分词,可以达到电脑自动识别语句含义的效果。它是信息提取、信息检索、机器翻译、文本分类、自动文摘、语音识别、文本语音转换、自然语言理解等中文信息处理领域的基础。

1.1 基础任务

  1. 实现基于词典的分词算法

实验一资料包下的“Dictionary_based”文件夹中提供了基础词典和分词算法的大致框架。分词算法的核心部分需要大家完成,实验中提供了若干测试样本用以帮助大家判断算法是否正确实现。

  1. 实现基于统计学习的分词算法

实验中给出Bi-LSTM+CRF模型的基础实现,相关代码及说明文档位于实验一资料包下的“Bi-LSTM+CRF”文件夹下。请根据给定的实验资料中README.md文件配置相应实验环境,说明:(1)提供源码PyTorch语言编写(可根据个人掌情况用其他语言编写),默认运行版本是CPU版本;(2)如希望运行NPU版本,大家可跟任课老师联系,申请华为云资源运行(需提前统计名单:姓名+学号+个人手机号码+邮箱);

1.2 选做任务

优化基础任务中实现的分词器,可考虑的优化方案有:

  1. 修改网络结构,例如引入BERT等预训练语言模型;
  2. 与命名实体识别算法相互配合,减少对命名实体的错误分割;
  3. 构造合适的词典集(可扩充+人工整理);
  4. 实现新词发现(登录)功能,识别测试集中的新词(未登录词);
  5. 调整、优化模型训练过程中的超参数。

完成优化后对测试文件“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) 基于统计学习的中文分词,将输出的分词文件进行在线测评,结果如下:

QQ图片20210601200158

图 1- SEQ 图 \* ARABIC \s 1 2 在线测评结果

5 实验小结

实验中总体的代码框架已给出,需要实现的部分为前向最大匹配、后向最大匹配和双向最大匹配三个核心函数。通过相关资料的参考以及对中文分词的个人理解,能够顺利实现各个匹配模式的实现。

基于统计学习的中文分词代码,由于已经给出了代码框架,所以只是在其基础上做了部分优化,并增加了一部分的训练数据。

实验中代码仅提供一种简单的 Bi-LSTM+CRF PyTorch 实现方案。

更优实现可参见:https://github.com/bamtercelboo/pytorch_NER_BiLSTM_CNN_CRF/

环境搭建

  1. 安装 Anaconda

windows 下安装教程

官方文档: anaconda install

  1. 搭建虚拟环境并安装 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 ```

运行方式

  1. 配置 PyCharm

安装 PyCharm,并在 PyCharm 中使用 Anaconda 虚拟环境 ( 参考 )

  1. 安装其他依赖

sh # 在 nlplab 虚拟环境中安装 pip install -r requirements.txt

  1. 训练

```shell # save 目录下存放了一个粗略训练过的模型,可先跳过训练过程直接进行推断

# 数据准备,data 目录下运行 python 0.split.py python 1.data_u_ner.py # 模型训练,项目根目录下运行 # 若安装并配置了 GPU 相关运行环境可添加命令行参数 --cuda 来使用 GPU 训练 python run.py ```

  1. 推断

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

相关推荐

发表回复

登录后才能评论