序列数据中,最常见的例子就是文本数据,例如,一篇文章可以被简单地看作一串单词序列,甚至是一串字符序列。 本节中,我们将解析文本的常见预处理步骤。
0 文本预处理步骤
- 将文本作为字符串加载到内存中。
- 将字符串拆分为词元(如单词和字符)。
- 建立一个词表,将拆分的词元映射到数字索引。
- 将文本转换为数字索引序列,方便模型操作。
0.1 下载数据集
from d2l import torch as d2l# 下载文本数据集
#@save
d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt','090b5e7e70c295757f55df93cb0a180b9691891a')
file_path = d2l.download('time_machine')
print(f'下载的文件路径: {file_path}')
运行结果
文本存储在代码的上一层文件夹目录中
1 读取数据
这里只做演示,所以为了方便处理,我们忽略标点符号和字母大写:
import re # 正则表达式def read_txt():with open(file_path,'r') as f:lines=f.readlines()processed_lines = []for line in lines:# 替换所有非字母字符为单个空格cleaned_line = re.sub('[^A-Za-z]+', ' ', line)# 移除每行字符串两端的空白字符stripped_line = cleaned_line.strip()# 将每行字符串转换为小写lower_line = stripped_line.lower()# 将处理后的行添加到结果列表中processed_lines.append(lower_line)return processed_lineslines = read_txt()
print(f'# 文本总行数: {len(lines)}')
print(lines[0])
print(lines[10])
运行结果
2 词元化
词元(token)是文本的基本单位,我们这里把文本行拆分为单词或字符词元
def tokenize(lines,token='word'):# 如果toekn要求是word,则按空格划分为单词if token=='word':word_lines=[]for line in lines:word_lines.append(line.split())# line.split() 默认按空格分割字符串,并返回一个单词列表。return word_lines# 如果toekn要求是char,则按使用list变为字符列表elif token=='char':char_lines=[]for line in lines:char_lines.append(list(line))# list(line) 将字符串拆分为字符列表。return char_lineselse:print('unkonw:',token)tokens = tokenize(lines, token='word')
for i in range(11):print(tokens[i])
运行结果
3 词表
词元的类型是字符串,而模型需要的输入是数字,构建一个字典,即词表(vocabulary), 用来将字符串类型的词元映射到从 0 0 0 开始的数字索引中。
先将训练集中的所有文档合并在一起,对它们的唯一词元进行统计, 得到的统计结果称之为语料(corpus),然后根据每个唯一词元的出现频率,为其分配一个数字索引。 很少出现的词元通常被移除,这可以降低复杂性。
另外,语料库中不存在或已删除的任何词元都将映射到一个特定的未知词元“”。 我们可以选择增加一个列表,用于保存那些被保留的词元, 例如:填充词元(“”); 序列开始词元(“”); 序列结束词元(“”)。下面是用于构建和管理文本数据的词汇表的代码:
import collectionsdef count_corpus(tokens): #@save"""统计词元的频率"""if len(tokens) == 0 or isinstance(tokens[0], list):# 将词元列表展平成一个列表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_toekns=None):if tokens is None:tokens=[]if reserved_toekns is None:reserved_tokens=[]# 按频率从高到低排序counter=count_corpus(tokens)self._token_freqs=sorted(counter.items(),key=lambda x: x[1],reverse=True)self.idx_to_token=['<unk>'] + reserved_tokensself.token_to_idx = {}for idx, token in enumerate(self.idx_to_token):# 将词元及其索引添加到字典中self.token_to_idx[token] = idxfor token, freq in self._token_freqs:if freq < min_freq:breakif token not in self.token_to_idx:self.idx_to_token.append(token)self.token_to_idx[token] = len(self.idx_to_token) - 1@propertydef unk(self): # 未知词元的索引为0'''返回未知词元的索引 0'''return 0@propertydef token_freqs(self):'''返回按频率排序的词元列表'''return self._token_freqsdef __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]vocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[:10])
for i in [0, 10]:print('文本:', tokens[i])print('索引:', vocab[tokens[i]])
运行结果
4 整合功能
我们将所有功能打包到load_corpus_time_machine函数中, 该函数返回corpus(词元索引列表)和vocab(时光机器语料库的词表),我们在这里所做的改变是:
- 为了简化后面章节中的训练,我们使用字符(而不是单词)实现文本词元化;
- 时光机器数据集中的每个文本行不一定是一个句子或一个段落,还可能是一个单词,因此返回的corpus仅处理为单个列表,而不是使用多词元列表构成的一个列表。
def load_corpus_time_machine(max_tokens=-1): #@save"""返回时光机器数据集的词元索引列表和词表"""lines = read_txt()tokens = tokenize(lines, 'char')vocab = Vocab(tokens)# 因为时光机器数据集中的每个文本行不一定是一个句子或一个段落,# 所以将所有文本行展平到一个列表中corpus = [vocab[token] for line in tokens for token in line]if max_tokens > 0:corpus = corpus[:max_tokens]return corpus, vocabcorpus, vocab = load_corpus_time_machine()
len(corpus), len(vocab)
# 打印词汇表的一些信息
print("词汇表大小:", len(vocab))
print("词汇表前10个词元及其索引:")
for token, idx in list(vocab.token_to_idx.items())[:31]:print(f"{token}: {idx}")
运行结果
索引表vocab
:
原字符转化为索引后的列表corpus