在訓練過程中,我們希望定期評估模型的表現。為此,我們需要實作一個自定義評估函式,在訓練過程中計算ROUGE分數:

def compute_metrics(pred):
    labels_ids = pred.label_ids
    pred_ids = pred.predictions
    
    # 所有標籤中的-100替換為tokenizer.pad_token_id
    labels_ids[labels_ids == -100] = tokenizer.pad_token_id
    
    # 解碼預測和標籤
    pred_str = tokenizer.batch_decode(pred_ids, skip_special_tokens=True)
    labels_ids[labels_ids == -100] = tokenizer.pad_token_id
    label_str = tokenizer.batch_decode(labels_ids, skip_special_tokens=True)
    
    # 計算ROUGE分數
    rouge_output = rouge_metric.compute(predictions=pred_str, references=label_str,
                                       rouge_types=["rouge2"])["rouge2"].mid
    
    return {
        "rouge2_precision": round(rouge_output.precision, 4),
        "rouge2_recall": round(rouge_output.recall, 4),
        "rouge2_fmeasure": round(rouge_output.fmeasure, 4),
    }

這個自定義評估函式接收預測結果和標籤,首先將所有-100(用於忽略損失計算的標記)替換為填充標記ID,然後解碼預測和標籤字串。最後,它使用ROUGE指標計算precision(精確率)、recall(召回率)和fmeasure(F1分數)。我選擇了ROUGE-2作為主要評估指標,因為它能較好地捕捉生成文字的流暢性和連貫性。

初始化訓練器並開始訓練

準備好所有元件後,我們可以初始化Trainer並開始訓練:

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset_samsum_pt["train"],
    eval_dataset=dataset_samsum_pt["validation"],
    data_collator=seq2seq_data_collator,
    compute_metrics=compute_metrics,
)

trainer.train()

這段程式碼初始化了Trainer,並傳入我們之前準備的模型、訓練引數、資料集、資料整理器和評估指標。然後,我們呼叫train()方法開始訓練過程。

Trainer會處理許多複雜的訓練細節,包括梯度計算、引數更新、評估和日誌記錄等。這是Hugging Face Transformers函式庫的一大優勢,它大簡化了模型訓練的工作流程。

評估微調後的模型效果

訓練完成後,讓我們評估微調後的PEGASUS模型在測試集上的表現:

score = evaluate_summaries_pegasus(dataset_samsum["test"], rouge_metric, model,
                                  tokenizer, column_text="dialogue",
                                  column_summary="summary", batch_size=8)
rouge_dict = dict((rn, score[rn].mid.fmeasure) for rn in rouge_names)
pd.DataFrame(rouge_dict, index=["pegasus-finetuned"])

評估結果可能如下:

                   rouge1    rouge2    rougeL   rougeLsum
pegasus-finetuned  0.481726  0.243817  0.376492  0.376328

這段程式碼評估了微調後的模型在測試集上的表現。與之前的基準線相比,各項ROUGE指標都有顯著提升。特別是ROUGE-2從0.088提高到了0.244,這表明模型現在能夠更好地捕捉連續片語的比對,生成更流暢的摘要。

讓我們看一個具體的例子,比較微調前後模型的摘要品質:

idx = 0
dialogue = dataset_samsum["test"][idx]["dialogue"]
reference = dataset_samsum["test"][idx]["summary"]

pipe_out = pipe(dialogue)
baseline = pipe_out[0]["summary_text"]

inputs = tokenizer(dialogue, return_tensors="pt", truncation=True).to(model.device)
output = model.generate(**inputs)
prediction = tokenizer.decode(output[0], skip_special_tokens=True)

print(f"Dialogue:\n{dialogue}\n")
print(f"Reference:\n{reference}\n")
print(f"Baseline:\n{baseline}\n")
print(f"Prediction:\n{prediction}")

輸出可能如下:

Dialogue:
Hannah: Hey, do you have Betty's number?
Amanda: Lemme check
Hannah: <file_gif>
Amanda: Sorry, can't find it.
Amanda: Ask Larry
Amanda: He called her last time we were at the park together
Hannah: I don't know him well
Hannah: <file_gif>
Amanda: Don't be shy, he's very nice
Hannah: If you say so..
Hannah: I'd rather you texted him
Amanda: Just text him
Hannah: Urgh.. Alright
Hannah: Bye
Amanda: Bye bye

Reference:
Hannah needs Betty's number but Amanda doesn't have it. She needs to contact Larry.

Baseline:
Amanda: Ask Larry Amanda: He called her last time we were at the park together.
Hannah: I'd rather you texted him.
Amanda: Just text him .

Prediction:
Hannah needs Betty's number but Amanda doesn't have it. Amanda suggests Hannah should ask Larry, but Hannah doesn't know him well and would rather Amanda texted him.

這個例子清楚地展示了微調前後模型摘要能力的差異。基準模型(未微調)主要是從對話中提取關鍵句子,而微調後的模型能夠生成更抽象、更連貫的摘要,更接近人類編寫的參考摘要。微調後的摘要不僅捕捉了對話的核心內容,還保持了良好的流暢性和連貫性。

對話摘要模型的實際應用與最佳化

解碼策略的影響

在實際應用文字摘要模型時,我發現解碼策略對摘要品質有很大影響。不同的解碼策略可以產生風格各異的摘要。例如:

# 貪婪解碼
output_greedy = model.generate(**inputs, max_length=50, num_beams=1)
prediction_greedy = tokenizer.decode(output_greedy[0], skip_special_tokens=True)

# 束搜尋
output_beam = model.generate(**inputs, max_length=50, num_beams=5)
prediction_beam = tokenizer.decode(output_beam[0], skip_special_tokens=True)

# 取樣
output_sampling = model.generate(**inputs, max_length=50, do_sample=True, top_k=50, top_p=0.95)
prediction_sampling = tokenizer.decode(output_sampling[0], skip_special_tokens=True)

print(f"Greedy:\n{prediction_greedy}\n")
print(f"Beam Search:\n{prediction_beam}\n")
print(f"Sampling:\n{prediction_sampling}")

這段程式碼展示了三種常見的解碼策略:貪婪解碼、束搜尋和取樣。在實際應用中,我發現束搜尋通常能產生最流暢、最連貫的摘要,但有時會過於保守;而取樣策略則能產生更多樣化的摘要,但可能會引入一些不一致性。根據具體需求,我們可以選擇不同的解碼策略或調整其引數。

