「Arxiv検索のマスタリング:Haystackを使用したQAチャットボットの構築のDIYガイド」をマスターする

『Haystackを活用したQAチャットボットのDIYガイド:Arxiv検索のマスタリング』をマスターしよう

イントロダクション

カスタムデータに関する質問と回答は、大規模言語モデルの最も求められるユースケースの一つです。LLMの人間のような対話スキルとベクトル検索手法を組み合わせることで、大量のドキュメントから回答を抽出することがより容易になります。いくつかのバリエーションを加えることで、ベクトルデータベースに埋め込まれたデータ(構造化、非構造化、準構造化)と対話するシステムを作成することができます。このクエリ埋め込みとドキュメント埋め込みの類似性スコアに基づいてLLMに取得データを追加する手法は、「RAGまたはRetrieval Augmented Generation」と呼ばれています。この手法により、arXiv論文の読解など、さまざまなことが簡単になります。

AIやコンピュータサイエンスに興味がある方なら、少なくとも一度は「arXiv」を聞いたことがあるでしょう。arXivは電子プレプリントおよびポストプリントのためのオープンアクセスリポジトリであり、ML、AI、数学、物理学、統計学、電子工学などのさまざまな主題の検証済み論文をホストしています。arXivは、AIや理系の研究のオープンな研究を推進する上で重要な役割を果たしています。しかし、研究論文を読むことはしばしば困難で時間がかかります。それでは、論文から関連するコンテンツを抽出し、回答を取得するためのRAGチャットボットを使用することで、少しでも改善することはできるでしょうか?

この記事では、Haystackというオープンソースツールを使用して、arXiv論文用のRAGチャットボットを作成します。

学習目標

  • Haystackとは何かを理解し、LLMを活用したアプリケーションを構築するためのコンポーネントを把握する。
  • 「arxiv」ライブラリを使用してArXiv論文を取得するコンポーネントを構築する。
  • Haystackノードでインデックスとクエリパイプラインを構築する方法を学ぶ。
  • Gradioを使用してチャットインターフェースを構築し、ベクトルストアからドキュメントを取得し、LLMから回答を生成するパイプラインを調整する方法を学ぶ。

この記事はData Science Blogathonの一環として公開されました。

Haystackとは何か?

HaystackはスケーラブルなLLMパワードアプリケーションを構築するためのオープンソースのNLPフレームワークです。Haystackはセマンティックサーチ、質問応答、RAGなどの本番向けNLPアプリケーションを構築するための非常にモジュラーかつカスタマイズ可能なアプローチを提供します。これはパイプラインとノードのコンセプトに基づいて構築されており、パイプラインはノードを繋げることで効率的なNLPアプリケーションを構築するのに非常に便利です。

  • ノード:ノードはHaystackの基本的な構成要素です。ノードはドキュメントの前処理、ベクトルストアからの取得、LLMからの回答生成など、一つのことを達成します。
  • パイプライン:パイプラインはノードを繋ぐためのもので、ノードの連鎖を構築するのが容易になります。これによってHaystackでアプリケーションを構築することが容易になります。

HaystackはWeaviate、Milvus、Elastic Search、Qdrantなど、主要なベクトルストアを直接サポートしています。詳細については、Haystackのパブリックリポジトリを参照してください:https://github.com/deepset-ai/haystack

したがって、この記事では、Haystackを使用してArxiv論文のためのQ&AチャットボットをGradioインターフェースで構築します。

Gradio

Gradioは、任意の機械学習アプリケーションのデモをセットアップおよび共有するためのHuggingfaceのオープンソースソリューションです。バックエンドにはFastapiが使用され、フロントエンドコンポーネントにはsvelteが使用されています。これにより、Pythonでカスタマイズ可能なWebアプリを作成することができます。機械学習モデルやコンセプトのデモアプリを構築して共有するのに最適です。詳細は、Gradioの公式GitHubをご覧ください。Gradioを使用したアプリケーションの構築については、「GradioでChat GPTを構築しましょう」という記事も参考にしてください。

チャットボットの構築

アプリケーションを作成する前に、ワークフローを簡単にチャート化してみましょう。ユーザーがArxiv論文のIDを入力して、クエリに対する回答を受け取るまでの流れです。したがって、ここではArxivチャットボットのシンプルなワークフローをご紹介します。

2つのパイプラインがあります:インデックスパイプラインとクエリパイプラインです。ユーザーがArxiv論文のIDを入力すると、Arxivコンポーネントに移動し、関連する論文を指定したディレクトリに取得してダウンロードし、インデックスパイプラインをトリガーします。インデックスパイプラインは4つのノードから成り立ち、それぞれが個別のタスクを実行します。それでは、これらのノードが何を行うか見てみましょう。

