Skip to content

从词袋到 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 加线性模型,语义检索用嵌入模型,开放式理解与生成才需要大模型。

二、文本表示的三个阶段 ​

词袋 / TF-IDF一维对应一个词,稀疏维度 = 词表大小词向量 Word2Vec几百维稠密向量语义相近的词距离近Transformer 上下文表示每个位置一个向量同一个词随上下文变化不知道词序,不知道同义词一个词只有一个向量计算量随长度平方增长短文本分类、关键词检索:词袋 + 线性模型往往就够,毫秒级、可解释语义检索(RAG 的召回):用嵌入模型把文本变成向量;生成与理解:用大模型
图 1 · 三种表示的差别在于「能不能表达语义」和「能不能随上下文变化」;后一种是在前一种的局限上发展出来的

每个阶段都是为了解决上一个阶段的某个缺陷,同时也付出了新的代价:词向量换来了语义,代价是需要大规模语料来训练;Transformer 换来了上下文,代价是计算量随文本长度平方增长。

三、词袋与 TF-IDF:统计出来的「重要性」 ​

3.1 词袋 ​

词袋(Bag of Words)把每篇文档表示成「每个词出现了几次」。对三条已经分好词的工单:

python
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer

docs = ["订单 支付 失败 请 重试",
        "支付 成功 订单 已 发货",
        "物流 延迟 订单 未 发货"]

# 中文需要先分词;这里按空格切分,实际项目常用 jieba 等分词工具
bow = CountVectorizer(token_pattern=r"(?u)\S+")
print(bow.fit_transform(docs).toarray())

输出(列顺序为 发货 失败 已 延迟 成功 支付 未 物流 订单 请 重试):

text
[[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 在词频上乘一个「逆文档频率」:出现的文档越多,权重越低。

python
tfidf = TfidfVectorizer(token_pattern=r"(?u)\S+")
matrix = tfidf.fit_transform(docs)

实测各词在三篇文档中的权重:

词工单 1工单 2工单 3出现在几篇文档里
订单0.2980.3150.2983
支付0.3840.40602
发货00.4060.3842
失败、请、重试0.505001
延迟、物流、未000.5051

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 写出来只有几行:

python
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.140.130.330.40失败0.150.160.450.24请0.080.080.220.61重试0.200.140.440.22softmax(Q·Kᵀ / √d)每一行归一化为概率分布输出 = 权重 × V按权重混合所有词的信息一次矩阵乘法就能让每个词看到整句话,这是 Transformer 能并行训练的原因
图 2 · 每一行是一个词在「看」其他词时分配的权重,每行加起来等于 1(深色 ≥ 0.4,浅色 ≥ 0.2);这里的权重来自随机初始化,训练的目的就是让它们变得有意义

这里的矩阵是随机初始化的,所以「请」对「重试」的 0.61 没有任何语言学含义。训练的过程,就是通过梯度下降调整 Wq、Wk、Wv,让这些权重变得有意义,比如让「失败」更多地关注「支付」。

6.2 为什么它取代了之前的方案 ​

  • 一步看到整句话:此前的循环神经网络(RNN)逐词处理,距离远的词之间信息要传递很多步,容易丢失;注意力让任意两个词直接交互。
  • 可以并行:上面的计算全是矩阵乘法,整句一次算完,适合 GPU。RNN 必须等上一个词算完才能算下一个。
  • 可以堆叠:多个注意力「头」并行关注不同的关系(多头注意力),多层堆叠后,表示逐层变得抽象。

代价是注意力矩阵的大小是「长度 × 长度」,计算量和显存随文本长度平方增长,这也是模型都有上下文长度上限的原因之一。

七、BERT 与 GPT:同一个结构的两种用法 ​

原始 Transformer 由编码器(Encoder)和解码器(Decoder)组成。后来的预训练模型大多只用其中一半:

BERT 系列GPT 系列
结构只用编码器只用解码器
注意力方向双向:每个词能看到左右两边单向:每个词只能看到它之前的词
预训练任务遮住部分词,预测被遮住的词预测下一个词
擅长理解类任务:分类、实体识别、生成句向量生成类任务:续写、对话、代码
典型用法预训练后在少量标注数据上微调直接用提示词完成任务,或再微调

今天的大语言模型基本都是 GPT 这一类的解码器结构。它们在海量文本上做「预测下一个词」的预训练,再经过指令微调和人类反馈对齐,才具备按指令回答问题的能力。

让预训练模型适配具体任务,常见的有三种方式,成本依次升高:

  1. 提示词:不改模型参数,只改输入。成本最低,效果依赖模型本身的能力。
  2. 检索增强(RAG):先用嵌入模型检索相关文档,拼进提示词里。适合知识经常变化、需要引用来源的场景。
  3. 微调:用标注数据继续训练模型(或其中一小部分参数)。适合输出格式、领域术语需要稳定遵循的场景。

八、放到服务端工程里 ​

  • 先问任务需不需要大模型。工单按关键词分类、敏感词过滤,TF-IDF 加线性模型在 CPU 上就能毫秒级完成,效果可解释、成本几乎为零。开放式的理解、摘要、生成才是大模型的主场。
  • token 是计费和容量的基本单位。模型处理的是分词器切出来的 token,不是字符。同样的内容,不同模型切出来的 token 数不同,中文通常比英文更「费」token。成本估算和上下文长度限制都要按目标模型的分词器来算。
  • 向量是一种新的数据类型。语义检索要把文本用嵌入模型转成向量,存进支持向量检索的存储里。换了嵌入模型,所有向量都要重建,这和换索引结构类似,要提前规划。
  • 上下文长度有真实成本。注意力的计算量随长度平方增长,塞进提示词的内容越多,延迟和费用越高,模型也越容易忽略中间的内容。检索时召回得准,比召回得多更重要。

Java 服务具体怎么接入模型,见 Java 接入大模型 和 模型部署。

九、常见误区 ​

  • 「NLP 任务都应该用大模型」:很多分类任务的信号就在关键词里,传统方法更快、更便宜、更可控。
  • 「TF-IDF 高的词就是关键词」:它只反映统计上的稀有程度,「请」和「失败」可以拿到同样的分数。
  • 「词向量能区分多义词」:静态词向量一词一个向量,区分多义词要靠上下文相关的表示。
  • 「注意力权重就是模型的解释」:权重反映的是信息混合的比例,不等于因果解释,多层多头叠加后更难直接解读。
  • 「上下文越长越好」:成本随长度上升,无关内容还会干扰模型。

小结 ​

从词袋到 Transformer,文本表示解决的始终是同一个问题:把一句话变成一组数字,并尽量保留有用的信息。词袋保留了「有哪些词」,词向量加上了「词是什么意思」,Transformer 又加上了「在这句话里是什么意思」。对服务端工程来说,这条演进线也是选型的依据:越往后能力越强,成本、延迟和不可控性也越高,按任务需要选择最靠前、够用的那一个。


配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权