有一天,一个销售出身的老板问我:你能解释下“词向量”吗?
通过交谈得知,老板是做保险业务的。他发现用户会咨询很多手册上的基础问题,这需要业务员一对一进行回复。于是,他想让系统自动回复,从而减少成本。
老板手里有很多行业资料,但做了几个版本都不如意。上一个版本是通过字符串匹配实现的。这种方式,问不到关键词,系统就答不上。比如,问“高血压”,系统能给出答案。问“三高”,就找不到了。于是,他们只能去后台将“三高”做关联。但是,词海茫茫,此起彼伏。
这个版本,技术人员说采用了词向量,老板想知道这是啥?靠不靠谱?
下面,我就来实现一个问答匹配的例子,然后再详解操作步骤、原理。
一、实战效果演示
我在教育行业行走,不懂保险……就拿教育产品来举例吧。
假设我们有这么一个问答知识库:
- "找回密码流程说明"
- "如何查看我的错题记录"
- "Sorry, an error occurred!"
- ……
用户要找他的错题本,可能会有以下几种问法:
- "我的错题从哪里可以查看?"
- "如何找到我做错的题目"
- "我没做对的题目怎么查找"
有些问法,甚至不包含“错题”,只是说“没做对的题目”。我们看看,从知识库里能找到吗?
以下是全部代码,用python实现:
# 加载模型
from text2vec import SentenceModel, semantic_search
model = SentenceModel("text2vec")
# 以下是系统的知识库
corpus = [
"找回密码流程说明",
"如何查看我的错题记录",
"Sorry, an error occurred!"]
corpus_embeddings = model.encode(corpus)
# 以下是用户的问题
queries = [
"我的错题从哪里可以查看?",
"如何找到我做错的题目",
"我没做对的题目怎么查找"]
for query in queries:
query_embedding = model.encode(query)
# 将问题通过模型从知识库匹配,取前3条
hits = semantic_search(query_embedding, corpus_embeddings, top_k=3)
print("\n问题:", query, "\n最优前3条:")
hits = hits[0]
for hit in hits:
print(corpus[hit['corpus_id']], "({:.2f})".format(hit['score']))
运行一下,查看结果:
问题: 我的错题从哪里可以查看?
最优前3条:
如何查看我的错题记录 (0.84)
Sorry, an error occurred! (0.46)
找回密码流程说明 (0.43)
问题: 如何找到我做错的题目
最优前3条:
如何查看我的错题记录 (0.80)
Sorry, an error occurred! (0.48)
找回密码流程说明 (0.46)
问题: 我没做对的题目怎么查找
最优前3条:
如何查看我的错题记录 (0.75)
Sorry, an error occurred! (0.48)
找回密码流程说明 (0.47)
可以看出,即便提问的关键字不一样,也可以匹配出预期的结果。
我们可以通过semantic_search这个方法,对文本进行匹配度计算,试验可得:
问题: 如何查看我的错题记录
匹配得分:
我的错题从哪里可以查看? (0.84)
如何找到我做错的题目 (0.80)
我没做对的题目怎么查找 (0.75)
找回密码流程说明 (0.44)
……
到这里,老板们可以回了:词向量比字符串的效果要好,请相信你们的技术。
如果你还想实际操作一下,可以继续往下看。
二、操作步骤讲解
上面示例用到了一个text2vec(文本转向量)类库。
项目开源地址是:github.com/shibing624/… 。
里面有更多的信息,比如它说:
“可免费用做商业用途。请在产品说明中附加text2vec的链接和授权协议。”
首先,我们安装它:
pip install torch
pip install -U text2vec
这一步,对应示例代码中的from text2vec import ……,表示导入类库并调用功能。
然后,我们需要下载它的模型权重。它能这么智能,是因为项目作者做了很多基础工作。我们是站在巨人肩膀上做事情,需要先把巨人搬到家里来。
模型下载地址:huggingface.co/shibing624/…
如果你只使用(推理),不训练,下载图中标记的4个文件就够了。
下载之后,新建了一个text2vec(叫啥都行)文件夹,将模型权重复制进去。
这个文件夹的地址,对应示例代码中,模型文件的加载路径:
model = SentenceModel("text2vec")
小提示:我放在代码的同级目录。你放D盘也行,那代码对应为
SentenceModel("D:\text2vec")。
最后,新建一个main.py文件,将示例代码复制进去,就能运行了。
到这里,功能应急的小伙伴们可以撤了:这个类库确实可以,简单解决文本匹配的问题。
如果你想进一步问个为什么,可以再继续往下看。
三、技术原理分析
词向量是大语言模型的必经之路。一问一答之间,尽显词向量的身影。
下面的流程图,就是一个基于本地文档库问答的开源项目。
项目地址是:github.com/imClumsyPan…
这里面有15个流程,其中3处涉及到了Vector(词向量)。另外还有两处的Embedding,是将文本转为向量的操作,这也和向量有关。
依托于词向量,我们可以实现从文档中找答案。我把上一篇博客《如何告诉后端出身的领导,前端需求难实现》导入知识库,然后问它文中的问题。它是能回答出来的。
好了,不卖关子了,开始聊聊词向量。
3.1 词向量
词向量难理解的地方不在于“词”,而在于“向量”。
向量,初中数学就开始学。简单来说,就是一个具有“方向”和“大小”的变量。
上图所示,两人拉车。一个向左,一个向右。瘦子力气小,胖子力气大。力是向量,它有方向和大小。
向量之间是可以计算的。
有一个著名的平行四边形法则:向量a和向量b一起使劲,作用等同于向量c。
你可以拿拉车的例子来还原:a和b两个人拉车的效果,等同于c一个人。
3.2 维度
上面说的向量是二维向量,也就是有x、y两个维度。
其实也有三维向量。它具有x、y、z三个维度。
2维向量用(x, y)来表示。3维向量用(x, y, z)来表示。一个向量,有几个维度,就有几个维度的数据来描述它。
所以,你到底想表达什么?
我想表达,其实我们的词汇也是能用维度来表达的。
3.3 嵌入
来,访问这个地址:projector.tensorflow.org/
上图是10000个词汇,经过200个维度的描述处理后,投射到3维空间所呈现的视觉效果。
下面,我们随机捕捉一个词man。
我们发现在这个语料的星系中,能和man发生关联的词语有5778个。
其中有“son”、“uncle”、“father”、“king”这些词。这可能是从角色维度来分析的,看它们都凑一堆儿。
另外也有“girls”、“love”、“woman”、“angry”这些词,这是啥?man的爱好?
总之,他们之间是有关联的。下面我带大家走进和“他”有关的世界。
能做到如上这些的基础,就是因为这些词汇被向量化了。
man这个词是一个200维度的向量。如果用数据来表示,它应该是这样的:
[5.82528770e-01, 1.65926769e-01, -8.73286307e-01, 1.16382980e+00
-8.05581883e-02, -2.68993825e-01, -5.55128217e-01, -7.28500545e-01
1.19629860e-01, 3.96491200e-01, -5.55124357e-02, ……-2.47849301e-02]
不省略的话,得有200位数据。
这种将词汇向量化的操作,我们叫“Embedding”,翻译成中文叫“嵌入”。
“嵌入”这个翻译太硬,我只能解释得软一些:就是给词找一个好的归宿,将它嵌入到合适的位置。
正是通过Embedding将文本变成了向量。而向量又能进行数学计算。所以,当我们用多种方式来提问时,到模型这里一计算,发现是同一个东西,这才实现了殊途同归。
- 比如将“单”定义为1,将“双”、“对”定义为2。你问AI模型:“我单身,又找了一个单身。我俩加一起叫啥?”。AI模型说:“成双成对!”
- 再比如,做如下定义:“男人”(1,0),“权力”(0,1),国王(1,1)。你问AI模型:“我和国王的区别是什么?”。AI模型说:“国王有权力!”
你感觉,哇,好神奇。模型心里想,切,这不就是1+1=2吗?!
上面提到的text2vec这个开源模型,它所做的就是将文本转为向量。这一步是NLP的基石,非常关键。能否给词汇做好Embedding,决定了AI模型的基本理解力。
如果你要想看一个词的向量值,可以这么操作:
from text2vec import SentenceModel
# 从目录加载离线模型
model = SentenceModel("text2vec")
# 将文本向量化
embeddings = model.encode(['男人'])
print(embeddings.shape, embeddings)
输出结果为:
embeddings.shape: (1, 768)
embeddings: [[ 5.82528770e-01 1.65926769e-01 5.58118522e-01
2.97882259e-02 -3.88074875e-01 7.49602675e-01
-9.67679083e-01 …… -2.47849301e-02]]
embeddings.shape是数据的形状,这里表示维度。这框架给每段embedding的文本赋予了768个维度。后面是这768个维度数据的具体值。
前面的那个语料“星云图”,只是个学习用的Demo。下面这个更多、更广,更震撼。
要对这么多数据进行计算,可不是像“1+1=2”这么轻松,其实不简单。
3.4 相似度计算
向量之间的相似度,有很多种计算方法,下面讲一个余弦相似度。
它通过计算两个向量间夹角的余弦值,来评估两者的相似度。
余弦!又来一个数学名词。下面发张图,帮大家温习一下。
看上图,可知:
- 角A变小,c朝向b不断下滑,两者方向一致时,b/c=1。
- 角A变大,c向上抬,b不断变小,两者垂直时,b为0,b/c=0。
- 角A继续变大,成为钝角,直到两者方向相反,b/c=-1。
来个动图,生动地表达意图。
为何要选择这个余弦公式来做相似度的评判?
通俗地讲,就是看两个向量是否一条心:
- 方向一致时,函数值为1。
- 方向相反时,函数值为-1。
- 当方向越来越偏离时,它的函数值也越来越小。
看!它真的是很合适。
利用text2vec中的cos_sim方法,就可以轻松实现余弦相似度的计算。
from text2vec import SentenceModel, cos_sim
# 从目录加载模型
model = SentenceModel("text2vec")
# 转为向量
emb1 = model.encode(["我的错题从哪里可以查看"])
emb2 = model.encode(["如何查看错题记录"])
# 利用cos_sim计算相似度
cosine_scores = cos_sim(emb1, emb2)
print("Score: {:.2f}".format(cosine_scores[0][0]))
试了两个,结果还行。
[我的错题从哪里可以查看] VS [如何查看错题记录] Score: 0.79
[我的错题从哪里可以查看] VS [Python是编程语言] Score: 0.24
请注意,余弦相似度是有缺陷的:它只评判向量的方向,不关注向量的长度。
这会导致什么问题呢?答案是:重视性质,忽视数量。
举个例子,假如某网店有甲乙丙三个顾客评分如下:
| 顾客 | 物流快递 | 商品描述 | 售后服务 |
|---|---|---|---|
| 甲 | 10 | 8 | 6 |
| 乙 | 7 | 8 | 9 |
| 丙 | 4 | 2 | 0 |
如果用余弦相似度进行计算,算法会认为丙和甲是一类人。
what?为啥?
因为余弦相似度只关注方向,不关注数值。
甲和丙的方向是一致的:物流快递最好,售后服务最差。它关注的是一种趋势。
如果换一种计算方式,比如欧氏距离。那么它会认为顾客甲和顾客乙是一类人,因为他们的评分距离接近,都是好评。
上图,从角度来看,A和B更近;从距离看,A和C更近。
好了,今天就说这么多。
上面说的一大堆,其实是专业领域里一笔带过的内容。我是从里面摘芝麻当冬瓜看,小题大做。
我在做AI的科普工作,这篇文章是 全民AI计划 的第3期。
- 第1期:《尝试你的第一个AI程序》
- 第2期:《文本摘要的实现原理》
但凡您有一丁点儿听不懂,请反馈给我。听不懂,必定是我的问题。
我是掘金@TF男孩,一个普及AI知识的民科IT男。