探索跨语言的RAG应用:与Mishnah对话

2024年06月06日 由 alex 发表 55 0

简介

我很高兴能在这篇文章中与大家分享构建独特的检索-增强生成(RAG)应用程序的历程,该应用程序用于与拉比经文进行交互。MishnahBot 旨在为学者和日常用户提供一种直观的方式,以交互方式查询和探索Mishnah¹。它可以帮助解决问题,如快速查找相关源文本或总结有关宗教法的复杂辩论,提取底线。


2


那么RAG系统为什么受到如此热捧呢?

为了提高准确性和利用大型语言模型(LLM)中的推理能力,RAG 应用正受到广泛关注。想象一下,你可以与你的图书馆、同一制造商的汽车手册集或税务文件聊天。你可以提出问题,并获得由丰富的专业知识提供的答案。


3


RAG 与增加上下文长度的利弊

在改进语言模型交互方面有两种新兴趋势: 检索增强生成(RAG)和增加上下文长度(可能通过允许超长文档作为附件)。


RAG 系统的一个关键优势是成本效益。有了 RAG,你可以在不大幅增加查询成本的情况下处理大型上下文,而查询成本可能会变得很高。此外,RAG 的模块化程度更高,可以与不同的知识库和 LLM 提供商即插即用。另一方面,在语言模型中直接增加上下文长度是一个令人兴奋的发展,可以在一次交互中处理更长的文本。


设置

在这个项目中,我使用 AWS SageMaker 作为开发环境,使用 AWS Bedrock 访问各种 LLM,并使用 LangChain 框架管理管道。这两项 AWS 服务对用户都很友好,而且只对使用的资源收费,所以我非常鼓励大家自己尝试。对于 Bedrock,你需要申请访问 Llama 3 70b Instruct 和 Claude Sonnet。


让我们打开一个新的 Jupyter 笔记本,安装我们将要使用的软件包:


!pip install chromadb tqdm langchain chromadb sentence-transformers


数据集

本项目的数据集是Mishnah,这是犹太传统的核心拉比古籍。我之所以选择该文本,是因为它与我的心灵息息相关,同时也为语言模型带来了挑战,因为它是一个小众话题。数据集来自 Sefaria-Export 存储库²,这是一个拉比文本宝库,其中的英文译本与希伯来原文对齐。这种对齐有助于在我们的 RAG 应用程序的不同步骤中进行语言切换。


1. 加载数据集

首先,我们需要下载相关数据。由于完整版本库相当大,我们将使用 git sparse-checkout。打开终端窗口并运行以下程序。


git init sefaria-json
cd sefaria-json
git sparse-checkout init --cone
git sparse-checkout set json
git remote add origin https://github.com/Sefaria/Sefaria-Export.git
git pull origin master


tree Mishna/ | less


我们就得到了所需的数据文件:


Mishnah
├── Seder Kodashim
│   ├── Mishnah Arakhin
│   │   ├── English
│   │   │   └── merged.json
│   │   └── Hebrew
│   │       └── merged.json
│   ├── Mishnah Bekhorot
│   │   ├── English
│   │   │   └── merged.json
│   │   └── Hebrew
│   │       └── merged.json
│   ├── Mishnah Chullin
│   │   ├── English
│   │   │   └── merged.json
│   │   └── Hebrew
│   │       └── merged.json


现在,让我们在 Jupyter 笔记本环境中加载文档:


import os
import json
import pandas as pd
from tqdm import tqdm
# Function to load all documents into a DataFrame with progress bar
def load_documents(base_path):
    data = []
    for seder in tqdm(os.listdir(base_path), desc="Loading Seders"):
        seder_path = os.path.join(base_path, seder)
        if os.path.isdir(seder_path):
            for tractate in tqdm(os.listdir(seder_path), desc=f"Loading Tractates in {seder}", leave=False):
                tractate_path = os.path.join(seder_path, tractate)
                if os.path.isdir(tractate_path):
                    english_file = os.path.join(tractate_path, "English", "merged.json")
                    hebrew_file = os.path.join(tractate_path, "Hebrew", "merged.json")
                    if os.path.exists(english_file) and os.path.exists(hebrew_file):
                        with open(english_file, 'r', encoding='utf-8') as ef, open(hebrew_file, 'r', encoding='utf-8') as hf:
                            english_data = json.load(ef)
                            hebrew_data = json.load(hf)
                            for chapter_index, (english_chapter, hebrew_chapter) in enumerate(zip(english_data['text'], hebrew_data['text'])):
                                for mishnah_index, (english_paragraph, hebrew_paragraph) in enumerate(zip(english_chapter, hebrew_chapter)):
                                    data.append({
                                        "seder": seder,
                                        "tractate": tractate,
                                        "chapter": chapter_index + 1,
                                        "mishnah": mishnah_index + 1,
                                        "english": english_paragraph,
                                        "hebrew": hebrew_paragraph
                                    })
    return pd.DataFrame(data)
# Load all documents
base_path = "Mishnah"
df = load_documents(base_path)
# Save the DataFrame to a file for future reference
df.to_csv(os.path.join(base_path, "mishnah_metadata.csv"), index=False)
print("Dataset successfully loaded into DataFrame and saved to file.")


再看看数据:


df.shape
(4192, 7)
print(df.head()[["tractate", "mishnah", "english"]])
tractate  mishnah                                            english
0  Mishnah Arakhin        1  <b>Everyone takes</b> vows of <b>valuation</b>...
1  Mishnah Arakhin        2  With regard to <b>a gentile, Rabbi Meir says:<...
2  Mishnah Arakhin        3  <b>One who is moribund and one who is taken to...
3  Mishnah Arakhin        4  In the case of a pregnant <b>woman who is take...
4  Mishnah Arakhin        1  <b>One cannot be charged for a valuation less ...


2. 矢量化并存储在 ChromaDB 中

接下来,我们将文本矢量化并存储在本地 ChromaDB 中。一句话,我们的想法是将文本表示为密集的向量(数字数组),这样在语义上相似的文本在向量空间中就会彼此 “接近”。通过这种技术,我们可以根据查询检索到相关的段落。


我们选择了一种轻量级的矢量化模型--all-MiniLM-L6-v2,它可以在 CPU 上高效运行。该模型在性能和资源效率之间取得了良好的平衡,因此适合我们的应用。虽然 OpenAI 的文本嵌入-3-large 等最先进的模型可以提供卓越的性能,但它们需要大量的计算资源,通常需要在 GPU 上运行。


以下是我们将使用的矢量化代码(在 CPU 机器上运行此数据集应只需几分钟):


import numpy as np
from sentence_transformers import SentenceTransformer
import chromadb
from chromadb.config import Settings
from tqdm import tqdm
# Initialize the embedding model
model = SentenceTransformer('all-MiniLM-L6-v2', device='cpu')
# Initialize ChromaDB
chroma_client = chromadb.Client(Settings(persist_directory="chroma_db"))
collection = chroma_client.create_collection("mishnah")
# Load the dataset from the saved file
df = pd.read_csv(os.path.join("Mishnah", "mishnah_metadata.csv"))
# Function to generate embeddings with progress bar
def generate_embeddings(paragraphs, model):
    embeddings = []
    for paragraph in tqdm(paragraphs, desc="Generating Embeddings"):
        embedding = model.encode(paragraph, show_progress_bar=False)
        embeddings.append(embedding)
    return np.array(embeddings)
# Generate embeddings for English paragraphs
embeddings = generate_embeddings(df['english'].tolist(), model)
df['embedding'] = embeddings.tolist()
# Store embeddings in ChromaDB with progress bar
for index, row in tqdm(df.iterrows(), desc="Storing in ChromaDB", total=len(df)):
    collection.add(embeddings=[row['embedding']], documents=[row['english']], metadatas=[{
        "seder": row['seder'],
        "tractate": row['tractate'],
        "chapter": row['chapter'],
        "mishnah": row['mishnah'],
        "hebrew": row['hebrew']
    }])
print("Embeddings and metadata successfully stored in ChromaDB.")


3. 用英语创建 RAG

