NLP中自然语言处理离不开对文本数据的预处理操作以方便后期神经网络的训练。
通常文本预处理包含有:
本文主要介绍的是最基础的英语文本预处理,包括原始数据读入和分词,需要了解更多预处理操作可以参考NLP入门-- 文本预处理Pre-processing。当前已经有很多比较好的分词库了,可以直接调用,但是李沐大神的code给我们阐述了如何从最基础开始构建一个分词库,个人认为还是非常有用的,从基础了解起来也能更加方便地调用各种库去高效工作。
一篇文章可以简单地看作是一个单词序列,甚至是一个字符序列。为了方便将来在试验中使用序列数据,这里对文本数据进行预处理,主要包括以下步骤:
import collections
import re
from d2l import torch as d2l
我们从H.G.Well的 时光机器 中加载文本作为开始。这是一个相当小的语料库,只有30000多个单词,但足够实现我们的目标,即介绍文本预处理。现实中的文档集合可能会包含数十亿个单词。下面的函数将数据集读取到由文本行组成的列表中,其中每行都是一个字符串。为简单起见,我们在这里忽略了标点符号和字母大写。
#@save
d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL+'timemachine.txt','090b5e7e70c295757f55df93cb0a180b9691891a')
def read_time_machine():
"""这里的预处理操作比较暴力,将标点符号和特殊字符全部剔除了,只剩下了26个字母和空格"""
with open(d2l.download('time_machine'),'r') as f:
lines = f.readlines()
return [re.sub('[^A-Za-z]+',' ',line).strip().lower() for line in lines]
lines = read_time_machine()
print(f'# text lines: {len(lines)}')
print(lines[0])
print(lines[10])
Downloading ../data/timemachine.txt from http://d2l-data.s3-accelerate.amazonaws.com/timemachine.txt...
# text lines: 3221
the time machine by h g wells
twinkled and his usually pale face was flushed and animated the
将文本序列拆分为一个标记列表,标记(token)是文本的基本单位。最后返回一个标记列表,其中每个标记都是一个字符串(string)
def tokenize(lines,token='word'):
if token=="word":
return [line.split() for line in lines]
elif token =="char":
return [list(line) for line in lines]
else:
print("Error:未知令牌类型:"+token)
tokens = tokenize(lines)
for i in range(11):
print(tokens[i])
['the', 'time', 'machine', 'by', 'h', 'g', 'wells']
[]
[]
[]
[]
['i']
[]
[]
['the', 'time', 'traveller', 'for', 'so', 'it', 'will', 'be', 'convenient', 'to', 'speak', 'of', 'him']
['was', 'expounding', 'a', 'recondite', 'matter', 'to', 'us', 'his', 'grey', 'eyes', 'shone', 'and']
['twinkled', 'and', 'his', 'usually', 'pale', 'face', 'was', 'flushed', 'and', 'animated', 'the']
标记的字符串类型不方便模型的使用,模型需要的输入是数字。我们构建一个字典(词表vocabulary),用来将字符串标记映射到从0开始的数字索引中。
def count_corpus(tokens):
"""统计标记的频率:这里的tokens是1D列表或者2D列表"""
if len(tokens) ==0 or isinstance(tokens[0],list):
# 将tokens展平成使用标记填充的一个列表
tokens = [token for line in tokens for token in line]
return collections.Counter(tokens)
class Vocab:
"""构建文本词表"""
def __init__(self,tokens=None,min_freq=0,reserved_tokens=None):
if tokens is None:
tokens=[]
if reserved_tokens is None:
reserved_tokens = []
# 根据出现频率排序
counter = count_corpus(tokens)
self.token_freqs = sorted(counter.items(),key=lambda x:x[1],reverse=True)
# 未知标记的索引为0
self.unk , uniq_tokens = 0, ['' ]+reserved_tokens
uniq_tokens += [token for token,freq in self.token_freqs
if freq >= min_freq and token not in uniq_tokens]
self.idx_to_token,self.token_to_idx = [],dict() # 根据索引找标记和根据标记找索引
for token in uniq_tokens:
self.idx_to_token.append(token)
self.token_to_idx[token] = len(self.idx_to_token)-1
def __len__(self):
return len(self.idx_to_token)
def __getitem__(self,tokens):
"""转换到一个一个的item进行输出"""
if not isinstance(tokens,(list,tuple)):
return self.token_to_idx.get(tokens,self.unk)
return [self.__getitem__(token) for token in tokens]
def to_tokens(self,indices):
"""如果是单个index直接输出,如果是list或者tuple迭代输出"""
if not isinstance(indices,(list,tuple)):
return self.idx_to_token[indices]
return [self.idx_to_token[index] for index in indices]
# 使用time machine数据集作为语料库来构建词汇表,然后打印前几个常见的标记和索引
vocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[:10])
[('', 0), ('the', 1), ('i', 2), ('and', 3), ('of', 4), ('a', 5), ('to', 6), ('was', 7), ('in', 8), ('that', 9)]
# 现在可以将每一行文本转换为一个数字索引
for i in [0,10]:
print('words:',tokens[i])
print('indeces:',vocab[tokens[i]])
words: ['the', 'time', 'machine', 'by', 'h', 'g', 'wells']
indeces: [1, 19, 50, 40, 2183, 2184, 400]
words: ['twinkled', 'and', 'his', 'usually', 'pale', 'face', 'was', 'flushed', 'and', 'animated', 'the']
indeces: [2186, 3, 25, 1044, 362, 113, 7, 1421, 3, 1045, 1]
将全部的内容打包到load_corpus_time_machine
函数之中,此函数返回corpus
(标记索引列表)和vocab
(时光机器语料库的词汇表)。需要修改两个地方:
corpus
是单个列表,而不是使用标记列表构成的一个列表,因为time machine数据集中的每行文本行不一定是一个句子或者一个段落。def load_corpus_time_machine(max_tokens=-1):
"""返回Time machine数据集中的标记索引列表和词汇表"""
lines = read_time_machine()
tokens = tokenize(lines,'char')
vocab = Vocab(tokens)
# 因为Time machine数据集中每一个文本行,不一定是一个句子或者段落
# 所以将所有文本行展平到一个列表之中
corpus = [vocab[token] for line in tokens for token in line]
if max_tokens >0:
corpus = corpus[:max_tokens]
return corpus,vocab
corpus,vocab = load_corpus_time_machine()
len(corpus),len(vocab)
(170580, 28)
list(vocab.token_to_idx.items())
[('', 0),
(' ', 1),
('e', 2),
('t', 3),
('a', 4),
('i', 5),
('n', 6),
('o', 7),
('s', 8),
('h', 9),
('r', 10),
('d', 11),
('l', 12),
('m', 13),
('u', 14),
('c', 15),
('f', 16),
('w', 17),
('g', 18),
('y', 19),
('p', 20),
('b', 21),
('v', 22),
('k', 23),
('x', 24),
('z', 25),
('j', 26),
('q', 27)]
lines = read_time_machine()
tokens = tokenize(lines,'char')
for i in [0,10]:
print('words:',tokens[i])
print('indeces:',vocab[tokens[i]])
words: ['t', 'h', 'e', ' ', 't', 'i', 'm', 'e', ' ', 'm', 'a', 'c', 'h', 'i', 'n', 'e', ' ', 'b', 'y', ' ', 'h', ' ', 'g', ' ', 'w', 'e', 'l', 'l', 's']
indeces: [3, 9, 2, 1, 3, 5, 13, 2, 1, 13, 4, 15, 9, 5, 6, 2, 1, 21, 19, 1, 9, 1, 18, 1, 17, 2, 12, 12, 8]
words: ['t', 'w', 'i', 'n', 'k', 'l', 'e', 'd', ' ', 'a', 'n', 'd', ' ', 'h', 'i', 's', ' ', 'u', 's', 'u', 'a', 'l', 'l', 'y', ' ', 'p', 'a', 'l', 'e', ' ', 'f', 'a', 'c', 'e', ' ', 'w', 'a', 's', ' ', 'f', 'l', 'u', 's', 'h', 'e', 'd', ' ', 'a', 'n', 'd', ' ', 'a', 'n', 'i', 'm', 'a', 't', 'e', 'd', ' ', 't', 'h', 'e']
indeces: [3, 17, 5, 6, 23, 12, 2, 11, 1, 4, 6, 11, 1, 9, 5, 8, 1, 14, 8, 14, 4, 12, 12, 19, 1, 20, 4, 12, 2, 1, 16, 4, 15, 2, 1, 17, 4, 8, 1, 16, 12, 14, 8, 9, 2, 11, 1, 4, 6, 11, 1, 4, 6, 5, 13, 4, 3, 2, 11, 1, 3, 9, 2]
参考:
【1】动手学深度学习 PyTorch版
【2】NLP入门-- 文本预处理Pre-processing
【3】《动手学深度学习》