76.文本预处理以及代码实现

对于序列数据处理问题,数据存在许多种形式,文本是最常见例子之一。 例如,一篇文章可以被简单地看作一串单词序列,甚至是一串字符序列。 本节中,我们将解析文本的常见预处理步骤。 这些步骤通常包括:

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

1. 读取数据集

首先,我们从H.G.Well的时光机器中加载文本。 这是一个相当小的语料库,只有30000多个单词,但足够我们小试牛刀, 而现实中的文档集合可能会包含数十亿个单词。 下面的函数(将数据集读取到由多条文本行组成的列表中),其中每条文本行都是一个字符串。 为简单起见,我们在这里忽略了标点符号和字母大写。

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'# 文本总行数: {len(lines)}')
print(lines[0])
print(lines[10])

运行结果:

在这里插入图片描述

2. 词元化

下面的tokenize函数将文本行列表(lines)作为输入, 列表中的每个元素是一个文本序列(如一条文本行)。 每个文本序列又被拆分成一个词元列表,词元(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('错误:未知词元类型:' + token)

tokens = tokenize(lines)
for i in range(11):
    print(tokens[i])

运行结果:

在这里插入图片描述

3. 词汇表

词元的类型是字符串,而模型需要的输入是数字,因此这种类型不方便模型使用。

现在,让我们构建一个字典,通常也叫做词表(vocabulary), 用来将字符串类型的词元映射到从 0 开始的数字索引中。

我们先将训练集中的所有文档合并在一起,对它们的唯一词元进行统计, 得到的统计结果称之为语料(corpus)。 然后根据每个唯一词元的出现频率,为其分配一个数字索引。 很少出现的词元通常被移除,这可以降低复杂性。 另外,语料库中不存在或已删除的任何词元都将映射到一个特定的未知词元“”。 我们可以选择增加一个列表,用于保存那些被保留的词元, 例如:填充词元(“”); 序列开始词元(“”); 序列结束词元(“”)。

python的sorted函数

class Vocab:  
    """文本词表"""
    # 如果一个token出现的次数小于min_freq,我就丢弃这个token
    # reserved_tokens是被保留的词元,如句子开始的token,句子结束的token
    def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
        if tokens is None: # 如果没有token,就为空
            tokens = []
        if reserved_tokens is None: # 如果没有reserved_tokens,也为空
            reserved_tokens = []
        # 按出现频率排序
        counter = count_corpus(tokens) # counter是一个字典
       # items()方法把字典中每对key和value组成一个元组,并把这些元组放在列表中返回,但整体是一个dict_items对象
        # 不过如果用 for循环遍历,如 for i in ... 还是能得到每个元组
        # 例如:dict_items([('apple',3),('banana',4)])
        # sorted可以对所有可迭代类型进行排序,并且返回新的已排序的列表。
        # sorted(iterable, cmp=None, key=None, reverse=False) --> new sorted list
        # 1. 可迭代参数,例如字典、列表,2.比较函数,
        # 3. 可迭代类型中某个属性,对给定元素的每一项进行排序,
        # 4. false 升序,true降序
        self._token_freqs = sorted(counter.items(), key=lambda x: x[1],
                                   reverse=True)
        # 未知词元是'<unk>',索引为0,其余的被保留的词元,索引依次递增
        # 不过默认reserved_tokens=None,包括后面调用函数也没有更改这个参数的值
        # 所以在这个语料库中只有<unk>,没有<bos>等
        # print(['<unk>'] + ['<bos>','<eos>'])
        # ['<unk>', '<bos>', '<eos>']
        self.idx_to_token = ['<unk>'] + reserved_tokens
        self.token_to_idx = {token: idx
                             for idx, token in enumerate(self.idx_to_token)}
        # 拿到token以及出现频率
        for token, freq in self._token_freqs:
            if freq < min_freq: # 如果出现频率小于min_freq,跳出循环
            # 因为已经按降序排列,找到了出现次数小于min_freq的,这个以及之后的词元都不需要了
                break
            if token not in self.token_to_idx: 
              # 如果不属于未知词元或者保留词元,出现频率也大于等于min_freq的词元
              # 其实就是一些文本中真正的单词,例如之前的'apple'、'banana'等类似
                self.idx_to_token.append(token)  # 将有意义的词元加入到 idx_to_token这个列表中
                # 换言之,idx_to_token这个列表保存了所有我需要的token,知道idx就得到token:
                # 先是'unk',再是reserved_tokens,再是'apple'、'banana'等
                # 所以例如:idx_to_token[0]='unk',idx_to_token[1]='bos',idx_to_token[2]='eos'
                # idx_to_token[3]='banana' 等等
                # 因此它的长度len(self.idx_to_token) 就是词元列表的长度
                self.token_to_idx[token] = len(self.idx_to_token) - 1 # 并且给这个词元一个索引
                # 但是token_to_idx是一个字典,token为key,idx为value:
                # 因此给一个token,是返回index,所以"self.token_to_idx[token] = 。。" 就是在重写index

    def __len__(self):
      # 计算得到unique token的个数,去重之后的token的个数
      # 因为相同的token我已经计算了出现次数
        return len(self.idx_to_token)

    # 给一个token tuple 或者 list of token,返回index
    def __getitem__(self, tokens):
        if not isinstance(tokens, (list, tuple)):
          # get()方法,是根据key返回value,如果指定键不存在,返回第二个参数
          # 第二个参数默认是None,在这里传入了unk
          # 也就是说 :遇到不认识的token就返回0作为index
            return self.token_to_idx.get(tokens, self.unk)
         # ps:这里用了递归,具体解释如下:
         # 假设tokens是一个数组['the','time','machine']
         # 遍历时先获取第1个token,即 'the',然后再进入函数__getitem__('the')
         # 因为'the'既不是元组,也不是list,而是字符串,所以会进入if语句
         # 就会传入token_to_idx.get('the')函数,根据'the'这个key 返回它的index
        return [self.__getitem__(token) for token in tokens]

    # 给一些index,把对应的token返回
    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]

    @property
    def unk(self):  # 未知词元的索引为0
        return 0

    @property
    def token_freqs(self):
        return self._token_freqs