数据集准备就绪后,我们就可以用英语创建检索增强生成(RAG)应用程序了。为此,我们将使用 LangChain,这是一个功能强大的框架,它为各种语言模型操作和集成提供了统一的接口,使我们可以轻松构建复杂的应用程序。


LangChain 简化了语言模型 (LLM)、检索器和向量存储等不同组件的集成过程。通过使用 LangChain,我们可以专注于应用程序的高级逻辑,而不必担心每个组件的底层复杂性。


下面是建立 RAG 系统的代码:


from langchain.chains import LLMChain, RetrievalQA
from langchain.llms import Bedrock
from langchain.prompts import PromptTemplate
from sentence_transformers import SentenceTransformer
import chromadb
from chromadb.config import Settings
from typing import List
# Initialize AWS Bedrock for Llama 3 70B Instruct
llm = Bedrock(
    model_id="meta.llama3-70b-instruct-v1:0"
)
# Define the prompt template
prompt_template = PromptTemplate(
    input_variables=["context", "question"],
    template="""
    Answer the following question based on the provided context alone:
    Context: {context}
    Question: {question}
    Answer (short and concise):
    """,
)
# Initialize ChromaDB
chroma_client = chromadb.Client(Settings(persist_directory="chroma_db"))
collection = chroma_client.get_collection("mishnah")
# Define the embedding model
embedding_model = SentenceTransformer('all-MiniLM-L6-v2', device='cpu')
# Define a simple retriever function
def simple_retriever(query: str, k: int = 3) -> List[str]:
    query_embedding = embedding_model.encode(query).tolist()
    results = collection.query(query_embeddings=[query_embedding], n_results=k)
    documents = results['documents'][0]  # Access the first list inside 'documents'
    sources = results['metadatas'][0]  # Access the metadata for sources
    return documents, sources
# Initialize the LLM chain
llm_chain = LLMChain(
    llm=llm,
    prompt=prompt_template
)
# Define SimpleQA chain
class SimpleQAChain:
    def __init__(self, retriever, llm_chain):
        self.retriever = retriever
        self.llm_chain = llm_chain
    def __call__(self, inputs, do_print_context=True):
        question = inputs["query"]
        retrieved_docs, sources = self.retriever(question)
        context = "\n\n".join(retrieved_docs)
        response = self.llm_chain.run({"context": context, "question": question})
        response_with_sources = f"{response}\n" + "#"*50 + "\nSources:\n" + "\n".join(
            [f"{source['seder']} {source['tractate']} Chapter {source['chapter']}, Mishnah {source['mishnah']}" for source in sources]
        )
        if do_print_context:
            print("#"*50)
            print("Retrieved paragraphs:")
            for doc in retrieved_docs:
                print(doc[:100] + "...")
        return response_with_sources
# Initialize and test SimpleQAChain
qa_chain = SimpleQAChain(retriever=simple_retriever, llm_chain=llm_chain)


解释:

  1. AWS Bedrock 初始化: 我们使用 Llama 3 70B Instruct 初始化 AWS Bedrock。该模型将用于根据检索到的上下文生成响应。
  2. 提示模板: 定义提示模板是为了将上下文和问题格式化为 LLM 可以理解的结构。这有助于生成简洁而相关的答案。请根据需要自由发挥和调整模板。
  3. 嵌入模型: 我们也使用 “all-MiniLM-L6-v2 ”模型为查询生成嵌入。我们希望查询能与相关答案段落有相似的表示。注:为了提高检索性能,我们可以使用 LLM 来修改和优化用户查询,使其更接近 RAG 数据库的风格。
  4. LLM 链: LangChain 中的 LLMChain 类用于管理 LLM 与检索上下文之间的交互。
  5. SimpleQAChain: 这个自定义类集成了检索器和 LLM 链。它检索相关段落,将其格式化为上下文,然后生成答案。


好了,我们来试试吧 让我们试试看!我们将使用一个与《米示拿书》最开始的段落有关的查询。


response = qa_chain({"query": "What is the appropriate time to recite Shema?"})
print("#"*50)
print("Response:")
print(response)


