文章目录

  • 文本预处理(Pre-processing)
  • 1. 读取数据集
  • 2. 标记化
  • 3. 词汇
  • 4. 整合上述功能
  • 总结


文本预处理(Pre-processing)

NLP中自然语言处理离不开对文本数据的预处理操作以方便后期神经网络的训练。
通常文本预处理包含有:

  1. 原始数据加载(raw data)
  2. 分词(segmentation)
  3. 数据清洗(Cleaning)
  4. 数据标准化(Normalization):Stemming / Lemmazation
  5. 特征提取(Feature extraction):tf-idf/word2vec
  6. 建模(modeling):相似度算法、分类算法

  本文主要介绍的是最基础的英语文本预处理,包括原始数据读入和分词,需要了解更多预处理操作可以参考NLP入门-- 文本预处理Pre-processing。当前已经有很多比较好的分词库了,可以直接调用,但是李沐大神的code给我们阐述了如何从最基础开始构建一个分词库,个人认为还是非常有用的,从基础了解起来也能更加方便地调用各种库去高效工作。

1. 读取数据集

一篇文章可以简单地看作是一个单词序列,甚至是一个字符序列。为了方便将来在试验中使用序列数据,这里对文本数据进行预处理,主要包括以下步骤:

  1. 将文本数据加载到内存之中
  2. 将字符串拆分为标记(如,单词和字符)
  3. 建立一个词汇表,将拆分的标记映射到数字索引
  4. 将文本转换为数字索引序列,以便模型可以轻松地对其进行其他操作
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

2. 标记化

  将文本序列拆分为一个标记列表,标记(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']

3. 词汇

  标记的字符串类型不方便模型的使用,模型需要的输入是数字。我们构建一个字典(词表vocabulary),用来将字符串标记映射到从0开始的数字索引中。

  • 为此我们需要首先统计训练集中所有文档中唯一的标记,称之为语料(corpus)。
  • 然后根据每个唯一标记出现的频率为其分配一个数字索引。(很少出现的标记通常被移除以降低复杂性)
  • 语料库(corpus)中不存在或者已经删除的任何标记都可以映射到一个特定的未知标记 “<unk >”.
  • 我们可以选择增加一个列表,用于保存保留的标记,例如“<pad>”表示填充;“<bos>”表示序列的开始;“<eos>”表示序列的结束。
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, ['<unk>']+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])
[('<unk>', 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]

4. 整合上述功能

  将全部的内容打包到load_corpus_time_machine函数之中,此函数返回corpus(标记索引列表)和vocab(时光机器语料库的词汇表)。需要修改两个地方:

  1. 我们将文本标记转换为字符,而不是单词,以便于简化后面章节的训练
  2. 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())
[('<unk>', 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