广州网站优化电话,怎么在中国做网站网站,宣传片拍摄总结,张家港保税区规划建设局网站当 OpenAI 于 2022 年 11 月发布 ChatGPT 时#xff0c;引发了人们对人工智能和机器学习的新一波兴趣。 尽管必要的技术创新已经出现了近十年#xff0c;而且基本原理的历史甚至更早#xff0c;但这种巨大的转变引发了各种发展的“寒武纪大爆炸”#xff0c;特别是在大型语…当 OpenAI 于 2022 年 11 月发布 ChatGPT 时引发了人们对人工智能和机器学习的新一波兴趣。 尽管必要的技术创新已经出现了近十年而且基本原理的历史甚至更早但这种巨大的转变引发了各种发展的“寒武纪大爆炸”特别是在大型语言模型和生成 transfors 领域。 一些怀疑论者认为这些模型是 “随机鹦鹉”只能生成他们所接受训练的内容的排列。 有些人认为这些模型是 “黑匣子”超出了人类理解范围甚至可能是“黑魔法”其工作原理完全深奥。
我对在语义搜索背景下使用机器学习模型的可能性感到特别兴奋。 Elasticsearch 是一家基于 Apache Lucene 的高级搜索和分析引擎。 充分了解倒排索引、评分算法、语言分析的特殊性等所有复杂性我偶然发现的一些例子看起来几乎就像……是的“黑魔法”。
在我们深入研究 Python 代码之前我想回顾一下历史。 正如我发现的机器学习或人工智能主题的困难之一是大量高度具体的术语并且缺乏关于技术如何工作的直观心理模型。 例如如果我通过说它们是 “密集向量dense vectors” 来解释上一段中的术语 “嵌入embeddings”那就无济于事了 —— 不仅你的眼睛会变得呆滞而且我还必须解释两个术语而不是解释其中的一个。 词汇和语义搜索lexical and semantic search
事实上用数字表示语言元素是传统全文检索的基础。 现代倒排索引与传统索引或书后索引之间的主要区别在于倒排索引存储的信息不仅仅是术语的出现。 它还跟踪它们在文档中的位置和出现的频率。 这已经允许某些算术运算例如短语搜索phrase search搜索以特定顺序出现的术语和邻近搜索查找出现在彼此一定数量的位置内的术语。
使用这些数字特别是文档中术语出现的频率以及整个文档集合中术语的总体频率是对搜索结果进行评分的传统方法 TF-IDF术语频率 vs 逆文档频率公式和更复杂的公式如 BM-25。 简而言之某个术语在特定文档中出现的频率越高该文档在相关文档列表中的排名就越高。 相反特定术语在整个集合中出现的频率越高该文档在列表中的排名就越少。 将有关术语的统计信息存储在集合中可以实现比简单查找例如 “此特定文档包含此特定单词”更复杂的操作。
传统的 “词汇lexical” 搜索和 “语义semantic” 搜索之间的根本区别在于词汇搜索只能找到包含查询中存在的确切术语的文档。 我们所说的 “术语” 是指搜索引擎识别为具有相同含义的单词的变体。 当然像 Elasticsearch 这样的现代搜索引擎拥有复杂的工具可以将 “words” 转换为 “terms”从简单的工具如删除大写到更高级的工具如词干提取删除后缀、walking ⇒ walk、词形还原将不同的屈折形式减少为基本的worst ⇒ bad或同义词。 这些有助于扩大查询范围并找到更多相关文档。
然而即使进行了这些转换如果文档中缺少这些特定术语你也无法使用 “a domestic animal which catches mice” 之类的查询来搜索 “cat”。 另一方面大型语言模型非常有能力为这样的 “间接” 查询检索文档。 这并不是因为它以天真的拟人化的方式 “理解” 了那个特定的短语。 这是因为它理解与不同想法相对应的不同符号系统人类语言。 在这个系统中占据最接近符号 “a domestic animal which catches mice” 的位置的概念是的是猫的概念。
因此在语义搜索中搜索结果的相关性是由系统内的语义接近度决定的而不仅仅是关键字匹配无论多么复杂。 顾名思义“词汇搜索” 的行为非常类似于在字典词典中搜索单词定义如果你知道要搜索的单词那么它会非常有效。 否则你不妨读整本字典。 使用 Elasticsearch 进行语义搜索
有趣的是语义搜索的支持基础设施多年来一直是 Elasticsearch 的一部分 —— dense_vector 映射字段在 2019 年 4 月发布的 7.0 版本中引入。几个月后发布的 7.3 版本增加了对指定维度的支持 type 并将预定义函数引入到 script_score 查询中从而能够计算文档的相似度分数。 2022 年 2 月发布的 8.0 版本进一步改进了dense_vector 实现并添加了 “Approximate Nearest Neighbor - 近似最近邻” 搜索端点有效地将关键组件捆绑在一起以全面实现语义搜索包括在集群内运行第三方模型的能力。 在最新版本 8.8 中Elastic 不仅专注于改进其 AI 功能的通信以响应当前的兴趣浪潮而且还添加了一些增强功能例如在密集向量字段中增加更高的维度从而允许存储大型嵌入像 OpenAI 开发的语言模型一样并提供了一个自定义的内置模型即 Elastic Learned Sparse Encoder。
在本博客的其余部分中我想演示如何使用 Sentence Transformers中的模型使用 James Briggs 文章中的查询。 希望你会看到 Elasticsearch 是一个非常强大的矢量数据库具有用于执行相似性搜索的高效且方便的 API。
但首先我想谈谈 “矢量” 这个术语。 你可能已经注意到我在开头段落中使用了三次 “dense_vector” 一词。如果你像我一样没有数学背景那么矢量这个词和概念一开始可能会让人感到害怕。 当通常的解释是矢量是 “具有大小和方向的对象” 时这并没有帮助因为很难在人类语言的背景下为这样的对象提出合理的心理模型。 一个更有用的模型可能是将 “矢量空间” 中的矢量视为坐标。
由于语义是由符号在共享符号系统中的 “位置” 给出的所以我们可以给出这个位置的 “坐标”。 此外我们可以使用这些坐标的数字表示从而开启算术运算的可能性。 这种数字表示通常称为嵌入。 如果我们抛开数学理论物理表示就是一个十进制数列表[0.01, 0.05, -0.04, 0.06, -0.1, ...]。 列表的长度称为维度每个维度代表含义的特定特征。
让我们使用由达姆施塔特技术大学 Ubiquitous Knowledge Processing Lab 提供的 Sentence Transformers 框架中的免费、开源、预训练模型来仔细研究其机制。 文本嵌入 - Text embeddings
为了更好地理解嵌入是语义搜索以及其他自然语言处理任务的基础让我们从 Hugging Face 加载模型并使用它来生成几个单词的嵌入。 但首先让我们安装必要的库并设置我们的环境。
%pip -q install \python-dotenv ipywidgets tqdm humanize \pandas numpy matplotlib altair \datasets sentence-transformers \elasticsearch%load_ext dotenv
%dotenvfrom tqdm.notebook import tqdm as notebook_tqdm
让我们下载并初始化全 MiniLM-L6-v2 模型。
from sentence_transformers import SentenceTransformerMODEL_IDall-MiniLM-L6-v2model SentenceTransformer(MODEL_ID)
print(Model dimensions:, model.get_sentence_embedding_dimension()) 正如我们所看到的该模型有 384 个维度。 这是模型向量空间的 “大小”。 它并不是特别大 —— 许多当前模型的嵌入有数千个维度但对于我们的目的来说已经足够了。 让我们编码即。 为单词 “cat” 创建嵌入
embeddings_for_cat model.encode(cat)
print(list(embeddings_for_cat)[:5] [...])
请注意输出被截断为前 5 个值以免一长串数字淹没显示。 另请注意将此模型用于单个单词只是说明性的因为它针对句子进行了优化。对于单词嵌入使用 Word2Vec 或 GloVe 等模型更为典型。
让我们编码一个不同的单词 “dog”
embeddings_for_dog model.encode(dog)
print(list(embeddings_for_dog)[:5] [...]) 输出说明了为此类文本编码提出合理的心理模型是多么具有挑战性作为人类我们很好地掌握了符号 “cat” 或 dog” 与其代表的家畜之间的关系。很难很好地理解这样的数字表示。
然而如前所述数字表示具有明显的优势 —— 我们可以对值进行数学运算。 在这种情况下我们可以尝试在散点图中将它们可视化。 让我们将列表包装在 Pandas dataframe 中这样我们就可以在 Jupyter 笔记本中显示时利用其丰富的格式并在后续步骤中利用其数据操作功能。
import pandas as pddf pd.DataFrame(embeddings_for_cat, columns[embedding])
df 我们可以使用内置的绘图功能来显示简单的图表
df.reset_index().plot.scatter(xindex, yembedding); 该图表只为我们提供了非常抽象的数据 “图片” 基本上值的粗略分布在 -0.15 到 0.23 的范围内。
就其本身而言这些数字毫无意义。 当我们将语言理论视为 “不同符号的系统” 时这实际上是预料之中的。 任何一个词单独存在都没有意义 它的意义来自于与系统中其他词的关系。 那么如果我们尝试想象 “cat” 和 “dog” 这两个词呢
让我们创建一个新的 dataframe使用 “cat” 和 “dog” 作为索引并将嵌入压缩到单个列。
df pd.DataFrame([[embeddings_for_cat],[embeddings_for_dog],],index[cat, dog], columns[embeddings]
)
df 为了绘制数据我们需要进行一些转换
# Add a new column to store the original index values (0-383) for each embedding
df[position] [list(range(len(df.embeddings[i]))) for i in df.index]# Convert the embeddings and position columns from wide to long format
df_exploded df.explode([embeddings, position])# Convert the index into a regular column
df_exploded df_exploded.reset_index()# Rename columns for more clarity
df_exploded df_exploded.rename(columns{index: animal, embeddings: embedding})# Add a new column with numerical values mapped from the animal column values
df_exploded[color] df_exploded[animal].map({cat: 1, dog: 2})df_exploded 现在我们可以绘制转换后的数据
(df_exploded.plot.scatter(xposition, yembedding, ccolor, colormaptab10).collections[0].colorbar.remove()) 像这样的简单可视化似乎不会有太大帮助。 然而它强调了多维向量空间的一个基本困难。 作为人类我们非常有能力在 2D 或 3D 空间中可视化物体。 更多维度根本不是我们能够有效想象的东西更不用说 “绘制” 了。
我们可以使用的一个技巧是减少维度在本例中从 384 维减少到 2 维。再次强调我们能够做到这一点是因为我们正在处理语言的数字表示。有很多算法可以用于 这样做 - 我们将使用主成分分析 (principal component analysis - PCA)因为它在 scikit-learn 包中很容易获得并且适用于小型数据集。 有关使用 t-SNE 和 UMAP 算法的示例请参阅 Plotly 包文档中的一篇优秀文章。
import numpy as np
from sklearn.decomposition import PCA# Drop the position column as its no longer needed
df.drop(columns[position], inplaceTrue, errorsignore)# Convert embeddings to a 2D array and display their shape
print(Embeddings shape:, np.stack(df[embeddings]).shape)# Initialize the PCA reducer to convert embeddings into arrays of length of 2
reducer PCA(n_components2)# Reduce the embeddings, store them in a new dataframe column and display their shape
df[reduced] reducer.fit_transform(np.stack(df[embeddings])).tolist()
print(Reduced embeddings shape:, np.stack(df[reduced]).shape)df 正如我们所看到的减少的嵌入只有两个维度因此我们可以使用 Vega-Altair 包将它们绘制在笛卡尔平面上作为 x 和 y 坐标。 让我们创建一个函数以便稍后重用代码。
import altair as altdef scatterplot(data: pd.DataFrame,tooltipsFalse,labelsFalse,width800,height200,
) - alt.Chart:base_chart (alt.Chart(data).encode(alt.X(x, scalealt.Scale(zeroFalse)),alt.Y(y, scalealt.Scale(zeroFalse)),).properties(widthwidth, heightheight))if tooltips:base_chart base_chart.encode(alt.Tooltip([text]))circles base_chart.mark_circle(size200, colorcrimson, strokewhite, strokeWidth1)if labels:labels base_chart.mark_text(fontSize13,alignleft,baselinebottom,dx5,).encode(texttext)chart circles labelselse:chart circlesreturn chart
source pd.DataFrame({text: df.index,x: df[reduced].apply(lambda x: x[0]).to_list(),y: df[reduced].apply(lambda x: x[1]).to_list(),}
)scatterplot(source, labelsTrue) 好的。 该图表相当平庸 —— 只有两个圆圈随机放置在画布上。 我们可能期望这些标记会彼此靠近地显示但事实并非如此。 毕竟猫和狗有很多共同的特征。 然而在语言作为一个系统的前提下我们有限的 “系统” 只包含两个词“cat” 和 “dog”。
作为人类我们可能会认为这些标志密切相关它们都代表有四足的毛茸茸的动物通常作为宠物饲养都是哺乳动物属的食肉动物等等。 但这种直觉来自于我们语言的一个非常大的系统其中有许多其他概念占据着不同的位置。 引用索绪尔的话“这些概念纯粹是差异性的不是由它们的积极内容来定义而是由它们与系统其他术语的关系来消极定义”。
然后让我们尝试向集合中添加更多单词看看图片是否会以有意义的方式发生变化。
words [cat, dog, table, chair, pizza, pasta, asymptomatic]# Create a new dataframe
df pd.DataFrame([[model.encode(word)] for word in words],columns[embeddings],indexwords,
)# Perform dimensionality reduction
df[reduced] reducer.fit_transform(np.stack(df[embeddings])).tolist()
df 让我们再次显示散点图。
source pd.DataFrame({text: df.index,x: df[reduced].apply(lambda x: x[0]).to_list(),y: df[reduced].apply(lambda x: x[1]).to_list(),}
)scatterplot(source, labelsTrue) 好多了 我们可以清楚地看到相关词的三个 “集群”dog ⇔ catpizza ⇔ pastachair ⇔ table。 我们还可以看到除了这三个集群之外“asymptomatic”一词是单独存在的。
这是人工智能的 “黑魔法” 吗 并不真的是。 全 MiniLM-L6-v2 模型已经在 Reddit、Stack Exchange、维基百科、Quora 和其他来源的大量人类编写的文本上进行了训练。 因此它确实具有这些词的含义几乎字面上 “嵌入” 在它生成的 384 维向量中。 装载数据集
通过更好、更实际地理解文本嵌入的工作原理和原理我们可以回到本博客的最初动机使用 Elasticsearch 而不是 Pinecone重新创建 James Briggs 文章中的语义搜索示例。
我们将使用 Hugging Face 的 datasets 包来加载 Quora 数据。 它是一个非常复杂的数据 “包装器”它提供了方便的功能例如下载文件的内置缓存和高效的处理功能我们将使用这些功能来操作数据。
Hugging Face 数据集主要面向为模型训练提供数据因此它们被分为 train、test、validation 等 split。 我们的特定数据集只有 train split 部分。 让我们加载它并显示有关数据集的一些元数据。
import humanize
import datasetsdataset datasets.load_dataset(quora, splittrain)print(Description:, dataset.info.description, \n)
print(Homepage:, dataset.info.homepage)
print(Downloaded size:, humanize.naturalsize(dataset.info.download_size))
print(Number of examples:, humanize.intcomma(dataset.info.splits[train].num_examples))
print(Features:, dataset.info.features) 正如我们所看到的该数据集包含超过 400,000 个 “question pairs”。 我们来看看前五条记录。
dataset[:5] 该数据集的主要重点是为重复检测提供可靠的数据 我们的第一个数据集与识别重复问题的问题有关。 Quora 的一个重要产品原则是每个逻辑上不同的问题都应该有一个问题页面。 举一个简单的例子查询“美国人口最多的州是哪个” 和“美国哪个州人口最多” 不应该单独存在于 Quora 上因为两者背后的意图是相同的。 ... 我们今天发布的数据集将使任何人都有机会根据实际的 Quora 数据来训练和测试语义等价模型。 ... — Kornél Csernai第一个 Quora 数据集发布Question Pairs 因此数据集包含问题对这些问题对被标记为重复或不重复。 让我们使用数据集包的实用程序来选择和过滤数据显示一些重复问题的示例。
(dataset.select(range(1000)).filter(lambda record: record[is_duplicate])[:3]) 有点矛盾的是数据集不包含 “美国人口最多的州是哪个” 的问题。 文章中提到。
dataset.filter(lambda record: What is the most populous state in the USA? in record[questions][text])[:] 让我们从清理和转换数据集开始这样我们就可以将各个问题作为单独的文档加载到 Elasticsearch 中。
首先我们将删除 is_duplicate 列并 “展平” questions 属性即。 将其展开为单独的列。
print(Original dataset:, dataset, \n)# Remove the is_duplicate column
dataset dataset.remove_columns(is_duplicate)# Flatten the dataset
dataset dataset.flatten()print(Transformed dataset:, dataset, \n)dataset[:5] 我们对结构进行了一些改进但问题文本字段中仍然有两个问题。 为了有效地索引问题最好将每个问题存储为单独的行。 我们将使用包提供的强大的 map() 功能扩展 questions.id 和 questions.text 列。
# Expand the values from the lists into separate lists
def expand_values(batch):ids []texts []for id_list, text_list in zip(batch[questions.id], batch[questions.text]):ids.extend(id_list)texts.extend(text_list)return {id: ids, text: texts}# Run the expand_values function for batches of rows in the dataset
dataset dataset.map(expand_values,batchedTrue,remove_columnsdataset.column_names,descExpand Questions,
)print(Transformed dataset:, dataset, \n)dataset[:5] 数据集包含两倍的行数因为每个问题现在都存储为单独的行。
下一步是删除重复的问题。 我们没有使用 is_duplicate 列进行重复数据删除因为我们仍然希望对所有问题建立索引即使它们在语义上相同“How can I be a good geologist?” 与 “What should I do to be a great geologist?”。 我们只是想删除文本完全相同的问题。 我们将再次使用 map() 函数。
# Create a Python set to keep track of processed questions
seen set()# Remove rows with exactly the same text value
def remove_duplicate_rows(batch):global seenoutput {id: [], text: []}for id, text in zip(batch[id], batch[text]):if text not in seen:seen.add(text)output[id].append(id)output[text].append(text)return output# Run the remove_duplicate_rows function for batches of rows in the dataset
dataset dataset.map(remove_duplicate_rows,batchedTrue,batch_size1000,remove_columnsdataset.column_names,descRemove Duplicates,
)dataset 该数据集现在包含 537,362 个独特的问题。
我们将使用之前用 “cat” 和 “dog” 演示的相同方法为这些问题生成文本嵌入。 稍后我们会将它们索引到 Elasticsearch 中以便使用称为“近似最近邻居appoximate nearest neigbors” 的专门查询类型来查找语义相似的文档。
让我们再次使用 map() 方法处理数据集。
import time%env TOKENIZERS_PARALLELISMtrue# Compute embeddings for batches of question text
def compute_embeddings(batch):return { embeddings: model.encode(sentencesbatch[text]) }try:start time.perf_counter()dataset dataset.map(compute_embeddings,batchedTrue,batch_size1000,descCompute Embeddings,)
except KeyboardInterrupt:print(Creating text embeddings interrupted by the user...)print(Dataset with embeddings:, dataset,f(duration: {humanize.precisedelta(time.perf_counter() - start)}),\n)# Print a sample of the embeddings for first question
print(list(dataset[:1][embeddings][0][:5]) [...]) 正如你所看到的这是一个资源密集型操作在配备 M1 Max 芯片的 Apple 笔记本上可能需要 16 多分钟。 要保留带有嵌入的完整数据集请使用 save_to_disk() 方法。 索引数据到 Elasticsearch
在下一步中我们将创建一个具有特定映射的 Elasticsearch 索引用于将嵌入存储在密集向量字段类型中并将问题文本存储在常规文本字段中并使用英语分析器进行处理。
如果你想尝试自己运行这些示例你需要一个 Elasticsearch 集群。 使用此存储库中提供的 Docker Compose 文件在本地启动集群。你可以参考如下的两篇文章来创建自己的 Elasticsearch 及 Kibana 如何在 LinuxMacOS 及 Windows 上进行安装 Elasticsearch Kibana如何在 LinuxMacOS 及 Windows 上安装 Elastic 栈中的 Kibana
在安装的时候我们选择使用 Elastic Stack 8.x 的安装手册来进行安装。在默认的情况下Elasticsearch 的安装是带有 https 的安全访问。
import os
from elasticsearch import ElasticsearchINDEX_NAME quora-with-embeddings-v1
# es Elasticsearch(hostsos.getenv(ELASTICSEARCH_URL), request_timeout300)CERT_FINGERPRINTbd0a26dc646ef1cb3cb5e132e77d6113e1b46d56ee390dd3c6f0b2d2b16962c4es Elasticsearch( [https://localhost:9200],basic_auth (elastic, h6yvgnen2vkbm6Dz6-),ssl_assert_fingerprint CERT_FINGERPRINT,http_compress True )if not es.indices.exists(indexINDEX_NAME):es.indices.create(indexINDEX_NAME,mappings{properties: {text: {type: text,analyzer: english,},embeddings: {type: dense_vector,dims: model.get_sentence_embedding_dimension(),index: true,similarity: cosine,},}},)print(fCreated Elasticsearch index at {os.getenv(ELASTICSEARCH_URL)}/{INDEX_NAME}?pretty)
else:print(fSkipping index creation, index already exists)
更多关于如何连接到 Elasticsearch 集群的知识请参阅文章 “Elasticsearch关于在 Python 中使用 Elasticsearch 你需要知道的一切 - 8.x”。你也可以参考文章 “Elasticsearch如何将整个 Elasticsearch 索引导出到文件 - Python 8.x”。 我们可以在在 Kibana 中进行查看 现在我们准备好对数据建立索引了。 我们将使用 Elasticsearch 客户端的 parallel_bulk() 帮助器因为它是加载数据的最方便的方式它通过在多个线程中运行客户端来优化进程并且它接受 Python 迭代器iterable或生成器generator从而提供 用于索引大型数据集的高级接口。 我们将使用数据集的 to_iterable_dataset() 方法将其转换为生成器。 这种转换对于大型数据集尤其有益因为它允许更节省内存的处理。
import os
import time
from elasticsearch.helpers import parallel_bulkif es.count(indexINDEX_NAME)[count] len(dataset):print(Skipping indexing, data already indexed.)
else:progress notebook_tqdm(unitdocs, totallen(dataset))indexed 0start time.perf_counter()# Remove the id column and convert the dataset to generatoriterable_dataset dataset.remove_columns([id]).to_iterable_dataset()try:print(fIndexing dataset to [{INDEX_NAME}]...)for ok, result in parallel_bulk(es,iterable_dataset,indexINDEX_NAME,thread_countos.cpu_count()//2,):indexed 1progress.update(1)print(fIndexed [{humanize.intcomma(indexed)}] documents in {humanize.precisedelta(time.perf_counter() - start)})except KeyboardInterrupt:print(fIndexing interrupted by the user, indexed [{humanize.intcomma(indexed)}] documents in {humanize.precisedelta(time.perf_counter() - start)}) 好的 看起来我们的文档已成功建立索引。 让我们使用 Cat Indices API 检查索引显示文档数量和磁盘上索引的大小。
res es.cat.indices(indexINDEX_NAME, formatjson)
print(fIndex [{INDEX_NAME}] contains [{humanize.intcomma(res.body[0][docs.count])}] documents,fand uses [{res.body[0][pri.store.size].upper()}] of disk space
) 搜索数据
至此我们终于可以使用 Elasticsearch 来搜索数据了。
我们将定义实用函数来包装搜索请求并以格式化的 Pandas 数据帧返回结果。 我们将使用匹配查询进行词法搜索使用 knn 选项进行语义搜索。
import pandas as pd# Lexical search with the match query
def search_keywords(query, size10):res es.search(indexINDEX_NAME,query{match: {text: query}},sizesize,source_includes[text, embeddings],)return pd.DataFrame([{text: hit[_source][text], embeddings: hit[_source][embeddings], score: hit[_score]}for hit in res[hits][hits]])# Semantic search with the knn option
# https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-api-knn
def search_embeddings(query, size10):res es.search(indexINDEX_NAME,knn{field: embeddings,query_vector: model.encode(query, normalize_embeddingsTrue),k: size,num_candidates: 1000,},sizesize,source_includes[text, embeddings],)return pd.DataFrame([{text: hit[_source][text], embeddings: hit[_source][embeddings], score: hit[_score]}for hit in res[hits][hits]])# Returns the dataframe without the embeddings column and with a formatted score column
def styled(df):return (df[[score, text]].style.set_table_styles([dict(selectorth,td, props[(text-align, left)])]).hide(axisindex).format({score: {:.3f}}).background_gradient(subset[score], cmapGreys))# Add the utility function to the dataframe class
pd.DataFrame.styled styled
让我们使用原始文章中的查询 “Which city has the highest population in the world?” 来执行词法搜索。
search_keywords(Which city has the highest population in the world?) 我们可以立即观察到大多数结果与我们的查询不太相关。 除了 “Which is the most populated city in the world.?” 等项目之外。 和 “What are the most populated cities in the world?”大多数结果与 “most populated city” 的概念几乎没有关系。 我们还可以观察默认评分算法如何增强问题开头的短语 “Which city (…)”尽管文本的其余部分不相关历史建筑的数量、生活水平等。
让我们使用相同的查询执行语义搜索看看是否得到不同的结果。
search_embeddings(Which city has the highest population in the world?) 很明显这些结果与我们的查询概念更加相关。 词汇搜索中最相关的结果返回在顶部接下来的几个结果几乎与 “most populated city” 概念同义例如。 “largest city” 或 “biggest city”。 另请注意“Which is the largest city in the world by area?” 结果列在与国家而非城市相关的结果之后。 这是非常令人期待的我们的查询是关于人口规模而不是面积。
让我们尝试一些意想不到的事情。 让我们重新措辞该查询使其不包含匹配文档中的任何重要关键字省略限定词 “which”将 “city” 替换为“urban location”将 “highest population” 替换为 “excessive concentration of homo sapiens” 诚然是一个非常不自然的短语。 此重新表述的所有功劳均归于詹姆斯·布里格斯James Briggs请参阅原始文章的特定版本。
search_embeddings(Urban locations with the highest concentration of homo sapiens) 也许令人惊讶的是我们得到的结果大多与我们的查询相关尤其是在列表顶部即使我们的查询是故意构造的查询术语和文档术语之间没有直接重叠。 这有力地展示了语义搜索的最强点。
让我们尝试使用词法搜索执行相同的查询。
search_keywords(Urban locations with the highest concentration of homo sapiens) 我们没有得到与我们的查询相关的结果。 根据我们对词汇搜索和语义搜索之间差异的理解这应该不足为奇。 事实上这种效应有一个技术描述词汇不匹配即查询术语与文档术语相差太大。 即使前面提到的词干提取或词形还原等术语操作也无法防止这种不匹配。 传统上解决方案是向搜索引擎提供同义词列表。 然而这很快就会变得复杂因为最终我们需要提供完整的同义词库。 此外由于评分算法通常的工作方式在计算每个结果的分数时它无法区分单词及其同义词。
让我们回到原来的查询使用稍微不同的措辞看看我们是否可以可视化嵌入和结果类似于我们使用 “cat” 和 “dog” 等单词进行的演示。
df search_embeddings(What is the most populated city in the world?)
df 我们需要使用 explode() 数据帧方法再次将数据帧从 “宽” 格式转换为 “长” 格式。
# Store the original index values (0-9) as position
df[position] [list(range(len(df.embeddings[i]))) for i in df.index]# Convert the embeddings and position columns from wide to long format
source df.explode([embeddings, position])# Rename the embeddings column to embedding
source source.rename(columns{ embeddings: embedding})source 让我们使用 Vega-Altair 创建每个结果的嵌入 “热图”。
import altair as altalt.Chart(source
).encode(alt.X(position:N, title).axis(labelsFalse, ticksFalse),alt.Y(text:N, title, sortsource[score].unique()).axis(labelLimit300, tickWidth0, labelFontWeightbold),alt.Color(embedding:Q).scale(schemegoldred).legend(None),
).mark_rect(width3
).properties(widthalt.Step(3), heightalt.Step(25)) 尽管进行 “reading from tea leaves” 类型的分析存在轻微风险但我们仍然可以辨别图表中的特定模式。 请注意前三个结果的视觉模式非常相似。 第四个结果在某种程度上打破了这种模式也许是因为它是关于人口最多的国家而不是城市。 同样与面积最大城市相关的两个结果形成了独特的视觉模式。
然而和以前一样我们可以看到理解具有大量维度的可视化是多么具有挑战性。 让我们再次尝试降低维度并将结果绘制在二维平面上。