模型最佳化與佈署考量

在將摘要模型佈署到生產環境之前,我們還需要考慮一些最佳化和實用性問題:

  1. 模型大小與推理速度:PEGASUS是一個相對較大的模型,可能不適合資源受限的環境。在這種情況下,我們可以考慮知識蒸餾或模型量化等技術來減小模型大小和加速推理。

  2. 批次處理:在處理大量對話時,批次處理可以顯著提高吞吐量。我們可以根據資源情況調整批次大小,找到速度和記憶體使用之間的最佳平衡。

  3. 錯誤處理:在實際應用中,我們需要處理各種邊緣情況,如極長的對話、包含特殊字元的內容等。確保模型能夠優雅地處理這些情況是很重要的。

  4. 後處理:有時候,模型生成的摘要可能需要一些後處理,如修正標點符號、調整格式等。這些後處理步驟可以幫助提高摘要的可讀性。

  5. 持續評估與更新:隨著時間推移,對話的模式和語言可能會變化。定期評估模型效能並在必要時重新訓練或微調模型是維持高品質摘要的關鍵。

在我的實踐中,我發現將ROUGE等自動評估指標與人工評估相結合是最有效的方法。自動評估提供了快速反饋,而人工評估則能捕捉到那些難以量化的品質方面,如事實準確性和上下文相關性。

結論與未來發展

透過本文的探討,我們深入瞭解了文字摘要評估的核心指標ROUGE和BLEU,並以SAMSum對話資料集為例,展示瞭如何評估和微調PEGASUS模型來提升對話摘要效果。從實驗結果來看,微調後的模型在各項ROUGE指標上都有顯著提升,能夠產生更接近人類編寫的抽象式摘要。

文字摘要技術在企業應用中有著廣泛的潛力,特別是在客戶支援、會議記錄和訊息過濾等場景。隨著大模型語言的發展,我們可以期待未來的摘要系統能夠產生更高品質、更符合人類期望的摘要。

然而,評估摘要品質仍然是一個開放性問題。雖然ROUGE等指標提供了有用的量化評估,但它們無法完全捕捉摘要的所有重要方面。開發更全面、更符合人類判斷的評估方法是未來研究的重要方向。

在實際應用中,我建議將自動評估與人工評估相結合,並根據具體應用場景調整模型和解碼策略。只有這樣,我們才能真正發揮文字摘要技術的潛力,為使用者提供有價值的訊息濃縮服務。

PEGASUS 模型訓練與對話摘要實戰

在自然語言處理領域中,文字摘要一直是一個具有挑戰性的任務。特別是對話摘要,由於其獨特的結構和語言特性,更需要專門的模型處理。本文將探討如何使用 PEGASUS 模型進行對話摘要任務,從模型的初始化、訓練到最終的評估和應用。

準備訓練環境

在開始訓練之前,我們需要確保已經登入 Hugging Face,這樣在訓練完成後可以將模型推播到 Hub 上:

from huggingface_hub import notebook_login
notebook_login()

執行上述程式碼後,會彈出一個登入介面,輸入你的 Hugging Face 帳號憑證即可完成登入。

初始化訓練器

在準備好模型、分詞器、訓練引數和資料整理器後,我們可以初始化 Trainer 物件:

trainer = Trainer(
    model=model, 
    args=training_args,
    tokenizer=tokenizer, 
    data_collator=seq2seq_data_collator,
    train_dataset=dataset_samsum_pt["train"],
    eval_dataset=dataset_samsum_pt["validation"]
)

這段程式碼建立了一個 Trainer 物件,它是 Hugging Face Transformers 函式庫中用於訓練模型的核心類別。我們傳入了以下引數:

  • model:預訓練的 PEGASUS 模型
  • args:訓練引數,如批次大小、學習率等
  • tokenizer:用於處理文字的分詞器
  • data_collator:用於批處理資料的整理器
  • train_dataseteval_dataset:訓練集和驗證集

開始訓練模型

初始化完成後,我們可以開始訓練模型:

trainer.train()

訓練完成後,我們可以使用 ROUGE 指標評估模型在測試集上的表現:

score = evaluate_summaries_pegasus(
    dataset_samsum["test"], 
    rouge_metric, 
    trainer.model, 
    tokenizer,
    batch_size=2, 
    column_text="dialogue", 
    column_summary="summary"
)

rouge_dict = dict((rn, score[rn].mid.fmeasure) for rn in rouge_names)
pd.DataFrame(rouge_dict, index=[f"pegasus"])

這段程式碼首先呼叫 trainer.train() 開始模型訓練過程。訓練完成後,我們使用 evaluate_summaries_pegasus 函式對測試集進行評估。這個函式接受多個引數:

  • 測試資料集
  • ROUGE 評估指標
  • 訓練好的模型
  • 分詞器
  • 批次大小
  • 輸入文字和目標摘要的列名

評估結果以 ROUGE 分數表示,我們將這些分數整理成一個 DataFrame 進行展示。ROUGE 是評估摘要品質的標準指標,數值越高表示生成摘要與參考摘要越相似。

ROUGE 評估結果

執行上述程式碼後,我們得到以下 ROUGE 分數:

           rouge1    rouge2   rougeL   rougeLsum
pegasus    0.427614  0.200571  0.340648  0.340738

從結果可以看出,微調後的 PEGASUS 模型在 SAMSum 對話摘要資料集上取得了不錯的成績。與未經微調的預訓練模型相比,這些分數有了顯著提升,證明瞭領域適應性微調的重要性。

將訓練好的模型推播到 Hugging Face Hub

訓練完成後,我們可以將模型推播到 Hugging Face Hub,方便日後使用:

trainer.push_to_hub("Training complete!")

這行程式碼將訓練好的模型、分詞器和相關設定檔案上載到 Hugging Face Hub。引數 "Training complete!" 是提交訊息,記錄了這次更新的內容。上載後,其他人可以直接使用這個模型進行推理或進一步微調。

訓練過程中的評估最佳化

值得一提的是,我們還可以在訓練迴圈中評估生成結果:

# 使用 Seq2SeqTrainingArguments 擴充套件 TrainingArguments
training_args = Seq2SeqTrainingArguments(
    # 其他引數
    predict_with_generate=True
)

