转眼间,接触NLP已经一年多了,虽然大部分时间是在打杂,但也多少积累了一点东西。今天在这里我就把我自己写的中文NLP数据预处理代码分享一下,代码基于python 3.6.5
在win 10
通过测试,涵盖了我自己常用的一些操作。
程序大致包括的功能如下表:
文件操作 | 去噪操作 | 其他 |
---|---|---|
读写文本 | 删除空行 | 分词 |
合并文件 | 删除中英文标点 | 词性标注 |
分割数据集 | 删除停用词 | 命名实体识别 |
- | 删除乱码和特殊符号 | 依存句法分析 |
- | 删除英文字符 | 语义角色标注 |
一般来说,中文NLP中数据预处理的步骤是:去除无意义符号、去除非中文、分词、去除停用词、向量化。除了向量化,其他步骤我的程序中都有写到,向量化一般使用TF-IDF
和Word2Vec
比较多。
另外,上表中其他
这一列的功能借助了工具pyltp。对于pyltp
的模型,有一点需要注意,我使用的语义角色标注模型是pisrl_win.model
,需要另外下载,而Linux平台下直接使用ltp_data_v3.4.0
文件夹下的pisrl.model
文件就可以了。这里是所有模型的下载链接。
使用代码之前需要安装一些依赖:
pip install numpy
pip install pyltp
pip install zhon
如果windows
平台下安装pyltp报错,可以使用这个链接下载python 3.6的版本,还有python 3.5版本。然后使用命令pip install xxx.whl
安装对应的文件即可。
下面直接看程序preprocess.py
,因为注释比较详细,我就不再解释了。
"""本文件包含中文NLP预处理常用的一些代码"""
import os
import re
import string
import numpy as np
from pyltp import *
from zhon.hanzi import punctuation
class Preprocess(object):
"""中文NLP预处理类"""
# 用来处理数据的正则表达式
DIGIT_RE = re.compile(r'\d+')
LETTER_RE = re.compile(r'[a-zA-Z]+')
SPECIAL_SYMBOL_RE = re.compile(r'[^\w\s\u4e00-\u9fa5]+') # 用以删除一些特殊符号
NAMED_ENTITY = re.compile(r'[SBIE]+')
STOPS = ['。', '.', '?', '?', '!', '!'] # 中英文句末字符
# 句子所限制的最小,最大长度
SENTENCE_MIN_LEN = 5
SENTENCE_MAX_LEN = 50
def __init__(self, ltp_model_dir):
self._cws_model_path = os.path.join(ltp_model_dir, 'cws.model')
self._pos_model_path = os.path.join(ltp_model_dir, 'pos.model')
self._ner_model_path = os.path.join(ltp_model_dir, 'ner.model')
self._par_model_path = os.path.join(ltp_model_dir, 'parser.model')
self._srl_model_path = os.path.join(ltp_model_dir, 'pisrl_win.model')
@staticmethod
def read_text_file(text_file):
"""读取文本文件,并返回由每行文本作为元素组成的list."""
with open(text_file, 'r', encoding='utf-8') as file:
lines = [line.strip() for line in file]
return lines
@staticmethod
def write_text_file(text_list, target_file):
"""将文本列表写入目标文件
Args:
text_list: 列表,每个元素是一条文本
target_file: 字符串,写入目标文件路径
"""
with open(target_file, 'w', encoding='utf-8') as writer:
for text in text_list:
writer.write(text + '\n')
@staticmethod
def merge_files(filedir, target_file):
"""
合并一个文件夹中的文本文件。注意:需要合并的每个文件的结尾要有换行符。
Args:
filedir: 需要合并文件的文件夹
target_file: 合并后的写入的目标文件
"""
filenames = os.listdir(filedir)
with open(target_file, 'a', encoding='utf-8') as f:
for filename in filenames:
filepath = os.path.join(filedir, filename)
f.writelines(open(filepath, encoding='utf-8').readlines())
@staticmethod
def partition_dataset(dataset, ratio):
"""将一个大的数据集按比例切分为训练集、开发集、测试集
Args:
dataset: 列表,原始数据集
ratio: 三元组,训练集、开发集、测试集切分比例,每个元素为0-1之间的小数
Returns: train, val, test, 表示训练集、开发集、测试集的三个列表
"""
data_len = len(dataset)
train_len = int(np.floor(data_len * ratio[0]))
val_len = int(np.ceil(data_len * ratio[1]))
test_len = data_len - train_len - val_len
return dataset[:train_len], dataset[train_len: -test_len], dataset[-test_len:]
@staticmethod
def is_equal(sent1, sent2):
"""判断两个句子是否完全相同"""
return sent1 == sent2
@staticmethod
def del_blank_lines(sentences):
"""删除句子列表中的空行,返回没有空行的句子列表
Args:
sentences: 字符串列表
"""
return [s for s in sentences if s.split()]
@staticmethod
def del_punctuation(sentence):
"""删除字符串中的中英文标点.
Args:
sentence: 字符串
"""
en_punc_tab = str.maketrans('', '', string.punctuation) # ↓ ① ℃处理不了
sent_no_en_punc = sentence.translate(en_punc_tab)
return re.sub(r'[%s]+' % punctuation, "", sent_no_en_punc)
@staticmethod
def del_stopwords(seg_sents, stopwords):
"""删除句子中的停用词
Args:
seg_sents: 嵌套列表,分好词的句子(列表)的列表
stopwords: 停用词列表
Returns: 去除了停用词的句子的列表
"""
return [[word for word in sent if word not in stopwords]for sent in seg_sents]
@classmethod
def is_length_valid(cls, sentence):
"""限制句子长度,判断是否合法."""
return cls.SENTENCE_MIN_LEN <= len(sentence) <= cls.SENTENCE_MAX_LEN
@classmethod
def is_simple_sentence(cls, sentence):
"""判断是否是简单句。
简单句在这里定义为句子中只有一个句末终止符的句子,这样的句子含义比较明确。"""
counter = 0
for word in sentence:
if word in cls.STOPS:
counter += 1
if counter > 1:
return False
return True
@classmethod
def del_special_symbol(cls, sentence):
"""删除句子中的乱码和一些特殊符号。"""
return cls.SPECIAL_SYMBOL_RE.sub('', sentence)
@classmethod
def del_english_word(cls, sentence):
"""删除句子中的英文字符"""
return cls.LETTER_RE.sub('', sentence)
@classmethod
def get_ne_index(cls, ne_sent):
"""获取命名实体在句子中的位置。
Args:
ne_sent: 命名实体标记构成的列表
"""
return [idw for idw, word in enumerate(ne_sent) if cls.NAMED_ENTITY.match(word)]
def seg_sentences(self, sentences):
"""对输入的字符串列表进行分词处理,返回分词后的字符串列表."""
segmentor = Segmentor()
segmentor.load(self._cws_model_path)
seg_sents = [list(segmentor.segment(sent)) for sent in sentences]
segmentor.release()
return seg_sents
def postag_sentences(self, seg_sents):
"""对分完词的句子列表进行词性标注,返回标注的词性列表
Args:
seg_sents: 分好词的语句列表,每个语句也是一个列表
"""
postagger = Postagger()
postagger.load(self._pos_model_path)
pos_sents = [list(postagger.postag(sent)) for sent in seg_sents]
postagger.release()
return pos_sents
def rec_named_entity(self, seg_sents, pos_sents):
"""命名实体识别
Args:
seg_sents: 分好词的语句列表,每个语句也是一个列表
pos_sents: 词性标注好的词性列表,每个语句的词性也是一个列表
Returns: 命名实体识别完的语句列表,每个语句的命名实体识别结果也是一个列表
"""
recognizer = NamedEntityRecognizer()
recognizer.load(self._ner_model_path)
ne_sents = [list(recognizer.recognize(seg_sents[i], pos_sents[i])) for i in range(len(seg_sents))]
recognizer.release()
return ne_sents
def parse_dependency(self, seg_sents, pos_sents):
"""依存句法分析
Args:
seg_sents: 分好词的语句列表,每个语句也是一个列表
pos_sents: 词性标注好的词性列表,每个语句的词性也是一个列表
Returns:
arc_objs: pyltp.VectorOfParseResult对象,依存句法分析结果对象的列表。
arc_sents: 依存句法分析完的语句列表,每个语句的依存句法分析结果也是一个列表
"""
parser = Parser()
parser.load(self._par_model_path)
arc_objs = [parser.parse(seg_sents[i], pos_sents[i]) for i in range(len(seg_sents))]
arc_sents = [[(a.head, a.relation) for a in arc] for arc in arc_objs]
parser.release()
return arc_objs, arc_sents
def label_sementic_role(self, seg_sents, pos_sents, arc_sents):
"""语义角色标注
Args:
seg_sents: 分好词的语句列表,每个语句也是一个列表
pos_sents: 词性标注好的词性列表,每个语句的词性也是一个列表
arc_sents: 依存句法分析结果列表,每个语句的依存句法分析结果也是一个列表
Returns: 语义角色标注完的语句列表,每个语句的语义角色标注结果也是一个列表
"""
labeler = SementicRoleLabeller()
labeler.load(self._srl_model_path)
roles = [labeler.label(seg_sents[i], pos_sents[i], arc_sents[i]) for i in range(len(seg_sents))]
_ret = []
for role in roles:
_role = []
for r in role:
_role.extend([(r.index, arg.name, arg.range.start, arg.range.end) for arg in r.arguments])
_ret.append(_role)
labeler.release()
return _ret
然后是我进行功能测试的程序,程序中print
的输出均写在注释里了。
from preprocess import Preprocess
test_equal1 = "句子1"
test_equal2 = "句子2"
print(Preprocess.is_equal(test_equal1, test_equal2)) # False
test_blank = ['句子1', '', '', '', '\n', '\t', '\r', '\f', '句子2']
print(Preprocess.del_blank_lines(test_blank)) # ['句子1', '句子2']
test_punc = ",标点符号*.s#?.<"
print(Preprocess.del_punctuation(test_punc)) # 标点符号s
test_seg_sents = [['今天', '天气', '真', '不错', '啊'], ['Tom', 'and', 'the', 'cat']]
test_stopwords = ['啊', '着', 'the', '真']
print(Preprocess.del_stopwords(test_seg_sents, test_stopwords))
# [['今天', '天气', '不错'], ['Tom', 'and', 'cat']]
test_length = "你好"
print(Preprocess.is_length_valid(test_length)) # False
test_simple_sent = "这不是一个简单句。真的不是。"
print(Preprocess.is_simple_sentence(test_simple_sent)) # False
test_special_symbol = "一些*&=-!特殊↓℃符号%"
print(Preprocess.del_special_symbol(test_special_symbol)) # 一些特殊符号
test_en_word = "需要nobody删除p20pro英文come符so0n号"
print(Preprocess.del_english_word(test_en_word)) # 需要删除20英文符0号
proc = Preprocess('./ltp_data_v3.4.0/')
test_ltp = ["小明,把电视安好,你若安好便是晴天,你若安不好...", "那咱家就可以换电视了"]
seged = proc.seg_sentences(test_ltp)
print(seged)
# [['小明', ',', '把', '电视', '安好', ',', '你', '若', '安好', '便是', '晴天', ',', '你', '若', '安', '不好', '...'],
# ['那', '咱', '家', '就', '可以', '换', '电视', '了']]
posed = proc.postag_sentences(seged)
print(posed)
# [['nh', 'wp', 'p', 'n', 'v', 'wp', 'r', 'v', 'a', 'v', 'n', 'wp', 'r', 'v', 'v', 'a', 'wp'],
# ['r', 'r', 'n', 'd', 'v', 'v', 'n', 'u']]
ne = proc.rec_named_entity(seged, posed)
print(ne)
# [['S-Nh', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O'],
# ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']]
ne_sent = ['S-Nh', 'B-Ns', 'O', 'I-Ni', 'O', 'E-Nh']
print(Preprocess.get_ne_index(ne_sent))
# [0, 1, 3, 5]
arc_objs, arcs = proc.parse_dependency(seged, posed)
print(arcs)
# [[(5, 'SBV'), (1, 'WP'), (5, 'ADV'), (3, 'POB'), (0, 'HED'), (5, 'WP'), (9, 'SBV'), (9, 'ADV'), (5, 'COO'), (9, 'COO'), (10, 'VOB'), (10, 'WP'), (15, 'SBV'), (15, 'ADV'), (10, 'COO'), (15, 'CMP'), (5, 'WP')],
# [(3, 'ATT'), (3, 'ATT'), (6, 'SBV'), (5, 'ADV'), (6, 'ADV'), (0, 'HED'), (6, 'VOB'), (6, 'RAD')]]
roles = proc.label_sementic_role(seged, posed, arc_objs)
print(roles)
# [[(9, 'A1', 10, 10), (14, 'A0', 12, 12)],
# [(5, 'A2', 0, 2), (5, 'ADV', 3, 3), (5, 'A2', 6, 6)]]
最后,如果各位读者朋友对于程序有什么疑问或者发现我的程序有bug和值得改进的地方,欢迎在评论区留言,我会及时回复和纠正。
pyltp在windows下的安装使用,参考链接为:哈工大自然语言处理ltp在windows10下的安装使用。