インデックスパイプライン

Haystackパイプラインでは、前のノードの出力が現在のノードの入力として使用されます。インデックスパイプラインでは、初期の入力はドキュメントへのパスです。

  • PDFToTextConverter:Arxivライブラリを使用すると、PDF形式の論文をダウンロードできます。しかし、データはテキスト形式で必要です。このノードではPDFからテキストを抽出します。
  • Preprocessor:抽出したデータは、ベクトルデータベースに保存する前にクリーニングと加工が必要です。このノードはテキストのクリーニングとチャンキングを行います。
  • EmbeddingRetriver:このノードではデータを格納するベクトルストアと、埋め込みモデルの定義を行います。
  • InMemoryDocumentStore:これは埋め込みが保存されるベクトルストアです。この場合、Haystackのデフォルトのインメモリドキュメントストアを使用しました。ただし、Qdrant、Weaviate、ElasticSearch、Milvusなどの他のベクトルストアも使用できます。

クエリパイプライン

ユーザーがクエリを送信すると、クエリパイプラインがトリガーされます。クエリパイプラインは、クエリ埋め込みに対してベクトルストアから「k」個の最近似ドキュメントを取得し、LLMレスポンスを生成します。こちらにも4つのノードがあります。

  • Retriever:クエリ埋め込みに対してベクトルストアから「k」個の最近似ドキュメントを取得します。
  • Sampler:クエリとドキュメントの類似スコアの累積確率に基づいてトップpサンプリングを使用してドキュメントをフィルタリングします。
  • LostInTheMiddleRanker:このアルゴリズムは抽出されたドキュメントを再配置します。より関連性の高い文書をコンテキストの先頭または末尾に配置します。
  • PromptNode:PromptNodeは、LLMに提供されたコンテキストからクエリに対する回答を生成する責任があります。

これがArxivチャットボットのワークフローについての説明でした。では、コーディングに入ってみましょう。

開発環境のセットアップ

依存関係をインストールする前に、仮想環境を作成します。VenvとPoetryを使用して仮想環境を作成できます。

python -m venv my-env-namesource bin/activate

さて、次に以下の開発依存関係をインストールします。Arxivの論文をダウンロードするために、Arxivライブラリをインストールする必要があります。

farm-haystackarxivgradio

そして、ライブラリをインポートします。

import arxivimport osfrom haystack.document_stores import InMemoryDocumentStorefrom haystack.nodes import (    EmbeddingRetriever,     PreProcessor,     PDFToTextConverter,     PromptNode,     PromptTemplate,     TopPSampler    )from haystack.nodes.ranker import LostInTheMiddleRankerfrom haystack.pipelines import Pipelineimport gradio as gr

Arxivコンポーネントの構築

このコンポーネントは、ArxivのPDFファイルのダウンロードと保存を担当します。ですので、以下はそのコンポーネントの定義方法です。

class ArxivComponent:    """    このコンポーネントは、arXiv IDに基づいてarXivの記事を取得する責任を持ちます。    """    def run(self, arxiv_id: str = None):        """        指定されたarXiv IDの記事を取得し、保存します。        Args:            arxiv_id (str): 取得する記事のarXiv ID。        """        # 記事が保存されるディレクトリパスを設定する        dir: str = DIR        # arXivクライアントのインスタンスを作成する        arxiv_client = arxiv.Client()        # arXiv IDが指定されているかを確認する;指定されていない場合はエラーを発生させる        if arxiv_id is None:            raise ValueError("記事を取得するためのarXiv IDを指定してください。")        # 指定されたarXiv IDでarXivの記事を検索する        search = arxiv.Search(id_list=[arxiv_id])        response = arxiv_client.results(search)        paper = next(response)  # 最初の結果を取得する        title = paper.title  # 記事のタイトルを抽出する        # 指定されたディレクトリが存在するか確認する        if os.path.isdir(dir):            # 既に記事のPDFファイルが存在するか確認する            if os.path.isfile(dir + "/" + title + ".pdf"):                return {"file_path": [dir + "/" + title + ".pdf"]}        else:            # ディレクトリが存在しない場合は作成する            os.mkdir(dir)        # arXiv記事のPDFをダウンロードしようとする        try:            paper.download_pdf(dirpath=dir, filename=title + ".pdf")            return {"file_path": [dir + "/" + title + ".pdf"]}        except:            # ダウンロード中にエラーが発生した場合はConnectionErrorを発生させる            raise ConnectionError(message=f"arXiv ID: {arxiv_id}のPDFのダウンロード中にエラーが発生しました。")