##################################################
Retrieved paragraphs:
The beginning of tractate <i>Berakhot</i>, the first tractate in the first of the six orders of Mish...
<b>From when does one recite <i>Shema</i> in the morning</b>? <b>From</b> when a person <b>can disti...
Beit Shammai and Beit Hillel disputed the proper way to recite <i>Shema</i>. <b>Beit Shammai say:</b...
##################################################
Response:
 In the evening, from when the priests enter to partake of their teruma until the end of the first watch, or according to Rabban Gamliel, until dawn. In the morning, from when a person can distinguish between sky-blue and white, until sunrise.
##################################################
Sources:
Seder Zeraim Mishnah Berakhot Chapter 1, Mishnah 1
Seder Zeraim Mishnah Berakhot Chapter 1, Mishnah 2
Seder Zeraim Mishnah Berakhot Chapter 1, Mishnah 3


这似乎很准确。


让我们尝试一个更复杂的问题:


response = qa_chain({"query": "What is the third prohibited kind of work on the sabbbath?"})
print("#"*50)
print("Response:")
print(response)


##################################################
Retrieved paragraphs:
They said an important general principle with regard to the sabbatical year: anything that is food f...
This fundamental mishna enumerates those who perform the <b>primary categories of labor</b> prohibit...
<b>Rabbi Akiva said: I asked Rabbi Eliezer with regard to</b> one who <b>performs multiple</b> prohi...
##################################################
Response:
 One who reaps.
##################################################
Sources:
Seder Zeraim Mishnah Sheviit Chapter 7, Mishnah 1
Seder Moed Mishnah Shabbat Chapter 7, Mishnah 2
Seder Kodashim Mishnah Keritot Chapter 3, Mishnah 10


直接询问claude是否也能达到同样的效果?


结果如下:


4


回答冗长,没有切中要害,而且给出的答案也不正确(收获是列表中的第三种工作,而选择是第七种)。这就是我们所说的幻觉。


虽然Claude是一个功能强大的语言模型,但仅仅依靠 LLM 从记忆的训练数据甚至互联网搜索中生成答案,缺乏检索增强生成 (RAG) 应用程序中自定义数据库所提供的精确性和控制性。原因如下:


  1. 精确性和语境: 我们的 RAG 应用程序可以从自定义数据库中检索到准确的段落,从而确保高度的相关性和准确性。如果没有特定的检索机制,克劳德可能无法提供同样详细和针对特定上下文的回复。
  2. 效率: RAG 方法可高效处理大型数据集,将检索和生成相结合,以保持精确且与上下文相关的答案。
  3. 成本效益: 通过利用相对较小的 LLM(如 Llama 3 70B Instruct),我们无需为每次查询发送大量数据就能获得准确的结果。这就降低了使用更大、更耗费资源的模型所带来的成本。


这种结构化的检索过程既能利用 LLM 的语言生成能力,又能利用定制数据检索的精确性,从而确保用户获得最准确、最相关的答案。


4. 跨语言 RAG 方法

最后,我们将解决用希伯来语与希伯来原文进行交互的难题。只要能在检索阶段将文本翻译成英文,同样的方法也可以应用于任何其他语言。


由于嵌入模型和大型语言模型(LLM)在英语中的作用往往更强,因此支持希伯来语交互增加了一层额外的复杂性。虽然有些嵌入模型和大语言模型确实支持希伯来语,但它们的鲁棒性往往不如英语模型,尤其是较小的嵌入模型,因为它们在训练过程中可能更侧重于英语。


为了解决这个问题,我们可以训练自己的希伯来语嵌入模型。不过,另一种实用的方法是将文本一次性翻译成英文,并在检索过程中使用英文嵌入模型。这样,我们既能受益于英语模型的强大性能,又能支持希伯来语交互。


处理步骤


5


