文章 Kelly Huang · 九月 1 15m read

《InterSystems入门指南》——IRIS向量搜索(第二部分)

在完成了一堂内容丰富的数学课后,我们将把新学到的知识付诸实践,创建一个最优秀的上下文相关数据库之一。

快把放大镜和帽子准备好,因为向量·福尔摩斯又回来了。

向量计算

为了计算文本、图像或声音对应的向量,我们将使用一个名为 sentence-transformers 的 Python 库,它能够将内容转换为向量。 要实现这一点,我们需要编写以下函数:

ClassMethod Embedding(Text) [ Language = python ]
{
    from sentence_transformers import SentenceTransformer

    model_name = 'sentence-transformers/all-MiniLM-L6-v2'

    # Cache in global variables of the embedded Python module (persistent per process).
    global _cached_embedding_model
    if '_cached_embedding_model' not in globals():
        _cached_embedding_model = SentenceTransformer(model_name)

    vector = _cached_embedding_model.encode(
        [Text],
        normalize_embeddings=True,
        convert_to_numpy=True,
        show_progress_bar=False
    )

    return str(vector[0].tolist())
}

该方法使用名为“all-MiniLM-L6-v2”的预训练库对文本内容进行“向量化”。不过,如果你愿意,也可以对其进行修改,并使用自己训练的库。

入门指南


注意:要访问 Docker 实例的终端,请使用以下命令: Docker-compose exec iris iris session iris


让我们以向量化搜索为例。为此,我们将使用一个名为St.vectorsearch.Feeling的表,该表包含以下字段:

字段 类型 描述
文本 %String 关于我感受的文字
%整数 我的情绪标识符(参见附表)
向量 %向量 文本向量值

您可以通过运行以下命令来生成数据:

Do ##class(St.vectorsearch.Data).Init()

接下来,我们将使用St.vectorsearch.Data类的 Populate 命令,从*/opt/irisbuild/data/training.csv*目录加载情感分析数据。

USER>do ##class(St.vectorsearch.Data).Populate()
Truncating table St_vectorsearch.Feeling
Preparing to load data from file training.csv
Loading data from file training.csv
Total records loaded: 2000
Calculating vectors for records...
Processing 496 of 2000

请耐心等待,因为系统会为每条记录创建一个向量,这可能需要相当长的时间。


注意:Populate 命令将初始化一个包含 2000 条记录的数据模型。如果您希望数据模型中包含更多信息,还有另一个包含 5410 条记录的文件。若要使用该文件,请改用 PopulateFull() 命令。


“感受”的取值如下:

情绪
悲伤 0
喜悦 1
2
愤怒 3
恐惧 4

例如:

“我腋下夹着一束红黄相间的郁金香离开了,心情比刚来时稍微乐观了一些。”其值为1(喜悦)。

“无论在哪里,只要我感到不自在,就无法走进任何一家商店。”其值为4(恐惧)。

如果我们使用这个函数来生成向量,传入的第一段文本将返回以下向量:

.034666668623685836791, .012147962115705013276, .020678628236055374146, 
.043785430490970611572, .030321707949042320251, -.0081106657162308692932, 
-.028869708999991416931, -.059950094670057296752, .067945346236228942871, 
-.070874534547328948974, -.060159780085086822509, .016239311546087265014, 
-.034665744751691818237, -.011642633005976676941, .080176420509815216064, 
………
.0099751437082886695861, .0098187308758497238159, .00016082286310847848653, .013545278459787368774, -.0049553057178854942321, .054155148565769195556, .025806473568081855773, -.038503900170326232911, .039657127112150192261, 
-.073851920664310455322, -.070615962147712707519, .066068030893802642822, 
-.082378372550010681152, -.043505564332008361816, -.0054294602014124393463

注:为避免文本过长,向量值已进行缩减;该向量实际包含 384 个值。


如果在创建向量时选择另一段欢快的文本,它将返回以下值。

示例:

“今天,我非常开心。”

[-0.007015716750174761, 0.05171322077512741, 0.0045058708637952805, -0.04008815810084343, 0.009171668440103531, -0.04867621883749962, 0.08268272876739502, 0.004975424613803625, -0.062008269131183624, 
……
0.036987580358982086, 0.027985544875264168, 0.05382953956723213, 0.031146947294473648, -0.011256292462348938, 0.005441852379590273]