# 使用 Seq2SeqTrainer 替代 Trainer
trainer = Seq2SeqTrainer(
    # 其他引數與前面相同
)

這段程式碼展示瞭如何使用 Seq2SeqTrainingArgumentsSeq2SeqTrainer 來替代標準的 TrainingArgumentsTrainer。關鍵區別在於設定了 predict_with_generate=True,這使得評估過程中使用 generate() 函式而不是模型的前向傳播來建立預測結果。這種方法能更準確地反映模型在實際生成任務中的表現。

生成對話摘要

透過損失曲線和 ROUGE 分數,我們可以看到模型相比僅在 CNN/DailyMail 上訓練的原始模型有了顯著改進。接下來,讓我們看模型在測試集樣本上的表現:

gen_kwargs = {"length_penalty": 0.8, "num_beams": 8, "max_length": 128}
sample_text = dataset_samsum["test"][0]["dialogue"]
reference = dataset_samsum["test"][0]["summary"]
pipe = pipeline("summarization", model="transformersbook/pegasus-samsum")

print("Dialogue:")
print(sample_text)
print("\nReference Summary:")
print(reference)
print("\nModel Summary:")
print(pipe(sample_text, **gen_kwargs)[0]["summary_text"])

這段程式碼展示瞭如何使用訓練好的模型生成摘要。我們首先定義生成引數:

  • length_penalty: 控制生成文字長度的懲罰系數,較低的值傾向於生成較短的摘要
  • num_beams: 束搜尋的束數,增加此值可提高生成品質但會降低速度
  • max_length: 生成摘要的最大長度

然後我們從測試集中選取一個對話樣本,並使用 pipeline API 載入訓練好的模型進行摘要生成。最後,我們列印原始對話、參考摘要和模型生成的摘要進行比較。

測試樣本結果

執行上述程式碼,我們得到以下結果:

對話:

Hannah: Hey, do you have Betty's number?
Amanda: Lemme check
Hannah: <file_gif>
Amanda: Sorry, can't find it.
Amanda: Ask Larry
Amanda: He called her last time we were at the park together
Hannah: I don't know him well
Hannah: <file_gif>
Amanda: Don't be shy, he's very nice
Hannah: If you say so..
Hannah: I'd rather you texted him
Amanda: Just text him
Hannah: Urgh.. Alright
Hannah: Bye
Amanda: Bye bye

參考摘要:

Hannah needs Betty's number but Amanda doesn't have it. She needs to contact Larry.

模型摘要:

Amanda can't find Betty's number. Larry called Betty last time they were at the park together. Hannah wants Amanda to text Larry instead of calling Betty.

從結果可以看出,模型生成的摘要捕捉到了對話的主要內容,包括 Hannah 需要 Betty 的電話號碼、Amanda 沒有這個號碼以及 Larry 可能有這個號碼等關鍵訊息。雖然生成的摘要與參考摘要在措辭上有所不同,但在語義上是一致的,並且提供了更多細節。這表明模型不僅是從對話中提取片段,而是能夠理解對話內容並進行綜合。

自定義對話測試

最終的測試是看模型在自定義輸入上的表現如何:

custom_dialogue = """\
Thom: Hi guys, have you heard of transformers?
Lewis: Yes, I used them recently!
Leandro: Indeed, there is a great library by Hugging Face.
Thom: I know, I helped build it ;)
Lewis: Cool, maybe we should write a book about it. What do you think?
Leandro: Great idea, how hard can it be?!
Thom: I am in!
Lewis: Awesome, let's do it together!
"""
print(pipe(custom_dialogue, **gen_kwargs)[0]["summary_text"])

生成的摘要:

Thom, Lewis and Leandro are going to write a book about transformers. Thom helped build a library by Hugging Face. They are going to do it together.

這段程式碼測試了模型在一個全新的、訓練時從未見過的對話上的表現。結果表明,模型能夠準確捕捉對話的主要內容:三個人決定一起寫一本關於 transformers 的書,以及 Thom 幫助構建了 Hugging Face 的函式庫。這證明瞭模型具有良好的泛化能力,能夠處理訓練集之外的對話。

特別值得注意的是,模型能夠將第三行和第四行的訊息綜合為一個邏輯組合:“Thom helped build a library by Hugging Face”,這表明它不僅是簡單地提取句子,而是能夠理解並綜合訊息。

文字摘要的特殊挑戰與評估

文字摘要與情感分析、命名實體識別或問答等可以被框架為分類別任務的其他任務相比,存在一些獨特的挑戰。常規的評估指標如準確率並不能反映生成文字的品質。正如我們所見,BLEU 和 ROUGE 等指標可以更好地評估生成的文字,但人類判斷仍然是最佳的衡量標準。

在處理摘要模型時,一個常見的問題是如何摘要那些文字長度超過模型上下文長度的檔案。遺憾的是,目前沒有單一的策略來解決這個問題,這仍然是一個開放與活躍的研究領域。例如,OpenAI 的最新研究展示瞭如何透過遞迴地將摘要應用於長檔案並在過程中使用人類反饋來擴充套件摘要能力。

解決長文字摘要的策略

當面對長於模型上下文視窗的文字時,我有幾種可行的策略:

  1. 分段摘要:將長文字分割成較小的段落,分別摘要後再整合。這種方法簡單但可能導致連貫性問題。

  2. 遞迴摘要:先對文字進行初步摘要,然後對摘要再次摘要,直到達到所需長度。這種方法可以保留更多全域訊息。

  3. 根據重要性的摘要:使用某種重要性評分機制(如 TF-IDF 或語義相似度)選擇最重要的段落進行摘要。

  4. 混合方法:結合上述策略,如先根據重要性選擇關鍵段落,再進行遞迴摘要。

在實際應用中,選擇哪種策略通常取決於具體任務需求、文字型別和可用計算資源。對於對話摘要這類別特定任務,由於對話通常有明確的主題和結構,分段摘要後再整合往往能取得不錯的效果。

在這篇文章中,我們探討瞭如何使用 PEGASUS 模型進行對話摘要任務。我們從模型訓練開始,使用 Hugging Face Transformers 函式庫提供的 Trainer API 對預訓練模型進行微調,然後使用 ROUGE 指標評估了模型效能,最後展示瞭如何使用訓練好的模型生成摘要。

