當(dāng) OpenAI 于 2022 年 11 月發(fā)布 ChatGPT 時,引發(fā)了人們對人工智能和機(jī)器學(xué)習(xí)的新一波興趣。 盡管必要的技術(shù)創(chuàng)新已經(jīng)出現(xiàn)了近十年,而且基本原理的歷史甚至更早,但這種巨大的轉(zhuǎn)變引發(fā)了各種發(fā)展的“寒武紀(jì)大爆炸”,特別是在大型語言模型和生成 transfors 領(lǐng)域。 一些懷疑論者認(rèn)為,這些模型是 “隨機(jī)鸚鵡”,只能生成他們所接受訓(xùn)練的內(nèi)容的排列。 有些人認(rèn)為這些模型是 “黑匣子”,超出了人類理解范圍,甚至可能是“黑魔法”,其工作原理完全深奧。
我對在語義搜索背景下使用機(jī)器學(xué)習(xí)模型的可能性感到特別興奮。 Elasticsearch 是一家基于 Apache Lucene 的高級搜索和分析引擎。 充分了解倒排索引、評分算法、語言分析的特殊性等所有復(fù)雜性,我偶然發(fā)現(xiàn)的一些例子看起來幾乎就像……是的,“黑魔法”。
在我們深入研究 Python 代碼之前,我想回顧一下歷史。 正如我發(fā)現(xiàn)的,機(jī)器學(xué)習(xí)或人工智能主題的困難之一是大量高度具體的術(shù)語,并且缺乏關(guān)于技術(shù)如何工作的直觀心理模型。 例如,如果我通過說它們是 “密集向量(dense vectors)” 來解釋上一段中的術(shù)語 “嵌入(embeddings)”,那就無濟(jì)于事了 —— 不僅你的眼睛會變得呆滯,而且我還必須解釋兩個術(shù)語,而不是解釋其中的一個。
詞匯和語義搜索(lexical and semantic search)
事實上,用數(shù)字表示語言元素是傳統(tǒng)全文檢索的基礎(chǔ)。 現(xiàn)代倒排索引與傳統(tǒng)索引(或書后索引)之間的主要區(qū)別在于,倒排索引存儲的信息不僅僅是術(shù)語的出現(xiàn)。 它還跟蹤它們在文檔中的位置和出現(xiàn)的頻率。 這已經(jīng)允許某些算術(shù)運(yùn)算,例如短語搜索(phrase search,搜索以特定順序出現(xiàn)的術(shù)語)和鄰近搜索(查找出現(xiàn)在彼此一定數(shù)量的位置內(nèi)的術(shù)語)。
使用這些數(shù)字,特別是文檔中術(shù)語出現(xiàn)的頻率,以及整個文檔集合中術(shù)語的總體頻率,是對搜索結(jié)果進(jìn)行評分的傳統(tǒng)方法 TF-IDF(術(shù)語頻率 vs 逆文檔頻率)公式和更復(fù)雜的公式,如 BM-25。 簡而言之,某個術(shù)語在特定文檔中出現(xiàn)的頻率越高,該文檔在相關(guān)文檔列表中的排名就越高。 相反,特定術(shù)語在整個集合中出現(xiàn)的頻率越高,該文檔在列表中的排名就越少。 將有關(guān)術(shù)語的統(tǒng)計信息存儲在集合中可以實現(xiàn)比簡單查找(例如 “此特定文檔包含此特定單詞”)更復(fù)雜的操作。
傳統(tǒng)的 “詞匯(lexical)” 搜索和 “語義(semantic)” 搜索之間的根本區(qū)別在于,詞匯搜索只能找到包含查詢中存在的確切術(shù)語的文檔。 我們所說的 “術(shù)語” 是指搜索引擎識別為具有相同含義的單詞的變體。 當(dāng)然,像 Elasticsearch 這樣的現(xiàn)代搜索引擎擁有復(fù)雜的工具,可以將 “words” 轉(zhuǎn)換為 “terms”,從簡單的工具(如刪除大寫)到更高級的工具,如詞干提?。▌h除后綴、walking ? walk)、詞形還原(將不同的屈折形式減少為基本的,worst ? bad),或同義詞。 這些有助于擴(kuò)大查詢范圍(并找到更多相關(guān)文檔)。
然而,即使進(jìn)行了這些轉(zhuǎn)換,如果文檔中缺少這些特定術(shù)語,你也無法使用 “a domestic animal which catches mice” 之類的查詢來搜索 “cat”。 另一方面,大型語言模型非常有能力為這樣的 “間接” 查詢檢索文檔。 這并不是因為它以天真的擬人化的方式 “理解” 了那個特定的短語。 這是因為它理解與不同想法相對應(yīng)的不同符號系統(tǒng):人類語言。 在這個系統(tǒng)中,占據(jù)最接近符號 “a?domestic animal which catches mice” 的位置的概念,是的,是貓的概念。
因此,在語義搜索中,搜索結(jié)果的相關(guān)性是由系統(tǒng)內(nèi)的語義接近度決定的,而不僅僅是關(guān)鍵字匹配,無論多么復(fù)雜。 顧名思義,“詞匯搜索” 的行為非常類似于在字典(詞典)中搜索單詞定義:如果你知道要搜索的單詞,那么它會非常有效。 否則,你不妨讀整本字典。
使用 Elasticsearch 進(jìn)行語義搜索
有趣的是,語義搜索的支持基礎(chǔ)設(shè)施多年來一直是 Elasticsearch 的一部分 —— dense_vector 映射字段在 2019 年 4 月發(fā)布的 7.0 版本中引入。幾個月后發(fā)布的 7.3 版本增加了對指定維度的支持 type 并將預(yù)定義函數(shù)引入到 script_score 查詢中,從而能夠計算文檔的相似度分?jǐn)?shù)。 2022 年 2 月發(fā)布的 8.0 版本進(jìn)一步改進(jìn)了dense_vector 實現(xiàn),并添加了 “Approximate Nearest Neighbor - 近似最近鄰” 搜索端點,有效地將關(guān)鍵組件捆綁在一起以全面實現(xiàn)語義搜索,包括在集群內(nèi)運(yùn)行第三方模型的能力。 在最新版本 8.8 中,Elastic 不僅專注于改進(jìn)其 AI 功能的通信以響應(yīng)當(dāng)前的興趣浪潮,而且還添加了一些增強(qiáng)功能,例如在密集向量字段中增加更高的維度,從而允許存儲大型嵌入像 OpenAI 開發(fā)的語言模型一樣,并提供了一個自定義的內(nèi)置模型,即 Elastic Learned Sparse Encoder。
在本博客的其余部分中,我想演示如何使用 Sentence Transformers中的模型,使用 James Briggs 文章中的查詢。 希望你會看到 Elasticsearch 是一個非常強(qiáng)大的向量數(shù)據(jù)庫,具有用于執(zhí)行相似性搜索的高效且方便的 API。
但首先,我想談?wù)?“向量” 這個術(shù)語。 (你可能已經(jīng)注意到,我在開頭段落中使用了三次 “dense_vector” 一詞。)如果你像我一樣沒有數(shù)學(xué)背景,那么向量這個詞和概念一開始可能會讓人感到害怕。 當(dāng)通常的解釋是向量是 “具有大小和方向的對象” 時,這并沒有幫助,因為很難在人類語言的背景下為這樣的對象提出合理的心理模型。 一個更有用的模型可能是將 “向量空間” 中的向量視為坐標(biāo)。
由于語義是由符號在共享符號系統(tǒng)中的 “位置” 給出的,所以我們可以給出這個位置的 “坐標(biāo)”。 此外,我們可以使用這些坐標(biāo)的數(shù)字表示,從而開啟算術(shù)運(yùn)算的可能性。 這種數(shù)字表示通常稱為嵌入。 如果我們拋開數(shù)學(xué)理論,物理表示就是一個十進(jìn)制數(shù)列表:[0.01, 0.05, -0.04, 0.06, -0.1, ...]。 列表的長度稱為維度,每個維度代表含義的特定特征。
讓我們使用由達(dá)姆施塔特技術(shù)大學(xué) Ubiquitous Knowledge Processing Lab?提供的 Sentence Transformers 框架中的免費(fèi)、開源、預(yù)訓(xùn)練模型來仔細(xì)研究其機(jī)制。
文本嵌入 - Text embeddings
為了更好地理解嵌入是語義搜索(以及其他自然語言處理任務(wù))的基礎(chǔ),讓我們從 Hugging Face 加載模型并使用它來生成幾個單詞的嵌入。 但首先,讓我們安裝必要的庫并設(shè)置我們的環(huán)境。
%pip -q install \
python-dotenv ipywidgets tqdm humanize \
pandas numpy matplotlib altair \
datasets sentence-transformers \
elasticsearch
%load_ext dotenv
%dotenv
from tqdm.notebook import tqdm as notebook_tqdm
讓我們下載并初始化全 MiniLM-L6-v2 模型。
from sentence_transformers import SentenceTransformer
MODEL_ID="all-MiniLM-L6-v2"
model = SentenceTransformer(MODEL_ID)
print("Model dimensions:", model.get_sentence_embedding_dimension())
正如我們所看到的,該模型有 384 個維度。 這是模型向量空間的 “大小”。 它并不是特別大 —— 許多當(dāng)前模型的嵌入有數(shù)千個維度,但對于我們的目的來說已經(jīng)足夠了。 讓我們編碼,即。 為單詞 “cat” 創(chuàng)建嵌入:
embeddings_for_cat = model.encode("cat")
print(list(embeddings_for_cat)[:5] + ["..."])
請注意,輸出被截斷為前 5 個值,以免一長串?dāng)?shù)字淹沒顯示。 (另請注意,將此模型用于單個單詞只是說明性的,因為它針對句子進(jìn)行了優(yōu)化。對于單詞嵌入,使用 Word2Vec 或 GloVe 等模型更為典型。)
讓我們編碼一個不同的單詞 “dog”:
embeddings_for_dog = model.encode("dog")
print(list(embeddings_for_dog)[:5] + ["..."])
輸出說明了為此類文本編碼提出合理的心理模型是多么具有挑戰(zhàn)性:作為人類,我們很好地掌握了符號 “cat” 或 dog” 與其代表的家畜之間的關(guān)系。很難很好地理解這樣的數(shù)字表示。
然而,如前所述,數(shù)字表示具有明顯的優(yōu)勢 —— 我們可以對值進(jìn)行數(shù)學(xué)運(yùn)算。 在這種情況下,我們可以嘗試在散點圖中將它們可視化。 讓我們將列表包裝在 Pandas dataframe 中,這樣我們就可以在 Jupyter 筆記本中顯示時利用其豐富的格式,并在后續(xù)步驟中利用其數(shù)據(jù)操作功能。
import pandas as pd
df = pd.DataFrame(embeddings_for_cat, columns=["embedding"])
df
?我們可以使用內(nèi)置的繪圖功能來顯示簡單的圖表:
df.reset_index().plot.scatter(x="index", y="embedding");
該圖表只為我們提供了非常抽象的數(shù)據(jù) “圖片”; 基本上,值的粗略分布(在 -0.15 到 0.23 的范圍內(nèi))。
就其本身而言,這些數(shù)字毫無意義。 當(dāng)我們將語言理論視為 “不同符號的系統(tǒng)” 時,這實際上是預(yù)料之中的。 任何一個詞單獨(dú)存在都沒有意義; 它的意義來自于與系統(tǒng)中其他詞的關(guān)系。 那么,如果我們嘗試想象 “cat” 和 “dog” 這兩個詞呢?
讓我們創(chuàng)建一個新的 dataframe,使用 “cat” 和 “dog” 作為索引并將嵌入壓縮到單個列。
df = pd.DataFrame(
[
[embeddings_for_cat],
[embeddings_for_dog],
],
index=["cat", "dog"], columns=["embeddings"]
)
df
為了繪制數(shù)據(jù),我們需要進(jìn)行一些轉(zhuǎn)換:
# 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
現(xiàn)在我們可以繪制轉(zhuǎn)換后的數(shù)據(jù):
(df_exploded
.plot
.scatter(x="position", y="embedding", c="color", colormap="tab10")
.collections[0].colorbar.remove())
像這樣的簡單可視化似乎不會有太大幫助。 然而,它強(qiáng)調(diào)了多維向量空間的一個基本困難。 作為人類,我們非常有能力在 2D 或 3D 空間中可視化物體。 更多維度根本不是我們能夠有效想象的東西,更不用說 “繪制” 了。
我們可以使用的一個技巧是減少維度,在本例中從 384 維減少到 2 維。(再次強(qiáng)調(diào):我們能夠做到這一點是因為我們正在處理語言的數(shù)字表示。)有很多算法可以用于 這樣做 - 我們將使用主成分分析 (principal component analysis - PCA),因為它在 scikit-learn 包中很容易獲得,并且適用于小型數(shù)據(jù)集。 (有關(guān)使用 t-SNE 和 UMAP 算法的示例,請參閱 Plotly 包文檔中的一篇優(yōu)秀文章。)
import numpy as np
from sklearn.decomposition import PCA
# Drop the `position` column as it's no longer needed
df.drop(columns=["position"], inplace=True, errors="ignore")
# 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_components=2)
# 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 坐標(biāo)。 讓我們創(chuàng)建一個函數(shù),以便稍后重用代碼。
import altair as alt
def scatterplot(
data: pd.DataFrame,
tooltips=False,
labels=False,
width=800,
height=200,
) -> alt.Chart:
base_chart = (
alt.Chart(data)
.encode(
alt.X("x", scale=alt.Scale(zero=False)),
alt.Y("y", scale=alt.Scale(zero=False)),
)
.properties(width=width, height=height)
)
if tooltips:
base_chart = base_chart.encode(alt.Tooltip(["text"]))
circles = base_chart.mark_circle(
size=200, color="crimson", stroke="white", strokeWidth=1
)
if labels:
labels = base_chart.mark_text(
fontSize=13,
align="left",
baseline="bottom",
dx=5,
).encode(text="text")
chart = circles + labels
else:
chart = circles
return 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, labels=True)
好的。 該圖表相當(dāng)平庸 —— 只有兩個圓圈,隨機(jī)放置在畫布上。 我們可能期望這些標(biāo)記會彼此靠近地顯示;但事實并非如此。 畢竟,貓和狗有很多共同的特征。 然而,在語言作為一個系統(tǒng)的前提下,我們有限的 “系統(tǒng)” 只包含兩個詞:“cat” 和 “dog”。
作為人類,我們可能會認(rèn)為這些標(biāo)志密切相關(guān):它們都代表有四足的毛茸茸的動物,通常作為寵物飼養(yǎng),都是哺乳動物屬的食肉動物,等等。 但這種直覺來自于我們語言的一個非常大的系統(tǒng),其中有許多其他概念占據(jù)著不同的位置。 引用索緒爾的話,“這些概念純粹是差異性的,不是由它們的積極內(nèi)容來定義,而是由它們與系統(tǒng)其他術(shù)語的關(guān)系來消極定義”。
然后,讓我們嘗試向集合中添加更多單詞,看看圖片是否會以有意義的方式發(fā)生變化。
words = ["cat", "dog", "table", "chair", "pizza", "pasta", "asymptomatic"]
# Create a new dataframe
df = pd.DataFrame(
[[model.encode(word)] for word in words],
columns=["embeddings"],
index=words,
)
# 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, labels=True)
?
好多了! 我們可以清楚地看到相關(guān)詞的三個 “集群”,dog ? cat,pizza ? pasta,chair ? table。 我們還可以看到,除了這三個集群之外,“asymptomatic”一詞是單獨(dú)存在的。
這是人工智能的 “黑魔法” 嗎? 并不真的是。 全 MiniLM-L6-v2 模型已經(jīng)在 Reddit、Stack Exchange、維基百科、Quora 和其他來源的大量人類編寫的文本上進(jìn)行了訓(xùn)練。 因此,它確實具有這些詞的含義,幾乎字面上 “嵌入” 在它生成的 384 維向量中。
裝載數(shù)據(jù)集
通過更好、更實際地理解文本嵌入的工作原理和原理,我們可以回到本博客的最初動機(jī):使用 Elasticsearch 而不是 Pinecone,重新創(chuàng)建 James Briggs 文章中的語義搜索示例。
我們將使用 Hugging Face 的 datasets 包來加載 Quora 數(shù)據(jù)。 它是一個非常復(fù)雜的數(shù)據(jù) “包裝器”,它提供了方便的功能,例如下載文件的內(nèi)置緩存和高效的處理功能,我們將使用這些功能來操作數(shù)據(jù)。
Hugging Face 數(shù)據(jù)集主要面向為模型訓(xùn)練提供數(shù)據(jù),因此它們被分為 train、test、validation 等 split。 我們的特定數(shù)據(jù)集只有 train split 部分。 讓我們加載它并顯示有關(guān)數(shù)據(jù)集的一些元數(shù)據(jù)。
import humanize
import datasets
dataset = datasets.load_dataset("quora", split="train")
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)
正如我們所看到的,該數(shù)據(jù)集包含超過 400,000 個 “question pairs”。 我們來看看前五條記錄。?
dataset[:5]
?該數(shù)據(jù)集的主要重點是為重復(fù)檢測提供可靠的數(shù)據(jù):
我們的第一個數(shù)據(jù)集與識別重復(fù)問題的問題有關(guān)。
Quora 的一個重要產(chǎn)品原則是,每個邏輯上不同的問題都應(yīng)該有一個問題頁面。 舉一個簡單的例子,查詢“美國人口最多的州是哪個?” 和“美國哪個州人口最多?” 不應(yīng)該單獨(dú)存在于 Quora 上,因為兩者背后的意圖是相同的。 (...)
我們今天發(fā)布的數(shù)據(jù)集將使任何人都有機(jī)會根據(jù)實際的 Quora 數(shù)據(jù)來訓(xùn)練和測試語義等價模型。 (...)
— Kornél Csernai,第一個 Quora 數(shù)據(jù)集發(fā)布:Question Pairs
因此,數(shù)據(jù)集包含問題對,這些問題對被標(biāo)記為重復(fù)或不重復(fù)。 讓我們使用數(shù)據(jù)集包的實用程序來選擇和過濾數(shù)據(jù),顯示一些重復(fù)問題的示例。
(dataset
.select(range(1000))
.filter(lambda record: record["is_duplicate"])[:3])
有點矛盾的是,數(shù)據(jù)集不包含 “美國人口最多的州是哪個?” 的問題。 文章中提到。
dataset.filter(lambda record: "What is the most populous state in the USA?" in record["questions"]["text"])[:]
?讓我們從清理和轉(zhuǎn)換數(shù)據(jù)集開始,這樣我們就可以將各個問題作為單獨(dú)的文檔加載到 Elasticsearch 中。
首先,我們將刪除 is_duplicate 列并 “展平” questions 屬性,即。 將其展開為單獨(dú)的列。
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]
我們對結(jié)構(gòu)進(jìn)行了一些改進(jìn),但問題文本字段中仍然有兩個問題。 為了有效地索引問題,最好將每個問題存儲為單獨(dú)的行。 我們將使用包提供的強(qiáng)大的 map() 功能,擴(kuò)展 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,
batched=True,
remove_columns=dataset.column_names,
desc="Expand Questions",
)
print("Transformed dataset:", dataset, "\n")
dataset[:5]
數(shù)據(jù)集包含兩倍的行數(shù),因為每個問題現(xiàn)在都存儲為單獨(dú)的行。
下一步是刪除重復(fù)的問題。 我們沒有使用 is_duplicate 列進(jìn)行重復(fù)數(shù)據(jù)刪除,因為我們?nèi)匀幌M麑λ袉栴}建立索引,即使它們在語義上相同(“How can I be a good geologist?” 與 “What should I do to be a great geologist?”)。 我們只是想刪除文本完全相同的問題。 我們將再次使用 map() 函數(shù)。
# 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 seen
output = {"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,
batched=True,
batch_size=1000,
remove_columns=dataset.column_names,
desc="Remove Duplicates",
)
dataset
該數(shù)據(jù)集現(xiàn)在包含 537,362 個獨(dú)特的問題。
我們將使用之前用 “cat” 和 “dog” 演示的相同方法為這些問題生成文本嵌入。 稍后,我們會將它們索引到 Elasticsearch 中,以便使用稱為“近似最近鄰居(appoximate nearest neigbors)” 的專門查詢類型來查找語義相似的文檔。
讓我們再次使用 map() 方法處理數(shù)據(jù)集。
import time
%env TOKENIZERS_PARALLELISM=true
# Compute embeddings for batches of question text
def compute_embeddings(batch):
return { "embeddings": model.encode(sentences=batch["text"]) }
try:
start = time.perf_counter()
dataset = dataset.map(
compute_embeddings,
batched=True,
batch_size=1000,
desc="Compute 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?多分鐘。 要保留帶有嵌入的完整數(shù)據(jù)集,請使用 save_to_disk() 方法。?
索引數(shù)據(jù)到 Elasticsearch
在下一步中,我們將創(chuàng)建一個具有特定映射的 Elasticsearch 索引,用于將嵌入存儲在密集向量字段類型中,并將問題文本存儲在常規(guī)文本字段中,并使用英語分析器進(jìn)行處理。
如果你想嘗試自己運(yùn)行這些示例,你需要一個 Elasticsearch 集群。 使用此存儲庫中提供的 Docker Compose 文件在本地啟動集群。你可以參考如下的兩篇文章來創(chuàng)建自己的 Elasticsearch 及 Kibana:
-
?如何在 Linux,MacOS 及 Windows 上進(jìn)行安裝 Elasticsearch
-
Kibana:如何在 Linux,MacOS 及 Windows 上安裝 Elastic 棧中的 Kibana
在安裝的時候,我們選擇使用 Elastic Stack 8.x 的安裝手冊來進(jìn)行安裝。在默認(rèn)的情況下,Elasticsearch 的安裝是帶有 https 的安全訪問。
import os
from elasticsearch import Elasticsearch
INDEX_NAME = "quora-with-embeddings-v1"
# es = Elasticsearch(hosts=os.getenv("ELASTICSEARCH_URL"), request_timeout=300)
CERT_FINGERPRINT="bd0a26dc646ef1cb3cb5e132e77d6113e1b46d56ee390dd3c6f0b2d2b16962c4"
es = Elasticsearch( ['https://localhost:9200'],
basic_auth = ('elastic', 'h6y=vgnen2vkbm6D+z6-'),
ssl_assert_fingerprint = CERT_FINGERPRINT,
http_compress = True )
if not es.indices.exists(index=INDEX_NAME):
es.indices.create(
index=INDEX_NAME,
mappings={
"properties": {
"text": {
"type": "text",
"analyzer": "english",
},
"embeddings": {
"type": "dense_vector",
"dims": model.get_sentence_embedding_dimension(),
"index": "true",
"similarity": "cosine",
},
}
},
)
print(f"Created Elasticsearch index at {os.getenv('ELASTICSEARCH_URL')}/{INDEX_NAME}?pretty")
else:
print(f"Skipping index creation, index already exists")
更多關(guān)于如何連接到 Elasticsearch 集群的知識,請參閱文章?“Elasticsearch:關(guān)于在 Python 中使用 Elasticsearch 你需要知道的一切 - 8.x”。你也可以參考文章 “Elasticsearch:如何將整個 Elasticsearch 索引導(dǎo)出到文件 - Python 8.x”。
我們可以在在 Kibana 中進(jìn)行查看:
現(xiàn)在我們準(zhǔn)備好對數(shù)據(jù)建立索引了。 我們將使用 Elasticsearch 客戶端的 parallel_bulk() 幫助器,因為它是加載數(shù)據(jù)的最方便的方式:它通過在多個線程中運(yùn)行客戶端來優(yōu)化進(jìn)程,并且它接受 Python 迭代器(iterable)或生成器(generator),從而提供 用于索引大型數(shù)據(jù)集的高級接口。 我們將使用數(shù)據(jù)集的 to_iterable_dataset() 方法將其轉(zhuǎn)換為生成器。 這種轉(zhuǎn)換對于大型數(shù)據(jù)集尤其有益,因為它允許更節(jié)省內(nèi)存的處理。
import os
import time
from elasticsearch.helpers import parallel_bulk
if es.count(index=INDEX_NAME)["count"] >= len(dataset):
print("Skipping indexing, data already indexed.")
else:
progress = notebook_tqdm(unit="docs", total=len(dataset))
indexed = 0
start = time.perf_counter()
# Remove the "id" column and convert the dataset to generator
iterable_dataset = dataset.remove_columns(["id"]).to_iterable_dataset()
try:
print(f"Indexing dataset to [{INDEX_NAME}]...")
for ok, result in parallel_bulk(
es,
iterable_dataset,
index=INDEX_NAME,
thread_count=os.cpu_count()//2,
):
indexed += 1
progress.update(1)
print(f"Indexed [{humanize.intcomma(indexed)}] documents in {humanize.precisedelta(time.perf_counter() - start)}")
except KeyboardInterrupt:
print(f"Indexing interrupted by the user, indexed [{humanize.intcomma(indexed)}] documents in {humanize.precisedelta(time.perf_counter() - start)}")
好的! 看起來我們的文檔已成功建立索引。 讓我們使用 Cat Indices API 檢查索引,顯示文檔數(shù)量和磁盤上索引的大小。
res = es.cat.indices(index=INDEX_NAME, format="json")
print(
f"Index [{INDEX_NAME}] contains [{humanize.intcomma(res.body[0]['docs.count'])}] documents",
f"and uses [{res.body[0]['pri.store.size'].upper()}] of disk space"
)
搜索數(shù)據(jù)
至此,我們終于可以使用 Elasticsearch 來搜索數(shù)據(jù)了。
我們將定義實用函數(shù)來包裝搜索請求并以格式化的 Pandas 數(shù)據(jù)幀返回結(jié)果。 我們將使用匹配查詢進(jìn)行詞法搜索,使用 knn 選項進(jìn)行語義搜索。
import pandas as pd
# Lexical search with the `match` query
def search_keywords(query, size=10):
res = es.search(
index=INDEX_NAME,
query={"match": {"text": query}},
size=size,
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, size=10):
res = es.search(
index=INDEX_NAME,
knn={
"field": "embeddings",
"query_vector": model.encode(query, normalize_embeddings=True),
"k": size,
"num_candidates": 1000,
},
size=size,
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(selector="th,td", props=[("text-align", "left")])])
.hide(axis="index")
.format({"score": "{:.3f}"})
.background_gradient(subset=["score"], cmap="Greys"))
# Add the utility function to the dataframe class
pd.DataFrame.styled = styled
讓我們使用原始文章中的查詢 “Which city has the highest population in the world?” 來執(zhí)行詞法搜索。
search_keywords("Which city has the highest population in the world?")
我們可以立即觀察到大多數(shù)結(jié)果與我們的查詢不太相關(guān)。 除了 “Which is the most populated city in the world.?
” 等項目之外。 和 “What are the most populated cities in the world?
”,大多數(shù)結(jié)果與 “most populated city” 的概念幾乎沒有關(guān)系。 我們還可以觀察默認(rèn)評分算法如何增強(qiáng)問題開頭的短語 “Which city (…)”,盡管文本的其余部分不相關(guān)(歷史建筑的數(shù)量、生活水平等)。
讓我們使用相同的查詢執(zhí)行語義搜索,看看是否得到不同的結(jié)果。
search_embeddings("Which city has the highest population in the world?")
很明顯,這些結(jié)果與我們的查詢概念更加相關(guān)。 詞匯搜索中最相關(guān)的結(jié)果返回在頂部,接下來的幾個結(jié)果幾乎與 “most populated city” 概念同義,例如。 “l(fā)argest city” 或 “biggest city”。 另請注意,“Which is the largest city in the world by area?
” 結(jié)果列在與國家(而非城市)相關(guān)的結(jié)果之后。 這是非常令人期待的:我們的查詢是關(guān)于人口規(guī)模,而不是面積。
讓我們嘗試一些意想不到的事情。 讓我們重新措辭該查詢,使其不包含匹配文檔中的任何重要關(guān)鍵字,省略限定詞 “which”,將 “city” 替換為“urban location”,將 “highest population” 替換為 “excessive concentration of homo sapiens” ,誠然是一個非常不自然的短語。 (此重新表述的所有功勞均歸于詹姆斯·布里格斯(James Briggs),請參閱原始文章的特定版本。)
search_embeddings("Urban locations with the highest concentration of homo sapiens")
也許令人驚訝的是,我們得到的結(jié)果大多與我們的查詢相關(guān),尤其是在列表頂部,即使我們的查詢是故意構(gòu)造的,查詢術(shù)語和文檔術(shù)語之間沒有直接重疊。 這有力地展示了語義搜索的最強(qiáng)點。
讓我們嘗試使用詞法搜索執(zhí)行相同的查詢。
search_keywords("Urban locations with the highest concentration of homo sapiens")
我們沒有得到與我們的查詢相關(guān)的結(jié)果。 根據(jù)我們對詞匯搜索和語義搜索之間差異的理解,這應(yīng)該不足為奇。 事實上,這種效應(yīng)有一個技術(shù)描述,詞匯不匹配,即查詢術(shù)語與文檔術(shù)語相差太大。 即使前面提到的詞干提取或詞形還原等術(shù)語操作也無法防止這種不匹配。 傳統(tǒng)上,解決方案是向搜索引擎提供同義詞列表。 然而,這很快就會變得復(fù)雜,因為最終我們需要提供完整的同義詞庫。 (此外,由于評分算法通常的工作方式,在計算每個結(jié)果的分?jǐn)?shù)時,它無法區(qū)分單詞及其同義詞。)
讓我們回到原來的查詢,使用稍微不同的措辭,看看我們是否可以可視化嵌入和結(jié)果,類似于我們使用 “cat” 和 “dog” 等單詞進(jìn)行的演示。
df = search_embeddings("What is the most populated city in the world?")
df
我們需要使用 explode() 數(shù)據(jù)幀方法再次將數(shù)據(jù)幀從 “寬” 格式轉(zhuǎn)換為 “長” 格式。
# 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 創(chuàng)建每個結(jié)果的嵌入 “熱圖”。
import altair as alt
alt.Chart(
source
).encode(
alt.X("position:N", title="").axis(labels=False, ticks=False),
alt.Y("text:N", title="", sort=source["score"].unique()).axis(labelLimit=300, tickWidth=0, labelFontWeight="bold"),
alt.Color("embedding:Q").scale(scheme="goldred").legend(None),
).mark_rect(
width=3
).properties(width=alt.Step(3), height=alt.Step(25))
盡管進(jìn)行 “reading from tea leaves” 類型的分析存在輕微風(fēng)險,但我們?nèi)匀豢梢员鎰e圖表中的特定模式。 請注意前三個結(jié)果的視覺模式非常相似。 第四個結(jié)果在某種程度上打破了這種模式,也許是因為它是關(guān)于人口最多的國家而不是城市。 同樣,與面積最大城市相關(guān)的兩個結(jié)果形成了獨(dú)特的視覺模式。文章來源:http://www.zghlxwxcb.cn/news/detail-625805.html
然而,和以前一樣,我們可以看到理解具有大量維度的可視化是多么具有挑戰(zhàn)性。 讓我們再次嘗試降低維度,并將結(jié)果繪制在二維平面上。文章來源地址http://www.zghlxwxcb.cn/news/detail-625805.html
到了這里,關(guān)于Elasticsearch:語義搜索 - Semantic Search in python的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!