动手学深度学习8.2 简单的文本预处理

参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战


这篇文章是讲述如何进行简单的文本预处理,真的是超级简单的那种。 我们要进行以下四个步骤:

  1. 将文本作为字符串加载到内存中。
  2. 将字符串拆分为词元(如单词和字符)。
  3. 建立一个词汇表,将拆分的词元映射到数字索引。
  4. 将文本转换为数字索引序列,方便模型操作。

这真的是最最最简单的文本预处理了,除了《动手学深度学习》,之前看鱼书的时候第一个预处理也是这样的。所以新入门的人看一下应该会很合适。

import collections
import re
from d2l import torch as d2l
复制代码

读取数据集

d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt',
                                '090b5e7e70c295757f55df93cb0a180b9691891a')

def read_time_machine():
    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])
复制代码

这段代码是从H.G.Well 的时光机器中加载文本,是一个3w词的小语料库。

  • 下载过程中会显示

    Downloading ..\data\timemachine.txt from d2l-data.s3-accelerate.amazonaws.com/timemachine…...

  • 下载完毕后输出多少航,以及第0行和第10行的内容。

    >>
    # text lines: 3221
    the time machine by h g wells
    twinkled and his usually pale face was flushed and animated the
    复制代码
  • read_time_machine()是从下载的文件中读取句子,去除掉其中大小写字母以外的其他字符,并将其存到列表中。 经过这步操作之后列表里只剩小写字母和空格。

    • for line in lines是一个生成器对象
      • [for line in lines]是将生成器对象返回到列表中

      • [对line进行操作 for line in lines]就是对列表lines中的每个元素进行操作

      • re.sub('[^A-Za-z]+', ' ', line)使用正则表达式替换line中除了字母以外的元素并将其替换为空格。

        • re.sub(*pattern*, *repl*, *string*, *count=0*, *flags=0*)

          使用 repl 替换在 string 中出现的 pattern 并返回结果字符串。

          repl 可以是字符串或函数;如为字符串,则其中任何反斜杠转义序列都会被处理。 也就是说,\n 会被转换为一个换行符,\r 会被转换为一个回车符,依此类推。

          可选参数 count 是要替换的最大次数;count 必须是非负整数。如果省略这个参数或设为 0,所有的匹配都会被替换。

      • .strip()去掉字符串两头的空格

      • .lower()将大写字母转换为小写

词元化

就是将文本拆成单词列表,次元是文本的基本单位,

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('错误:未知词元类型:' + token)
复制代码

这一步是将文本拆成单词(或空格)还是拆成单个字母(或空格),

  • token = word就是将其拆分为单词
  • token = char就是将其拆分为字母

试一下tokenize函数的效果:

  • word

    tokens = tokenize(lines)
    for i in range(10,13):
        print(f"\nline[{i}]:",lines[i])
        print(f"\ntokens[{i}]:",tokens[i])
    复制代码
    >>
    line[10]: twinkled and his usually pale face was flushed and animated the
    
    tokens[10]: ['twinkled', 'and', 'his', 'usually', 'pale', 'face', 'was', 'flushed', 'and', 'animated', 'the']
    
    line[11]: fire burned brightly and the soft radiance of the incandescent
    
    tokens[11]: ['fire', 'burned', 'brightly', 'and', 'the', 'soft', 'radiance', 'of', 'the', 'incandescent']
    
    line[12]: lights in the lilies of silver caught the bubbles that flashed and
    
    tokens[12]: ['lights', 'in', 'the', 'lilies', 'of', 'silver', 'caught', 'the', 'bubbles', 'that', 'flashed', 'and']
    复制代码
  • char

    tokens = tokenize(lines, 'char')
    for i in range(10,13):
        print(f"\nline[{i}]:",lines[i])
        print(f"\ntokens[{i}]:",tokens[i])
    复制代码
    >>
    line[10]: twinkled and his usually pale face was flushed and animated the
    
    tokens[10]: ['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']
    
    line[11]: fire burned brightly and the soft radiance of the incandescent
    
    tokens[11]: ['f', 'i', 'r', 'e', ' ', 'b', 'u', 'r', 'n', 'e', 'd', ' ', 'b', 'r', 'i', 'g', 'h', 't', 'l', 'y', ' ', 'a', 'n', 'd', ' ', 't', 'h', 'e', ' ', 's', 'o', 'f', 't', ' ', 'r', 'a', 'd', 'i', 'a', 'n', 'c', 'e', ' ', 'o', 'f', ' ', 't', 'h', 'e', ' ', 'i', 'n', 'c', 'a', 'n', 'd', 'e', 's', 'c', 'e', 'n', 't']
    
    line[12]: lights in the lilies of silver caught the bubbles that flashed and
    
    tokens[12]: ['l', 'i', 'g', 'h', 't', 's', ' ', 'i', 'n', ' ', 't', 'h', 'e', ' ', 'l', 'i', 'l', 'i', 'e', 's', ' ', 'o', 'f', ' ', 's', 'i', 'l', 'v', 'e', 'r', ' ', 'c', 'a', 'u', 'g', 'h', 't', ' ', 't', 'h', 'e', ' ', 'b', 'u', 'b', 'b', 'l', 'e', 's', ' ', 't', 'h', 'a', 't', ' ', 'f', 'l', 'a', 's', 'h', 'e', 'd', ' ', 'a', 'n', 'd']
    复制代码