實驗結果表明,即使是預訓練在新聞文章上的模型,經過適當的微調也能很好地適應對話摘要這一不同領域的任務。這證明瞭遷移學習在自然語言處理中的強大能力。

文字摘要作為一項挑戰性任務,仍有許多開放性問題等待解決,特別是長文字摘要領域。隨著研究的不斷深入和技術的不斷發展,相信這些挑戰最終會得到有效解決。

構建根據評論的問答系統

當我們在網路上購物時,顧客評論往往是做決定的關鍵依據。這些評論能解答許多產品描述中未提及的具體問題,例如「這把吉他有附帶背帶嗎?」或「這台相機能在夜間使用嗎?」。然而,熱門產品通常會有數百甚至上千條評論,要找到與特定問題相關的內容往往耗時費力。

雖然許多電商平台如Amazon提供社群問答功能,但通常需要等待數天才能得到回覆(如果有人回覆的話)。如果能像Google搜尋結果那樣立即獲得答案,豈不是更好?接下來,讓我探討如何利用Transformers技術實作這一目標。

資料集介紹

為了構建問答系統,我選擇使用SubjQA資料集,這是一個包含超過10,000條英文顧客評論的資料集,涵蓋六個領域:TripAdvisor、餐廳、電影、書籍、電子產品和雜貨。如圖所示,每條評論都與一個問題相關聯,這個問題可以透過評論中的一個或多個句子來回答。

這個資料集的獨特之處在於大多數問題和答案都是主觀的,即它們取決於使用者的個人體驗。例如,當問題是關於「品質差」時,這是主觀的,取決於使用者對品質的定義。更重要的是,問題中的重要部分可能完全不會出現在評論中,這意味著不能透過簡單的關鍵字搜尋或對輸入問題進行改寫來回答。

這些特點使SubjQA成為一個真實的資料集,適合用來評估根據評論的問答模型。因為使用者生成的內容往往就是這樣,與我們在實際應用中可能遇到的情況相似。

問答系統的分類別

問答系統通常根據回答查詢時可存取的資料領域進行分類別:

  • 封閉領域問答:處理關於狹窄主題的問題(如單一產品類別)
  • 開放領域問答:處理幾乎任何問題(如Amazon的整個產品目錄)

一般來説,封閉領域問答涉及搜尋的檔案比開放領域問答要少。

資料集準備與探索

首先,讓我們從Hugging Face Hub下載資料集。我們可以使用get_dataset_config_names()函式來檢視可用的子集:

from datasets import get_dataset_config_names
domains = get_dataset_config_names("subjqa")
domains

輸出顯示有六個領域:

['books', 'electronics', 'grocery', 'movies', 'restaurants', 'tripadvisor']

這段程式碼使用Hugging Face的datasets函式庫來取得SubjQA資料集的所有可用設定名稱。get_dataset_config_names()函式會回傳一個列表,包含所有可用的子資料集名稱,在這個案例中,這些名稱代表不同的產品或服務領域。

在這個例子中,我將專注於構建一個電子產品領域的問答系統。為了下載電子產品子集,只需要將這個值傳遞給load_dataset()函式的name引數:

from datasets import load_dataset
subjqa = load_dataset("subjqa", name="electronics")

與Hub上的其他問答資料集一樣,SubjQA將每個問題的答案儲存為巢狀字典。例如,檢查answers列中的一行:

print(subjqa["train"]["answers"][1])

輸出:

{'text': ['Bass is weak as expected', 'Bass is weak as expected, even with EQ adjusted up'], 
'answer_start': [1302, 1302], 
'answer_subj_level': [1, 1],
'ans_subj_score': [0.5083333253860474, 0.5083333253860474], 
'is_ans_subjective': [True, True]}

這段輸出顯示了資料集中答案的結構。我們可以看到:

  1. text欄位包含實際答案文字
  2. answer_start提供答案在原始評論中的起始字元索引
  3. 其他欄位如answer_subj_levelis_ans_subjective表明答案的主觀性程度

為了更方便地探索資料集,我將使用flatten()方法展平這些巢狀列,並將每個分割轉換為Pandas DataFrame:

import pandas as pd
dfs = {split: dset.to_pandas() for split, dset in subjqa.flatten().items()}
for split, df in dfs.items():
    print(f"Number of questions in {split}: {df['id'].nunique()}")

輸出:

Number of questions in train: 1295
Number of questions in test: 358
Number of questions in validation: 255

這段程式碼將資料集的每個分割(訓練、測試、驗證)轉換成Pandas DataFrame,並計算每個分割中唯一問題的數量。我們可以看到這個資料集相對較小,總共只有1,908個例子。

這種情況實際上模擬了現實世界的場景,因為取得領域工作者來標註提取式問答資料集是勞動密集型與昂貴的過程。例如,CUAD法律合約提取式問答資料集的估值為200萬美元,這考慮到了標註其13,000個例子所需的法律專業知識。

資料結構分析

SubjQA資料集中有很多列,但對於構建問答系統,最有用的幾列如下:

列名描述
title與每個產品相關的Amazon標準識別碼(ASIN)
question問題
answers.text標註者標記的評論中的文字片段
answers.answer_start答案片段的起始字元索引
context顧客評論

讓我們專注於這些列,並檢視一些訓練範例。可以使用sample()方法選擇隨機樣本:

qa_cols = ["title", "question", "answers.text",
           "answers.answer_start", "context"]
sample_df = dfs["train"][qa_cols].sample(2, random_state=7)
sample_df

這段程式碼從訓練集中隨機選擇了兩個樣本,並只顯示了與問答系統相關的列。從結果中可以觀察到幾點:

  1. 問題不一定是語法正確的,這在電商網站的FAQ部分很常見
  2. 空的answers.text表示「無法回答」的問題,即答案在評論中找不到
  3. 我們可以使用起始索引和答案片段的長度,從評論中切片出對應於答案的文字

可以透過以下方式從評論中提取答案:

start_idx = sample_df["answers.answer_start"].iloc[0][0]
end_idx = start_idx + len(sample_df["answers.text"].iloc[0][0])
sample_df["context"].iloc[0][start_idx:end_idx]

輸出:

'this keyboard is compact'

問題型別分析

為了更好地理解訓練集中的問題型別,讓我們計算以幾個常見起始詞開頭的問題數量:

