文本如何变成数字表示

开篇问题

神经网络只能处理数字。可是我们输入给大语言模型的是文字、代码和符号。

那么问题来了:const user = "Ada" 这样的文本,如何变成模型可以计算的东西?模型又如何知道“猫”和“狗”比“猫”和“数据库连接池”更接近?

这就要进入 embedding,也就是“把离散符号变成连续数字表示”的第一层机制。

本章你会理解什么

读完本章,你应该能够理解:

  • 文本进入模型前为什么必须先变成 token ID。
  • embedding table 的作用是什么。
  • 向量表示为什么能承载语义、语法和位置相关信息。
  • 语言模型内部的 embedding 和应用里的向量检索 embedding 有什么区别。
  • 为什么 embedding 不是可逆压缩,也不是人工设计的标签。

核心概念

语言模型不能直接计算字符串。它通常会经历三步:

  1. 文本被 tokenizer 切成 token。
  2. token 被映射成词表里的数字 ID。
  3. 每个 token ID 被查表得到一个向量。

这个向量就是模型内部使用的数字表示。它不是一个普通编号,而是一组可以参与神经网络计算的浮点数。

可以粗略写成:

type TokenId = number;
type Vector = number[];

const embeddingTable: Record<TokenId, Vector> = {
  1024: [0.12, -0.03, 0.88, ...],
  1025: [-0.44, 0.19, 0.07, ...],
};

真实模型里的向量维度可能有几百、几千甚至更多。每一维通常没有人类可直接命名的含义,但整个向量会在训练中变成有用的表示。

机制拆解

1. 从文本到 token ID

第一步是 tokenizer。它负责把文本切成模型词表中的 token。

例如:

function getUser() {}

可能被切成类似这样的片段:

function | get | User | () | {}

这只是示意,真实切分取决于具体 tokenizer。重点是:模型最终看到的不是原始字符串,而是一串 token ID。

[3182, 642, 1178, 2901, 905]

这些 ID 本身没有语义。ID 为 3182 并不比 ID 为 642 “更大”或“更重要”。它们只是词表中的索引。

2. 从 token ID 到向量

如果只把 token ID 直接喂给神经网络,会有一个问题:数字 ID 的大小会制造假的关系。

例如 token 100 和 token 101 不一定语义接近,token 100 和 token 9000 也不一定语义遥远。ID 只是编号,不是意义。

所以模型会使用 embedding table。每个 token ID 对应一个可训练向量。输入 token ID 时,本质上是在表中取出那一行向量。

训练开始时,这些向量可能接近随机。随着模型反复学习下一个 token 预测,向量会被不断调整。那些在相似上下文中出现、承担相似功能的 token,往往会形成更接近的表示。

3. 向量承载的是关系,不是标签

embedding 不是给每个词贴上“名词”“动词”“技术术语”这样的人工标签。它更像一种从训练任务中自动形成的坐标。

如果两个 token 经常出现在相似上下文中,它们的向量可能在某些方向上更接近。比如“北京”和“上海”都可能出现在城市、天气、人口、交通等语境中;“React”和“Vue”都可能出现在组件、状态、前端框架等语境中。

这种接近不是人手写进去的,而是训练目标逼出来的:为了更好预测下一个 token,模型需要把有相似作用的符号放到可计算的相近表示里。

4. 位置也必须变成数字

同一组 token,顺序不同,意义可能完全不同。

用户 删除 文件
文件 删除 用户

如果模型只看到 token 向量而不知道顺序,就无法区分这两句话。

因此 Transformer 类模型还需要位置信息。常见做法是加入 positional embedding 或使用其他位置编码机制,让模型知道每个 token 在序列中的位置以及 token 之间的相对关系。

简化理解就是:

输入表示 = token 的向量表示 + 位置相关表示

有了 token 表示和位置表示,模型才能进一步计算“谁和谁有关”“当前 token 应该关注前文哪里”。

5. embedding 会进入后续层反复加工

embedding 只是第一步。它把离散 token 转成连续向量,让后续神经网络可以计算。

进入 Transformer 后,每一层都会根据上下文更新这些向量。也就是说,一个 token 的表示不是固定不变的。

例如“Java”在不同上下文中可能表示编程语言,也可能表示咖啡或地名。初始 embedding 只提供基础表示,后续层会结合上下文,让当前位置的向量逐渐带上更具体的含义。

这就是为什么我们不能把 embedding 理解成一本静态词典。它是模型计算的起点,而不是最终理解结果。

开发者常见误解

误解一:embedding 是把文本压缩成可以还原的编码

embedding 不是 zip,也不是 Base64。它通常不能还原成原文。

模型内部会保留 token ID 序列作为输入结构,embedding 只是把这些 ID 转成可计算向量。向量的作用是参与神经网络计算,而不是作为可逆存储格式。

误解二:向量每一维都有明确人类含义

不要试图解释“第 17 维代表语气,第 33 维代表技术性”。真实模型中的维度通常是分布式表示:意义分散在许多维度和它们的组合里。

开发者更应该关注整体关系,而不是单独维度。

误解三:token ID 越接近,语义越接近

token ID 是词表编号,不代表语义距离。真正参与语义关系的是 embedding 向量,而不是 ID 数值。

这和数据库自增 ID 类似:user_id = 1001user_id = 1002 不表示两个用户更相似。

误解四:所有 embedding 都是一回事

语言模型内部的 token embedding,和应用中常说的“把一段文档转成向量”的 embedding,不是同一个层面的概念。

本章讨论的是模型内部的输入表示:每个 token 如何变成向量,以便 Transformer 继续计算。

应用里的文本向量通常是把一句话、一段文本或一个文档片段变成一个整体向量,用于相似度比较等任务。两者都叫 embedding,因为都把离散内容映射到连续向量空间,但用途、粒度和训练方式并不完全相同。

小实验/思考实验

想象你要用二维坐标表示一些技术词:

React
Vue
PostgreSQL
MySQL
Docker
Kubernetes

你可能会把 React 和 Vue 放得近一些,把 PostgreSQL 和 MySQL 放得近一些,把 Docker 和 Kubernetes 放得近一些。

这不是因为你给它们写了分类标签,而是因为你知道它们在工程语境中经常承担相似角色。

现在把二维扩展到几千维,把人工摆放改成模型训练,把几个词扩展到几十万 token。这就是 embedding 的直觉版本:在一个高维空间里,为 token 学习一套有助于预测和计算的位置。

再思考一个问题:如果一个词在不同语境中有多个含义,单个初始 embedding 是否足够?为什么还需要后续 Transformer 层结合上下文继续更新表示?

本章小结

文本进入语言模型前,需要先被切成 token,再映射成 token ID,最后通过 embedding table 转成向量。token ID 只是索引,embedding 向量才是模型可以计算的表示。

embedding 的意义不是可逆压缩,也不是人工标签,而是在训练中形成的连续表示。它让模型能够把离散符号放入可计算空间,并在后续 Transformer 层中结合上下文不断更新。

理解文本如何变成数字,是理解 attention 和 Transformer 的前置基础:只有当 token 被表示成向量,模型才有可能计算 token 之间的关系。

自测题

  1. 为什么语言模型不能直接把 token ID 当作有意义的数值使用?
  2. embedding table 在模型输入阶段起什么作用?
  3. 为什么说 embedding 向量的单个维度通常不适合直接解释?
  4. token embedding 和应用中的文档 embedding 有什么区别?
  5. 为什么模型除了 token 表示,还需要位置相关表示?