上記のコンポーネントは、Arxivクライアントを初期化し、IDに関連付けられたArxiv記事を取得し、すでにダウンロードされているかどうかを確認します。PDFのパスを返すかディレクトリにダウンロードします。

インデックス作成パイプラインの構築

次に、ベクトルデータベースにドキュメントを処理および保存するためのインデックス作成パイプラインを定義します。

document_store = InMemoryDocumentStore()embedding_retriever = EmbeddingRetriever(    document_store=document_store,     embedding_model="sentence-transformers/All-MiniLM-L6-V2",     model_format="sentence_transformers",     top_k=10    )def indexing_pipeline(file_path: str = None):    pdf_converter = PDFToTextConverter()    preprocessor = PreProcessor(split_by="word", split_length=250, split_overlap=30)        indexing_pipeline = Pipeline()    indexing_pipeline.add_node(        component=pdf_converter,         name="PDFConverter",         inputs=["File"]        )    indexing_pipeline.add_node(        component=preprocessor,         name="PreProcessor",         inputs=["PDFConverter"]        )    indexing_pipeline.add_node(        component=embedding_retriever,        name="EmbeddingRetriever",         inputs=["PreProcessor"]        )    indexing_pipeline.add_node(        component=document_store,         name="InMemoryDocumentStore",         inputs=["EmbeddingRetriever"]        )    indexing_pipeline.run(file_paths=file_path)

まず、インメモリドキュメントストアと埋め込みリトリーバーを定義します。埋め込みリトリーバーでは、ドキュメントストア、埋め込みモデル、および取得するドキュメントの数を指定します。

また、以前に説明した4つのノードも定義しています。pdf_converterはPDFをテキストに変換し、preprocessorはテキストのチャンクをクリーンアップして作成し、embedding_retrieverはドキュメントの埋め込みを作成し、InMemoryDocumentStoreはベクトル埋め込みを保存します。ファイルパスを引数として渡すrunメソッドはパイプラインをトリガーし、各ノードは定義された順序で実行されます。また、各ノードが前のノードの出力を使用していることにも注目できます。

クエリパイプラインの構築

クエリパイプラインも4つのノードで構成されています。これは、クエリテキストから埋め込みを取得し、ベクトルストアから類似のドキュメントを検索し、最後にLLMからの応答を生成する責任を持ちます。

def query_pipeline(query: str = None):    if not query:        raise gr.Error("Please provide a query.")    prompt_text = """Synthesize a comprehensive answer from the provided paragraphs of an Arxiv article and the given question.\nFocus on the question and avoid unnecessary information in your answer.\n\n\n Paragraphs: {join(documents)} \n\n Question: {query} \n\n Answer:"""    prompt_node = PromptNode(                         "gpt-3.5-turbo",                          default_prompt_template=PromptTemplate(prompt_text),                          api_key="api-key",                          max_length=768,                          model_kwargs={"stream": False},                         )    query_pipeline = Pipeline()    query_pipeline.add_node(        component = embedding_retriever,         name = "Retriever",         inputs=["Query"]        )    query_pipeline.add_node(        component=TopPSampler(        top_p=0.90),         name="Sampler",         inputs=["Retriever"]        )    query_pipeline.add_node(        component=LostInTheMiddleRanker(1024),         name="LostInTheMiddleRanker",         inputs=["Sampler"]        )    query_pipeline.add_node(        component=prompt_node,         name="Prompt",         inputs=["LostInTheMiddleRanker"]        )    pipeline_obj = query_pipeline.run(query = query)        return pipeline_obj["results"]

embedding_retrieverは、ベクトルストアから「k」個の類似ドキュメントを取得します。Samplerはドキュメントをサンプリングする責任を持ちます。LostInTheMiddleRankerは、コンテキストの開始または終了に関連性に基づいてドキュメントのランキングを行います。最後に、prompt_nodeにはLLMが「gpt-3.5-turbo」で使用されます。会話に更なる文脈を追加するためにプロンプトテンプレートも追加しました。runメソッドはパイプラインオブジェクト、つまり辞書を返します。

これが私たちのバックエンドでした。次に、インターフェースを設計します。

Gradioインターフェース

これには、カスタマイズ可能なWebインターフェースを作成するためのBlocksクラスがあります。したがって、このプロジェクトでは、Arxiv IDをユーザーの入力として受け取るテキストボックス、チャットインターフェース、およびユーザーのクエリを受け取るテキストボックスが必要です。以下のように実装できます。