counts = {}
question_types = ["What", "How", "Is", "Does", "Do", "Was", "Where", "Why"]
for q in question_types:
    counts[q] = dfs["train"]["question"].str.startswith(q).value_counts()[True]
pd.Series(counts).sort_values().plot.barh()
plt.title("Frequency of Question Types")
plt.show()

這段程式碼分析了訓練集中不同型別問題的頻率分佈。它計算了以"What"、“How”、“Is"等常見問詞開頭的問題數量,並以水平條形圖的形式展示結果。這種分析有助於瞭解資料集中問題的分佈情況,對於設計問答系統的策略非常有價值。

主觀性問答的挑戰

在處理SubjQA這樣的主觀問答資料集時,我發現有幾個獨特的挑戰:

  1. 主觀性判斷:與事實性問題相比,主觀問題的答案取決於個人經驗和偏好,難以確定一個"正確"答案

  2. 隱含訊息理解:問題中的關鍵字可能不直接出現在評論中,需要模型理解隱含義

  3. 語言變化:顧客評論中的語言往往非正式、不規範,包含拼寫錯誤和俚語

  4. 上下文敏感性:答案的適當性可能高度依賴於特定產品、使用者經驗或使用場景的上下文

在構建根據評論的問答系統時,這些挑戰需要特別關注。Transformers模型的上下文理解能力使其特別適合處理這類別問題,因為它們能夠捕捉到文字中的細微差別和隱含義。

在這一部分中,我介紹了根據評論的問答系統的概念和SubjQA資料集的特點。這個資料集的主觀性使其成為評估問答模型的理想選擇,因為它模擬了實際使用者在電商平台上提出的問題。

資料集雖然相對較小,但這反映了現實世界中取得標註資料的困難。透過初步的資料探索,我們瞭解了資料的結構和問題型別的分佈,為下一步構建問答模型奠定了基礎。

在接下來的部分中,我將探討如何使用Transformers模型來構建一個能夠從電子產品評論中提取答案的問答系統,特別關注如何處理主觀性問題和評估模型效能。

根據評論的問答系統:從資料分析到模型實作

問答系統是現代自然語言處理的重要應用,尤其在電子商務平台上,能夠從產品評論中自動提取相關問題的答案,大幅提升使用者經驗。在這篇文章中,我將探討如何構建一個根據評論的問答系統,並分析其中的技術細節和實作方法。

問題型別分析與常見模式

在開發問答系統時,理解使用者提問的模式至關重要。我發現以"How”、“What"和"Is"開頭的問題在實際應用中最為常見。讓我們看一些例子:

for question_type in ["How", "What", "Is"]:
    for question in (
        dfs["train"][dfs["train"].question.str.startswith(question_type)]
        .sample(n=3, random_state=42)['question']):
        print(question)

執行這段程式碼,我們得到以下結果:

How is the camera?
How do you like the control?
How fast is the charger?
What is direction?
What is the quality of the construction of the bag?
What is your impression of the product?
Is this how zoom works?
Is sound clear?
Is it a wireless keyboard?

這段程式碼從訓練資料集中抽取了三種常見問題型別的樣本。透過這種分析,我們可以看到使用者實際提問的模式和關注點。“How"類別問題通常關注產品效能和使用體驗,“What"類別問題多為產品特性和屬性查詢,而"Is"類別問題則是尋求確認性的答案。瞭解這些模式有助於我們最佳化模型的訓練和特徵提取流程。

Stanford問答資料集的啟示

SubjQA資料集採用了(問題, 評論, [答案句子])的格式,這種格式最初由Stanford問答資料集(SQuAD)開創。SQuAD是一個廣泛用於測試機器閲讀理解能力的著名資料集。

SQuAD的建立過程頗為嚴謹:研究人員從維基百科中選取了數百篇英文章,將每篇文章分為段落,然後請眾包工作者為每個段落生成一系列問題和答案。在SQuAD的第一個版本中,每個問題的答案都保證存在於對應的文欄位落中。

然而,序列模型很快就在提取正確答案文字片段的任務上超越了人類表現。為了提高難度,研究者建立了SQuAD 2.0,它在SQuAD 1.1的基礎上增加了一組對抗性問題——這些問題與給定段落相關,但無法僅從文字中找到答案。

大多數模型自2019年以來在SQuAD 2.0基準測試中已經超越了人類表現。這一發展令人印象深刻,但我認為這種"超人類"表現可能並不真正反映出真實的閲讀理解能力,因為對於"無法回答"的問題,模型通常可以透過識別段落中的反義詞等模式來辨別。

為瞭解決這些問題,Google發布了Natural Questions (NQ)資料集,其中包含來自Google搜尋使用者的事實性問題。NQ中的答案比SQuAD中的答案長得多,提供了更具挑戰性的基準。

從文字中提取答案的技術

構建問答系統的第一步是找到一種方法來識別客戶評論中可能包含答案的文字片段。例如,如果問題是"它防水嗎?",評論文字是"這款手錶在30米深度防水”,那麼模型應該輸出"在30米深度防水”。

要實作這一點,我們需要理解以下幾個關鍵方面:

  1. 如何構建監督學習問題
  2. 如何為問答任務對文字進行標記和編碼
  3. 如何處理超出模型最大上下文大小的長文欄位落

跨度分類別:問答的核心機制

提取答案的最常見方法是將問題構建為跨度分類別任務,其中答案跨度的起始和結束標記作為模型需要預測的標籤。這個過程可以理解為:模型需要確定答案在原文中的開始位置和結束位置。

由於我們的訓練集相對較小(僅1,295個範例),一個好的策略是從已經在大規模問答資料集(如SQuAD)上微調過的語言模型開始。這些模型通常具有較強的閲讀理解能力,可以作為構建更準確系統的良好基礎。

這與之前章節中的方法略有不同,我們之前通常是從預訓練模型開始,然後自己微調特定任務的頭部。例如在分類別任務中,我們需要微調分類別頭,因為類別數量與具體資料集相關。而對於提取式問答,我們可以從已微調的模型開始,因為標籤的結構在不同資料集之間保持一致。

選擇合適的問答模型

在Hugging Face Hub上搜尋"squad”,我們可以找到超過350個問答模型。那麼應該選擇哪一個呢?這通常取決於多種因素,如語料函式庫是單語言還是多語言,以及在生產環境中執行模型的限制條件。

以下是一些提供良好基礎的模型:

模型描述引數量SQuAD 2.0上的F1分數
MiniLMBERT-base的蒸餾版本,保留了99%的效能,同時速度提高了一倍66M79.5
RoBERTa-base效能優於BERT對應模型,可在大多數問答資料集上使用單個GPU進行微調125M83.0
ALBERT-XXL在SQuAD 2.0上達到最先進效能,但計算密集與難以佈署235M88.1
XLM-RoBERTa-large支援100種語言的多語言模型,具有強大的零樣本效能570M83.8

在實際應用中,我更傾向於使用MiniLM模型,因為它訓練速度快,允許我們快速迭代和最佳化技術。這是一個經過精心蒸餾的模型,在保留核心效能的同時大幅減少了計算資源需求。

問答任務的文字標記化

首先,我們需要從Hugging Face Hub載入MiniLM模型檢查點:

from transformers import AutoTokenizer
model_ckpt = "deepset/minilm-uncased-squad2"
tokenizer = AutoTokenizer.from_pretrained(model_ckpt)

為了展示模型的實際效果,我們先嘗試從一段短文字中提取答案。在提取式問答任務中,輸入以(問題, 上下文)對的形式提供,所以我們將它們一起傳遞給標記器:

question = "How much music can this hold?"
context = """An MP3 is about 1 MB/minute, so about 6000 hours depending on \
file size."""
inputs = tokenizer(question, context, return_tensors="pt")

這裡我們使用了預訓練的MiniLM標記器來處理問題和上下文文字。return_tensors="pt"引數指定回傳PyTorch張量,這是為了稍後能夠直接將這些輸入傳遞給模型。標記器會將文字轉換為模型可以理解的數字表示。

如果我們將標記化後的輸入視為表格,可以看到:

input_ids 
101 2129 2172 2189 2064 2023 ... 5834 2006 5371 2946 1012 102

token_type_ids 
0 0 0 0 0 0 ... 1 1 1 1 1 1

attention_mask 
1 1 1 1 1 1 ... 1 1 1 1 1 1

在這裡,我們可以看到熟悉的input_idsattention_mask張量,而token_type_ids張量則指示輸入的哪一部分對應於問題和上下文(0表示問題標記,1表示上下文標記)。

要了解標記器如何為問答任務格式化輸入,可以解碼input_ids張量:

print(tokenizer.decode(inputs["input_ids"][0]))

輸出結果為:

[CLS] how much music can this hold? [SEP] an mp3 is about 1 mb / minute, so about 6000 hours depending on file size. [SEP]

我們可以看到,對於每個問答範例,輸入採用以下格式:

[CLS] 問題標記 [SEP] 上下文標記 [SEP]

問答模型的工作原理

問答模型的核心機制是透過識別答案在文字中的起始和結束位置來提取答案。當模型接收到問題和上下文後,它會計算每個標記成為答案起始和結束位置的機率。

在實際應用中,我發現這種方法非常有效,特別是對於需要從大量文字中精確定位訊息的場景。例如,在電子商務平台上,使用者可能想知道某產品的特定功能或規格,模型可以直接從產品評論中提取相關訊息,而不是回傳整個評論。

這種跨度分類別方法的優勢在於它能夠提供精確的答案,而不是簡單的"是/否"回答或預定義的回應。它允許模型從文字中找出最相關的部分,即使該部分在原始文字中的表達方式與問題不完全比對。

問答系統的進階技術考量

在構建根據評論的問答系統時,還有幾個關鍵技術點需要考慮:

  1. 長文書處理:當評論或文章超過模型的最大上下文大小時,需要採用特殊策略,如文字分段和答案合併。

  2. 無答案處理:有些問題可能在給定上下文中沒有答案,模型需要能夠識別這種情況而不是強制提供不正確的答案。

  3. 答案排名:當從多個文字片段中提取可能的答案時,需要一個排名機制來選擇最佳答案。

  4. 多語言支援:在國際平台上,問答系統需要處理多種語言的問題和評論。

在實踐中,我發現結合使用預訓練模型和特定領域的微調可以顯著提高問答系統的效能。例如,對於電子產品評論,可以先在通用問答資料集上微調模型,然後再使用特定領域的資料進行進一步調整。

此外,將問答系統與其他NLP任務(如情感分析或實體識別)結合使用,可以提供更全面的使用者經驗。例如,不僅回答"相機品質如何?",還可以提供該特性的整體情感評價。

問答系統的發展正朝著更加精確、上下文感知和多模態的方向發展。未來的系統將能夠理解更複雜的問題,整合多種訊息來源,並提供更自然、更有用的回答。

隨著Transformer模型的不斷發展和改進,我們可以期待問答系統在準確性和效率方面取得更大的進步,為使用者提供更加人工智慧和個人化的體驗。

深入解析問答系統:從模型輸出到實際應用

模型輸出的解析與處理

在建立問答系統時,模型的輸出解析是關鍵環節。在前面的步驟中我們已完成了文字標記化,現在需要例項化問答模型並執行前向傳播:

import torch
from transformers import AutoModelForQuestionAnswering

model = AutoModelForQuestionAnswering.from_pretrained(model_ckpt)
with torch.no_grad():
    outputs = model(**inputs)
print(outputs)

這段程式碼載入了預訓練的問答模型,並在不計算梯度的情況下(torch.no_grad())執行推論。這是評估階段的標準做法,可以節省記憶體並加速計算。輸出是一個QuestionAnsweringModelOutput物件,包含開始位置和結束位置的邏輯值(logits)。

模型輸出的核心部分是兩組邏輯值:start_logitsend_logits。這表明問答任務被視為一種標記分類別問題,模型需要預測答案的開始和結束位置。這與我們在命名實體識別等任務中看到的標記分類別方法類別似。

從邏輯值到實際答案

要從模型輸出中提取實際答案,我們需要處理這些邏輯值:

start_logits = outputs.start_logits
end_logits = outputs.end_logits

print(f"Input IDs shape: {inputs.input_ids.size()}")
print(f"Start logits shape: {start_logits.size()}")
print(f"End logits shape: {end_logits.size()}")

邏輯值的形狀與輸入ID的形狀相同,這表明每個輸入標記都有對應的開始和結束邏輯值。較大的正邏輯值表示該位置更可能是答案的開始或結束標記。在這個例子中,模型為數字"1"和"6000"分配了最高的開始標記邏輯值,這很合理,因為問題是關於數量的。同樣,具有最高邏輯值的結束標記是"minute"和"hours"。

