Skip to content

15 向量与嵌入模型:揭秘人类与机器语言转化的奥秘

你好,我是叶伟民。

我们前面的案例都使用了人类语言作为系统的输入和输出。然而人类语言与计算机语言差别很大。

我们结合例子来体会一下,例如,假设有如下事物:

  1. 老婆饼
  2. 老婆
  3. 夫妻肺片
  4. 菠萝
  5. 菠萝包

如果使用简单的字符串查找方式来查找,我们很容易就会通过“老婆饼”查找到“老婆”,通过“菠萝包”找到“菠萝”。但这显然是错误的,它们都不是同一类事物。

为什么会这样呢?

为什么直接用简单的字符串查找方式不靠谱?

人类语言跟计算机不一样,现代计算机(冯诺依曼结构计算机)是数字计算机,所有事物进入计算机之后,归根到底都会变成数字。所以我们输入的人类语言最终将会转换为数字。

说到这里,有同学可能会说,这题我知道,这题我熟,所有字符串最终都会转换为 Unicode编码之类的数字

但是使用Unicode编码这种数字来表示字符串会面临一个问题——人类语言里面很多事物的形式和意义之间关系相差很大。例如前面提到的“老婆饼”和“老婆”、“菠萝包”和“菠萝”,英文里面也有同类问题,例如dog(狗)和log(日志),只有一个字母不同。

如果只是简单粗暴地使用Unicode编码之类的数字进行比较甚至计算的话,很明显是不行的。

使用枚举类型来表示事物

我们不妨换一种方式。你是否还记得每门计算机编程语言课程上都会提到的枚举类型?

是的,枚举类型也是用数字来表示事物的。

好,这里我们用以下数字来表示前面的事物:

老婆饼=1,
老婆=2,
夫妻肺片=3,
菠萝=4,
菠萝包=5

版本1

那么如何通过输入字符串查找事物呢?

我们可以通过计算数字的差值来查找。例如:

  1. 用户输入“老婆饼”。
  2. 计算机程序根据“老婆饼”得出数字1。
  3. 计算机程序逐个计算其他事物与数字1的绝对差值(更专业的说法是 距离,后面我们将统一使用距离这个专业说法)。
|2 - 1| = 1老婆
|3 - 1| = 2夫妻肺片
|4 - 1| = 3菠萝
|5 - 1| = 4菠萝包
  1. 通过计算结果的对比,“老婆”与“老婆饼”的距离最小(为1)。因此计算机程序得出“老婆饼”最相近的事物是“老婆”。

看到这里你可能更疑惑了,这个结果与前面的Unicode编码数字的查找方式一样啊!它的意义何在呢?

这时候就体现出这种方法比Unicode编码数字先进的地方了!

首先,Unicode编码数字的值是固定的,我们无法更改。但是我们这种方式里面的数字是我们自己定义的,是可以改的。我们可以将事物的数字改为:

老婆饼=1,
老婆=5,
夫妻肺片=4,
菠萝=3,
菠萝包=2

版本2

然后再重新走一遍查找流程:

  1. 用户输入“老婆饼”。
  2. 计算机程序根据“老婆饼”得出数字1。
  3. 计算机程序逐个计算其他事物与数字1的距离。
|5 - 1| = 4老婆
|4 - 1| = 3夫妻肺片
|3 - 1| = 2菠萝
|2 - 1| = 1菠萝包
  1. 其中“菠萝包”与“老婆饼”的距离最小(为1),因此计算机程序得出“老婆饼”最相近的事物是“菠萝包”

这次我们得出了正确的结果!

然而聪明如你,估计很快就会发现这种方法有一个致命的问题:

  1. 用户输入“菠萝”
  2. 计算机程序根据“菠萝”得出数字3。
  3. 计算机程序逐个计算其他事物与数字3的距离。
|1 - 3| = 2老婆饼
|5 - 3| = 2老婆
|4 - 3| = 3夫妻肺片
|2 - 3| = 1菠萝包
  1. 其中“老婆”、“老婆饼”与“菠萝”的距离都是一样的,都为2,这显然不对!

那么如何解决这个问题呢?

使用向量来表示事物

我们可以使用一个二维数组来表示以上事物,其中第一维表示该事物在食物这一类别上的值,第二维表示该事物在人物关系这一类别上的值。

于是以上事物可以表示为:

老婆饼=[1,0],
老婆=[0,1],
夫妻肺片=[4,0],
菠萝=[3,0],
菠萝包=[2,0]
整个二维数组=[[1,0],[0,1],[4,0],[3,0],[2,0]]

版本3

