什麼是 RAG|就像讓裸考的你 open book 考試

這次希望和大家介紹:

  1. RAG 是什麼,為什麼要使用 RAG
  2. 簡單使用 Hugging Face 實作具有檢索功能的聊天機器人

RAG(Retrieval-Augmented Generation)由 Patrick Lewis 等人於 2020 年提出,是一種 AI 框架,旨在通過提供外部資料知識來提升 LLM(大型語言模型)的回答質量和準確性。這有點像在沒有學習過相關知識的情況下進行考試的考生,在考試中使用開卷答題的方式來彌補知識的不足。具體來說,當 LLM 對某些特定資料的回覆不足時,RAG 可以提供外部資料來輔助回答問題。

舉一個簡單的例子來說明,假設我們問 LLM「為什麼 ML 需要正規化?」如果這個模型沒有學過機器學習的相關知識,它可能不知道正規化是什麼意思,以及正規化對機器學習建模的影響。但如果我們能夠提供一份關於機器學習的書籍,LLM 就能夠在回答「為什麼 ML 需要正規化?」這個問題時,檢索書籍中語意最相近的段落,並基於這些段落來回答問題。這個過程就是 RAG 的工作流程:提供額外資訊給 LLM,讓它進行檢索並基於檢索到的資訊來回答問題。

為什麼要使用 RAG?

如同一開始所提到的,LLM 因為訓練資料的限制,先天上有一些問題導致在實際應用時需要進行微調,否則 LLM 的回答可能不會達到我們的預期。

LLM 不擅長的事

  1. 缺乏最新、私人的資料
    LLM 只根據訓練資料進行回答,它對於語言的理解以及基本常識有很強的能力,但如果使用者的問題過於特定,超出訓練集的資料內容,LLM 無法提供準確、有用的答案。例如,詢問 LLM「現在的總統是誰」或「我們公司的軟體操作問題」。

  2. 對於專業問題回答的幻覺
    由於資料集的分佈問題,某些特定領域的資料一定佔少數,而通用型 LLM 並非在專業領域知識的背景下訓練,回答時常會充滿幻覺,並非真正有用的答案。例如,當涉及法律、醫療等專業知識時,LLM 的回答通常會失準。

什麼是 RAG?

針對以上問題,提出了一種將 LLM 與額外知識庫整合的架構,即為 RAG。透過給 LLM 額外的知識、資料,讓 LLM 可以給出可靠、準確的回應。以下介紹 RAG 的工作方式及其各環節涉及到的問題和可能的解決技術。

簡單來說 RAG 得步驟有兩點:

檢索(Retrieval)

依據 User 的問題從額外提供的資料中找出最相關、可以回答該問題的文件、段落。

在檢索階段,我們需要

  1. 收集資料
    我們需要收集該應用所需要的可能資料,例如:內外規比對機器人需要內部與外部的法規文件、商品客服機器人需要各商品的資料。

  2. 資料切分
    以內外法規比對為例,內部法規可能是一份超過千頁的 pdf 檔案,我們需要將其依據章節、字數分成不同的部分,這樣每個資料都代表特定的主題,避免我們在檢索時,提供太多不需要的資訊,而增加 LLM 回答的不可控性。而將資料切分的過程我們稱為 Chunking,每一份資料則為一份 Chunk。
    以下為一些進行 Chunk 時可以使用的技術,以提升 Chunk 品質:

    1. Sliding window
      在切分時,每一份 Chunk 會保留前後 Chunk 的資訊。
    2. Metadata filtering
      Metadata 包含了許多關於資料的重要資訊,可以在一開始藉由 Metadata 過濾掉不相關的資料,例如:時間、類別等。
    3. Abstract Embedding
      將每一份 Chunk 先進行摘要,可有效的壓縮並保留資訊,以提升後續做 Embedding 時的表現。
    4. Graph Indexing
      加入每一份 Chunk 的關係。
    5. Semantic Chunkking:基於語意而非字數做文本的切割。
  3. 語意搜尋
    準備好 Chunk 後,我們需要一個嵌入模型 Embedding Model 幫我們將 Chunk 以向量的方式表達其語意特徵,我們可以透過將其與 User 的向量進行相似度運算,比較出哪一部分的 Chunk 與 User 的問題最相似。
    而如何真正理解 User 的問題,進而找到相關的段落,我們可以做一些 Query Translation:

    1. Query Rewriting
      指示 LLMs 針對檢索任務重新撰寫 Query。
    2. Multi-query
      有時後 User 的問題可能包含多個含義、問題,透過 LLM 將 User 的問題拆成多個子問題再進行檢索。
    3. RAG-Fusion
      當 User 對於問題不夠明確時,透過 LLM 將問題換句話說,轉換成多個意思相近的 Query 後進行檢索,將檢索結果進行綜合排序得到較好的檢索結果。
    4. Step-back
      利用 LLM 猜想 User 問題的抽象概念,也就退後一步思考 User 可能的意圖。
    5. HyDE
      藉由 LLM 生成「假想的回答」,再依據假想的回答進行答案的檢索,通過比對「假想」和「真實」資料中的差異,捕捉語意。