要獲得最終答案,我們計算開始和結束標記邏輯值的argmax,然後從輸入中切片出相應的文字:

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()函式回傳張量中最大值的索引。我們將結束索引加1是因為Python的切片是半開區間(包含開始索引但不包含結束索引)。然後使用標記器的decode方法將標記ID轉換迴文字。

使用Transformers管道簡化流程

在實際應用中,Transformers函式庫提供了便捷的管道功能,封裝了所有預處理和後處理步驟:

from transformers import pipeline
pipe = pipeline("question-answering", model=model, tokenizer=tokenizer)
pipe(question=question, context=context, topk=3)

管道(pipeline)是Transformers函式庫中的高階API,整合了模型推論的所有步驟。透過設定topk=3,管道回傳前三個最可能的答案及其對應的置信度分數。分數是透過對邏輯值應用softmax函式運算得出的機率估計。這在需要比較單一上下文中多個答案時非常有用。

管道還能處理無法回答的問題。在這些情況下,模型會為[CLS]標記分配較高的開始和結束分數,管道將此輸出對映為空字元串:

pipe(question="Why is there no data?", context=context, handle_impossible_answer=True)

處理長文欄位落

問答模型面臨的一個挑戰是上下文文字長度經常超過模型的最大序列長度(通常最多幾百個標記)。SubjQA訓練集中有相當一部分問題-上下文對無法完全適應MiniLM的512標記的上下文大小。

對於文字分類別等任務,我們通常直接截斷長文字,假設[CLS]標記的嵌入包含足夠的訊息以生成準確預測。但對於問答任務,這種策略可能有問題,因為問題的答案可能位於上下文的末尾,而截斷會移除這部分內容。

標準解決方案是應用滑動視窗技術,每個視窗包含一段適合模型上下文的標記:

example = dfs["train"].iloc[0][["question", "context"]]
tokenized_example = tokenizer(example["question"], example["context"],
                             return_overflowing_tokens=True, max_length=100,
                             stride=25)

在Transformers中,設定return_overflowing_tokens=True可以啟用滑動視窗功能。max_length控制視窗大小,而stride控制相鄰視窗之間的重疊程度。這樣處理後,我們得到一個輸入ID列表,每個視窗對應一個元素。

我們可以檢查每個視窗的標記數量:

for idx, window in enumerate(tokenized_example["input_ids"]):
    print(f"Window #{idx} has {len(window)} tokens")

也可以透過解碼輸入來檢視視窗之間的重疊部分:

for window in tokenized_example["input_ids"]:
    print(f"{tokenizer.decode(window)} \n")

滑動視窗技術的工作原理

滑動視窗技術是處理長文字的關鍵策略,它將長文字分割成多個重疊的片段。每個片段都包含問題和上下文的一部分,並且片段之間有一定的重疊,確保不會遺漏可能的答案。

這種方法的優點是:

  1. 能夠處理任意長度的上下文文字
  2. 透過片段重疊確保答案不會因為分割而丟失
  3. 允許模型在整個檔案範圍內搜尋答案

處理多個視窗時,模型會對每個視窗生成預測,然後選擇具有最高置信度分數的答案作為最終結果。這種方法雖然增加了計算量,但大提高了模型處理長文字的能力。

實際應用中的考量

在實際應用問答系統時,還需要考慮以下幾點:

  1. 答案驗證:簡單地取邏輯值的argmax可能會產生不合理的答案,例如選擇屬於問題而非上下文的標記。實際使用的管道會計算開始和結束索引的最佳組合,並施加各種約束,如確保答案在範圍內、開始索引必須先於結束索引等。

  2. 處理無答案問題:不是所有問題都能在給定上下文中找到答案。高品質的問答系統需要能夠識別這種情況並適當回應。

  3. 上下文檢索:在實際系統中,我們通常需要先從大型檔案集合中檢索相關上下文,然後再應用問答模型。這涉及到檢索技術的整合。

  4. 答案的多樣性:有些問題可能有多個正確答案。透過設定topk引數,可以取得多個可能的答案,以增加系統的靈活性。

問答系統的構建需要結合多種技術,從文字預處理、模型推論到後處理和答案驗證。透過合理運用滑動視窗等技術,我們可以構建能夠處理實際應用場景的問答系統,為使用者提供準確、相關的答案。

使用 Haystack 開發高效問答系統

在現代電子商務平台中,能夠從大量產品評論中快速提取答案的問答系統變得越來越重要。在前面的簡單答案提取範例中,我們同時提供了問題和背景資料給模型。然而,在實際應用中,使用者只會提供關於產品的問題,系統需要從眾多評論中找出相關段落。

問答系統面臨的實際挑戰

一個直觀的方法是將所有產品評論串接在一起,作為單一長文字提供給模型。這種方法雖然簡單,但會導致處理時間過長,嚴重影響使用者經驗。

讓我們假設一個場景:平均每個產品有30條評論,每條評論需要100毫秒處理時間。如果系統需要處理所有評論才能回答一個問題,那麼平均延遲時間將達到3秒 — 對電商網站而言這絕對是難以接受的等待時間!

檢索-閲讀架構:現代問答系統的基礎

為瞭解決這個問題,現代問答系統通常採用檢索-閲讀(retriever-reader)架構,包含兩個主要元件:

檢索器(Retriever)

負責為給定查詢檢索相關檔案。檢索器通常分為兩類別:

  • 稀疏檢索器(Sparse Retriever):使用詞頻來將每個檔案和查詢表示為稀疏向量。查詢與檔案的相關性透過計算向量內積來確定。

  • 密集檢索器(Dense Retriever):使用如 Transformer 等編碼器將查詢和檔案表示為語境化嵌入(密集向量)。這些嵌入編碼了語義意義,使密集檢索器能夠透過理解查詢內容來提高搜尋準確性。

閲讀器(Reader)

負責從檢索器提供的檔案中提取答案。閲讀器通常是一個閲讀理解模型,不過在本文後面部分,我們也會看到能夠生成自由格式答案的模型範例。

除了這兩個核心元件外,問答系統還可能包含其他處理元件,對檢索器取得的檔案或閲讀器提取的答案進行後處理。例如,檢索到的檔案可能需要重新排序,以消除可能混淆閲讀器的噪聲或不相關內容。同樣地,當正確答案來自長檔案中的多個段落時,常需要對閲讀器的答案進行後處理。

