文本处理:让自然语言变成模型能理解的数字
学习 Transformer 之前,有一个非常重要的前置问题:
机器学习模型只能处理数字,那自然语言文本应该怎样变成数字?
比如一句话:
我喜欢自然语言处理
人可以直接理解它的意思,但模型不能直接处理汉字、单词和标点。我们需要先把文本拆成一个个 token,再把 token 映射成编号或向量,最后才能送进模型。
这篇文章就来系统梳理 NLP 中最基础的文本处理流程。
1. 文本处理在 NLP 中的位置
在前面的机器学习文章中,我们处理的大多是结构化数据,例如:
学习时长、睡眠时长、房屋面积、历史成绩
这些特征天然就是数字,或者很容易转成数字。
但 NLP 处理的是文本,例如:
这部电影很好看
这个商品质量一般
今天天气不错
模型不能直接理解这些句子,所以需要一个转换流程:
原始文本
-> 清洗与规范化
-> 分词 / tokenization
-> 构建词表
-> token 转 id
-> id 转向量
-> 输入模型
传统机器学习中,文本常被转成词袋、TF-IDF 这样的稀疏向量;深度学习和 Transformer 中,文本通常先转成 token id,再通过 embedding 层变成稠密向量。
2. 文本清洗与规范化
文本数据通常比较脏。比如:
“这个手机太好用了!!! http://xxx.com”
“This Movie is GREAT!!!”
“我 喜欢 NLP”
常见清洗操作包括:
- 去掉多余空格。
- 统一大小写。
- 去掉或保留标点符号。
- 去掉 HTML 标签。
- 去掉 URL、邮箱、手机号等特殊内容。
- 中文繁简转换。
- 表情、emoji、特殊符号处理。
是否清洗,要看任务。
例如情感分析中,感叹号和 emoji 可能很重要:
太好吃了!!!
太好吃了🙂
太好吃了😡
这些符号可能影响情绪判断,不能随便删除。
而在主题分类中,URL、HTML 标签通常没什么帮助,可以先清理。
所以文本清洗没有固定标准,核心原则是:
保留对任务有用的信息,去掉会干扰模型的噪声。
3. 什么是分词
分词(Tokenization)就是把一段文本切成更小的单位。
这些单位叫 token。
英文中,最简单的分词可以按空格切:
I love natural language processing
切分后:
["I", "love", "natural", "language", "processing"]
中文更麻烦,因为中文词之间没有天然空格。
例如:
我喜欢自然语言处理
可以按字切:
["我", "喜", "欢", "自", "然", "语", "言", "处", "理"]
也可以按词切:
["我", "喜欢", "自然语言处理"]
切分方式不同,模型看到的信息粒度就不同。
4. 中文分词为什么困难
中文分词难在边界不明显。
例如:
南京市长江大桥
可能被切成:
["南京市", "长江大桥"]
也可能被误切成:
["南京", "市长", "江大桥"]
同一串字,不同切法含义完全不同。
再比如:
研究生命起源
可以理解为:
研究 / 生命 / 起源
也可以在某些语境下理解为:
研究生 / 命 / 起源
虽然第二种很奇怪,但它说明中文分词需要结合上下文。
传统中文分词会使用词典、统计模型或机器学习模型;深度学习时代,很多 Transformer 模型使用字粒度或子词粒度,减少对传统中文分词的依赖。
5. 词表:给每个 token 一个编号
分词以后,我们得到的是 token 列表。
例如:
["我", "喜欢", "NLP"]
接下来需要构建词表(Vocabulary):
| token | id |
|---|---|
[PAD] |
0 |
[UNK] |
1 |
| 我 | 2 |
| 喜欢 | 3 |
| NLP | 4 |
| 学习 | 5 |
于是句子:
我 喜欢 NLP
可以转换为:
[2, 3, 4]
这里 [UNK] 表示未知词。如果遇到词表里没有的 token,就用 [UNK] 代替。
例如:
我 喜欢 Transformer
如果词表里没有 Transformer,就会变成:
[2, 3, 1]
这也是早期词级分词的一个问题:词表不可能包含所有新词、错别字、人名、专业术语。
6. One-hot 编码
最简单的 token 表示方式是 One-hot。
假设词表有 5 个 token:
| token | id |
|---|---|
| 我 | 0 |
| 喜欢 | 1 |
| NLP | 2 |
| 学习 | 3 |
| 文本 | 4 |
那么 NLP 的 One-hot 向量为:
$$
[0,0,1,0,0]
$$
喜欢 的 One-hot 向量为:
$$
[0,1,0,0,0]
$$
One-hot 的特点是:
- 向量长度等于词表大小。
- 每个词只有一个位置是 1。
- 不同词之间看起来完全独立。
问题是:One-hot 没有语义相似性。
例如:
喜欢
热爱
讨厌
从语义上看,“喜欢”和“热爱”更接近,但 One-hot 无法体现这种关系。
7. 词袋模型
词袋模型(Bag of Words)不关心词序,只统计词出现次数。
假设词表为:
["我", "喜欢", "学习", "NLP"]
句子:
我 喜欢 学习 NLP
可以表示为:
$$
[1,1,1,1]
$$
句子:
我 喜欢 喜欢 NLP
可以表示为:
$$
[1,2,0,1]
$$
词袋模型的优点是简单,适合和传统机器学习模型结合,例如逻辑回归、朴素贝叶斯、SVM。
缺点也明显:它丢失了词序。
例如:
我 喜欢 你
你 喜欢 我
词袋表示完全一样,但语义并不完全相同。
8. n-gram:保留局部词序
为了弥补词袋模型完全丢失词序的问题,可以使用 n-gram。
如果 $n=1$,就是 unigram:
我 / 喜欢 / NLP
如果 $n=2$,就是 bigram:
我 喜欢
喜欢 NLP
如果 $n=3$,就是 trigram:
我 喜欢 NLP
例如句子:
我 不 喜欢 这部电影
如果只看单词:
我, 不, 喜欢, 这部电影
模型可能难以捕捉“不喜欢”这个组合。
如果加入 bigram:
不 喜欢
模型就更容易识别负面情绪。
n-gram 的问题是特征数量会迅速膨胀。词表越大,n 越大,特征空间越大。
9. TF-IDF:让关键词更重要
词袋模型只看词频,但有些词虽然出现很多,却没什么区分度。
例如中文文本中常见词:
的、是、了、我们、这个
这些词出现频率高,但通常不太能代表文本主题。
TF-IDF 用来衡量一个词对某篇文档的重要程度。
它由两部分组成:
TF:词在当前文档中出现得多不多
IDF:词在整个语料库中稀不稀有
TF 可以写成:
$$
TF(t,d)=\frac{\text{词 }t\text{ 在文档 }d\text{ 中出现次数}}{\text{文档 }d\text{ 的总词数}}
$$
IDF 可以写成:
$$
IDF(t)=\log\frac{N}{DF(t)+1}
$$
其中:
- $N$ 表示总文档数。
- $DF(t)$ 表示包含词 $t$ 的文档数量。
- 加 1 是为了避免分母为 0。
TF-IDF 为:
$$
TFIDF(t,d)=TF(t,d)\times IDF(t)
$$
9.1 一个具体例子
假设语料库有 100 篇文章。
词“Transformer”只出现在 5 篇文章中:
$$
IDF(\text{Transformer})=\log\frac{100}{5+1}\approx2.81
$$
词“的”出现在 90 篇文章中:
$$
IDF(\text{的})=\log\frac{100}{90+1}\approx0.09
$$
如果某篇文章中两个词的 TF 都是 0.02,那么:
$$
TFIDF(\text{Transformer})=0.02\times2.81=0.0562
$$
$$
TFIDF(\text{的})=0.02\times0.09=0.0018
$$
可以看到,“Transformer”的权重远高于“的”。
这就是 TF-IDF 的直觉:当前文档中常见、但整个语料中不太常见的词,通常更重要。
10. 词向量:让语义进入向量空间
One-hot、词袋和 TF-IDF 大多是稀疏向量。
深度学习更常用稠密向量,也就是词向量(Word Embedding)。
例如:
我 -> [0.12, -0.08, 0.31, ...]
喜欢 -> [0.40, 0.22, -0.17, ...]
NLP -> [-0.11, 0.56, 0.09, ...]
词向量的维度通常远小于词表大小,例如 100 维、300 维、768 维。
它的目标是让语义相近的词在向量空间里距离更近。
例如理想情况下:
喜欢 和 热爱 更接近
喜欢 和 冰箱 距离更远
这和前面 KNN 文章中的“距离”思想也能联系起来:向量空间中距离近,往往表示语义更接近。
相关回顾:KNN:从距离到分类的直觉算法
11. Embedding 层做了什么
在深度学习模型中,token id 本身只是编号,不包含语义。
例如:
我 -> 2
喜欢 -> 3
NLP -> 4
数字 2、3、4 只是索引,不能直接理解成大小关系。
Embedding 层可以看成一张参数表:
| token id | embedding |
|---|---|
| 2 | $[0.12,-0.08,0.31]$ |
| 3 | $[0.40,0.22,-0.17]$ |
| 4 | $[-0.11,0.56,0.09]$ |
输入 token id:
[2, 3, 4]
Embedding 层查表后得到:
[
[0.12, -0.08, 0.31],
[0.40, 0.22, -0.17],
[-0.11, 0.56, 0.09]
]
如果 embedding 维度是 $d$,序列长度是 $L$,那么输出形状是:
$$
L \times d
$$
Transformer 的输入就是这样一串向量。
12. 子词切分:为什么 Transformer 不只按词切
早期 NLP 常用词级分词,但词级分词有一个大问题:词表很难覆盖所有词。
例如:
Transformer
Transformers
Transformer-based
unbelievable
unbelievably
如果每个词都单独进词表,词表会很大,而且遇到新词容易变成 [UNK]。
子词切分(Subword Tokenization)就是折中方案:
既不要按字符切得太碎
也不要按完整词切得太死
例如英文:
unbelievable -> un + believe + able
中文里也可能按字、词或子词混合:
自然语言处理 -> 自然 / 语言 / 处理
常见子词算法包括:
- BPE
- WordPiece
- SentencePiece
很多 Transformer 模型都使用这类 tokenizer。
13. BPE 的直觉
BPE(Byte Pair Encoding)的思想可以简化理解为:
从字符开始,不断合并最常见的相邻片段。
假设语料中经常出现:
自然 语言
自然 科学
自然 风景
如果“自然”出现频率很高,就可以把:
自 + 然
合并成:
自然
类似地,如果“语言”经常一起出现,也可以合并:
语 + 言 -> 语言
最终 tokenizer 会形成一个子词词表,既能表示常见词,也能拆开不常见词。
这对 Transformer 很重要,因为它减少了 [UNK] 的出现,也控制了词表大小。
14. 特殊 token
Transformer 中经常会看到一些特殊 token。
| token | 含义 |
|---|---|
[PAD] |
补齐长度 |
[UNK] |
未知 token |
[CLS] |
分类任务的整体表示 |
[SEP] |
分隔句子 |
[MASK] |
掩码语言模型中被遮住的位置 |
例如 BERT 处理句子分类时,可能会把:
我 喜欢 NLP
变成:
[CLS] 我 喜欢 NLP [SEP]
其中 [CLS] 位置的输出常被用来做整个句子的分类。
15. Padding 与 Attention Mask
深度学习通常会批量训练,但一个 batch 里的句子长度可能不同。
例如:
句子 A:我 喜欢 NLP
句子 B:今天 学习 文本 处理
为了放进同一个矩阵,需要把短句补齐:
句子 A:[我, 喜欢, NLP, [PAD]]
句子 B:[今天, 学习, 文本, 处理]
但 [PAD] 只是补位,不应该参与模型理解。
因此需要 attention mask:
句子 A mask:[1, 1, 1, 0]
句子 B mask:[1, 1, 1, 1]
其中:
- 1 表示真实 token。
- 0 表示 padding token。
在 Transformer 中,attention mask 会告诉模型:
不要关注 [PAD] 位置
这对理解 Transformer 的注意力机制非常重要。
16. 文本处理与传统机器学习
在进入深度学习之前,文本也可以用传统机器学习处理。
常见组合是:
文本 -> TF-IDF -> 逻辑回归 / SVM / 朴素贝叶斯
例如情感分类:
输入:这个商品很好用
输出:正面
可以先用 TF-IDF 得到向量,再用逻辑回归分类。
这和前面逻辑回归文章可以联系起来:
相关回顾:逻辑回归:从直线到概率的分类算法
如果特征维度很高,还要注意正则化:
相关回顾:正则化与过拟合:让模型不只记住训练集
文本任务中,词表可能有几万甚至几十万个特征,过拟合风险很常见,L1/L2 正则化会很有用。
17. Python 实战:词袋与 TF-IDF
下面用 scikit-learn 演示如何把文本转成词袋和 TF-IDF。
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
texts = [
"我 喜欢 自然语言处理",
"我 喜欢 机器学习",
"自然语言处理 和 机器学习 很有趣",
]
# 词袋模型
count_vectorizer = CountVectorizer(token_pattern=r"(?u)\b\w+\b")
count_matrix = count_vectorizer.fit_transform(texts)
print("词表:")
print(count_vectorizer.vocabulary_)
print("词袋矩阵:")
print(count_matrix.toarray())
# TF-IDF
tfidf_vectorizer = TfidfVectorizer(token_pattern=r"(?u)\b\w+\b")
tfidf_matrix = tfidf_vectorizer.fit_transform(texts)
print("TF-IDF 矩阵:")
print(tfidf_matrix.toarray())
这里为了演示,文本已经提前用空格分好了词。
实际中文任务中,可以先用分词工具把句子切开,再交给 CountVectorizer 或 TfidfVectorizer。
18. Python 实战:文本分类 Pipeline
下面用 TF-IDF + 逻辑回归做一个简单文本分类流程。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
texts = [
"这个 手机 很 好用",
"物流 很 快 包装 很 好",
"质量 太 差 了",
"屏幕 有 划痕 很 失望",
]
# 1 表示正面,0 表示负面
y = [1, 1, 0, 0]
pipe = Pipeline([
("tfidf", TfidfVectorizer(token_pattern=r"(?u)\b\w+\b")),
("model", LogisticRegression()),
])
pipe.fit(texts, y)
new_texts = [
"这个 手机 质量 很 好",
"包装 很 差 很 失望",
]
pred = pipe.predict(new_texts)
proba = pipe.predict_proba(new_texts)
print(pred)
print(proba)
这个流程和前面交叉验证文章中的 Pipeline 思想是一致的:
预处理和模型训练应该放在同一个 Pipeline 中
这样做交叉验证时,可以避免数据泄漏。
相关回顾:交叉验证与网格搜索:让模型选择更可靠
19. Transformer 前需要理解什么
如果你正在学习 Transformer,文本处理里最重要的是下面几个概念:
- token:模型处理的基本单位。
- tokenizer:把文本切成 token 的工具。
- vocabulary:token 到 id 的映射表。
- input_ids:token id 序列。
- embedding:把 token id 映射成向量。
- padding:把不同长度句子补齐。
- attention_mask:告诉模型哪些位置是真实 token。
- subword:用子词减少未知词问题。
Transformer 不直接处理文字,它处理的是:
input_ids + attention_mask
然后模型内部通过 embedding 层把 input_ids 变成向量序列,再进入注意力层。
20. 总结
文本处理的核心问题是:
如何把自然语言变成模型能计算的数字表示?
传统 NLP 中,常见方法包括:
- One-hot
- 词袋模型
- n-gram
- TF-IDF
深度学习和 Transformer 中,更常见的是:
- tokenization
- token id
- embedding
- subword
- padding
- attention mask
理解这些内容以后,再学习 Transformer 时就会顺很多。因为 Transformer 的注意力机制虽然很重要,但它接收的输入并不是原始文本,而是经过 tokenizer 和 embedding 处理后的向量序列。