现在我们一眼可以看出,“老婆”这个事物在数字表示上与众不同。因为计算多维数组之间距离的方法比较复杂,就不详细展开了。我们只需要知道,根据距离值的计算结果,“老婆”这个事物与其他事物距离相差很大。

沿着这个思路扩展,我们用一个三维数组来表示以上事物。其中第一维表示该事物在点心这一类别上的值,第二维表示该事物在其他食物这一类别上的值,第三维表示该事物在人物关系这一类别上的值。

于是以上事物可以表示为:

老婆饼=[1,0,0],
老婆=[0,0,1],
夫妻肺片=[0,4,0],
菠萝=[0,3,0],
菠萝包=[2,0,0]
整个三维数组=[[1,0,0],[0,0,1],[0,4,0],[0,3,0],[2,0,0]]

版本4

现在我们一眼可以看出,不但“老婆”这个事物在数字表示上是如此与众不同,“老婆饼”与“菠萝包”的相似程度明显高于其他事物。

我们沿着这个思路扩展,换成用一个四维数组来表示以上事物。其中第一维表示该事物在点心这一类别上的值,第二维表示该事物在菜肴这一类别上的值,第三维表示该事物在水果这一类别上的值,第四维表示该事物在人物关系这一类别上的值。

于是以上事物可以表示为:

老婆饼=[1,0,0,0],
老婆=[0,0,0,1],
夫妻肺片=[0,4,0,0],
菠萝=[0,0,3,0],
菠萝包=[2,0,0,0]
整个四维数组=[[1,0,0,0],[0,0,0,1],[0,4,0,0],[0,0,3,0],[2,0,0,0]]

版本5

现在我们一眼可以看出,除了“老婆饼”与“菠萝包”的相似程度明显高于其他事物之外,其他事物的差别也比前面方式更明显了。另外我们也发现了,“老婆”与“老婆饼”、“菠萝”与“菠萝包”的差别也更大了,明显地看出不是同一类。

以此类推, 我们使用的数组维数越多,效果就越明显,准确率(更专业的说法是精度)就越高。不过,维数越多,计算距离所花的资源和存储成本也会越高。

以OpenAI最新一代嵌入模型为例,其中small版本的维数是512维,large版本的维数是1536维。我们一眼可以得知,large版本的精度会比small版本更高,但是费用也更高,因为计算和存储成本也更高。

这里的多维数组有个更专业的说法,就是 向量。所以我们在看OpenAI嵌入模型资料的时候,当提到向量维度的时候,其实就对应着多维数组的维度。而 嵌入,就是这里将事物转换为多维数组这一过程的专业说法。

那么问题来了,到目前为止的例子中,我们只有五个事物,所以我们可以很轻松地人工指定事物具体的多维数组值(专业叫法就是向量值)。但是在实际工作中,我们有五万、五十万甚至五百万个事物,这种人工指定的方法肯定是不可行的。那么如何解决这个问题呢?

通过嵌入模型获取事物的向量值

答案是我们可以通过AI来生成这些事物的向量值。

像是OpenAI这样知名的AI厂商,其实已经帮我们生成了这些事物的向量值,并得出一个AI模型,来让我们调用以获得具体事物的向量值。这个AI模型的专业叫法就是嵌入模型。

那么我们如何找到这些嵌入模型呢?

有好几种途径,其中一种途径就是通过嵌入模型的排行榜MTEB。

MTEB是大规模文本嵌入基准(Massive Text Embedding Benchmark)的简称,它是衡量文本嵌入模型性能的重要参考,涵盖了八大核心嵌入任务,横跨58个不同的数据集及112种语言,提供了迄今为止最为全面的文本嵌入性能评估。

MTEB又分为英文排行榜和中文排行榜,中文排行榜又称C-MTEB,其网址是: https://github.com/FlagOpen/FlagEmbedding/tree/master/C_MTEB

如果一个人没有见过“老婆饼”、“菠萝包”,他是无法对这些事物正确分类的。嵌入模型也一样,如果一个嵌入模型所学习的数据里面没有“老婆饼”、“菠萝包”,这个嵌入模型也无法得出正确的向量值。

因此我们需要针对我们自己的应用场景选择合适的嵌入模型。因为我们的实战案例的语言是中文,所以我们并没有采用OpenAI的嵌入模型,而是采用了目前中文排行榜中名列前茅的一个嵌入模型——bge-large-zh-v1.5。

当然,如果你发现bge-large-zh-v1.5在你的应用场景中效果不佳,也可以从MTEB或C-MTEB里面选择一个适合你的嵌入模型,或者使用你自己的数据微调,甚至从零开始训练一个嵌入模型。