檢索-閲讀架構

Haystack:問答系統開發的強大工具

為了建立我們的問答系統,我將使用由德國 NLP 公司 deepset 開發的 Haystack 函式庫。Haystack 根據檢索-閲讀架構,抽象了建立這些系統所涉及的大部分複雜性,並且 Transformers 緊密整合。

除了檢索器和閲讀器,使用 Haystack 建立問答系統時還涉及兩個元件:

檔案儲存(Document Store)

一種導向檔案的資料函式庫,儲存在查詢時提供給檢索器的檔案和中繼資料。

管道(Pipeline)

將問答系統的所有元件組合在一起,實作自定義查詢流程、合併來自多個檢索器的檔案等功能。

接下來,我將向你展示如何使用這些元件快速構建一個原型問答系統,並探討如何提升其效能。

版本説明

本文使用的是 Haystack 函式庫的 0.9.0 版本。在 0.10.0 版本中,管道和評估 API 已經重新設計,使檢索器或閲讀器的效能影響更容易被檢測。如果你想檢視使用新 API 的程式碼,可以參考 GitHub 儲存函式庫。

初始化檔案儲存

在 Haystack 中,有多種檔案儲存可供選擇,每種都可以與特定的檢索器搭配使用。下表展示了稀疏檢索器(TF-IDF、BM25)和密集檢索器(Embedding、DPR)與各種可用檔案儲存的相容性:

檢索器/檔案儲存記憶體內ElasticsearchFAISSMilvus
TF-IDF
BM25
Embedding
DPR

由於我們將在本文中探索稀疏和密集檢索器,我選擇使用 ElasticsearchDocumentStore,它與兩種型別的檢索器都相容。

Elasticsearch 的優勢

Elasticsearch 是一個能夠處理各種資料型別的搜尋引擎,包括文字、數值、地理空間、結構化和非結構化資料。它能夠儲存大量資料並透過全文搜尋功能快速過濾,使其特別適合開發問答系統。它還有一個優勢是作為基礎設施分析的行業標準,所以你的公司很可能已經有一個可以使用的叢集。

設定 Elasticsearch

要初始化檔案儲存,我們首先需要下載並安裝 Elasticsearch。按照 Elasticsearch 的,我們可以使用 wget 取得 Linux 最新版本,並使用 tar 命令解壓:

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 伺服器。由於我們在 Jupyter notebook 中執行所有程式碼,我們需要使用 Python 的 Popen() 函式來生成一個新程式。同時,我們還將使用 chown shell 命令在後台執行子程式:

import os
from subprocess import Popen, PIPE, STDOUT

# 將 Elasticsearch 作為背景程式執行
!chown -R daemon:daemon elasticsearch-7.9.2
es_server = Popen(args=['elasticsearch-7.9.2/bin/elasticsearch'],
                 stdout=PIPE, stderr=STDOUT, preexec_fn=lambda: os.setuid(1))

# 等待 Elasticsearch 啟動
!sleep 30

這段程式碼使用 Python 的 subprocess 模組來啟動 Elasticsearch 作為背景程式。首先用 chown 命令修改 Elasticsearch 目錄的所有權,然後使用 Popen 函式啟動服務。引數中的 stdout=PIPE 和 stderr=STDOUT 將標準輸出和錯誤輸出重定向到同一個管道,而 preexec_fn 引數指定了子程式的 ID。啟動後,程式碼等待 30 秒讓 Elasticsearch 完全啟動。

在 Popen() 函式中,args 引數指定要執行的程式,stdout=PIPE 為標準輸出建立一個新管道,stderr=STDOUT 將錯誤收集到同一管道中。preexec_fn 引數指定了我們希望使用的子程式 ID。預設情況下,Elasticsearch 在本地的 9200 連線埠執行,所以我們可以透過向 localhost 傳送 HTTP 請求來測試連線:

curl -X GET "localhost:9200/?pretty"

如果一切正常,你會看到類別似以下的 JSON 回應,包含 Elasticsearch 的版本訊息和一些設定詳情。

例項化檔案儲存

現在 Elasticsearch 伺服器已經啟動並執行,接下來要做的是例項化檔案儲存:

from haystack.document_store.elasticsearch import ElasticsearchDocumentStore

# 回傳檔案嵌入以便後續與密集檢索器一起使用
document_store = ElasticsearchDocumentStore(return_embedding=True)

這段程式碼從 Haystack 函式庫匯入 ElasticsearchDocumentStore 類別,然後建立一個例項。引數 return_embedding=True 表示當我們從檔案儲存中檢索檔案時,它會回傳檔案的嵌入向量,這對於後續使用密集檢索器非常重要。

預設情況下,ElasticsearchDocumentStore 在 Elasticsearch 上建立兩個索引:一個名為 document 的用於儲存檔案,另一個名為 label 的用於儲存已標註的答案範圍。現在,我們只需用 SubjQA 評論填充 document 索引,Haystack 的檔案儲存期望一個包含 text 和 meta 鍵的字典列表,格式如下:

{
  "text": "<上下文內容>",
  "meta": {
    "field_01": "<額外中繼資料>",
    "field_02": "<額外中繼資料>",
    ...
  }
}

meta 中的欄位可用於在檢索過程中應用過濾器。對於我們的目的,我們將包括 SubjQA 的 item_id 和 q_review_id 列,以便我們可以按產品和問題 ID 進行過濾,還有相應的訓練分割。然後我們可以遍歷每個 DataFrame 中的範例,並使用 write_documents() 方法將它們增加到索引中:

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"已載入 {document_store.get_document_count()} 份檔案")

這段程式碼遍歷每個資料分割(如訓練集、驗證集等)及其對應的 DataFrame,為每條評論建立一個檔案字典。透過 drop_duplicates(subset=“context”) 確保我們不增加重複的評論內容。每個檔案包含評論文字和中繼資料,包括專案 ID、問題 ID 和資料分割訊息。最後,使用 document_store.write_documents() 方法將這些檔案寫入 Elasticsearch 的 document 索引,並列印出已載入的檔案數量。

太棒了!我們已經將所有評論載入到索引中。接下來,我們需要一個檢索器來搜尋這個索引,並找出與使用者問題最相關的評論內容。