词汇表

上面处理之后我们得到的词元是字符串类型的,而模型需要的输入是数字,因此我们需要给每个词元打上编号,方便模型使用编号。

构建一个词汇表(vocabulary) 字典,用来将字符串类型的词元映射到从0开始的数字索引中。

def count_corpus(tokens): 
    if len(tokens) == 0 or isinstance(tokens[0], list):
        # 将词元列表展平成使用词元填充的一个列表
        tokens = [token for line in tokens for token in line]
    return collections.Counter(tokens)
复制代码

这个函数用于统计词元的频率。

将训练集中的所有文档合并在一起,对它们的唯一词元进行统计,得到的统计结果称之为语料(corpus)。 然后根据每个唯一词元的出现频率,为其分配一个数字索引。

  • if len(tokens) == 0 or isinstance(tokens[0], list)当tokens是空列表或者二维列表的时候返回true(当然三维四维也返回true,但是我们对前边tokens操作决定了它最多二维)。
  • tokens = [token for line in tokens for token in line]用于列表扁平化, 将原来的二维转化为一维。
  • 最后返回给collections.Counter
    • class collections.Counter([*iterable-or-mapping*])

      一个 Counter 是一个 dict 的子类

      它是一个集合,元素像字典键(key)一样存储,它们的计数存储为值。计数可以是任何整数值,包括0和负数。 Counter 类有点像其他语言中的 bags或multisets。

      c = collections.Counter('gallahad')   
      print(c)
      c = collections.Counter({'red': 4, 'blue': 2})
      print(c)
      c = collections.Counter(['eggs', 'ham'])
      print(c)
      复制代码
      >>
      Counter({'a': 3, 'l': 2, 'g': 1, 'h': 1, 'd': 1})
      Counter({'red': 4, 'blue': 2})
      Counter({'eggs': 1, 'ham': 1})
      复制代码

      如果引用的键没有任何记录,就返回一个0,而不是弹出一个 KeyError

      c = collections.Counter('gallahad')   
      print(c['z'])
      复制代码
      >>
      0
      复制代码

处理预料的时候将出现频率较低的词移除,可以降低处理的复杂度。

语料库中不存在或已删除的任何词元都将映射到一个特定的未知词元 “<unk>” 。

我们还可以增加一个列表,用于保存那些被保留的词元,例如:填充词元(“<pad>”);序列开始词元(“<bos>”);序列结束词元(“<eos>”)。

现在写一个类实现文本词汇表功能:

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):
        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):
        if not isinstance(indices, (list, tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]
复制代码
  • 参数:

    • token:就是你的文本是按照word还是char分类的
    • min_freq:设定一个阈值,如果某些词频率过低就将其忽略
    • reserved_tokens:句子开始或者终止的token
  • 开头两个if语句,如果接受不到tokens或者reserved_tokens就将其设为空防止报错。

  • counter接受统计好词频的语料

  • self.token_freqs得到按频率降序的字典

    • sorted(iterable, key=None, reverse=False)

      参数说明:

      • iterable -- 可迭代对象。
      • key -- 主要是用来进行比较的元素,只有一个参数,具体的函数的参数就是取自于可迭代对象中,指定可迭代对象中的一个元素来进行排序。
      • reverse -- 排序规则,reverse = True 降序 , reverse = False 升序(默认)。
    • 使用counter的.items()进行排序,也就是键值对的元组。key设置为x[1],每个键值对的元组被视为x,x[1]即按照后边的数值排序。reverse = True对其进行降序排列。

  • 最后一部分就是写了self.idx_to_token, self.token_to_idx两个,用于获取其词频或者单词。


  1. 本文是《动手学深度学习》的pytorch版读书笔记,该系列更多文章可以看这里:《动手学深度学习》专栏(juejin.cn)

  2. 笔记Github地址:DeepLearningNotes/d2l(github.com)

还在更新中…………

猜你喜欢

转载自juejin.im/post/7034421928945418276