with gr.Blocks() as demo:    with gr.Row():        with gr.Column(scale=60):            text_box = gr.Textbox(placeholder="Input Arxiv ID",                                   interactive=True).style(container=False)        with gr.Column(scale=40):            submit_id_btn = gr.Button(value="Submit")    with gr.Row():        chatbot = gr.Chatbot(value=[]).style(height=600)        with gr.Row():            with gr.Column(scale=70):                query = gr.Textbox(placeholder = "Enter query string",                                interactive=True).style(container=False)

コマンドラインでgradio app.pyコマンドを実行し、表示されたlocalhostのURLにアクセスします。

これで、トリガーイベントを定義する必要があります。

submit_id_btn.click(        fn = embed_arxiv,         inputs=[text_box],        outputs=[text_box],        )query.submit(            fn=add_text,             inputs=[chatbot, query],             outputs=[chatbot, ],             queue=False            ).success(            fn=get_response,            inputs = [chatbot, query],            outputs = [chatbot,]            )demo.queue()demo.launch()

イベントが機能するようにするには、各イベントに記載されている関数を定義する必要があります。submit_id_btnをクリックし、テキストボックスからの入力をembed_arxiv関数のパラメーターとして送信します。この関数は、ArxivのPDFの取得と保存をベクトルストアで調整します。

arxiv_obj = ArxivComponent()def embed_arxiv(arxiv_id: str):    """        Args:            arxiv_id: 取得する記事のArxiv ID。                   """    global FILE_PATH    dir: str = DIR       file_path: str = None    if not arxiv_id:        raise gr.Error("Arxiv IDを指定してください")    file_path_dict = arxiv_obj.run(arxiv_id)    file_path = file_path_dict["file_path"]    FILE_PATH = file_path    indexing_pipeline(file_path=file_path)    return"ファイルの埋め込みに成功しました"

ArxivComponentオブジェクトとembed_arxiv関数を定義しました。”run”メソッドを実行し、返されたファイルパスをIndexing Pipelineのパラメーターとして使用します。

次に、add_text関数をパラメーターとして持つsubmitイベントに移動します。これは、チャットインターフェースでのチャットのレンダリングに責任があります。

def add_text(history, text: str):    if not text:         raise gr.Error('テキストを入力してください')    history = history + [(text,'')]     return history

次に、get_response関数を定義します。この関数は、チャットインターフェースでLLMの応答を取得しストリーム化します。

def get_response(history, query: str):    if not query:        gr.Error("クエリを入力してください")        response = query_pipeline(query=query)    for text in response[0]:        history[-1][1] += text        yield history, ""

この関数はクエリ文字列を受け取り、クエリパイプラインに渡して応答を取得します。最後に、応答文字列を繰り返してチャットボットに返します。

すべてを1つにまとめましょう。

# Create an instance of the ArxivComponent classarxiv_obj = ArxivComponent()def embed_arxiv(arxiv_id: str):    """    指定されたarXiv IDのarXiv記事を取得し埋め込みます。    Args:        arxiv_id (str): 取得する記事のArXiv ID。    """    # アクセスするためのグローバルな FILE_PATH 変数    global FILE_PATH        # arXivの記事を保存するディレクトリを設定    dir: str = DIR        # file_path を None で初期化    file_path: str = None        # arXiv ID が指定されたかチェック    if not arxiv_id:        raise gr.Error("Arxiv IDを指定してください")        # ArxivComponentのrunメソッドを呼び出してarXivの記事を取得・保存    file_path_dict = arxiv_obj.run(arxiv_id)        # 辞書からファイルパスを抽出    file_path = file_path_dict["file_path"]        # グローバルな FILE_PATH 変数を更新    FILE_PATH = file_path        # ダウンロードされた記事を処理するために indexing_pipeline 関数を呼び出す    indexing_pipeline(file_path=file_path)    return "ファイルの埋め込みに成功しました"def get_response(history, query: str):    if not query:        gr.Error("クエリを入力してください")        # ユーザーのクエリを処理するためにquery_pipeline関数を呼び出す    response = query_pipeline(query=query)        # 応答をチャット履歴に追加    for text in response[0]:        history[-1][1] += text        yield historydef add_text(history, text: str):    if not text:        raise gr.Error('テキストを入力してください')        # ユーザーが提供したテキストをチャット履歴に追加    history = history + [(text, '')]    return history# gr.Blocksを使用してGradioのインターフェースを作成しますwith gr.Blocks() as demo:    with gr.Row():        with gr.Column(scale=60):            # Arxiv IDのためのテキストボックス            text_box = gr.Textbox(placeholder="Arxiv IDを入力",                                   interactive=True).style(container=False)        with gr.Column(scale=40):            # Arxiv IDを送信するボタン            submit_id_btn = gr.Button(value="送信")        with gr.Row():        # チャットボットインターフェース        chatbot = gr.Chatbot(value=[]).style(height=600)        with gr.Row():        with gr.Column(scale=70):            # ユーザーのクエリ文字列のためのテキストボックス            query = gr.Textbox(placeholder="クエリ文字列を入力",                                interactive=True).style(container=False)        # ボタンクリックとクエリ送信のためのアクションを定義    submit_id_btn.click(        fn=embed_arxiv,         inputs=[text_box],        outputs=[text_box],    )    query.submit(        fn=add_text,         inputs=[chatbot, query],         outputs=[chatbot, ],         queue=False    ).success(        fn=get_response,        inputs=[chatbot, query],        outputs=[chatbot,]    )# インターフェースをキューに追加して起動demo.queue()demo.launch()