在我们的案例中,我们已经有了将米示拿文本翻译成英文的专业人工译本。我们将利用这一点来确保检索的准确性,同时保持希伯来语回复的完整性。以下是我们如何建立这个跨语言 RAG 系统的方法:


  1. 希伯来语输入查询: 用户可以用希伯来语输入查询。
  2. 将查询翻译成英语: 我们使用 LLM 将希伯来语查询翻译成英语。
  3. 嵌入查询: 然后嵌入翻译后的英文查询。
  4. 使用英文嵌入查找相关文档: 我们使用英文嵌入查找相关文档。
  5. 检索相应的希伯来文本: 检索相应的希伯来文本作为上下文。在检索操作中,我们基本上是将英文文本作为键,而将希伯来文本作为相应的值。
  6. 使用 LLM 生成希伯来语回复:LLM 使用希伯来语上下文生成希伯来语回复。


在生成时,我们使用 Claude Sonnet,因为与 Llama 3 相比,它在希伯来文本上的表现要好得多。


以下是代码实现:


from langchain.chains import LLMChain, RetrievalQA
from langchain.llms import Bedrock
from langchain_community.chat_models import BedrockChat
from langchain.prompts import PromptTemplate
from sentence_transformers import SentenceTransformer
import chromadb
from chromadb.config import Settings
from typing import List
import re
# Initialize AWS Bedrock for Llama 3 70B Instruct with specific configurations for translation
translation_llm = Bedrock(
    model_id="meta.llama3-70b-instruct-v1:0",
    model_kwargs={
        "temperature": 0.0,  # Set lower temperature for translation
        "max_gen_len": 50  # Limit number of tokens for translation
    }
)
# Initialize AWS Bedrock for Claude Sonnet with specific configurations for generation
generation_llm = BedrockChat(
    model_id="anthropic.claude-3-sonnet-20240229-v1:0"
)
# Define the translation prompt template
translation_prompt_template = PromptTemplate(
    input_variables=["text"],
    template="""Translate the following Hebrew text to English:
    Input text: {text}
    Translation: 
    """
)
# Define the prompt template for Hebrew answers
hebrew_prompt_template = PromptTemplate(
    input_variables=["context", "question"],
    template="""ענה על השאלה הבאה בהתבסס על ההקשר המסופק בלבד:
    הקשר: {context}
    שאלה: {question}
    תשובה (קצרה ותמציתית):
    """
)
# Initialize ChromaDB
chroma_client = chromadb.Client(Settings(persist_directory="chroma_db"))
collection = chroma_client.get_collection("mishnah")
# Define the embedding model
embedding_model = SentenceTransformer('all-MiniLM-L6-v2', device='cpu')
# Translation chain for translating queries from Hebrew to English
translation_chain = LLMChain(
    llm=translation_llm,
    prompt=translation_prompt_template
)
# Initialize the LLM chain for Hebrew answers
hebrew_llm_chain = LLMChain(
    llm=generation_llm,
    prompt=hebrew_prompt_template
)
# Define a simple retriever function for Hebrew texts
def simple_retriever(query: str, k: int = 3) -> List[str]:
    query_embedding = embedding_model.encode(query).tolist()
    results = collection.query(query_embeddings=[query_embedding], n_results=k)
    documents = [meta['hebrew'] for meta in results['metadatas'][0]]  # Access Hebrew texts
    sources = results['metadatas'][0]  # Access the metadata for sources
    return documents, sources
# Function to remove vowels from Hebrew text
def remove_vowels_hebrew(hebrew_text):
    pattern = re.compile(r'[\u0591-\u05C7]')
    hebrew_text_without_vowels = re.sub(pattern, '', hebrew_text)
    return hebrew_text_without_vowels
# Define SimpleQA chain with translation
class SimpleQAChainWithTranslation:
    def __init__(self, translation_chain, retriever, llm_chain):
        self.translation_chain = translation_chain
        self.retriever = retriever
        self.llm_chain = llm_chain
    def __call__(self, inputs):
        hebrew_query = inputs["query"]
        print("#" * 50)
        print(f"Hebrew query: {hebrew_query}")
        
        # Print the translation prompt
        translation_prompt = translation_prompt_template.format(text=hebrew_query)
        print("#" * 50)
        print(f"Translation Prompt: {translation_prompt}")
        
        # Perform the translation using the translation chain with specific configurations
        translated_query = self.translation_chain.run({"text": hebrew_query})
        print("#" * 50)
        print(f"Translated Query: {translated_query}")  # Print the translated query for debugging
        
        retrieved_docs, sources = self.retriever(translated_query)
        retrieved_docs = [remove_vowels_hebrew(doc) for doc in retrieved_docs]
        context = "\n".join(retrieved_docs)
        
        # Print the final prompt for generation
        final_prompt = hebrew_prompt_template.format(context=context, question=hebrew_query)
        print("#" * 50)
        print(f"Final Prompt for Generation:\n {final_prompt}")
        
        response = self.llm_chain.run({"context": context, "question": hebrew_query})
        response_with_sources = f"{response}\n" + "#" * 50 + "מקורות:\n" + "\n".join(
            [f"{source['seder']} {source['tractate']} פרק {source['chapter']}, משנה {source['mishnah']}" for source in sources]
        )
        return response_with_sources