增強生成(Augmented-Generation)

將檢索到的資料與 User 的問題結合,透過生成器產生順暢、可理解、準確地回答,通常將資料一起餵給訓練好的 LLM,讓其統整上下文訊息回答。

依賴訓練好的 LLM 回答通常可以達到不錯的效果,但依舊會遇到一些問題,例如 1. 上下文長度限制 2. 受冗餘資訊影響生成 3. 檢索的結果並不包含有用的資訊,所以有一些研究在於專注擷取資訊的的後處理,處理第一階段檢索的資料,將其過濾、壓縮、最佳化提升檢索的資料品質,藉以提升模型回答,主要有兩種做法:

  1. Re-Ranking
    LLM 會受到上下文長度限制,故藉由 Re-Ranking 將真正重要、相關的文件排序在前面,限制文件數量,解決上下文長度限制以及檢索資料的品質。
  2. Self-RAG
    透過 LLM 反思、修正檢索的結果,其中涉及到 1. 評估是否要進行檢索 2. 評估檢索語回答的相關性與準確性,挑選最佳組進行回答。

簡單的 RAG 實作

到了 Demo 時間!我們將簡單實作一個具有檢索功能的聊天機器人,使用 Hugging Face 的 Pipeline 與 Conversation 能非常迅速的實作。在這之前,還是跟大家介紹一下實作 RAG 你會需要的工具。

  1. Models
    簡單來說我們需要兩種模型:1. Embedding Model 2. ChatModel。
    我們需要 Embedding Model 協助我們進行語意搜尋,ChatModel 將檢索結果與 User 問題結合,進行回覆。

    • Embedding Model: 我們可以從 Hugging face 中的 feature-extraction Model 尋找,或是直接使用 Sentence-Transformers 套件。
    • ChatModel:我們可以從開源的模型庫,例如 Hugging face hub or Ollama 下載,或是使用閉源模型的 API。
  2. Frameworks
    如果 Pandas 之於數據分析、Scikit-learn 之於機器學習,我們也需要一些框架來協助我們實作 RAG,以更快速完成上面所介紹的技術(當然你也可以自行手刻),目前主流框架有 1. Langchain 2. Llamaindex。

在這次的 demo 中,我們將簡單的使用 Hugging face 的 Pipeline 與 Conversation 來實作 RAG。

demo

code

  1. 準備 ChatModel
    我們使用 Hugging Face 下載模型,範例中我們使用聯發科的 MediaTek-Research/Breeze-7B-32k-Instruct-v1_0,利用 AutoModelForCasualLM 實例化模型,再傳入帶有 text-generation 任務的 pipeline 中,完成 chatmodel 的初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from transformers import pipeline
from transformers import AutoModelForCausalLM, AutoTokenizer

model_id = 'MediaTek-Research/Breeze-7B-32k-Instruct-v1_0'
tokenizer = AutoTokenizer.from_pretrained(
model_id)
hf_model = AutoModelForCausalLM.from_pretrained(
model_id,
trust_remote_code=True,
device_map='auto'
)

chatbot = pipeline(
"text-generation",
model=hf_model,
tokenizer=tokenizer, # Tokenizer,要與模型匹配,主要提供 chat 模式時的特殊符號
max_new_tokens=1024, # 模型最多可以生成多少字
return_full_text=False # 控制 pipeline 只輸出 AI Message
)
  1. 聊天記錄前處理
    聊天式模型,例如 ChatGPT,基本上是透過 text-generation 作為基礎模型,進一步訓練模型以實現聊天功能。因此,在 Hugging Face 上找到的任何支持 text-generation 任務的模型都可以用來實現聊天功能。

在進行聊天任務時,模型需要一些特殊符號來區分每一段訊息是來自於使用者(User)、AI 還是系統提示(System Prompt)。各個模型的特殊符號可能不盡相同,需要參考官方文件來了解。例如,在聯發科的 Breeze 模型中,使用 [INST] 和 [/INST] 作為區隔符號。

使用模型時,我們需要在文字中加入這些特殊符號,才能充分發揮模型的聊天能力。通常,我們會使用 list of dict 的方式儲存聊天記錄,使用 role 區別 user 和 ai,content 代表內容,而 Hugging Face 的模型也支援這樣的格式。例如:

1
2
3
4
5
6
chat_history = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello, who are you?"},
{"role": "assistant", "content": "I am an AI developed by OpenAI. How can I assist you today?"}
]

我們可以透過實例化 Conversation 這個 class,透過 add_user_input 與 append_response 新增歷史使用者輸入與模型回覆,將資料變為上述的資料格式再送給模型進行推理。

1
2
from transformers import Conversation
conversation = Conversation() # 建立一個對話 Conversation 物件