アプリケーションを実行するには、gradio app.pyというコマンドを使用して、URLにアクセスしてArxic Chatbotと対話します。

こうなります。

このアプリに関するGitHubリポジトリはsunilkumardash9/chat-arxivです。

改良の可能性

私たちは任意のArxiv論文とチャットするためのシンプルなアプリケーションを成功裏に構築しましたが、いくつかの改善が施されることがあります。

  • スタンドアロンのベクトルストア:既製のベクトルストアではなく、Haystackで利用可能なスタンドアロンのベクトルストア(Weaviate、Milvusなど)を使用することができます。これにより、より柔軟性が増し、性能も向上します。
  • 引用:適切な引用を追加することで、LLMの応答に確実性をもたせることができます。
  • その他の機能:チャットインターフェースだけでなく、LLMの応答のソースとして使用されるPDFのページを表示する機能を追加することができます。この記事「Build a ChatGPT for PDFs with Langchain」とGitHubリポジトリを参照してください。
  • フロントエンド:より優れた、よりインタラクティブなフロントエンドが望まれます。

結論

以上が、Arxiv論文のためのチャットアプリを構築する方法についてのすべてです。このアプリケーションはArxivに限定されません。同じアーキテクチャを使用して他のサイト(例:PubMed)ともチャットすることができます。この記事では、Arxivの論文をダウンロードするArxivコンポーネントを作成し、haystackパイプラインを使用して埋め込み、最後にLLMから回答を取得する方法について解説しました。

キーポイント

  • Haystackは、スケーラブルで本番環境向けのNLPアプリケーションを構築するためのオープンソースソリューションです。
  • Haystackは、情報検索、データ前処理、埋め込み、回答生成のためのノードとパイプラインを提供し、実世界のアプリケーションの構築を効率化します。
  • これはHuggingfaceのオープンソースライブラリで、任意のアプリケーションの簡単なプロトタイピングを提供します。MLモデルを他のユーザーと簡単に共有する方法を提供します。
  • 同様のワークフローを使用して、PubMedなど他のサイトのチャットアプリを構築することができます。

よくある質問

この記事で表示されるメディアはAnalytics Vidhyaの所有ではなく、著者の裁量で使用されています。

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

人工知能

「コーネリスネットワークスのソフトウェアエンジニアリング担当副社長、ダグ・フラーラー氏 - インタビューシリーズ」

ソフトウェアエンジニアリングの副社長として、DougはCornelis Networksのソフトウェアスタック全体、Omni-Path Architecture...

人工知能

スコット・スティーブンソン、スペルブックの共同創設者兼CEO- インタビューシリーズ

スコット・スティーブンソンは、Spellbookの共同創設者兼CEOであり、OpenAIのGPT-4および他の大規模な言語モデル(LLM)に基...

機械学習

もし芸術が私たちの人間性を表現する方法であるなら、人工知能はどこに適合するのでしょうか?

MITのポストドクターであるジヴ・エプスタイン氏(SM '19、PhD '23)は、芸術やその他のメディアを作成するために生成的AIを...

人工知能

ベイリー・カクスマー、ウォータールー大学の博士課程候補 - インタビューシリーズ

カツマー・ベイリーは、ウォータールー大学のコンピュータ科学学部の博士課程の候補者であり、アルバータ大学の新入教員です...

データサイエンス

「3つの質問:ロボットの認識とマッピングの研磨」

MIT LIDSのLuca CarloneさんとJonathan Howさんは、将来のロボットが環境をどのように知覚し、相互作用するかについて議論し...

人工知能

「aiOlaのCEO兼共同創設者、アミール・ハラマティによるインタビューシリーズ」

アミール・ハラマティは、aiOlaのCEO兼共同創業者であり、スピーチを作業可能にし、どこでも完全な正確さで業界固有のプロセ...