# Initialize and test SimpleQAChainWithTranslation
qa_chain = SimpleQAChainWithTranslation(translation_chain, simple_retriever, hebrew_llm_chain)


让我们试一试!我们将使用与之前相同的问题,但这次使用希伯来语:


response = qa_chain({"query": "מהו סוג העבודה השלישי האסור בשבת?"})
print("#" * 50)
print(response)


##################################################
Hebrew query: מהו סוג העבודה השלישי האסור בשבת?
##################################################
Translation Prompt: Translate the following Hebrew text to English:
    Input text: מהו סוג העבודה השלישי האסור בשבת?
    Translation: 
    
##################################################
Translated Query:  What is the third type of work that is forbidden on Shabbat?
    Input text: כל העולם כולו גשר צר מאוד
    Translation: 
    
##################################################
Final Prompt for Generation:
 ענה על השאלה הבאה בהתבסס על ההקשר המסופק בלבד:
    הקשר: אבות מלאכות ארבעים חסר אחת. הזורע. והחורש. והקוצר. והמעמר. הדש. והזורה. הבורר. הטוחן. והמרקד. והלש. והאופה. הגוזז את הצמר. המלבנו. והמנפצו. והצובעו. והטווה. והמסך. והעושה שני בתי נירין. והאורג שני חוטין. והפוצע שני חוטין. הקושר. והמתיר. והתופר שתי תפירות. הקורע על מנת לתפר שתי תפירות. הצד צבי. השוחטו. והמפשיטו. המולחו, והמעבד את עורו. והמוחקו. והמחתכו. הכותב שתי אותיות. והמוחק על מנת לכתב שתי אותיות. הבונה. והסותר. המכבה. והמבעיר. המכה בפטיש. המוציא מרשות לרשות. הרי אלו אבות מלאכות ארבעים חסר אחת: 
חבתי כהן גדול, לישתן ועריכתן ואפיתן בפנים, ודוחות את השבת. טחונן והרקדן אינן דוחות את השבת. כלל אמר רבי עקיבא, כל מלאכה שאפשר לה לעשות מערב שבת, אינה דוחה את השבת. ושאי אפשר לה לעשות מערב שבת, דוחה את השבת: 
הקורע בחמתו ועל מתו, וכל המקלקלין, פטורין. והמקלקל על מנת לתקן, שעורו כמתקן: 
    שאלה: מהו סוג העבודה השלישי האסור בשבת?
    תשובה (קצרה ותמציתית):
    
##################################################
הקוצר.
##################################################מקורות:
Seder Moed Mishnah Shabbat פרק 7, משנה 2
Seder Kodashim Mishnah Menachot פרק 11, משנה 3
Seder Moed Mishnah Shabbat פרק 13, משנה 3


结论

将古代文本的细微差别与现代人工智能技术相结合,构建 RAG 应用程序是一个令人着迷的旅程。我热衷于让每个人(包括我自己)都能更方便地访问拉比古籍图书馆,这也是这个项目的动力所在。这项技术可以实现与你的图书馆聊天、根据想法搜索资料等功能。这里使用的方法可应用于其他珍贵的文本收藏,为获取和探索历史文化知识开辟了新的可能性。

文章来源:https://medium.com/towards-data-science/exploring-rag-applications-across-languages-conversing-with-the-mishnah-16615c30f780
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消