从词袋到 Transformer:文本是怎么变成模型输入的
三条客服工单里都出现了「订单」,TF-IDF 给它的权重只有 0.298,而只出现一次的「失败」「重试」是 0.505。找出一句话里真正有区分度的词,这件事从词频统计一路演进到 Transformer 的注意力机制,思路一直没变,变的是「区分度」由人来定义还是由模型自己学。
本文沿着文本表示的演进梳理 NLP 的基本概念,最后落到服务端工程关心的问题:什么任务用什么方案、token 和向量在系统里意味着什么。实验在 Python 3、scikit-learn 1.9.1 和 NumPy 上完成,不依赖外部数据集。
一、先说结论
- 模型只能处理数字。所有 NLP 方法的第一步都是把文本变成向量,区别在于向量里装了多少信息。
- 词袋和 TF-IDF 只统计词频:维度等于词表大小,不知道词序,也不知道「退款」和「退钱」是一个意思。但它们快、可解释,短文本分类常常够用。
- 词向量让语义相近的词在空间里靠近,但一个词只有一个向量,分不清「苹果手机」和「吃苹果」。
- Transformer 用注意力让每个词的表示随上下文变化,这是 BERT、GPT 等预训练模型的共同基础。
- 工程上的选择按任务来:关键词明确的分类用 TF-IDF 加线性模型,语义检索用嵌入模型,开放式理解与生成才需要大模型。
二、文本表示的三个阶段
每个阶段都是为了解决上一个阶段的某个缺陷,同时也付出了新的代价:词向量换来了语义,代价是需要大规模语料来训练;Transformer 换来了上下文,代价是计算量随文本长度平方增长。
三、词袋与 TF-IDF:统计出来的「重要性」
3.1 词袋
词袋(Bag of Words)把每篇文档表示成「每个词出现了几次」。对三条已经分好词的工单:
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
docs = ["订单 支付 失败 请 重试",
"支付 成功 订单 已 发货",
"物流 延迟 订单 未 发货"]
# 中文需要先分词;这里按空格切分,实际项目常用 jieba 等分词工具
bow = CountVectorizer(token_pattern=r"(?u)\S+")
print(bow.fit_transform(docs).toarray())输出(列顺序为 发货 失败 已 延迟 成功 支付 未 物流 订单 请 重试):
[[0 1 0 0 0 1 0 0 1 1 1]
[1 0 1 0 1 1 0 0 1 0 0]
[1 0 0 1 0 0 1 1 1 0 0]]三句话被变成 11 维向量,每一维对应一个词。真实语料的词表有几万到几十万个词,向量绝大多数位置是 0,所以通常用稀疏矩阵存储。
3.2 TF-IDF
词袋的问题是,每篇文档都有的词(「订单」)和只在一篇里出现的词(「失败」)被同等对待。TF-IDF 在词频上乘一个「逆文档频率」:出现的文档越多,权重越低。
tfidf = TfidfVectorizer(token_pattern=r"(?u)\S+")
matrix = tfidf.fit_transform(docs)实测各词在三篇文档中的权重:
| 词 | 工单 1 | 工单 2 | 工单 3 | 出现在几篇文档里 |
|---|---|---|---|---|
| 订单 | 0.298 | 0.315 | 0.298 | 3 |
| 支付 | 0.384 | 0.406 | 0 | 2 |
| 发货 | 0 | 0.406 | 0.384 | 2 |
| 失败、请、重试 | 0.505 | 0 | 0 | 1 |
| 延迟、物流、未 | 0 | 0 | 0.505 | 1 |
scikit-learn 默认的计算方式是:idf = ln((1 + 文档总数) / (1 + 包含该词的文档数)) + 1,再乘以词频,最后把每篇文档的向量归一化为单位长度。「订单」的 idf 是 ln(4/4) + 1 = 1,「失败」是 ln(4/2) + 1 ≈ 1.69,权重的差距就是这样算出来的。
「请」和「失败」拿到了同样的权重 0.505。在 TF-IDF 看来,它们都只出现过一次,重要性没有区别,这就是纯统计方法的天花板。实际项目里会用停用词表过滤掉「请」「的」这类词,但这是人工补的规则,模型自己学不会。
3.3 它的局限
- 不知道词序:「订单支付失败」和「支付订单失败」得到同一个向量。
- 不知道语义:「退款」和「退钱」是两个互不相关的维度。
- 维度随词表膨胀:新词进来就要重建词表。
但它有两个优点经常被低估:速度(不需要 GPU,毫秒级)和可解释性(每一维都能对应到一个具体的词)。工单分类、垃圾内容过滤这类关键词本身就有很强信号的任务,TF-IDF 加逻辑回归或朴素贝叶斯往往就是一个很强的基线。
四、词向量:让语义可以计算
Word2Vec(2013 年)的想法是:出现在相似上下文里的词,意思也相近。它在大规模语料上训练一个浅层神经网络,让每个词得到一个几百维的稠密向量。训练完成后:
- 语义相近的词,向量的余弦相似度高,「退款」和「退钱」会靠得很近;
- 向量之间可以做类比运算,经典例子是「国王 − 男人 + 女人」最接近「女王」。
这一步把「语义」变成了可以计算的距离,后来的语义检索就建立在这个能力上。
局限在于一个词只有一个向量。「苹果发布了新手机」和「我吃了一个苹果」里的「苹果」拿到的是同一个向量,模型只能学到两种意思的平均。要解决这个问题,每个词的表示就必须依赖它所在的句子。
五、模型是怎么「学」的
在进入 Transformer 之前,先把训练的几个基本概念对齐,它们在所有神经网络里都一样:
| 概念 | 含义 | 对应到上一篇的实验 |
|---|---|---|
| 损失函数 | 衡量预测和真实答案差多远的一个数,训练的目标是把它降下来 | 分类任务常用交叉熵 |
| 梯度下降 | 计算损失对每个参数的导数,沿着让损失变小的方向小步调整参数 | 逻辑回归的训练过程 |
| 反向传播 | 用链式法则从输出层往回逐层计算导数,让多层网络也能做梯度下降 | — |
| 过拟合 | 训练集上表现很好,没见过的数据上表现差 | k=1 时训练准确率 100% |
| 正则化 | 限制模型复杂度,比如惩罚过大的参数、随机丢弃部分神经元(Dropout) | 限制决策树深度 |
这些概念的实验与数字,见 机器学习的一次完整流程。
六、Transformer:注意力机制
2017 年的论文《Attention Is All You Need》提出了 Transformer,核心是自注意力:计算每个词的表示时,让它按相关程度参考句子里的所有词。
6.1 计算过程
每个词的输入向量分别乘以三个可学习的矩阵,得到 Query、Key、Value 三个向量。一个词的 Query 和所有词的 Key 做点积,得到「我应该多关注谁」的分数,经过 softmax 归一化后,用这组权重对所有词的 Value 加权求和,就是这个词新的表示。
用 NumPy 写出来只有几行:
import numpy as np
tokens = ["支付", "失败", "请", "重试"]
rng = np.random.default_rng(7)
d = 8
X = rng.normal(size=(len(tokens), d)) # 每个词的输入向量,真实模型里来自嵌入层
Wq, Wk, Wv = (rng.normal(size=(d, d)) / np.sqrt(d) for _ in range(3))
Q, K, V = X @ Wq, X @ Wk, X @ Wv
scores = Q @ K.T / np.sqrt(d) # 两两之间的相关度
weights = np.exp(scores - scores.max(axis=1, keepdims=True))
weights /= weights.sum(axis=1, keepdims=True) # softmax,每行和为 1
output = weights @ V # 按权重混合所有词的信息这里的矩阵是随机初始化的,所以「请」对「重试」的 0.61 没有任何语言学含义。训练的过程,就是通过梯度下降调整 Wq、Wk、Wv,让这些权重变得有意义,比如让「失败」更多地关注「支付」。
6.2 为什么它取代了之前的方案
- 一步看到整句话:此前的循环神经网络(RNN)逐词处理,距离远的词之间信息要传递很多步,容易丢失;注意力让任意两个词直接交互。
- 可以并行:上面的计算全是矩阵乘法,整句一次算完,适合 GPU。RNN 必须等上一个词算完才能算下一个。
- 可以堆叠:多个注意力「头」并行关注不同的关系(多头注意力),多层堆叠后,表示逐层变得抽象。
代价是注意力矩阵的大小是「长度 × 长度」,计算量和显存随文本长度平方增长,这也是模型都有上下文长度上限的原因之一。
七、BERT 与 GPT:同一个结构的两种用法
原始 Transformer 由编码器(Encoder)和解码器(Decoder)组成。后来的预训练模型大多只用其中一半:
| BERT 系列 | GPT 系列 | |
|---|---|---|
| 结构 | 只用编码器 | 只用解码器 |
| 注意力方向 | 双向:每个词能看到左右两边 | 单向:每个词只能看到它之前的词 |
| 预训练任务 | 遮住部分词,预测被遮住的词 | 预测下一个词 |
| 擅长 | 理解类任务:分类、实体识别、生成句向量 | 生成类任务:续写、对话、代码 |
| 典型用法 | 预训练后在少量标注数据上微调 | 直接用提示词完成任务,或再微调 |
今天的大语言模型基本都是 GPT 这一类的解码器结构。它们在海量文本上做「预测下一个词」的预训练,再经过指令微调和人类反馈对齐,才具备按指令回答问题的能力。
让预训练模型适配具体任务,常见的有三种方式,成本依次升高:
- 提示词:不改模型参数,只改输入。成本最低,效果依赖模型本身的能力。
- 检索增强(RAG):先用嵌入模型检索相关文档,拼进提示词里。适合知识经常变化、需要引用来源的场景。
- 微调:用标注数据继续训练模型(或其中一小部分参数)。适合输出格式、领域术语需要稳定遵循的场景。
八、放到服务端工程里
- 先问任务需不需要大模型。工单按关键词分类、敏感词过滤,TF-IDF 加线性模型在 CPU 上就能毫秒级完成,效果可解释、成本几乎为零。开放式的理解、摘要、生成才是大模型的主场。
- token 是计费和容量的基本单位。模型处理的是分词器切出来的 token,不是字符。同样的内容,不同模型切出来的 token 数不同,中文通常比英文更「费」token。成本估算和上下文长度限制都要按目标模型的分词器来算。
- 向量是一种新的数据类型。语义检索要把文本用嵌入模型转成向量,存进支持向量检索的存储里。换了嵌入模型,所有向量都要重建,这和换索引结构类似,要提前规划。
- 上下文长度有真实成本。注意力的计算量随长度平方增长,塞进提示词的内容越多,延迟和费用越高,模型也越容易忽略中间的内容。检索时召回得准,比召回得多更重要。
Java 服务具体怎么接入模型,见 Java 接入大模型 和 模型部署。
九、常见误区
- 「NLP 任务都应该用大模型」:很多分类任务的信号就在关键词里,传统方法更快、更便宜、更可控。
- 「TF-IDF 高的词就是关键词」:它只反映统计上的稀有程度,「请」和「失败」可以拿到同样的分数。
- 「词向量能区分多义词」:静态词向量一词一个向量,区分多义词要靠上下文相关的表示。
- 「注意力权重就是模型的解释」:权重反映的是信息混合的比例,不等于因果解释,多层多头叠加后更难直接解读。
- 「上下文越长越好」:成本随长度上升,无关内容还会干扰模型。
小结
从词袋到 Transformer,文本表示解决的始终是同一个问题:把一句话变成一组数字,并尽量保留有用的信息。词袋保留了「有哪些词」,词向量加上了「词是什么意思」,Transformer 又加上了「在这句话里是什么意思」。对服务端工程来说,这条演进线也是选型的依据:越往后能力越强,成本、延迟和不可控性也越高,按任务需要选择最靠前、够用的那一个。
配套实验
- codesphere-labs/ai/ml-and-nlp-basics:三条工单的词袋矩阵与 TF-IDF 权重、一次缩放点积注意力的权重矩阵;在固定版本的 Python 3.14.7 容器里运行(验证记录)
参考资料
- scikit-learn:Text feature extraction(词袋与 TF-IDF)
- Mikolov et al., 2013:Efficient Estimation of Word Representations in Vector Space
- Vaswani et al., 2017:Attention Is All You Need
- Devlin et al., 2018:BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
- Jay Alammar:The Illustrated Transformer