利用 add_user_input 新增 user 聊天記錄:

1
2
conversation.add_user_input("provided information: the name of repo is bert-base-uncased. Based on the provided information, what is the name of repo?")
print(f"目前聊天記錄:{conversation.messages}") # conversation.messages 可以直接丟給 chatbot 得到回覆

目前聊天記錄:[{‘role’: ‘user’, ‘content’: ‘provided information: the name of repo is bert-base-uncased. Based on the provided information, what is the name of repo?’}]

conversation.messages 丟給 chatbot 得到回覆:

1
2
chatbot_result = chatbot(conversation.messages)
print(chatbot_result)

[{‘generated_text’: ‘根據提供的信息,repo的名称是”bert-base-uncased”。’}]

將 chatbot 的回覆以 append_respons 的方法加入 conversation 中

1
2
conversation.append_response(chatbot_result[0]['generated_text'])
print(f"目前聊天記錄:{conversation.messages}")

目前聊天記錄:[{‘role’: ‘user’, ‘content’: ‘provided information: the name of repo is bert-base-uncased. Based on the provided information, what is the name of repo?’}, {‘role’: ‘assistant’, ‘content’: ‘根據提供的信息,repo的名称是”bert-base-uncased”。’}]

  1. Embedding Model
    我們透過 Embedding Model 將文件與 User query 轉成語意向量,使用相似度進行匹配,我們可以在 Hugging Face 中於 Task 以 feature-extraction 找到 Embedding model,也可以直接使用 sentence-transformers 套件將句子轉成向量。
1
2
from sentence_transformers import SentenceTransformer
embedding_model = SentenceTransformer("intfloat/multilingual-e5-large")
1
2
# 利用 encode 得到 sentence 的 embedding
embedding_model.encode("哈囉,這是一個句子")

array([ 0.03142083, -0.01894771, -0.00766944, …, -0.02039895,
-0.01210877, 0.03742645], dtype=float32)

有了向量之後,我們就可以利用迴圈與 cosine simialrity 進行匹配,找出最相似的句子,下面舉一個簡單的例子,我們計算 query 與 source_sentence 中所有句子 embedding 的 cosine similarity,並選出最相似的句子,得到與「為什麼 ML 需要做正規化」最相似文本是 「Regularization is important!」。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

import numpy as np
def calculate_cosine_similarity(vec1, vec2):
return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))

query = "為什麼 ML 需要做正規化"

source_sentence = [
'Regularization is important!',
'Dropout is important!',
'Missing Data Handling is important!'
]

most_related_sentence = None
max_similarity = 0

for sentence in source_sentence:
sim = calculate_cosine_similarity(
embedding_model.encode(query),
embedding_model.encode(sentence)
)

if sim > max_similarity:
most_related_sentence = sentence
max_similarity = sim

print(f"{query} vs {sentence} similarity: {sim}")

print("="*10)
print(f"與「{query}」最相似文本:{most_related_sentence}")

為什麼 ML 需要做正規化 vs Regularization is important! similarity: 0.86

為什麼 ML 需要做正規化 vs Dropout is important! similarity: 0.80

為什麼 ML 需要做正規化 vs Missing Data Handling is important! similarity: 0.81

==========

與「為什麼 ML 需要做正規化」最相似文本:Regularization is important!

  1. 整合在一起!
    我們準備一個 list 當中包含了想要檢索的資料,在 while 迴圈中:
  2. User 輸入 query
  3. 透過 Embedding Model 計算 query 與 source_sentence 中所有句子的 cosine similarity,選出最相近的句子
  4. 將 query 與最相近的句子一起傳入 conversation.messages 並送入 chatbot 得到回覆
  5. 將 chatbot 回覆加入 conversation.messages
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
qa_data = [
...,

...,

...,
]

from typing import List
import numpy as np

def get_answer(query: str, source: List[str]):
most_related_sentence = None
max_similarity = 0

for sentence in source:
sim = calculate_cosine_similarity(
embedding_model.encode(query),
embedding_model.encode(sentence)
)

if sim > max_similarity:
most_related_sentence = sentence
max_similarity = sim

return most_related_sentence

def calculate_cosine_similarity(vec1, vec2):
return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
user_query = input(">>>")
conversation = Conversation()

while user_query.lower() != "bye":
print(f"user: {user_query}")
# 尋找最相似的文件
answer = get_answer(user_query, qa_data)
llm_input = f"""請你基於以下資訊回答使用者的問題
{answer}
===
問題:{user_query}
"""
conversation.add_user_input(llm_input)
# 將 conversation.messages 丟給 chatbot
chatbot_result = chatbot(conversation.messages)[0]['generated_text']
print(f"AI: {chatbot_result}")
conversation.append_response(chatbot_result)

user_query = input(">>>")

如此一來,就完成一個簡單的具有檢索功能 chatbot 了!🎉

參考資料

Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks
RAG (Retrieval Augmented Generation): 為自然語言處理揭開新篇章