news 2026/4/14 19:51:56

动手学深度学习——文本预处理代码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
动手学深度学习——文本预处理代码

1. 前言

上一篇我们已经进入了序列模型部分。

我们知道,文本本质上也是一种序列,例如一句话:

  • 正在

  • 学习

  • 深度学习

对于人来说,文字天然可以理解。
但对于神经网络来说,文字本身只是字符串,模型并不能直接处理。

所以在真正训练语言模型、RNN 之前,必须先解决一个基础问题:

如何把原始文本处理成模型可以读懂的数字形式?

这就是本节“文本预处理代码”的核心任务。

这一节主要要解决:

  • 如何读取原始文本

  • 如何把文本拆成词元(token)

  • 如何建立词表(vocabulary)

  • 如何把词元转换成数字索引

这几步看起来基础,但它们是后面语言模型和 RNN 的真正起点。


2. 文本预处理到底在做什么

文本预处理可以概括成一句话:

把原始自然语言文本,变成可供模型训练的离散符号序列和数字序列。

原始文本例如:

Time traveller for so it will be convenient to speak of him

模型不能直接拿这串字符训练,
因为神经网络需要的是数值输入。

所以必须一步步转换:

第一步:读取文本

从文件中读出原始句子。

第二步:清洗文本

统一大小写、去掉多余符号等。

第三步:分词

把句子拆成更小单位,例如单词或字符。

第四步:建立词表

为每个词元分配唯一编号。

第五步:文本数字化

把词元序列转换成索引序列。

这样,文本才真正变成模型能吃进去的数据。


3. 李沐这里为什么常用《时间机器》

《动手学深度学习》在文本预处理部分,一个经典示例就是:

H. G. Wells 的小说《The Time Machine》

原因很简单:

3.1 文本公开、经典

它是公开文本,适合教学使用。

3.2 长度适中

不至于太小,也不会大到不方便讲解。

3.3 英文词汇结构清晰

适合展示分词、词表构建等基础操作。

所以这一节你会经常看到“time machine”文本作为例子。


4. 读取原始文本

第一步通常是把文本文件读进来。

常见代码大致如下:

import re def read_time_machine(): with open('timemachine.txt', 'r') as f: lines = f.readlines() return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]

这段代码很经典,里面做了几件关键事情。


5. 这段代码怎么理解

我们逐行看。

5.1f.readlines()

lines = f.readlines()

表示把文本文件按行读出来。
最终得到的是一个列表,每个元素是一行字符串。


5.2 正则替换

re.sub('[^A-Za-z]+', ' ', line)

它的作用是:

把所有不是英文字母的字符,都替换成空格。

也就是说:

  • 标点符号去掉

  • 数字去掉

  • 特殊符号去掉

  • 多余非字母内容统一替换成空格

这样做的目的是让后面分词更干净。


5.3strip()

.strip()

去掉首尾空格。


5.4lower()

.lower()

把所有大写字母变成小写。

例如:

  • Time变成time

  • Machine变成machine

这样可以避免:

  • Time

  • time

被当成两个不同词。


6. 为什么要先清洗文本

因为原始文本里往往有很多不稳定因素,例如:

  • 大小写不同

  • 标点符号混杂

  • 换行符

  • 多余空格

  • 特殊字符

如果不先清洗,后面构建词表时就会很混乱。

例如:

  • Hello

  • hello

  • hello,

  • hello!

本来其实应该看作同一个词,
但不清洗的话,模型会把它们当作不同 token。

所以清洗文本是必要的第一步。


7. 什么是词元(token)

在文本处理中,一个很核心的概念就是:

词元(token)

词元可以简单理解为:

文本中被拆分出来的最小处理单位

这个单位可以是:

  • 单词

  • 字符

  • 子词

在李沐这一节最开始,通常先讲最简单的两种:

  • 按单词切分

  • 按字符切分


8. 分词函数怎么写

常见写法如下:

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)

这段代码就是最基本的分词器。


9. 按单词切分是什么意思

如果:

token='word'

那么:

line.split()

会把一行按空格拆开。

例如:

"time traveller for so it will be convenient"

会变成:

['time', 'traveller', 'for', 'so', 'it', 'will', 'be', 'convenient']

也就是说,每个单词是一个 token。

这很符合日常直觉,也是最常见的文本表示方式之一。


10. 按字符切分是什么意思

如果:

token='char'

那么:

list(line)

会把整行拆成一个个字符。

例如:

"time"

变成:

['t', 'i', 'm', 'e']

这种方式粒度更细。

字符级建模的好处是:

  • 词表更小

  • 不会遇到“未知单词”那么严重的问题

但缺点是序列更长,语义表达更分散。


