GPT-4VをつかったMulti-modal RAGの実装 (2) 

GPT-4VをつかったMulti-modal RAGの実装について解説します。

この記事は前回の記事の続きになります。前回の記事(2.4まで）はこちらです。

https://qiita.com/mashmoeiar11/items/c89ae3f5084680676611

### 2.5 Multivector Retrieverの作成

次は抽出された元データおよびサマリを用いて`MultivectorRetriever`を構築していきます。

```python:
from langchain.vectorstores import Chroma
from langchain.storage import InMemoryStore
from langchain.schema.document import Document
from langchain.embeddings import OpenAIEmbeddings
from langchain.retrievers.multi_vector import MultiVectorRetriever

def create_vectorstore(vectorstore, docstore, texts, table_summaries, image_summaries, tables, img_base64_list):
    
    id_key='doc_id'
    retriever = MultiVectorRetriever(vectorstore=vectorstore, docstore=docstore, id_key=id_key)
    
    # Add texts
    doc_ids = [str(uuid.uuid4()) for _ in texts]  
    for i, s in enumerate(texts):
        retriever.vectorstore.add_documents([Document(page_content=s, metadata={id_key: doc_ids[i]})])
    retriever.docstore.mset(list(zip(doc_ids, texts)))  
        
    # Add tables
    table_ids = [str(uuid.uuid4()) for _ in tables]
    for i, s in enumerate(table_summaries):
        retriever.vectorstore.add_documents([Document(page_content=s, metadata={id_key: table_ids[i]})])
    retriever.docstore.mset(list(zip(table_ids, tables)))
        
    # Add image summaries
    img_ids = [str(uuid.uuid4()) for _ in img_base64_list]
    for i, s in enumerate(image_summaries):
        retriever.vectorstore.add_documents([Document(page_content=s, metadata={id_key: img_ids[i]})])
    retriever.docstore.mset(list(zip(img_ids, img_base64_list)))

    return retriever
```
基本的にここでやっていることはサマリ（テキストデータ）を`vectorstore`へ、そして元データ（画像やテーブルなど）を`docstore`へ追加して、`MultivectorRetriever`を構成します。これら2つのデータベースはお互いに`id_key`を通して紐づいています。このような構成にすることで、通常のRAGと同様に、ベクトル検索はテキストに対してのみ行われることになります。

`uuid.uuid4()`はランダムな`id_key`を生成するために使用されています。ここでは`retriever`のみが返り値になっていますが、あとで`table_ids`や`img_ids`なども使いたい場合には、これらも返り値に含めてもよいかもしれません。

### 2.6 RAG (Retrieval-Augmented Generation)の適用

ようやく終盤です。それではRAG部分をみていきましょう:point_up:

```python:
from langchain.schema.runnable import RunnablePassthrough, RunnableLambda
from langchain.callbacks import get_openai_callback

def rag_application(question, retriever):
    docs = retriever.get_relevant_documents(question)
    docs_by_type = split_image_text_types(docs)
    
    # テーブルの取得と表示
    if len(docs_by_type["texts"]):
        for doc_id in docs_by_type["texts"]:
            doc = retriever.docstore.mget([doc_id])
            try:
                doc_html = convert_html(doc)
                display(HTML(doc_html))
            except Exception as e:
                print(doc)
                
    # 画像の取得と表示           
    if len(docs_by_type["images"]):
        plt_img_base64(docs_by_type["images"][0])

    model = ChatOpenAI(model="gpt-4-vision-preview", max_tokens=1024, temperature=0)
    chain = (
        {"context": retriever | RunnableLambda(split_image_text_types), "question": RunnablePassthrough()}
        | RunnableLambda(generate_prompt)
        | model
        | StrOutputParser()
    )
    answer = chain.invoke(question)
    
    return answer
```
上から順番に行きます。まず`get_relevant_documents`部分で、質問に関連するチャンク文書を取ってきます。そのチャンクを`split_image_text_types`をつかって画像かどうかで分類しています。その次の部分では画像でないもの（すわなち、テキストかテーブル）に対して`convert_html`を実行し、チャンクがテーブルのサマリである場合はHTMLに変換してテーブルを表示する、そうでない場合はテキストを表示する、ようなことをさせています。

が、おそらくこのようにするよりは、先ほど`MultivectorRetriever`を定義したところから、`table_ids`をとってきて、テーブルのみにループをまわすか、もしくは例外処理では何もしないようにした方がいい気がしてます:sweat_smile:

次に画像の扱いです。関連するチャンクに画像が含まれている場合は、`plt_img_base64`という関数をつかって実際に画像を表示させます。

最後は`chain`の部分です。まずretrieverで質問に関連するチャンク(`context`)を検索し、画像とテキスト（またはテーブル）に分類します。元の質問は`Runnablepassthrough()`を通して、そのまま次に渡されます。次に、`RunnableLambda(generate_prompt)`で、画像とテキストのデータからプロンプトを生成します。ここでは質問に回答するための追加情報として画像以外のチャンク情報を渡し、画像サマリのチャンクに関しては、エンコードされた画像をそのまま追加情報として入れ込み、GPT-4Vでまとめて解釈＆回答してもらうという形式をとっています。単なる画像サマリではなく、画像そのものを追加情報とすることで、より質問の文脈に近い形で画像が解釈されることになります。RAGは`chain.invoke`メソッドを使って実行されます。

以下は`rag_application`の内部で呼ばれている関数です。
```python:
from base64 import b64decode
from IPython.display import display, HTML
from langchain.chat_models import ChatOpenAI
from langchain.schema.messages import HumanMessage

def split_image_text_types(docs):
    b64 = []
    text = []
    for doc in docs:
        try:
            b64decode(doc)
            b64.append(doc)
        except Exception:
            text.append(doc)
    return {"images": b64, "texts": text}

def plt_img_base64(img_base64):
    image_html = f'<img src="data:image/jpeg;base64,{img_base64}" />'
    display(HTML(image_html))

def convert_html(element):
    input_text = str(element)
    prompt_text = f"""
    回答例にならって、テキストをHTMLテーブル形式に変換してください:
    
    テキスト：
    {input_text}
    
    回答例：
    項目 果物(kg) お菓子(kg) ナッツ(kg) 飲み物(L) 予想 45 20 15 60 実績 50 25 10 80 
    差(実績-予想) 5 5 -5 -20
    →
    <table>
      <tr>
        <th>項目</th>
        <th>果物(kg)</th>
        <th>お菓子(kg)</th>
     ・・・
      </tr>
    </table>
    """
    message = HumanMessage(content=[
                {"type": "text", "text": prompt_text}
              ])
    model = ChatOpenAI(model="gpt-3.5-turbo", max_tokens=1024)
    response = model.invoke([message])
    return response.content

def generate_prompt(dict):
        format_texts = "\n\n".join(dict["context"]["texts"])
        prompt_text = f"""
        以下の質問に基づいて回答を生成してください。
        回答は、提供された追加情報を考慮してください。
    
        質問: {dict["question"]}

        追加情報: {format_texts}
        """
        message_content = [{"type": "text", "text": prompt_text}]

        # 画像が存在する場合のみ画像URLを追加
        if dict["context"]["images"]:
            image_url = f"data:image/jpeg;base64,{dict['context']['images'][0]}"
            message_content.append({"type": "image_url", "image_url": {"url": image_url}})

        return [HumanMessage(content=message_content)]
```

### 2.7 実行部分

それでは今まで説明した個々の関数がどのように実行されていくのかを全体の流れを追いながらみていきます。
今回は、簡単な物語をもとに、PDFでの実装試験をしました。レオドットがポルカドットパーティーを企画する話です！かわいい:heart_eyes_cat:

![Screenshot 2023-11-25 112930.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3565029/a4b483fc-3393-15a1-8ce3-161d2166ce6b.png)

```python: 実行部分の実装
def main():
    # PDFの分解（テキスト、テーブル、画像）
    tables, texts = process_pdf("input/レオドットの話.pdf")
    # テーブルのサマリ作成
    table_summaries = summarize_tables(tables)
    # 画像のサマリ作成
    img_base64_list, image_summaries = summarize_images()
    
    #Multivector Retrieverの作成
    vectorstore = Chroma(collection_name="multi_modal_rag", embedding_function=OpenAIEmbeddings())
    docstore = InMemoryStore()
    retriever = create_vectorstore(vectorstore, docstore, texts, table_summaries, image_summaries, tables, img_base64_list)

    questions = [
        "レオドットの容姿について教えてください。",
        "ポルカドットパーティでの飲み物の消費量をおしえてください。"
    ]
    
    for query in questions:
        print(f"Q: {query}")
        result = rag_application(query, retriever)
        print(f"A: {result}\n\n")
        print("----------------------------------")
    
if __name__ == "__main__":
    main()
```
全体の流れのおさらいです。
(1) PDFを分解し、画像、テーブル、テキストなどの要素を抽出
(2) 抽出された画像やテーブルをもとにサマリを作成
(3) サマリと元データをもとに`MultivectorRetriever`を作成
(4) RAGの実行

# 3. 実行結果

それでは実行結果をみていきましょう！

```txt
Q: レオドットの容姿について教えてください。
A: レオドットは、キャラクターとして描かれたライオンのようです。彼は黄褐色の体を持ち、顔と耳の周りには赤と
白のドットがある赤いたてがみをしています。彼の耳も赤いドットで飾られており、顔には優しい表情をしています。
目は黒くて丸く、鼻は小さくて茶色です。彼は片足を上げており、動きのあるポーズをとっているように見えます。全
体的に、彼は楽しげでフレンドリーなキャラクターとしてデザインされているようです。
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3565029/d282e411-9929-8e64-421a-a20f406dde38.png)

容姿についての質問に関してはきちんと画像も出してくれました！かわいい:relaxed:
次はパーティーに関する詳細情報を聞いてみましょう。テーブルを出してもらうための質問です。

```txt
Q: ポルカドットパーティでの飲み物の消費量をおしえてください。
A: レオドットの「ポルカドットパーティ」での飲み物の消費量は、予想されていた60リットルを上回り、実際には
80リットルが消費されました。これは予想よりも20リットル多い量です。パーティでは、参加者たちが熱心にダンス
を楽しんだため、特に飲み物の消費が多かったようです。
```
![Screenshot 2023-11-25 114838.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3565029/a3b29916-c926-35a4-2e35-ddb890d539d9.png)

Jupyter NotebookだとHTML形式のテーブルを出してくれました！

# 4. まとめ
本記事では、Multi-modal RAG（Retrieval Augmented Generation）の実装における、異なるデータタイプ（画像、テキスト、テーブル）の統合処理について詳しく見てきました。このアプローチでは、テキスト情報だけでなく、画像やテーブルといった非テキスト要素もベクトルストアに組み込むことで、RAGの精度を高めることが可能です。これにより、よりリッチで多様な情報源を活用し、複雑なクエリに対してより正確な回答を生成することが可能になります。

今回の実装では、PDFからテキスト、画像、テーブルを抽出し、これらの要素を効果的に処理して、LangChainライブラリを活用したRAGシステムに組み込みました。これにより、質問に対する回答を生成する際に、画像やテーブルからの情報も考慮することができます。特に、画像に関しては、GPT-4モデルのビジョン機能を活用することで、直接的なビジュアル情報も含めた回答生成が可能になりました。

このように、Multi-modal RAGの実装は、AIによる情報解析の範囲を大幅に拡張し、より豊かな情報提供を可能にします。これは、AIと人間のインタラクションをよりナチュラルで、情報量豊富なものに変えるための重要な一歩です。今後も、このようなテクノロジーの進化により、我々の日常生活やビジネスのあり方がよりスマートで効率的なものになっていくことでしょう。

最後まで読んでいただきありがとうございます。最近X(Twitter)もはじめたのでこちらもよろしくお願いします！

https://x.com/keisuke_yoshiha

＊本記事はcode spnippetをもとにChatGPTにつくらせています。

# 参考文献
https://blog.langchain.dev/semi-structured-multi-modal-rag/

https://github.com/langchain-ai/langchain/blob/master/cookbook/Semi_structured_multi_modal_RAG_LLaMA2.ipynb?ref=blog.langchain.dev

https://github.com/sudarshan-koirala/youtube-stuffs/blob/main/langchain/LangChain_Multi_modal_RAG.ipynb