在解决 NLP 任务之前, 首先就要构建自己的词表。词表的作用就是给定语料,将文本中的以字为单位、或者以词为单位转换为整数序号,该序号可用于在词嵌入的 lookup table 中搜索词向量。
接下来,我们介绍下词表的构建过程,当然每个人的词表构建过程中对语料的某些处理细节不同,但是大步骤基本都是相同的。我们将使用 LCSTS 数据集构建词表。
LCSTS 数据集是哈工大基于新闻媒体在微博上发布的新闻构建的,内容包含:
- 总样本有 210 万样本
- 一篇短文(约100个字符)对应一篇摘要(约20个字符)
共有三个文件,如下所示:
文件内的每条样本如下:
我们就编写 Vocab 类,对上图中的 short_text、summary 内容构建词表。
import glob
import re
import jieba
import time
from collections import Counter
from multiprocessing import Pool
from multiprocessing import cpu_count
import random
from os.path import exists
def timer(func):
def inner(*args, **kwargs):
print(args[0].flag)
print('function [%s] starts runing' % (func.__name__))
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print('function [%s] executed for %.2f seconds' % (func.__name__, end - start))
print('-' * 51)
return result
return inner
# 分词函数
def cut_word(data):
"""
:param data: 要分词的多行文本
:return: 分词之后的结果
"""
# 加载停用词
stop_words = []
for line in open('data/stopword.txt'):
stop_words.append(line)
content_word = []
for line in data:
words = jieba.lcut(line)
content_word.append(words)
# 停用词过滤
# temp = []
# for word in words:
# if word not in stop_words:
# temp.append(word)
# content_word.append(temp)
return content_word
# 统计词频函数
def statistics_words(data):
word_freq = Counter()
for words in data:
word_freq += Counter(words)
return word_freq
# 并行结巴分词
def parallel(task, data):
# 进程池开启
pool = Pool()
cut_word_results = pool.map(task, data)
pool.close()
pool.join()
return cut_word_results
def split_data(data, part_num):
"""
:param data: 需要拆分的数据
:param part_num: 将数据拆分成几块
:return: 返回块列表
"""
data_number = len(data)
step, rest = divmod(data_number, part_num)
blocks = [data[start * step: start * step + step + 1] for start in range(part_num)]
# 如果无法按块的数量整除,则剩余部分数据追加到最后一个块中
if rest > 0:
blocks[-1].extend(data[-rest:])
return blocks
# 设置结巴分词不显示日志
jieba.setLogLevel(20)
class Vocab:
# 原始文本分词之后的内容
SHORT_SAVE_PATH = 'vocab/short_%s.txt'
# 基于原始文本构建的词表
VOCAB_SAVE_PATH = 'vocab/vocab_%s.txt'
def __init__(self, flag='encoder'):
"""
:param flag: encoder 加载原始文本, decoder 加载摘要文本
"""
# 处理原文还是摘要
self.flag = flag
# 文本内容路径
self.short_path = Vocab.SHORT_SAVE_PATH % self.flag
self.vocab_path = Vocab.VOCAB_SAVE_PATH % self.flag
# 获得数据集文件列表
self.filenmes = glob.glob('data/DATA/*.txt')
if exists(self.short_path) and exists(self.vocab_path):
self.load()
else:
self.build()
def load(self):
# 存储词到索引的映射
self.word_to_index = {}
# 存储索引到词的映射
self.index_to_word = {}
# 存储词在语料中的频数
self.freq_of_word = {}
# 加载词表
for line in open(self.vocab_path, 'r'):
word, idx, freq = line.split()
idx, freq = int(idx), int(freq)
self.word_to_index[word] = idx
self.index_to_word[idx] = word
self.freq_of_word[word] = freq
# 加载数据
self.content_words = []
for line in open(self.short_path, 'r'):
self.content_words.append(line.split())
# 开始记录索引
self.token_index = 0
def build(self):
# 存储词到索引的映射
self.word_to_index = {'PAD': 0, 'UNK': 1, 'SOS': 2, 'EOS': 3}
# 存储索引到词的映射
self.index_to_word = {value: key for key, value in self.word_to_index.items()}
# 存储词在语料中的频数
self.freq_of_word = Counter()
# 开始记录索引
self.token_index = len(self.word_to_index)
# 语料的分词结果
self.content_words = []
# 1. 加载文件内容
contents = self._load_from_txt()
# 2. 分词处理
self._cut_word(contents)
# 3. 构建词表
self._build_vocab()
# 4. 存储词表
self._save_dict()
@timer
def _load_from_txt(self):
"""读取文件内容"""
contents = []
for filename in self.filenmes:
content = open(filename, 'r').read()
# 提取短文内容
if self.flag == 'encoder':
match_content = re.findall(r'<short_text>(.*?)</short_text>', content, re.S)
if self.flag == 'decoder':
match_content = re.findall(r'<summary>(.*?)</summary>', content, re.S)
for content in match_content:
contents.append(content.strip())
return contents
@timer
def _cut_word(self, contents):
"""内容分词"""
# 1. 拆分数据
blocks = split_data(contents, 12)
# 2. 并行分词
cut_word_result = parallel(cut_word, blocks)
# 合并多进程分词结果
for result in cut_word_result:
self.content_words.extend(result)
@timer
def _build_vocab(self):
"""构建词典"""
# 1. 词频数据分块
blocks = split_data(self.content_words, 12)
# 2. 并行统计词频
statistics_results = parallel(statistics_words, blocks)
# 3. 合并统计结果
for result in statistics_results:
self.freq_of_word += result
# 4. 构建词索引映射
for word, _ in self.freq_of_word.items():
if word not in self.word_to_index:
self.word_to_index[word] = self.token_index
self.index_to_word[self.token_index] = word
self.token_index += 1
@timer
def _save_dict(self):
"""存储词表"""
with open(self.vocab_path, 'w') as file:
for word, idx in self.word_to_index.items():
if word.strip() == '':
continue
file.write("%s %s %s\n" % (word, idx, self.freq_of_word[word]))
with open(self.short_path, 'w') as file:
for content in self.content_words:
file.write(' '.join(content) + '\n')
if __name__ == '__main__':
encoder_vocab = Vocab('encoder')
decoder_vocab = Vocab('decoder')
print('encoder_vocab:', len(encoder_vocab.word_to_index))
print('decoder_vocab:', len(decoder_vocab.word_to_index))
在对语料库构建词表过程中,一开始发现整个执行过程极其的慢,通过分析程序,对分词、统计词频的实现增加进程池并发,提升了至少 8 倍的效率。