返回文章列表

評論問答系統技術深度解析

本文探討如何建構根據評論的問答系統,包含模型輸出後處理、使用 Transformers Pipeline 簡化流程、處理長文字、使用 Haystack 建構問答系統、檢索器-讀取器架構、Haystack

自然語言處理 問答系統

現代問答系統常根據檢索器-讀取器架構,本文將探討如何運用此架構,特別針對評論資料,構建高效的問答系統。首先,我們會解析模型如何處理輸入文字並輸出答案,包含 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}")

內容解密:

  1. torch.argmax(start_logits):找出具有最高logit值的起始token索引。
  2. torch.argmax(end_logits) + 1:找出具有最高logit值的結束token索引,並加1以確保範圍包含結束token。
  3. inputs["input_ids"][0][start_idx:end_idx]:根據起始和結束索引,從輸入ID中擷取答案範圍。
  4. 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)

內容解密:

  1. pipeline("question-answering", model=model, tokenizer=tokenizer):建立一個問答pipeline,使用指定的模型和tokenizer。
  2. 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)

內容解密:

  1. return_overflowing_tokens=True:啟用滑動視窗功能,傳回多個輸入ID列表。
  2. max_length=100:設定每個視窗的最大token數量。
  3. 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=PIPEstderr=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_idsplit,用於限制查詢結果。

檢視檢索結果

最後,我們可以檢視檢索結果。

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_lengthdoc_stride 是用於控制滑動視窗行為的引數,分別指定了最大序列長度和檔案步長。
  • return_no_answer=True 表示當沒有答案時,傳回一個特殊的標記。

將所有元件組合在一起

Haystack 提供了一個 Pipeline 抽象,允許我們將檢索器、讀取器和其他元件組合在一起,形成一個可以輕鬆自定義的圖形。每個 Pipeline 都有一個 run() 方法,用於指定查詢流程的執行方式。

from haystack.pipeline import ExtractiveQAPipeline

pipe = ExtractiveQAPipeline(reader, es_retriever)

內容解密:

  • ExtractiveQAPipeline 是 Haystack 中的一個預定義管道,用於提取式問答系統。
  • readeres_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_retrievertop_k_reader 分別指定了要檢索的檔案數量和要提取的答案數量。
  • filters 引數用於對 item ID 和分割進行過濾。

改進我們的 QA 管道

雖然最近對 QA 的研究主要集中在改進閱讀理解模型上,但在實踐中,如果檢索器無法在第一時間找到相關檔案,那麼再好的讀取器也無濟於事。因此,評估檢索器的效能對於改進 QA 管道至關重要。

未來改進方向

為了進一步改進我們的 QA 管道,我們需要評估檢索器和讀取器的效能,並根據評估結果進行調整。這包括使用不同的檢索器和讀取器,調整超引數等。未來,我們還可以探索使用更先進的技術,如密集檢索和閱讀理解模型,以進一步提高 QA 管道的效能。