目前这些数据没有实际意义,因为我们不会进行逐个值的比较。这意味着我们不会寻找与该向量值完全相同的记录,因为每个向量都是不同的。为此,我们将使用第一部分中讲解的公式。

我们将使用以下 SQL 命令来查看哪些向量与我们计算出的向量最接近。

=

点积搜索

正如我们在第一部分所指出的,点积搜索可以告诉我们两个不同的向量之间有多么一致。这意味着该值越接近1,这两个向量就越接近,或者说越相似。

为了进行搜索,我们将使用“VECTOR_DOT_PRODUCT”条件,该条件通过比较两个向量来确定它们的对齐程度:

SELECT TOP 5 Text, Value
 FROM St_vectorsearch.Feeling
ORDER BY VECTOR_DOT_PRODUCT(Vector, TO_VECTOR('[-0.007015716750174761, 0.05171322077512741, 0.0045058708637952805, -0.04008815810084343, 0.009171668440103531, -0.04867621883749962, 
......
-0.036987580358982086, 0.027985544875264168, 0.05382953956723213, 0.031146947294473648, -0.011256292462348938, 0.005441852379590273]', float)) DESC

此搜索给出了以下结果:

因此*……“今天,我非常开心”*返回的大多数结果其值为1,即“喜悦”。

但是……我们的文本与其他检索到的值相比,其相似度百分比是多少呢?

我们可以通过将其与当前向量进行比较,并将结果以百分比形式显示出来,来找出这个数值。

SELECT TOP 5 Text, Value, TO_CHAR(VECTOR_DOT_PRODUCT(Vector, TO_VECTOR('[-0.007015716750174761, 0.05171322077512741, 0.0045058708637952805, -0.04008815810084343, 0.009171668440103531, -0.04867621883749962, 
......
 -0.036987580358982086, 0.027985544875264168, 0.05382953956723213, 0.031146947294473648, -0.011256292462348938, 0.005441852379590273]', float)) * 100 , '990.99%') AS Percentage
 FROM St_vectorsearch.Feeling
ORDER BY VECTOR_DOT_PRODUCT(Vector, TO_VECTOR('[-0.007015716750174761, 0.05171322077512741, 0.0045058708637952805, -0.04008815810084343, 0.009171668440103531, -0.04867621883749962, 
......
-0.036987580358982086, 0.027985544875264168, 0.05382953956723213, 0.031146947294473648, -0.011256292462348938, 0.005441852379590273]', float)) DESC

此次搜索给出了以下结果:

与最匹配文本的相似度百分比为65%。我们的思维知道,在所有文本中*,“今天,我非常开心”所表达的是一种喜悦之情,这意味着它与“我感到非常开心”*这一文本的相似度应该更接近100%。

余弦相似度搜索

余弦相似度搜索是通过计算待比较向量之间夹角的余弦值来实现的。

如果我们要使用余弦相似度进行向量搜索,就必须使用“VECTOR_COSINE”条件,就像我们之前做的那样:

SELECT TOP 5 Text, Value, TO_CHAR(VECTOR_COSINE(Vector, TO_VECTOR('[-0.007015716750174761, 0.05171322077512741, 0.0045058708637952805, -0.04008815810084343, 0.009171668440103531, -0.04867621883749962, 
......
 -0.036987580358982086, 0.027985544875264168, 0.05382953956723213, 0.031146947294473648, -0.011256292462348938, 0.005441852379590273]', float)) * 100 , '990.99%') AS Percentage
 FROM St_vectorsearch.Feeling
ORDER BY VECTOR_COSINE(Vector, TO_VECTOR('[-0.007015716750174761, 0.05171322077512741, 0.0045058708637952805, -0.04008815810084343, 0.009171668440103531, -0.04867621883749962, 
......
-0.036987580358982086, 0.027985544875264168, 0.05382953956723213, 0.031146947294473648, -0.011256292462348938, 0.005441852379590273]', float)) DESC

哦……惊喜!!它给出了完全相同的搜索结果:

正如第一部分所述,这种搜索方式更适合比较多个参数,例如电影类型、主演等。换句话说,这就是 Netflix 或 HBO 电影推荐系统的运作原理。

它支持多种语言吗?

如果我们不用*“今天,我非常开心”这句话而是用西班牙语表达,*会发生什么情况?

“Hoy, estoy muy feliz。”

生成的矢量图(出于显而易见的原因,文字已进行缩减)将呈现如下效果:

[-0.020053215324878693, 0.08766797184944153, 0.04118209332227707, 0.027171766385436058, -0.026769554242491722, -0.045022152364254,
…..
 0.038541924208402634, 0.018977241590619087, 0.03448185324668884, 0.10012426972389221, 0.08426760882139206, -0.09713190793991089]

如果像之前那样使用它进行搜索,我们将得到以下结果:

我们得到了“爱”(2)、“恐惧”(4)和“喜悦”(1)的组合,而其含义与英文原文完全一致。

出现这种差异较大的结果,是因为*“feliz”一词与“feel”的关联度高于“happy”*。

正如你所注意到的,向量的生成更贴近文本的发音,而非其含义。

我建议用不同语言测试同一篇文本。

以下是使用法语“Aujourd'hui, je suis très heureux”进行搜索的结果*:*

因此,如果我们要利用该系统在已经完全“分词”的文档中搜索文本,这个问题可以通过非常简单的方式解决。

简而言之,我们将把之前用于生成词元的模型替换为paraphrase-multilingual-MiniLM-L12-v2 模型:

model_name = 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2'

因此,如果我们在 Embedding 方法中修改这一行代码,就可以在数据检索中使用多语言支持。

以下是使用该模型进行西班牙语搜索的结果:

那么,既然创建的是基于语言的令牌,为什么系统内部会将“Feliz”“翻译”成*“Happy”*呢? 是的,我在“translate”一词周围加了双引号,因为这并非像其他在线词典那样基于上下文的翻译。它是通过创建词元来解释该词的,当使用相似词汇时,这可能会造成一些混淆。

例如,在西班牙语中*,“tiempo”这个词既表示“时间*”,也表示*“天气*”,因此该词的词标可能会造成混淆。 幸运的是,该词标并非基于单个词,而是基于构成该短语的一组词。这意味着短语其余部分的上下文有助于我们生成一个更接近最终结果的词标。因此,我们的向量似乎已经将我们要搜索的文本进行了翻译。

案例研究


非常重要的说明:我在开发这些实例时遇到了一些困难。当尝试从 IRIS 生产环境中创建向量时,整个过程会完全卡死。

要确保St.Vectorsearch.Vector.Embedding方法能够无故障运行,解决方案是将模型下载到本地,而不是每次请求都向在线模型发起查询。当使用本地模型时,从生产环境调用该类无需互联网连接来下载模型,从而大大加快了向量生成过程。


我制作了一个小型演示程序,展示如何将表单中输入的文本进行“分词”,并在数据库中搜索以找出最接近的五个结果。

http://localhost:52773/csp/user/feeling.html


注意:请确保生产环境正在运行。若生产环境已停止,请登录管理门户并启动生产环境。


首次查询“感觉”时,可能需要稍等片刻,因为系统会为后续查询缓存该模型。

该网站会调用运行在IRIS上的API,将文本转换为向量。随后,它会像我之前提到的那样执行SQL搜索。

它会搜索与计算出的向量最接近的5个短语。随后,网站会显示这些结果,并指出哪种情感出现频率更高,同时展示出现频最高的结果的算术平均值。

创建向量时可以使用多个元素吗?

在情感分析的示例中,该短语包含我们用于构建搜索向量的信息。但是,如果我们需要根据多个列进行搜索,又该如何处理呢?

如果我们希望系统推荐一部与我们已经看过的电影相似的电影,结果将取决于许多因素。这可能与电影的上映年份有关(因为你喜欢老电影),也可能与类型(喜剧、剧情、科幻)等有关。

在这种情况下,我们将基于包含要索引字段的文本构建一个向量。这样,我们就能获得查询所需的所有字段:


注意:数据的顺序很重要,因为如果我们关注的是同年份和同一类型的电影,那么这些列在索引中将具有最大的权重。


如果我们要根据剧情、类型、导演和演员来获取推荐,顺序应为:

“Overview: [Overview]. Genre: [Genre]. Directed by [Director] and starring by [Star1], [Star2], [Star3] and [Star4]. Movie year [Year]. Ranking: [Rating]."

我们创建了名为 St.Vectorsearch.Movie 的表,其中包含以下字段: |字段|类型|描述| |-|-| |Link|%String|电影海报链接| |Title|%String|电影名称| |Year|%Integer|电影上映年份| |Certificate|%String|年龄分级(参见附表)| |Runtime|%Integer|电影时长(单位:分钟)| |类型|%字符串|电影类型| |评分|%小数(3,1)|IMDb评分| |简介|%字符串|电影简介| |导演|%字符串|导演姓名| |主演1|%字符串|演员姓名| |主演2|%String|演员姓名| |主演3|%String|演员姓名| |主演4|%String|演员姓名| |向量|%Vector|电影卡片向量值|

为了更好地理解年龄分级,我将向您说明这些值的含义:

分级证书 说明 备注
A A级为成人级(相当于美国的R级)。 英国分级
UA UA级适用于12岁及以上观众,需在家长监督下观看。 英国分级
U U 代表“通用级”(适合所有人)。 英国分级
PG-13 PG-13(家长需特别注意):向家长发出强烈警告。部分内容可能不适合13岁以下儿童观看。 当前(MPA)
R R(限制级):因包含成人内容,17岁以下观众必须由父母或成年监护人陪同观看。 现行(MPA)
PG PG(建议家长指导):建议家长进行指导。部分内容可能不适合年幼儿童。 现行(MPA)
G G(全龄观众):适合所有观众。 当前(MPA)
通过 该影片符合当时在影院放映的严格道德标准。 这些分级标准于1930年代至1960年代期间,在著名的《海斯法典》下使用。
TV-14 TV-14:强烈建议家长陪同观看。本节目包含许多家长认为不适合14岁以下儿童观看的内容。 电视分级指南
16 不建议 16 岁以下儿童观看。 欧洲或拉丁美洲系统中典型的数字年龄分级。
TV-MA 可能不适合17岁以下观众观看  
因包含血腥暴力、露骨的性行为或粗俗语言。 电视家长指导分级  
未分级 未分级。  
GP GP:这是20世纪70年代初期使用的一种临时分级代码,相当于如今我们所说的PG(建议家长指导)。  
已获批准 该影片符合当时的严格道德标准,因此获准在影院上映。 这些评级在1930年代至1960年代期间,根据著名的《海斯法典》使用。
TV-PG 建议家长指导。 电视家长指导
U/A U 代表“通用”(适合所有人)。 英国分级制度

与我们初始化表并从“Feeling”表加载数据的方式相同,我们使用以下命令:


注意:要访问 Docker 实例的终端,请使用以下命令: Docker-compose exec iris iris session iris


Do ##class(St.vectorsearch.Data).InitMovie()

接下来,我们将使用St.vectorsearch.Data类中的 PopulateMovie 命令,从*/opt/irisbuild/data/imdb_top_1000.csv*目录加载电影数据。

USER>do ##class(St.Vectorsearch.Data).PopulateMovie()
Truncating table St_Vectorsearch.Movie
Preparing to load data from file imdb_top_1000.csv
Loading data from file imdb_top_1000.csv
Total records loaded: 999
Calculating vectors for records...
Processing 116 of 999

注意:在 Docker 中的演示初始化过程中,测试数据的加载操作已经完成。


对于每部电影,我们已使用通过不同字段创建的短语初始化了向量值。建议大家根据自身需求,对 St.Vectorsearch.Data.PopulateMovie 类进行适当修改,以更好地满足您的优先级要求。

正如我在第一部分提到的,还有另一种向量逼近方法(基于余弦相似度),它允许我们搜索形成最接近角度的向量(即在同一方向上距离最近的向量)。

因此,如果您希望获取数据推荐,这便是最佳的系统选择。

要使用该方法,我们需要“VECTOR_COSINE”条件,它能告诉我们与指定向量角度最接近的向量。

SELECT TOP 5 ID, Link, Title, Year
FROM St_Vectorsearch.Movie 
WHERE Title <> ? 
ORDER BY VECTOR_COSINE(Vector, TO_VECTOR(?, float)) DESC

在这种情况下,我们将寻找那些与我们所选电影相关向量角度接近的电影,当然,不会推荐该电影本身。

您可以通过以下链接访问演示:

http://localhost:52773/csp/user/movies.html

在这种情况下,推荐的电影与1988年的《阿基拉》相似,不过我个人会把《攻壳机动队》(又名《Ghost in the Shell》)排在第一位。它们之所以相似,是因为都是动画电影。 备忘:我还没看《帕普里卡》……

那么,今天就先到这里。关于向量索引的操作,你已经掌握了所需的一切,包括实际示例以及可用于实验的应用程序。

欢迎在评论区留下您的建议或想法,包括如何将这些知识付诸实践等……

我们下期“InterSystems入门指南”再见!