11. 为什么既可以按词切,也可以按字符切

因为不同任务、不同模型的需求不一样。

按词切分

优点:

  • 更符合语言语义单位

  • 序列更短

  • 表达更自然

缺点:

  • 词表可能非常大

  • 容易有未登录词问题

按字符切分

优点:

  • 词表很小

  • 几乎没有 OOV(未登录词)问题

缺点:

  • 序列会更长

  • 模型更难捕捉高层语义

所以两种方式各有取舍。
李沐这里先都展示一下,让你建立基本认识。


12. 什么是词表(Vocabulary)

当文本被切成 token 后,还不能直接送入模型。
因为模型还是不能读字符串。

所以必须再做一步:

给每个 token 分配一个唯一的整数编号

这个“token 到编号”的映射表,就叫:

词表(Vocabulary)

例如:

{'time': 0, 'traveller': 1, 'for': 2, 'so': 3, ...}

以后只要看到time,就用编号0表示。

这样文本就变成数字序列了。


13. 为什么要先统计词频

在建立词表之前,通常会先统计所有 token 的出现次数。

因为词频信息很重要:

13.1 可以按频率排序

高频词通常更值得优先保留。

13.2 可以过滤低频词

有些出现极少的词可能噪声较大,可以统一处理成<unk>

13.3 便于分析文本分布

例如哪些词最常见,是否存在长尾现象。

所以词频统计是词表构建的前置步骤。


14. 统计词频的代码

常见写法如下:

from collections import Counter def count_corpus(tokens): if len(tokens) == 0 or isinstance(tokens[0], list): tokens = [token for line in tokens for token in line] return Counter(tokens)

这段代码也很经典。


15. 这段代码怎么理解

15.1 为什么要展平

如果输入是:

[['time', 'traveller'], ['for', 'so']]

这其实是“按行切好的 token 列表”。

Counter需要的是一个平铺的一维列表,
所以先用列表推导式把它展开成:

['time', 'traveller', 'for', 'so']

15.2Counter(tokens)

Counter(tokens)

会统计每个 token 出现的次数。

例如:

Counter({'the': 2261, 'i': 1267, 'and': 1245, ...})

这就得到了词频表。


16. 词表类怎么写

李沐这里通常会封装一个Vocab类,大致形式如下:

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) self.idx_to_token = ['<unk>'] + reserved_tokens self.token_to_idx = {token: idx for idx, token in enumerate(self.idx_to_token)} for token, freq in self.token_freqs: if freq < min_freq: break if token not in self.token_to_idx: self.idx_to_token.append(token) self.token_to_idx[token] = len(self.idx_to_token) - 1

这是文本预处理部分最核心的一段代码之一。


17.Vocab类的核心思想是什么

它本质上在做两件事:

第一件:建立 token 到 index 的映射

self.token_to_idx

例如:

{'<unk>': 0, 'the': 1, 'i': 2, 'and': 3, ...}

第二件:建立 index 到 token 的映射

self.idx_to_token

例如:

['<unk>', 'the', 'i', 'and', ...]

这两个方向都要有。

为什么?

因为:

  • 训练时常常要把 token 转成 index

  • 生成文本或解释结果时又常常要把 index 转回 token


18. 为什么要有<unk>

<unk>表示:

unknown token(未知词元)

它通常对应索引0

这样做是因为:

  • 测试时可能会遇到词表中没见过的词

  • 不能让程序直接报错

  • 所以统一把未知词映射到<unk>

例如,训练集中没出现过lycoris
那测试时看到它,就可以映射成<unk>

这是 NLP 中非常常见的做法。


19.min_freq有什么作用

min_freq表示最小出现频率阈值。

如果一个 token 出现次数太低,例如只出现 1 次,
有时我们会选择不把它放进词表。

这样做的好处是:

  • 减小词表规模

  • 减少稀有词带来的噪声

  • 提高模型训练稳定性

所以min_freq是控制词表大小的重要参数。


20. 如何把 token 转成索引

通常Vocab类里还会定义:

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]

这意味着:

单个 token

vocab['time']

会返回它的编号。

一串 token

vocab[['time', 'traveller']]

会返回一个编号列表。

这就完成了“文本数字化”。


21. 如何把索引转回 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]

这样就能把模型输出的数字再还原成可读文本。

例如:

vocab.to_tokens([1, 2, 3])

可能返回:

['the', 'i', 'and']

这一步在后面语言模型生成文本时很重要。


22. 一个完整的小流程

把这节内容串起来,大概就是:

lines = read_time_machine() tokens = tokenize(lines, 'word') vocab = Vocab(tokens)