def count_corpus(tokens):  
    """统计词元的频率"""
    # 这里的tokens是1D列表或2D列表
    if len(tokens) == 0 or isinstance(tokens[0], list):
        # 将词元列表展平成一个列表
        tokens = [token for line in tokens for token in line]
    
    # collections.Counter:给一堆token,计算每一个token出现的次数
    # 返回一个字典,如{'apple':3,'banana':4}
    return collections.Counter(tokens)

我们首先使用时光机器数据集作为语料库来构建词表,然后打印前几个高频词元及其索引。

vocab = Vocab(tokens)
# items()方法把字典中每对key和value组成一个元组,并把这些元组放在列表中返回,
# 再作为一个整体成为dict_item对象
print(list(vocab.token_to_idx.items())[:10])

运行结果:

在这里插入图片描述

现在,我们可以(将每一条文本行转换成一个数字索引列表)。

for i in [0, 10]:
  # 分别大于第1句话和第10句话以及对应的数字索引
    print('文本:', tokens[i])
    # vocab[]默认调用__getitem__函数
    # 因为__getitem__就是重载了[]运算符
    print('索引:', vocab[tokens[i]])

__ getitem__的作用是什么呢:如果在类中定义了__getitem__()方法,那么它的实例对象(假设为P)就可以这样P[key]取值。当实例对象做P[key]运算时,就会调用__getitem__()方法。

运行结果:

在这里插入图片描述

4. 整合所有功能

在使用上述函数时,我们将所有功能打包到load_corpus_time_machine函数中, 该函数返回corpus(词元索引列表)和vocab(时光机器语料库的词表)。 我们在这里所做的改变是:

  1. 为了简化后面章节中的训练,我们使用字符(而不是单词)实现文本词元化;
  2. 时光机器数据集中的每个文本行不一定是一个句子或一个段落,还可能是一个单词,因此返回的corpus仅处理为单个列表,而不是使用多词元列表构成的一个列表。
def load_corpus_time_machine(max_tokens=-1): 
    """返回时光机器数据集的词元索引列表和词表"""
    lines = read_time_machine()
    # ps:这里是按字符分,那么假设list('the time machine')
    # 则会变成['t','h','e',' ','t','i',....,'e']
    tokens = tokenize(lines, 'char')
    vocab = Vocab(tokens)
    # 因为时光机器数据集中的每个文本行不一定是一个句子或一个段落,
    # 所以将所有文本行展平到一个列表中
    # ps:下面这个二层for循环是如下:
    # for line in tokens: //取出每一行
    #   for token in line :// 再取出每一行的字符
    #     corpus.append(vocab[token]) 再把每一个字符对应的idx加到corpus中
    # ps:这个idx是和该字符的出现频率成反比的,该字符出现频率越大,对应idx越小
    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)
# len(corpus)=170580 表示这篇文章共有170580个字符
# 为什么len(vocab)是28呢?
# 答: char是字母,16个字母+<unk>+空格 = 28

python中__len__()和 getitem()函数,__dict__属性学习小结

运行结果:

在这里插入图片描述

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值