我们来看看使用bge-large-zh-v1.5对“老婆饼”嵌入的向量结果:

[0.035559508949518204,
 0.006563871167600155,
 -0.0157638993114233,
 -0.03997838869690895,
 0.007958737201988697,
  ...此处省略其他一千多个维度的值...]

我们发现,与前面的版本1-5相比,以上向量结果(也就是多维数组)里面的每个值 是浮点数而不是整数而且这些浮点数值都小于1。这里就涉及到很多AI细节了,因为与RAG无关,我们就不深入讨论了。我们只需要先有个印象就可以,第17节课学习相似度比较的时候,我们还会应用今天学到的原理。

另外,我们还发现,与前面的版本1~5相比,以上向量结果并没有体现“点心”“菜肴”“水果”等类别。那是因为前面的版本1-5里面,我们只有五个事物,人工分类就可以,人类是明确知道这些事物的分类的。但是嵌入模型是要处理成千上万种事物,只能通过算法对这些事物进行分类,AI本身是不知道“点心”“菜肴”“水果”这些类别的,但是其原理是相同的。

虽然AI本身是不知道这些类别,其使用的算法也得不出精确的类别,但是很多时候我们不需要做到百分百准确。现代计算机行业鼻祖冯·诺依曼在其著名遗作《计算机与人脑》第1章里面就明确指出, 近似计算在很多时候精度已经能够满足需求了。如果精度无法满足你的需求,并且这个需求足够重要到值得花额外成本去解决,那么我们可以微调甚至从零开始训练一个嵌入模型。

讲到这里,可能有同学会问,既然Office 2019可以打开Office 2007的文件,那么同一模型最新一代版本得出的向量值,可以与上一代的向量值兼容不?

从“老婆饼”在前面第2节和第3节的5个版本里面的值可以看出,“老婆饼”在不同版本之间的值是不同的,因此同一模型最新一代版本得出的向量值,很可能无法兼容上一代的向量值,保守起见,建议你使用最新一代版本重新计算。

那么不同嵌入模型得出的向量值能通用不?

答案也是否定的。 即使是同一个模型的small和large版本,其向量维度都是不同的。

如果细心观察我们前面5个版本里面“老婆饼”的值,就会发现,“老婆饼”在不同维度的数组里面的值是不一样的,所以不同维度数组值之间的计算是没有意义的。那么不同模型得出的向量值更加无法通用了。因此每换一个嵌入模型甚至不同版本,向量值就可能要用新模型重新做计算。

另外,还需要补充一点,前面的例子里,我们都是对一个单词或者词组进行嵌入。同样的道理,我们也可以对一句话,甚至一段话进行嵌入,通过这样的方式对比他们的相似程度。虽然句子或者段落的距离计算起来,要比前面计算两个事物的距离复杂得多,但原理是一样的。

OpenAI的嵌入模型和我们课程里使用的嵌入模型bge-large-zh-v1.5,都是 支持一个句子甚至一段话嵌入的,因此我们可以使用它们来比较一句话或一段话的相似程度。

最后,声音、图像、视频也是事物,也一样可以通过嵌入转换为现代计算机所用的数字格式。不过目前OpenAI的嵌入模型和我们课程使用的嵌入模型bge-large-zh-v1.5只支持人类语言,而不支持声音、图像、视频。

小结

好了,今天这一讲到这里就结束了,最后我们来回顾一下。这一讲我们学会了五件事情。

第一件事,现代计算机是使用数字来表示任何事物的,包括我们的人类语言。

第二件事,我们需要使用值可以自定义的方式来表示事物,例如多维数组。

第三件事,维度越高的数组,能够表示的事物精度就越高,当然成本也越高。

第四件事,具体向量值我们可以用AI训练出一个模型来获得,这种模型称为嵌入模型。出于高精度考虑,这时候得出的向量值一般是用浮点数数组而不是整数数组。

第五件事,不但一个单词或词组可以使用向量来表示,一个句子、甚至一段话都可以使用向量来表示。

最后说点题外话,如果你之前接触过向量和嵌入,可能会觉得这节课一些说法并不严谨,甚至与自己的认知相冲突。例如没有使用其他文章常用的多轴坐标图,而是使用多维数组的表达方式。

这是因为对于程序员来说,对数组的熟悉程度远远高于多轴坐标图。所以今天这节课里我选择了这种更通俗易懂的方式来讲解向量和嵌入,方便从未接触过这些概念的同学,快速建立起感性的认识。

思考题

即使在中文里面,同一个事物在不同的方言里面也有不同的叫法,如何解决这个问题呢?

欢迎你在留言区和我交流互动,如果这节课对你有启发,也推荐分享给身边更多朋友。