第02回 词的数字化——独热编码与词袋模型
字海无涯先立典,一词一位列如兵。
满袋不问先后序,频疏高下自分明。
上回说到,万物皆可化作向量,住进同一座“空间”里,方能相似可量、远近可算。可看官心中怕还悬着一问:
“向量我懂了,可一句‘我爱吃饭’,究竟怎么变成一串数字?”
这便是本回要传的入门心法:先教你“立字典”,再教你“独热点兵”,最后教你“词袋装粮”,并以 TF‑IDF 做一把秤,称出哪些词真有分量。
一、先立字典:没有名册,何来点兵
要把词变成数字,第一步不是算,而是“编名册”。
设我们只管三句话:
- 我 爱 吃饭 我
- 你 爱 吃饭 吗
- 吃饭 吃饭 真 好吃
把出现过的词收集起来,得一册名录(词表):
词表大小 就是向量维度。
这一步看似土,却是后面所有文本表示的起手式:
无论你后来用的是词袋、Word2vec、Transformer 还是更前沿的 tokenizer‑free 模型,你都绕不开一个问题:你要用什么“符号单位”来承载意义?
二、独热编码:一词一位,点亮一盏灯
独热编码(one‑hot),做法最直白:
给词表里每个词分一个位置;出现哪个词,就把那一位点成 1,其余皆为 0。
若词表按顺序为 ,则:
- “我” =
- “吃饭” =
独热的妙处是“清清楚楚”:
它把“这个词是谁”表达得毫不含糊。
可独热也有两大硬伤:
- 维度灾难:词表若有十万词,向量就十万维,且大多数位永远为 0。
- 语义断情:在独热世界里,“吃饭”与“用餐”不管多像,点积仍是 0,余弦仍是 0,仿佛素不相识。
看官记住这句话:
独热能“点名”,却不会“认亲”。
下一回的“嵌入(embedding)”,正是为了让词之间有亲疏远近。
三、词袋模型:不问顺序,只数多少
一句话里不止一个词。把一句话变成向量,最常见的入门法叫词袋模型(Bag of Words, BoW)。
它有个江湖规矩:
只管每个词出现几次,不问先后顺序。
于是上面三句话可写成 7 维向量(每一维对应词表中的一个词,值是出现次数):
- 句1:“我 爱 吃饭 我”
- 句2:“你 爱 吃饭 吗”
- 句3:“吃饭 吃饭 真 好吃”
此时两句话的“像不像”,便可用上回的余弦相似度来衡量。
词袋的好处是“简单能用”,坏处也很明显:
- 忽略顺序: “我 打 你” 与 “你 打 我” 在词袋里长得一模一样
- 偏爱高频词: “的、是、了” 这种词几乎篇篇都有,反而会干扰相似度
于是江湖再出一把秤:TF‑IDF。
四、TF‑IDF:一把称词分量的秤
TF‑IDF 的思想一句话就能讲明白:
- 一个词在某篇文档里出现越多,越像“主题词”(TF 大)
- 但如果这个词在所有文档里到处都是,它又不稀奇,分量应当变轻(IDF 小)
写成式子(只保留直觉,不纠缠细枝末节):
其中 IDF 常见取法是:
为文档总数, 为包含词 的文档数。
这把秤的用处,是把“常见却无用”的词压下去,把“稀有却关键”的词抬起来。
在很多检索与分类的入门任务里,TF‑IDF 依旧是一个非常强的基线:
它不华丽,却可靠,像老镖局的路数——走得慢,但不容易翻车。
五、从词袋到“分词”:为什么大模型仍绕不开这一步
看官或许听说过“大模型的 token”。你可能会问:
“既然词袋都能表示,何必又弄出 token、BPE、SentencePiece 这些名堂?”
原因很现实:词袋维度太大、太稀疏、且不保序;而神经网络要吃的是“序列”,并且希望单位更细、更灵活。于是出现了各种分词与子词方案,把文本切成更适合训练的单位。
更进一步,2024–2025 又有人把矛头指向“分词本身”,认为它是一种人为的启发式预处理,并尝试直接从字节学习,做 tokenizer‑free 的语言模型。比如 Byte Latent Transformer(BLT)就以“从原始字节出发、用更强的压缩与 patching 机制”来规避传统 tokenization 的偏置。1
你不必立刻把这些前沿细节学全,但要记住这条主线:
无论叫“词”、叫“子词”、叫“字节”,你总要把文本变成离散单位;而离散单位如何选,会影响模型的世界观。
六、极简代码:从词袋到 TF‑IDF(纯 Python 可跑)
下面给一段极简可运行代码:输入多条句子,输出词表、词袋向量与 TF‑IDF 向量。
import math
from collections import Counter
def build_vocab(docs):
vocab = {}
for doc in docs:
for w in doc:
if w not in vocab:
vocab[w] = len(vocab)
return vocab
def bow_vector(doc, vocab):
vec = [0] * len(vocab)
counts = Counter(doc)
for w, c in counts.items():
if w in vocab:
vec[vocab[w]] = c
return vec
def tf_idf_vectors(docs, vocab):
n = len(docs)
df = [0] * len(vocab)
doc_counts = []
for doc in docs:
counts = Counter(doc)
doc_counts.append(counts)
for w in set(doc):
if w in vocab:
df[vocab[w]] += 1
idf = [math.log(n / (df_i + 1)) for df_i in df]
out = []
for counts in doc_counts:
vec = [0.0] * len(vocab)
for w, c in counts.items():
i = vocab[w]
vec[i] = float(c) * idf[i]
out.append(vec)
return out
if __name__ == "__main__":
docs = [
"我 爱 吃饭 我".split(),
"你 爱 吃饭 吗".split(),
"吃饭 吃饭 真 好吃".split(),
]
vocab = build_vocab(docs)
print("vocab:", vocab)
for i, doc in enumerate(docs, 1):
print("bow", i, ":", bow_vector(doc, vocab))
for i, vec in enumerate(tf_idf_vectors(docs, vocab), 1):
print("tfidf", i, ":", [round(x, 3) for x in vec])
运行之后,你会亲眼看到:
同样是“吃饭”,在不同文档集合里,它的分量会变;同样是“我”,因为太常见,分量就会被压下去。
七、小结:本回留下的两处悬念
本回的三把兵器,你已经握在手中:
- 独热:点名最清楚,但不懂亲疏
- 词袋:能做相似度与分类基线,但不懂顺序
- TF‑IDF:能称词分量,压制无用高频词
可它们共同的痛点也显而易见:
维度大、稀疏、语义不连。
因此下一回,我们要请“嵌入(embedding)”出场:
让词在低维空间里彼此认亲,让“吃饭”与“用餐”不再形同陌路。
正是:独热点兵名册立,词袋装粮计数勤。
若问语义何处寄,还看嵌入点化神。
欲知后事如何,且听下回分解。
引用与溯源
Footnotes
-
Pagnoni, A., et al. Byte Latent Transformer: Patches Scale Better Than Tokens arXiv:2412.09871(2024-12)https://arxiv.org/abs/2412.09871 ↩