利用預訓練的 GPT-2 模型可以有效地將文字轉換為向量表示,進而進行相似度搜尋。透過均值池化技術,我們可以將 GPT-2 模型的輸出轉換為單一向量嵌入,捕捉文字的語義資訊。利用 FAISS 函式庫,可以高效地建立索引並執行相似度搜尋,尤其是在處理大規模資料集時。選擇合適的 k 值和 m 值對於搜尋效能至關重要,需要根據具體任務和資料集進行調整。嵌入查詢方法在某些任務中表現出色,但在其他任務中可能不如其他分類別方法。理解 FAISS 的工作原理,例如 K-Means 分群演算法,有助於更好地應用和最佳化相似度搜尋。
利用GPT-2進行文字嵌入與相似度搜尋
由於GPT-3僅能透過OpenAI API取得,我們將使用GPT-2來測試嵌入技術。具體而言,我們將使用在Python程式碼上訓練的GPT-2變體,希望能夠捕捉到GitHub議題中的部分上下文。
建立文字嵌入輔助函式
首先,我們需要建立一個輔助函式,該函式接受一系列文字,並利用模型為每個文字生成單一向量表示。由於像GPT-2這樣的轉換器模型會為每個詞元傳回一個嵌入向量,因此我們需要使用池化技術來獲得整個句子的單一嵌入向量。最簡單的池化方法是平均詞元嵌入,即均值池化。
實作均值池化與文字嵌入函式
import torch
from transformers import AutoTokenizer, AutoModel
model_ckpt = "miguelvictor/python-gpt2-large"
tokenizer = AutoTokenizer.from_pretrained(model_ckpt)
model = AutoModel.from_pretrained(model_ckpt)
def mean_pooling(model_output, attention_mask):
# 提取詞元嵌入
token_embeddings = model_output[0]
# 計算注意力遮罩
input_mask_expanded = (attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float())
# 對嵌入進行求和,但忽略被遮罩的詞元
sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 1)
sum_mask = torch.clamp(input_mask_expanded.sum(1), min=1e-9)
# 傳回平均值作為單一向量
return sum_embeddings / sum_mask
def embed_text(examples):
inputs = tokenizer(examples["text"], padding=True, truncation=True, max_length=128, return_tensors="pt")
with torch.no_grad():
model_output = model(**inputs)
pooled_embeds = mean_pooling(model_output, inputs["attention_mask"])
return {"embedding": pooled_embeds.cpu().numpy()}
內容解密:
mean_pooling函式:該函式對模型的輸出進行均值池化,以獲得單一的句子嵌入向量。它首先提取詞元嵌入,然後利用注意力遮罩計算有效詞元的平均值。embed_text函式:該函式將輸入文字進行標記化處理,並利用GPT-2模型生成嵌入向量。然後,它呼叫mean_pooling函式對輸出進行池化,最終傳回嵌入向量。
取得嵌入向量並建立FAISS索引
由於GPT-2模型沒有預設的填充標記,我們需要手動設定填充標記。
tokenizer.pad_token = tokenizer.eos_token
embs_train = ds["train"].map(embed_text, batched=True, batch_size=16)
embs_valid = ds["valid"].map(embed_text, batched=True, batch_size=16)
embs_test = ds["test"].map(embed_text, batched=True, batch_size=16)
embs_train.add_faiss_index("embedding")
內容解密:
- 設定填充標記:由於GPT-2沒有內建的填充標記,我們將結束標記(EOS)重複利用為填充標記,以確保批次處理的正確性。
- 建立FAISS索引:利用
add_faiss_index方法為訓練集的嵌入向量建立FAISS索引,以便進行高效的相似度搜尋。
執行相似度搜尋與結果分析
i, k = 0, 3
query = np.array(embs_valid[i]["embedding"], dtype=np.float32)
scores, samples = embs_train.get_nearest_examples("embedding", query, k=k)
print(f"QUERY LABELS: {embs_valid[i]['labels']}")
print(f"QUERY TEXT:\n{embs_valid[i]['text'][:200]}")
print("="*50)
print(f"Retrieved documents:")
for score, label, text in zip(scores, samples["labels"], samples["text"]):
print("="*50)
print(f"TEXT:\n{text[:200]}")
print(f"SCORE: {score:.2f}")
print(f"LABELS: {label}")
內容解密:
- 查詢與檢索:我們選取驗證集中的一個樣本作為查詢,並檢索訓練集中最相似的
k個樣本。 - 結果輸出:列印查詢樣本的標籤和文字,以及檢索到的檔案的文字、相似度分數和標籤。
系統評估與引數調優
為了評估系統的效能,我們需要嘗試不同的k值,並觀察宏觀和微觀指標的變化,以確定最佳引陣列態。
內容解密:
k值的選擇:k值代表檢索到的鄰居數量。選擇合適的k值對於最終的分類別效能至關重要。- 效能評估:透過改變
k值並記錄對應的宏觀和微觀效能指標,可以系統地評估不同引數下的系統效能。
高效相似性搜尋與嵌入查詢最佳化
在處理少量標籤資料時,嵌入查詢(Embedding Lookup)是一種有效的分類別方法。我們可以利用FAISS(Facebook AI Similarity Search)函式庫來實作高效的相似性搜尋,並結合嵌入向量進行分類別。
使用FAISS進行相似性搜尋
FAISS是一種用於快速相似性搜尋的函式庫,特別是在處理大量高維向量資料時表現出色。與傳統的文字搜尋不同,嵌入向量的相似性搜尋需要比較向量的相似度,而不是精確匹配。
嵌入查詢的實作
首先,我們定義了一個函式get_sample_preds來根據樣本的標籤進行預測:
def get_sample_preds(sample, m):
return (np.sum(sample["label_ids"], axis=0) >= m).astype(int)
這個函式計算樣本中標籤的總和,如果總和大於或等於閾值m,則將對應的標籤設為1。
接著,我們定義了find_best_k_m函式來找出最佳的k和m值:
def find_best_k_m(ds_train, valid_queries, valid_labels, max_k=17):
max_k = min(len(ds_train), max_k)
perf_micro = np.zeros((max_k, max_k))
perf_macro = np.zeros((max_k, max_k))
for k in range(1, max_k):
for m in range(1, k + 1):
_, samples = ds_train.get_nearest_examples_batch("embedding", valid_queries, k=k)
y_pred = np.array([get_sample_preds(s, m) for s in samples])
clf_report = classification_report(valid_labels, y_pred, target_names=mlb.classes_, zero_division=0, output_dict=True)
perf_micro[k, m] = clf_report["micro avg"]["f1-score"]
perf_macro[k, m] = clf_report["macro avg"]["f1-score"]
return perf_micro, perf_macro
內容解密:
max_k被設定為訓練資料集的大小與17之間的最小值,以確保不會超出資料集的大小。perf_micro和perf_macro用於儲存不同k和m值下的微觀和宏觀F1分數。- 對於每個
k值,我們遍歷所有可能的m值,並計算相應的F1分數。 - 使用
get_nearest_examples_batch函式檢索最近鄰樣本,並根據這些樣本進行預測。 - 使用
classification_report評估預測結果,並提取F1分數。
視覺化與最佳引數選擇
透過視覺化perf_micro和perf_macro,我們可以觀察到當m/k約為1/3時,效能最佳。
fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(10, 3.5), sharey=True)
ax0.imshow(perf_micro)
ax1.imshow(perf_macro)
ax0.set_title("micro scores")
ax0.set_ylabel("k")
ax1.set_title("macro scores")
for ax in [ax0, ax1]:
ax.set_xlim([0.5, 17 - 0.5])
ax.set_ylim([17 - 0.5, 0.5])
ax.set_xlabel("m")
plt.show()
內容解密:
- 使用
imshow函式視覺化效能矩陣。 - 設定標題和坐標軸標籤,以便清楚理解圖表內容。
最佳引數的選擇
根據視覺化結果,我們可以找到最佳的k和m值:
k, m = np.unravel_index(perf_micro.argmax(), perf_micro.shape)
print(f"Best k: {k}, best m: {m}")
內容解密:
- 使用
argmax函式找到效能最佳的索引。 - 將索引轉換為對應的
k和m值。
與其他方法的比較
嵌入查詢方法在微觀F1分數上與其他方法競爭力強,但宏觀F1分數略低。這表明該方法的效能取決於具體的任務和資料集。
FAISS的工作原理
FAISS是一種高效的相似性搜尋函式庫,特別適用於處理高維向量資料。它與傳統的文字搜尋不同,需要比較向量的相似度而不是精確匹配。
為什麼需要FAISS?
在處理大量高維向量資料時,傳統的搜尋方法效率低下。FAISS透過建立高效的索引結構,實作快速的相似性搜尋。
使用 FAISS 提升搜尋效率
當我們處理大量資料時,傳統的搜尋方法可能會變得非常緩慢。FAISS(Facebook AI Similarity Search)是一種能夠有效提升搜尋效率的技術,尤其是在處理高維度向量資料時。
FAISS 的核心概念
FAISS 的主要思想是將資料集進行分割槽(partitioning),這樣在進行搜尋時,只需要比較查詢向量與資料集中的一部分資料,從而大大加快搜尋速度。但是,如何決定要搜尋哪個分割槽,以及如何保證能夠找到最相似的資料呢?
K-Means 分群
FAISS 使用 K-Means 分群演算法將資料集中的向量分組成多個群集,每個群集都有一個中心向量(centroid vector),代表該群集的平均值。如下圖所示:
此圖示顯示了 FAISS 索引的結構:灰色點代表加入索引的資料點,黑色的粗體點代表透過 K-Means 分群找到的群集中心,彩色區域代表屬於某個群集中心的區域。
提升搜尋效率
有了這樣的分群結構後,搜尋 $n$ 個向量就變得容易多了:首先,在 $k$ 個中心向量中找出與查詢向量最相似的那一個($k$ 次比較),然後在該群集中進行搜尋($\frac{n}{k}$ 個元素需要比較)。這樣,比較的次數就從 $n$ 減少到 $k + \frac{n}{k}$。
最佳化 $k$ 值
那麼,$k$ 應該取什麼值呢?如果 $k$ 太小,每個群集仍然包含很多樣本,需要在第二步進行很多比較;如果 $k$ 太大,則需要搜尋很多個中心向量。對函式 $f(k) = k + \frac{n}{k}$ 進行最小化,可以發現 $k = \sqrt{n}$ 是最佳選擇。
內容解密:
- K-Means 分群的作用:將資料分成多個群集,以便快速搜尋。
- 中心向量的意義:代表每個群集的平均值,用於快速比較查詢向量。
- 搜尋效率的提升:透過分群和中心向量的使用,將比較次數從 $n$ 減少到 $k + \frac{n}{k}$。
- 最佳化 $k$ 值的重要性:選擇合適的 $k$ 值可以最大限度地提升搜尋效率。
FAISS 的其他功能
除了分割槽搜尋外,FAISS 還提供了其他功能,例如利用 GPU 加速搜尋,以及使用量化技術壓縮向量以節省記憶體。
微調 Vanilla Transformer
如果我們有標記好的資料,可以嘗試微調預訓練的 Transformer 模型。在本文中,我們將使用標準的 BERT 模型作為起點。
載入預訓練模型和 Tokenizer
import torch
from transformers import AutoTokenizer, AutoConfig, AutoModelForSequenceClassification
model_ckpt = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_ckpt)
def tokenize(batch):
return tokenizer(batch["text"], truncation=True, max_length=128)
ds_enc = ds.map(tokenize, batched=True)
ds_enc = ds_enc.remove_columns(['labels', 'text'])
內容解密:
tokenize函式的作用:對文字資料進行 tokenization,將其轉換為模型可以接受的輸入格式。ds_enc的處理:對資料集進行 tokenization,並移除不需要的欄位。- 多標籤損失函式的要求:需要將標籤轉換為浮點數型別,以支援類別機率。
資料格式轉換
ds_enc.set_format("torch")
ds_enc = ds_enc.map(lambda x: {"label_ids_f": x["label_ids"].to(torch.float)}, remove_columns=["label_ids"])
ds_enc = ds_enc.rename_column("label_ids_f", "label_ids")
內容解密:
- 資料格式轉換的原因:多標籤損失函式需要浮點數型別的標籤。
set_format方法的作用:將資料集轉換為 PyTorch 可以接受的格式。map方法的作用:對資料集中的每個元素進行轉換,將標籤轉換為浮點數型別。
微調模型
from transformers import Trainer, TrainingArguments
training_args_fine_tune = TrainingArguments(
output_dir="./results", num_train_epochs=20, learning_rate=3e-5,
lr_scheduler_type='constant', per_device_train_batch_size=4,
per_device_eval_batch_size=32, weight_decay=0.0,
evaluation_strategy="epoch", save_strategy="epoch", logging_strategy="epoch",
load_best_model_at_end=True, metric_for_best_model='micro f1',
save_total_limit=1, log_level='error')
內容解密:
TrainingArguments的作用:定義模型的訓練引數,例如訓練輪數、學習率等。load_best_model_at_end的作用:在訓練結束時載入最佳模型。metric_for_best_model的作用:根據指定的評估指標(這裡是 micro F1-score)選擇最佳模型。