学习 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。

Padding 与 Attention Mask

在 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())

这里为了演示,文本已经提前用空格分好了词。

实际中文任务中,可以先用分词工具把句子切开,再交给 CountVectorizerTfidfVectorizer

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,文本处理里最重要的是下面几个概念:

  1. token:模型处理的基本单位。
  2. tokenizer:把文本切成 token 的工具。
  3. vocabulary:token 到 id 的映射表。
  4. input_ids:token id 序列。
  5. embedding:把 token id 映射成向量。
  6. padding:把不同长度句子补齐。
  7. attention_mask:告诉模型哪些位置是真实 token。
  8. 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 处理后的向量序列。