这里:

  • lines是清洗后的文本行

  • tokens是分词结果

  • vocab是建立好的词表

然后你就可以看:

tokens[0] vocab[tokens[0]]

第一句看原始 token,
第二句看它们对应的数字索引。

这就把文本从“字符串世界”带到了“数字世界”。


23. 为什么文本预处理是后面语言模型的基础

因为不管后面是:

  • 语言模型

  • RNN

  • LSTM

  • GRU

  • Transformer

它们都不能直接吃字符串。

它们真正看到的永远是:

  • token 序列

  • index 序列

  • embedding 向量

而这里的文本预处理,就是整个链条的起点。

没有这一节,后面的模型就根本没法开始。


24. 这节代码最容易混的点

这里顺手帮你总结几个特别容易混的点。

24.1 行、token、索引不是一回事

  • 行:原始文本的一行

  • token:切分后的最小单位

  • 索引:token 对应的数字编号

24.2 清洗文本和分词不是一回事

  • 清洗:统一大小写、去掉符号

  • 分词:把文本拆开

24.3 词表不是文本本身

词表只是“token 到编号”的映射表,
它不是训练数据本身。

24.4<unk>不是普通单词

它是一个占位符,用来处理词表外 token。


25. 本节总结

这一节我们学习了文本预处理代码,核心内容可以总结为以下几点。

25.1 文本预处理的目标

把原始文本转换成模型可处理的数字序列。

25.2 读取文本时通常要先清洗

包括:

  • 去掉非字母字符

  • 统一小写

  • 去掉多余空格

25.3 文本可以按词或按字符切分

这两种 tokenization 方式各有特点。

25.4 词表负责 token 和索引之间的映射

它是后续语言模型训练的基础组件。

25.5<unk>用于处理未知词元

这是 NLP 中非常常见的机制。


26. 学习感悟

这一节看起来很基础,甚至不像“深度学习模型”,
但它其实特别重要。

因为你会慢慢发现:

很多 NLP 任务真正开始的地方,不是网络结构,而是“如何把语言变成机器可处理的形式”。

也就是说,文本预处理不是边角料,
而是语言建模这条链路上的第一块地基。

地基不清楚,后面的语言模型和 RNN 都会显得很乱。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/14 19:45:47

VS2022+Qt开发必备:3种方法让你的std::cout调试信息不再‘消失‘

VS2022Qt开发实战&#xff1a;3种高效捕获std::cout调试信息的专业方案 当你在Visual Studio 2022中开发Qt应用程序时&#xff0c;是否经常遇到这样的困扰&#xff1a;精心插入的std::cout调试信息如同石沉大海&#xff0c;在程序运行时完全看不到任何输出&#xff1f;这种&quo…

作者头像 李华
网站建设 2026/4/14 19:43:14

UML用例建模实战:从零开始绘制高效用例图

1. 什么是UML用例建模&#xff1f; UML用例建模是软件开发中最基础也最重要的需求分析技术之一。简单来说&#xff0c;就是用图形化的方式描述系统该做什么&#xff0c;而不是怎么做。我第一次接触用例图是在大学软件工程课上&#xff0c;当时觉得这些"小人"和"…

作者头像 李华
网站建设 2026/4/14 19:38:46

FlowState Lab参数调优实战:如何获得更稳定、高质量的时序生成结果

FlowState Lab参数调优实战&#xff1a;如何获得更稳定、高质量的时序生成结果 1. 为什么需要关注参数调优 时序数据生成是许多AI应用的核心需求&#xff0c;从音乐创作到传感器数据模拟&#xff0c;再到金融时间序列预测&#xff0c;都需要模型能够产生连贯、合理的序列输出…

作者头像 李华
网站建设 2026/4/14 19:37:18

GO语言开发的afrog漏扫工具实战:从安装到漏洞报告分析

GO语言开发的afrog漏扫工具实战&#xff1a;从安装到漏洞报告分析 在当今快速发展的网络安全领域&#xff0c;自动化漏洞扫描工具已成为安全研究人员和开发者的必备武器。afrog作为一款基于GO语言开发的开源漏洞扫描工具&#xff0c;凭借其轻量级、高性能和易扩展的特性&#…

作者头像 李华
网站建设 2026/4/14 19:34:59

如何选择轻量级热修复方案?主流框架对比与实施指南

1、 开篇引入 热修复&#xff0c;是指在应用运行时不通过商店审核即可动态替换部分代码或资源&#xff0c;以快速修正缺陷或优化功能的轻量级技术方案。其核心目标是保障业务连续性、缩短故障恢复周期并降低版本迭代风险。与传统整包更新相比&#xff0c;热修复可减少用户流失、…

作者头像 李华