文本的常见预处理步骤这些步骤通常包括将文本作为字符串加载到内存中。将字符串拆分为词元如单词和字符。建立一个词表将拆分的词元映射到数字索引。将文本转换为数字索引序列方便模型操作。import collections import re from d2l import torch as d2l #save d2l.DATA_HUB[time_machine] (d2l.DATA_URL timemachine.txt, 090b5e7e70c295757f55df93cb0a180b9691891a) def read_time_machine(): #save 将时间机器数据集加载到文本行的列表中 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]#.strip() 移除字符串开头和结尾的空格 lines read_time_machine() print(f# 文本总行数: {len(lines)}) print(lines[0]) print(lines[10]) Downloading ../data\timemachine.txt from http://d2l-data.s3-accelerate.amazonaws.com/timemachine.txt... # 文本总行数: 3221 the time machine by h g wells twinkled and his usually pale face was flushed and animated there.sub(pattern, replacement, string)[^A-Za-z] 是一个正则表达式模式[^...]表示“否定字符集”即匹配不在这个集合中的任意字符。A-Za-z表示所有英文字母大写和小写。所以 [^A-Za-z]匹配任何非字母的字符如数字、标点、空格、换行等。表示前面的字符出现一次或多次即连续的非字母字符会被当作一个整体匹配。第二个参数 是替换内容这里是一个空格。第三个参数 line 是要处理的原始字符串。因此整句的作用是将字符串 line中所有连续的非字母字符包括多个连续的非字母字符都替换成一个空格。例如输入 Hello, World! 123→ 输出 Hello World注意末尾可能有一个空格然后你后面还用了 .strip()去掉首尾空格.lower()转成小写最终得到纯英文单词组成的干净文本。词元化:def tokenize(lines, tokenword): #save 将文本行拆分为单词或字符词元 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]) print(type(tokens)) # class list print(type(tokens[0])) # class list 因为 tokens[0] 也是一个列表 print(type(tokens[0][0])) # class str最终tokens是一个二维列表外层是行索引内层是该行的词元列表单词或字符word和 char的主要区别在于词元化的粒度不同tokenword使用 line.split()将每行文本按空白字符空格、制表符等 分割成一个个单词词元。因为前面已经用 re.sub([^A-Za-z], , line)把所有非字母字符替换成了空格所以此时每行只有连续的英文字母和空格split()会返回一个由单词组成的列表。例如the time machine→ [the, time, machine]tokenchar使用 list(line)直接将字符串转换为字符列表每个字符包括空格都是一个独立的词元。例如the time→ [t,h,e, ,t,i,m,e]单词级词元化常用于大多数自然语言处理任务如文本分类、翻译。字符级词元化用于处理拼写错误、罕见词或某些序列建模场景如字符级 RNN。两种处理方式区别:line the time machine by h g wellswords line.split()print(words) # [the, time, machine, by, h, g, wells]它自动识别连续的空格并忽略开头和结尾的空格。每个单词成为一个独立的字符串元素。------------------------------list()是一个内置函数可以将任何可迭代对象如字符串、元组、集合等转换为一个列表。当参数是字符串时它会将字符串中的每个字符包括空格、标点作为列表的一个元素。line the time machine by h g wellschars list(line)print(chars)# 输出[t, h, e, , t, i, m, e, , m, a, c, h, i, n, e, , b, y, , h, , g, , w, e, l, l, s]在列表推导式中[list(line) for line in lines]是一种简写形式等价于result [] for line in lines: result.append(list(line))它遍历lines中的每一行对每一行执行list(line)然后把结果收集到一个新列表result中。2.range(11)是什么意思range(11)生成整数序列0, 1, 2, ..., 10共 11 个数。所以for i in range(11)会执行 11 次循环依次打印tokens[0],tokens[1], …,tokens[10]。词表:构建一个字典通常也叫做词表vocabulary 用来将字符串类型的词元映射到从开始的数字索引中]。 我们先将训练集中的所有文档合并在一起对它们的唯一词元进行统计 得到的统计结果称之为语料corpus。 然后根据每个唯一词元的出现频率为其分配一个数字索引。 很少出现的词元通常被移除这可以降低复杂性。另外语料库中不存在或已删除的任何词元都将映射到一个特定的未知词元“unk”。 我们可以选择增加一个列表用于保存那些被保留的词元 例如填充词元“pad” 序列开始词元“bos” 序列结束词元“eos”。import collections def count_corpus(tokens): #save 统计词元的频率 # 如果tokens是空列表或者第一个元素是列表即二维列表则需要展平 if len(tokens) 0 or isinstance(tokens[0], list): # 使用双重循环列表推导式将二维列表展平为一维列表 # 外层for line in tokens遍历每一行内层for token in line遍历行内每个词元 tokens [token for line in tokens for token in line] # collections.Counter返回一个字典键为词元值为出现次数 return collections.Counter(tokens) class Vocab: #save 文本词表 def __init__(self, tokensNone, min_freq0, reserved_tokensNone): #min_freq的意思是 “最小词频阈值”Minimum Frequency。设置它的主要目的是为了过滤掉罕见词生僻字/词。 # 如果未提供tokens初始化为空列表 if tokens is None: tokens [] # 如果未提供reserved_tokens初始化为空列表 if reserved_tokens is None: reserved_tokens [] # 统计词频并排序按频率降序 counter count_corpus(tokens) # counter.items()返回(key, value)对sorted按value频率降序排列 self._token_freqs sorted(counter.items(), keylambda x: x[1], reverseTrue) # 初始化索引到词元的列表索引0固定为unk未知词元然后追加预留词元 self.idx_to_token [unk] reserved_tokens # 根据idx_to_token建立词元到索引的映射字典 self.token_to_idx {token: idx for idx, token in enumerate(self.idx_to_token)} # 遍历排序后的词频列表已按频率从高到低 for token, freq in self._token_freqs: # 如果当前词元频率小于min_freq由于列表已排序后续频率只会更低直接跳出 if freq min_freq: break # 如果该词元还未添加到词表中即不在token_to_idx中 if token not in self.token_to_idx: # 将词元追加到idx_to_token末尾索引为当前列表长度减1 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)): # 尝试在token_to_idx中查找找不到则返回self.unk即0对应unk return self.token_to_idx.get(tokens, self.unk) # 如果输入是列表或元组递归地对每个元素调用__getitem__返回索引列表 return [self.__getitem__(token) for token in tokens] def to_tokens(self, indices): 将索引或索引列表转换为词元 # 如果输入不是列表或元组即单个索引 if not isinstance(indices, (list, tuple)): # 直接从idx_to_token中取对应位置的词元 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索引建立的核心机制Vocab类使用两个数据结构协同工作列表self.idx_to_token存储所有词元列表的下标就是该词元的索引。字典self.token_to_idx存储每个词元对应的索引用于快速查找。索引的分配过程完全由这两个结构的同步更新来保证唯一性。预留词元已预先占用索引在遍历词频列表之前unk和预留词元已经占据了索引0,1,…后续添加的新词元从下一个空闲索引开始不会覆盖它们。当多个词元出现频率相同时它们的索引分配顺序取决于sorted函数的稳定性和Counter的迭代顺序。关键点sorted是稳定排序在 Python 中sorted采用稳定排序算法Timsort这意味着当比较键此处为频率相等时元素的相对顺序保持不变。也就是说如果两个词元频率相同它们在counter.items()中的原始出现顺序决定了谁先谁后。Counter的顺序Python 3.7 中字典包括Counter保持插入顺序。count_corpus函数中Counter(tokens)会按照词元第一次出现的时间顺序记录。因此频率相同的词元会按照它们在原始语料中首次出现的先后顺序进入排序后的列表。def load_corpus_time_machine(max_tokens-1): #save 返回时光机器数据集的词元索引列表和词表 参数: max_tokens (int): 最大词元数量-1表示使用全部 返回: corpus (list): 一维整数列表每个元素是字符对应的索引 vocab (Vocab): 词表对象包含字符到索引的映射 # 1. 读取原始文本行已预处理为小写、去除非字母字符 lines read_time_machine() # 2. 将文本行按字符级别分词得到二维列表 tokens # tokens[i] 是第 i 行的字符列表如 [t,h,e, ,t,i,m,e,...] tokens tokenize(lines, char) # 3. 基于所有字符构建词表 Vocab # 自动统计每个字符的频率分配索引0为unk其余按频率降序 vocab Vocab(tokens) # 4. 将二维 tokens 展平为一维 corpus并将每个字符转换为索引 # 双重循环外层遍历每一行内层遍历行内的每个字符 # vocab[token] 调用 __getitem__ 返回字符对应的索引 corpus [vocab[token] for line in tokens for token in line] # 5. 如果指定了 max_tokens正数截取前 max_tokens 个词元 if max_tokens 0: corpus corpus[:max_tokens] # 6. 返回词元索引列表和词表对象 return corpus, vocab # 调用函数使用全部数据max_tokens-1 默认 corpus, vocab load_corpus_time_machine() # 打印结果 # len(corpus)总字符数展平后的长度 # len(vocab)词表大小包含unk在内的不同字符种类数 print(len(corpus), len(vocab)) (170580, 28) len(corpus) 170580整个《时间机器》文本的总字符数包括空格。 len(vocab) 28词表中不同的字符种类数26个小写字母 空格 unk。corpus是一个一维整数列表例如 [12, 15, 18, ...]每个数字对应一个字符的索引。vocab是 Vocab类的实例可以通过 vocab.token_to_idx查看字符到索引的映射通过 vocab.idx_to_token查看索引到字符的映射。len(corpus)是整个《时光机器》文本去掉非字母字符后的总字符数。len(vocab)是词表大小通常包括 unk、空格和所有出现过的字母a-z等。
学习文本处理
发布时间:2026/6/11 6:27:52
文本的常见预处理步骤这些步骤通常包括将文本作为字符串加载到内存中。将字符串拆分为词元如单词和字符。建立一个词表将拆分的词元映射到数字索引。将文本转换为数字索引序列方便模型操作。import collections import re from d2l import torch as d2l #save d2l.DATA_HUB[time_machine] (d2l.DATA_URL timemachine.txt, 090b5e7e70c295757f55df93cb0a180b9691891a) def read_time_machine(): #save 将时间机器数据集加载到文本行的列表中 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]#.strip() 移除字符串开头和结尾的空格 lines read_time_machine() print(f# 文本总行数: {len(lines)}) print(lines[0]) print(lines[10]) Downloading ../data\timemachine.txt from http://d2l-data.s3-accelerate.amazonaws.com/timemachine.txt... # 文本总行数: 3221 the time machine by h g wells twinkled and his usually pale face was flushed and animated there.sub(pattern, replacement, string)[^A-Za-z] 是一个正则表达式模式[^...]表示“否定字符集”即匹配不在这个集合中的任意字符。A-Za-z表示所有英文字母大写和小写。所以 [^A-Za-z]匹配任何非字母的字符如数字、标点、空格、换行等。表示前面的字符出现一次或多次即连续的非字母字符会被当作一个整体匹配。第二个参数 是替换内容这里是一个空格。第三个参数 line 是要处理的原始字符串。因此整句的作用是将字符串 line中所有连续的非字母字符包括多个连续的非字母字符都替换成一个空格。例如输入 Hello, World! 123→ 输出 Hello World注意末尾可能有一个空格然后你后面还用了 .strip()去掉首尾空格.lower()转成小写最终得到纯英文单词组成的干净文本。词元化:def tokenize(lines, tokenword): #save 将文本行拆分为单词或字符词元 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]) print(type(tokens)) # class list print(type(tokens[0])) # class list 因为 tokens[0] 也是一个列表 print(type(tokens[0][0])) # class str最终tokens是一个二维列表外层是行索引内层是该行的词元列表单词或字符word和 char的主要区别在于词元化的粒度不同tokenword使用 line.split()将每行文本按空白字符空格、制表符等 分割成一个个单词词元。因为前面已经用 re.sub([^A-Za-z], , line)把所有非字母字符替换成了空格所以此时每行只有连续的英文字母和空格split()会返回一个由单词组成的列表。例如the time machine→ [the, time, machine]tokenchar使用 list(line)直接将字符串转换为字符列表每个字符包括空格都是一个独立的词元。例如the time→ [t,h,e, ,t,i,m,e]单词级词元化常用于大多数自然语言处理任务如文本分类、翻译。字符级词元化用于处理拼写错误、罕见词或某些序列建模场景如字符级 RNN。两种处理方式区别:line the time machine by h g wellswords line.split()print(words) # [the, time, machine, by, h, g, wells]它自动识别连续的空格并忽略开头和结尾的空格。每个单词成为一个独立的字符串元素。------------------------------list()是一个内置函数可以将任何可迭代对象如字符串、元组、集合等转换为一个列表。当参数是字符串时它会将字符串中的每个字符包括空格、标点作为列表的一个元素。line the time machine by h g wellschars list(line)print(chars)# 输出[t, h, e, , t, i, m, e, , m, a, c, h, i, n, e, , b, y, , h, , g, , w, e, l, l, s]在列表推导式中[list(line) for line in lines]是一种简写形式等价于result [] for line in lines: result.append(list(line))它遍历lines中的每一行对每一行执行list(line)然后把结果收集到一个新列表result中。2.range(11)是什么意思range(11)生成整数序列0, 1, 2, ..., 10共 11 个数。所以for i in range(11)会执行 11 次循环依次打印tokens[0],tokens[1], …,tokens[10]。词表:构建一个字典通常也叫做词表vocabulary 用来将字符串类型的词元映射到从开始的数字索引中]。 我们先将训练集中的所有文档合并在一起对它们的唯一词元进行统计 得到的统计结果称之为语料corpus。 然后根据每个唯一词元的出现频率为其分配一个数字索引。 很少出现的词元通常被移除这可以降低复杂性。另外语料库中不存在或已删除的任何词元都将映射到一个特定的未知词元“unk”。 我们可以选择增加一个列表用于保存那些被保留的词元 例如填充词元“pad” 序列开始词元“bos” 序列结束词元“eos”。import collections def count_corpus(tokens): #save 统计词元的频率 # 如果tokens是空列表或者第一个元素是列表即二维列表则需要展平 if len(tokens) 0 or isinstance(tokens[0], list): # 使用双重循环列表推导式将二维列表展平为一维列表 # 外层for line in tokens遍历每一行内层for token in line遍历行内每个词元 tokens [token for line in tokens for token in line] # collections.Counter返回一个字典键为词元值为出现次数 return collections.Counter(tokens) class Vocab: #save 文本词表 def __init__(self, tokensNone, min_freq0, reserved_tokensNone): #min_freq的意思是 “最小词频阈值”Minimum Frequency。设置它的主要目的是为了过滤掉罕见词生僻字/词。 # 如果未提供tokens初始化为空列表 if tokens is None: tokens [] # 如果未提供reserved_tokens初始化为空列表 if reserved_tokens is None: reserved_tokens [] # 统计词频并排序按频率降序 counter count_corpus(tokens) # counter.items()返回(key, value)对sorted按value频率降序排列 self._token_freqs sorted(counter.items(), keylambda x: x[1], reverseTrue) # 初始化索引到词元的列表索引0固定为unk未知词元然后追加预留词元 self.idx_to_token [unk] reserved_tokens # 根据idx_to_token建立词元到索引的映射字典 self.token_to_idx {token: idx for idx, token in enumerate(self.idx_to_token)} # 遍历排序后的词频列表已按频率从高到低 for token, freq in self._token_freqs: # 如果当前词元频率小于min_freq由于列表已排序后续频率只会更低直接跳出 if freq min_freq: break # 如果该词元还未添加到词表中即不在token_to_idx中 if token not in self.token_to_idx: # 将词元追加到idx_to_token末尾索引为当前列表长度减1 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)): # 尝试在token_to_idx中查找找不到则返回self.unk即0对应unk return self.token_to_idx.get(tokens, self.unk) # 如果输入是列表或元组递归地对每个元素调用__getitem__返回索引列表 return [self.__getitem__(token) for token in tokens] def to_tokens(self, indices): 将索引或索引列表转换为词元 # 如果输入不是列表或元组即单个索引 if not isinstance(indices, (list, tuple)): # 直接从idx_to_token中取对应位置的词元 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索引建立的核心机制Vocab类使用两个数据结构协同工作列表self.idx_to_token存储所有词元列表的下标就是该词元的索引。字典self.token_to_idx存储每个词元对应的索引用于快速查找。索引的分配过程完全由这两个结构的同步更新来保证唯一性。预留词元已预先占用索引在遍历词频列表之前unk和预留词元已经占据了索引0,1,…后续添加的新词元从下一个空闲索引开始不会覆盖它们。当多个词元出现频率相同时它们的索引分配顺序取决于sorted函数的稳定性和Counter的迭代顺序。关键点sorted是稳定排序在 Python 中sorted采用稳定排序算法Timsort这意味着当比较键此处为频率相等时元素的相对顺序保持不变。也就是说如果两个词元频率相同它们在counter.items()中的原始出现顺序决定了谁先谁后。Counter的顺序Python 3.7 中字典包括Counter保持插入顺序。count_corpus函数中Counter(tokens)会按照词元第一次出现的时间顺序记录。因此频率相同的词元会按照它们在原始语料中首次出现的先后顺序进入排序后的列表。def load_corpus_time_machine(max_tokens-1): #save 返回时光机器数据集的词元索引列表和词表 参数: max_tokens (int): 最大词元数量-1表示使用全部 返回: corpus (list): 一维整数列表每个元素是字符对应的索引 vocab (Vocab): 词表对象包含字符到索引的映射 # 1. 读取原始文本行已预处理为小写、去除非字母字符 lines read_time_machine() # 2. 将文本行按字符级别分词得到二维列表 tokens # tokens[i] 是第 i 行的字符列表如 [t,h,e, ,t,i,m,e,...] tokens tokenize(lines, char) # 3. 基于所有字符构建词表 Vocab # 自动统计每个字符的频率分配索引0为unk其余按频率降序 vocab Vocab(tokens) # 4. 将二维 tokens 展平为一维 corpus并将每个字符转换为索引 # 双重循环外层遍历每一行内层遍历行内的每个字符 # vocab[token] 调用 __getitem__ 返回字符对应的索引 corpus [vocab[token] for line in tokens for token in line] # 5. 如果指定了 max_tokens正数截取前 max_tokens 个词元 if max_tokens 0: corpus corpus[:max_tokens] # 6. 返回词元索引列表和词表对象 return corpus, vocab # 调用函数使用全部数据max_tokens-1 默认 corpus, vocab load_corpus_time_machine() # 打印结果 # len(corpus)总字符数展平后的长度 # len(vocab)词表大小包含unk在内的不同字符种类数 print(len(corpus), len(vocab)) (170580, 28) len(corpus) 170580整个《时间机器》文本的总字符数包括空格。 len(vocab) 28词表中不同的字符种类数26个小写字母 空格 unk。corpus是一个一维整数列表例如 [12, 15, 18, ...]每个数字对应一个字符的索引。vocab是 Vocab类的实例可以通过 vocab.token_to_idx查看字符到索引的映射通过 vocab.idx_to_token查看索引到字符的映射。len(corpus)是整个《时光机器》文本去掉非字母字符后的总字符数。len(vocab)是词表大小通常包括 unk、空格和所有出现过的字母a-z等。