現代問答系統常根據檢索器-讀取器架構,本文將探討如何運用此架構,特別針對評論資料,構建高效的問答系統。首先,我們會解析模型如何處理輸入文字並輸出答案,包含 logits 的解讀與後續的答案擷取。接著,會介紹如何利用 Transformers Pipeline 簡化繁瑣的預處理和後處理步驟,並探討如何克服長文字的處理瓶頸,例如滑動視窗技術的應用。此外,本文還會詳細介紹如何使用 Haystack 框架構建問答系統,包含 Elasticsearch 的設定、資料匯入、檢索器與讀取器的選擇與使用,以及評估檢索器效能的方法,例如召回率的計算。最後,我們將會討論如何改進問答系統,例如使用密集檢索器和更進階的閱讀理解模型等。
建構根據評論的問答系統:技術深度解析
在問答系統(QA)的建構過程中,我們面臨著一個重要的技術挑戰:如何有效地從長篇文字中擷取答案。為瞭解決這個問題,我們需要深入理解模型的內部運作機制,並採用適當的技術策略來最佳化系統的效能。
模型輸出與後處理
當我們將輸入文字傳遞給模型時,模型會輸出與每個輸入token相關聯的起始和結束logits。這些logits代表了模型對每個token成為答案起始或結束的信心度。舉例來說,在一個關於音樂儲存容量的問題中,模型可能會給予數字“1”和“6000”較高的起始logits,因為問題詢問的是某種數量。
import torch
start_idx = torch.argmax(start_logits)
end_idx = torch.argmax(end_logits) + 1
answer_span = inputs["input_ids"][0][start_idx:end_idx]
answer = tokenizer.decode(answer_span)
print(f"Question: {question}")
print(f"Answer: {answer}")
內容解密:
torch.argmax(start_logits):找出具有最高logit值的起始token索引。torch.argmax(end_logits) + 1:找出具有最高logit值的結束token索引,並加1以確保範圍包含結束token。inputs["input_ids"][0][start_idx:end_idx]:根據起始和結束索引,從輸入ID中擷取答案範圍。tokenizer.decode(answer_span):將擷取的答案範圍解碼為可讀文字。
使用Transformers Pipeline簡化流程
Transformers函式庫提供了一個方便的pipeline,可以簡化預處理和後處理步驟。我們可以透過傳遞tokenizer和微調後的模型來例項化這個pipeline。
from transformers import pipeline
pipe = pipeline("question-answering", model=model, tokenizer=tokenizer)
pipe(question=question, context=context, topk=3)
內容解密:
pipeline("question-answering", model=model, tokenizer=tokenizer):建立一個問答pipeline,使用指定的模型和tokenizer。pipe(question=question, context=context, topk=3):使用pipeline回答問題,並傳回前3個可能的答案。
處理長文字
當面對長文字時,模型的上下文大小限制可能會成為一個瓶頸。為瞭解決這個問題,我們可以採用滑動視窗的方法,將長文字分割成多個小段,並分別處理。
tokenized_example = tokenizer(example["question"], example["context"],
return_overflowing_tokens=True, max_length=100,
stride=25)
內容解密:
return_overflowing_tokens=True:啟用滑動視窗功能,傳回多個輸入ID列表。max_length=100:設定每個視窗的最大token數量。stride=25:設定滑動視窗之間的步長。
建構根據評論的問答系統
在前面的章節中,我們已經瞭解了問答(QA)模型如何從文字中提取答案。現在,讓我們來看看構建一個端對端問答系統的其他必要元件。
使用 Haystack 建構問答系統
在簡單的答案提取例子中,我們同時向模型提供了問題和上下文。然而,在現實中,系統使用者只會提供關於某個產品的問題,因此我們需要一些方法從所有評論中選擇相關的段落。一個簡單的方法是將某個產品的所有評論連線起來,並將它們作為一個長的上下文輸入到模型中。
儘管這種方法很簡單,但它的缺點是上下文可能會變得非常長,從而為使用者的查詢引入無法接受的延遲。例如,假設平均每個產品有 30 條評論,每條評論需要 100 毫秒來處理。如果我們需要處理所有評論才能得到答案,那麼這將導致平均每個使用者查詢的延遲為 3 秒——對於電子商務網站來說太長了!
檢索器-讀取器架構
為了處理這個問題,現代問答系統通常根據檢索器-讀取器(retriever-reader)架構,它有兩個主要元件:
檢索器
負責檢索與給定查詢相關的檔案。檢索器通常分為稀疏(sparse)和密集(dense)兩類別。稀疏檢索器使用詞頻來表示每個檔案和查詢作為稀疏向量。
# 稀疏向量的例子
import numpy as np
# 假設有一個包含三個檔案的語料函式庫
docs = ["這是第一個檔案", "這是第二個檔案", "這是第三個檔案"]
# 簡單地使用詞頻來表示每個檔案
def simple_sparse_representation(docs):
vocab = set(" ".join(docs).split())
vectors = []
for doc in docs:
vector = [doc.count(word) for word in vocab]
vectors.append(vector)
return vectors
sparse_vectors = simple_sparse_representation(docs)
print(sparse_vectors)
內容解密:
上述程式碼展示瞭如何簡單地使用詞頻來表示檔案。首先,我們從所有檔案中提取出一個詞彙表,然後對於每個檔案,我們計算每個詞在該檔案中出現的頻率。這樣就得到了每個檔案的稀疏向量表示。
查詢和檔案的相關性然後透過計算向量的內積來確定。另一方面,密集檢索器使用像 Transformer 這樣的編碼器來表示查詢和檔案作為上下文嵌入(contextualized embeddings),這些是密集向量。這些嵌入編碼了語義含義,並允許密集檢索器透過理解查詢的內容來提高搜尋準確性。
讀取器
負責從檢索器提供的檔案中提取答案。讀取器通常是一個閱讀理解模型,儘管在本章末尾,我們將看到可以生成自由格式答案的模型示例。
Haystack 元件
為了構建我們的問答系統,我們將使用由德國公司 deepset 開發的 Haystack 函式庫。Haystack 根據檢索器-讀取器架構,抽象了構建這些系統所涉及的大部分複雜性,並與 Transformers 緊密整合。
除了檢索器和讀取器之外,使用 Haystack 構建問答管道還涉及兩個其他元件:
- 檔案儲存(Document Store):一個導向檔案的資料函式庫,用於儲存在查詢時提供給檢索器的檔案和後設資料。
- 管道(Pipeline):結合問答系統的所有元件,以實作自定義查詢流、合併來自多個檢索器的檔案等。
初始化檔案儲存
在 Haystack 中,有多種檔案儲存可供選擇,每種都可以與一組專用的檢索器配對。由於我們將在本章中探索稀疏和密集檢索器,因此我們將使用與這兩種型別的檢索器都相容的 ElasticsearchDocumentStore。
# 下載並安裝 Elasticsearch
url = """https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.9.2-linux-x86_64.tar.gz"""
!wget -nc -q {url}
!tar -xzf elasticsearch-7.9.2-linux-x86_64.tar.gz
內容解密:
上述程式碼用於下載並安裝 Elasticsearch。首先,我們使用 wget 命令下載 Elasticsearch 的壓縮檔,然後使用 tar 命令解壓。
接下來,我們需要啟動 Elasticsearch 伺服器。由於我們在 Jupyter 筆記本中執行所有程式碼,因此我們需要使用 Python 的 Popen() 函式來啟動 Elasticsearch 程式。
import subprocess
# 啟動 Elasticsearch 伺服器
es_server = subprocess.Popen(["elasticsearch-7.9.2/bin/elasticsearch"])
內容解密:
上述程式碼展示瞭如何使用 Python 的 subprocess 模組來啟動 Elasticsearch 伺服器。我們使用 Popen 函式來建立一個新的程式,並在其中執行 Elasticsearch 的啟動命令。這樣,Elasticsearch 伺服器就會在後台執行。
在Haystack框架中建立根據評論的問答系統
初始化Elasticsearch並啟動背景程式
為了建立一個根據評論的問答系統,我們首先需要初始化Elasticsearch並將其作為背景程式執行。這涉及到使用subprocess模組來啟動Elasticsearch服務。
import os
from subprocess import Popen, PIPE, STDOUT
# 將Elasticsearch的擁有者變更為daemon
!chown -R daemon:daemon elasticsearch-7.9.2
# 以背景程式的方式啟動Elasticsearch
es_server = Popen(args=['elasticsearch-7.9.2/bin/elasticsearch'],
stdout=PIPE, stderr=STDOUT, preexec_fn=lambda: os.setuid(1))
# 等待Elasticsearch啟動
!sleep 30
內容解密:
Popen函式用於建立一個新的子程式,用於執行Elasticsearch。args引數指定了要執行的程式及其引數。stdout=PIPE和stderr=STDOUT用於將標準輸出和標準錯誤重定向到同一個管道。preexec_fn=lambda: os.setuid(1)用於設定子程式的使用者ID。
測試Elasticsearch連線
在啟動Elasticsearch後,我們需要測試是否能夠成功連線。可以使用curl命令向localhost:9200傳送HTTP請求。
!curl -X GET "localhost:9200/?pretty"
內容解密:
- 此命令用於測試Elasticsearch是否正常執行。
- 回應包含了Elasticsearch的版本資訊、叢集名稱等。
初始化檔案儲存
接下來,我們需要初始化一個檔案儲存(Document Store),這裡使用的是ElasticsearchDocumentStore。
from haystack.document_store.elasticsearch import ElasticsearchDocumentStore
document_store = ElasticsearchDocumentStore(return_embedding=True)
內容解密:
ElasticsearchDocumentStore用於在Elasticsearch中儲存檔案。return_embedding=True表示檔案的嵌入表示將被傳回。
將評論資料寫入檔案儲存
我們需要將SubjQA評論資料寫入到檔案儲存中。
for split, df in dfs.items():
docs = [{"text": row["context"],
"meta":{"item_id": row["title"], "question_id": row["id"],
"split": split}}
for _,row in df.drop_duplicates(subset="context").iterrows()]
document_store.write_documents(docs, index="document")
print(f"Loaded {document_store.get_document_count()} documents")
內容解密:
- 將DataFrame中的評論資料轉換為字典列表,每個字典包含文字和後設資料。
- 使用
write_documents方法將這些檔案寫入到Elasticsearch中。
初始化檢索器
接下來,我們需要初始化一個檢索器(Retriever),這裡使用的是根據BM25的稀疏檢索器。
from haystack.retriever.sparse import ElasticsearchRetriever
es_retriever = ElasticsearchRetriever(document_store=document_store)
內容解密:
ElasticsearchRetriever是Haystack提供的一個檢索器,預設使用BM25演算法。- 將之前建立的
document_store傳入檢索器。
使用檢索器進行查詢
現在我們可以使用檢索器來查詢特定的評論。
item_id = "B0074BW614"
query = "Is it good for reading?"
retrieved_docs = es_retriever.retrieve(
query=query, top_k=3, filters={"item_id":[item_id], "split":["train"]})
內容解密:
- 使用
retrieve方法進行查詢,指定查詢字串、傳回的檔案數量以及篩選條件。 - 篩選條件包括
item_id和split,用於限制查詢結果。
檢視檢索結果
最後,我們可以檢視檢索結果。
print(retrieved_docs[0])
內容解密:
- 列印第一個檢索到的檔案,包括其文字、評分等資訊。
- 評分代表了檔案與查詢之間的相關性。
建立根據評論的問答系統
在前面的章節中,我們探討瞭如何使用 Haystack 來建立一個問答系統。在本章節中,我們將深入瞭解如何使用 Haystack 的各個元件來建立一個根據評論的問答系統。
初始化讀取器
在 Haystack 中,有兩種型別的讀取器可以用來從給定的上下文中提取答案:
- FARMReader:根據 deepset 的 FARM 框架,用於微調和佈署轉換器。與使用 Transformers 訓練的模型相容,可以直接從 Hugging Face Hub 載入模型。
- TransformersReader:根據 Transformers 的 QA 管道,僅適用於執行推斷。
雖然這兩種讀取器都以相同的方式處理模型的權重,但在將預測轉換為答案的過程中存在一些差異:
- 在 Transformers 中,QA 管道在每個段落中對開始和結束 logits 進行 softmax 歸一化。這意味著只有在比較從同一段落中提取的答案時,答案分數才有意義,因為機率之和為 1。例如,一個段落中的答案分數為 0.9 不一定比另一個段落中的答案分數 0.8 更好。在 FARM 中,logits 不會被歸一化,因此可以更容易地比較不同段落之間的答案。
- TransformersReader 有時會預測相同的答案兩次,但具有不同的分數。當答案位於兩個重疊的視窗中時,就會發生這種情況。在 FARM 中,這些重複的答案會被刪除。
由於我們稍後將對讀取器進行微調,因此我們將使用 FARMReader。與 Transformers 相同,要載入模型,我們只需要指定 Hugging Face Hub 上的 MiniLM 檢查點以及一些特定於 QA 的引數:
from haystack.reader.farm import FARMReader
model_ckpt = "deepset/minilm-uncased-squad2"
max_seq_length, doc_stride = 384, 128
reader = FARMReader(model_name_or_path=model_ckpt, progress_bar=False,
max_seq_len=max_seq_length, doc_stride=doc_stride,
return_no_answer=True)
內容解密:
FARMReader是 Haystack 中用於提取答案的元件,它根據 deepset 的 FARM 框架。model_ckpt指定了要使用的預訓練模型的檢查點。max_seq_length和doc_stride是用於控制滑動視窗行為的引數,分別指定了最大序列長度和檔案步長。return_no_answer=True表示當沒有答案時,傳回一個特殊的標記。
將所有元件組合在一起
Haystack 提供了一個 Pipeline 抽象,允許我們將檢索器、讀取器和其他元件組合在一起,形成一個可以輕鬆自定義的圖形。每個 Pipeline 都有一個 run() 方法,用於指定查詢流程的執行方式。
from haystack.pipeline import ExtractiveQAPipeline
pipe = ExtractiveQAPipeline(reader, es_retriever)
內容解密:
ExtractiveQAPipeline是 Haystack 中的一個預定義管道,用於提取式問答系統。reader和es_retriever分別是讀取器和檢索器的例項,它們被傳遞給ExtractiveQAPipeline的建構函式。
評估檢索器
評估檢索器的一個常見指標是召回率(recall),它衡量了檢索到的相關檔案的比例。在本章節中,我們將介紹如何使用召回率來評估檢索器的效能。
n_answers = 3
preds = pipe.run(query=query, top_k_retriever=3, top_k_reader=n_answers,
filters={"item_id": [item_id], "split":["train"]})
內容解密:
pipe.run()方法執行查詢流程,並傳回預測結果。top_k_retriever和top_k_reader分別指定了要檢索的檔案數量和要提取的答案數量。filters引數用於對 item ID 和分割進行過濾。
改進我們的 QA 管道
雖然最近對 QA 的研究主要集中在改進閱讀理解模型上,但在實踐中,如果檢索器無法在第一時間找到相關檔案,那麼再好的讀取器也無濟於事。因此,評估檢索器的效能對於改進 QA 管道至關重要。
未來改進方向
為了進一步改進我們的 QA 管道,我們需要評估檢索器和讀取器的效能,並根據評估結果進行調整。這包括使用不同的檢索器和讀取器,調整超引數等。未來,我們還可以探索使用更先進的技術,如密集檢索和閱讀理解模型,以進一步提高 QA 管道的效能。