Gemini Pro を使用してマルチモーダル RAG で Q&A アプリを作成する

1. はじめに

RAG とは

検索拡張生成(RAG)は、大規模言語モデル(LLM)の能力と、外部のナレッジソースから関連情報を取得する機能を組み合わせた手法です。つまり、LLM は社内のトレーニング データだけでなく、回答を生成する際に特定の最新の情報にアクセスし、それを組み込むこともできます。

936b7eedba773cac.png

RAG の人気が高まっている理由はいくつかあります

  • 精度と関連性の向上: RAG を使用すると、LLM を外部ソースから取得した事実に基づく情報に基づかせることで、LLM はより正確で関連性の高い回答を提供できます。これは、時事問題に関する質問に答えたり、特定のトピックに関する情報を提供する場合など、最新情報が重要な場面で特に役立ちます。
  • ハルシネーションの低減: LLM は、もっともらしく見えるものの、実際には不正確または意味をなさない回答を生成することがあります。RAG は、生成された情報を外部ソースと照らし合わせて検証することで、この問題を軽減します。
  • 適応性の向上: RAG を使用すると、さまざまな分野やタスクに対する LLM の適応性が向上します。さまざまな情報源を活用することで、LLM を簡単にカスタマイズして幅広いトピックに関する情報を提供できます。
  • ユーザー エクスペリエンスの向上: RAG は、より有益で信頼性の高い、関連性の高いレスポンスを提供することで、全体的なユーザー エクスペリエンスを改善できます。

マルチモーダルを選ぶ理由

昨今のデータが豊富な世界では、ドキュメントでテキストと画像を組み合わせて情報を包括的に伝達することがよくあります。しかし、ほとんどの検索拡張生成(RAG)システムは、画像にロックされた貴重な分析情報を見落としています。マルチモーダルの大規模言語モデル(LLM)が注目されるにつれ、RAG でビジュアル コンテンツをテキストとともに活用し、情報環境をより深く理解する方法を探ることが重要になっています。

マルチモーダル RAG の 2 つのオプション

  • マルチモーダル エンベディング - マルチモーダル エンベディング モデルは、提供された入力に基づいて 1,408 次元のベクトル* を生成します。これには、画像、テキスト、動画データの組み合わせが含まれます。画像エンベディング ベクトルとテキスト エンベディング ベクトルは、同じ次元を持つ同じセマンティック空間にあります。そのため、これらのベクトルは、テキストによる画像の検索や画像による動画の検索などのユースケースでも使用できます。こちらのデモをご覧ください。
  1. マルチモーダル エンベディングを使用してテキストや画像を埋め込む
  2. 類似度検索を使用して両方を取得する
  3. 取得した未加工画像とテキスト チャンクの両方をマルチモーダル LLM に渡して回答の合成を行う
  • テキスト エンベディング -
  1. マルチモーダル LLM を使用して画像の要約テキストを生成する
  2. 埋め込みとテキストの取得
  3. 回答の合成のためにテキスト チャックを LLM に渡す

Multi-Vector Retriever とは

マルチベクトル取得では、ドキュメント セクションの要約を使用して、回答の合成のために元のコンテンツを取得します。特に、表、グラフ、チャートなど集中的なタスクにおいて、RAG の品質が向上します。詳細については、Langchain のブログ.

作成するアプリの概要

ユースケース: Gemini Pro を使用した質問応答システムの開発

情報が詰まった複雑なグラフや図を含むドキュメントがあるとします。このデータを抽出して、質問やクエリの答えを得ようとしています。

この Codelab では、以下のことを行います。

  • LangChain document_loaders を使用したデータの読み込み
  • Google の gemini-pro モデルを使用してテキスト要約を生成する
  • Google の gemini-pro-vision モデルを使用して画像の要約を生成します
  • Croma Db をベクトルストアとする Google の textembedding-gecko モデルを使用してマルチベクトル取得を作成します。
  • 質問応答用のマルチモーダル RAG チェーンを開発する

2. 始める前に

  1. Google Cloud コンソールのプロジェクト選択ページで、Google Cloud プロジェクトを選択または作成します。
  2. Google Cloud プロジェクトに対して課金が有効になっていることを確認します。詳しくは、プロジェクトで課金が有効になっているかどうかを確認する方法をご覧ください。
  3. Vertex AI ダッシュボードですべての推奨 API を有効にする
  4. Colab ノートブックを開き、現在アクティブな Google Cloud アカウントと同じアカウントにログインします。

3. マルチモーダル RAG の構築

この Codelab では、Vertex AI SDK for PythonLangchain を使用して、こちらで説明されている「オプション 2」を Google Cloud で実装する方法を示します。

完全なコードは、参照されているリポジトリMulti-modal RAG with Google Cloud ファイルで確認できます。

4. ステップ 1: 依存関係をインストールしてインポートする

!pip install -U --quiet langchain langchain_community chromadb  langchain-google-vertexai
!pip install --quiet "unstructured[all-docs]" pypdf pillow pydantic lxml pillow matplotlib chromadb tiktoken

プロジェクト ID を入力して認証を完了する

#TODO : ENter project and location
PROJECT_ID = ""
REGION = "us-central1"

from google.colab import auth
auth.authenticate_user()

Vertex AI Platform を初期化する

import vertexai
vertexai.init(project = PROJECT_ID , location = REGION)

5. ステップ 2: データを準備して読み込む

使用する ZIP ファイルには、こちらのブログ投稿から抽出した画像と PDF のサブセットが含まれています。完全なフローに従う場合は、元のを使用してください。

まずデータをダウンロード

import logging
import zipfile
import requests

logging.basicConfig(level=logging.INFO)

data_url = "https://storage.googleapis.com/benchmarks-artifacts/langchain-docs-benchmarking/cj.zip"
result = requests.get(data_url)
filename = "cj.zip"
with open(filename, "wb") as file:
   file.write(result.content)

with zipfile.ZipFile(filename, "r") as zip_ref:
   zip_ref.extractall()

ドキュメントからテキスト コンテンツを読み込む

from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("./cj/cj.pdf")
docs = loader.load()
tables = []
texts = [d.page_content for d in docs]

最初のページのコンテンツを確認する

texts[0]

出力が表示されます。

2c5c257779c0f52a.png

ドキュメントのページの合計数

len(texts)

想定される出力は次のとおりです。

b5700c0c1376abc2.png

6. ステップ 3: テキストの要約を生成する

必要なライブラリを最初にインポートする

from langchain_google_vertexai import VertexAI , ChatVertexAI , VertexAIEmbeddings
from langchain.prompts import PromptTemplate
from langchain_core.messages import AIMessage
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda

テキストの要約を取得

# Generate summaries of text elements
def generate_text_summaries(texts, tables, summarize_texts=False):
   """
   Summarize text elements
   texts: List of str
   tables: List of str
   summarize_texts: Bool to summarize texts
   """

   # Prompt
   prompt_text = """You are an assistant tasked with summarizing tables and text for retrieval. \
   These summaries will be embedded and used to retrieve the raw text or table elements. \
   Give a concise summary of the table or text that is well optimized for retrieval. Table or text: {element} """
   prompt = PromptTemplate.from_template(prompt_text)
   empty_response = RunnableLambda(
       lambda x: AIMessage(content="Error processing document")
   )
   # Text summary chain
   model = VertexAI(
       temperature=0, model_name="gemini-pro", max_output_tokens=1024
   ).with_fallbacks([empty_response])
   summarize_chain = {"element": lambda x: x} | prompt | model | StrOutputParser()

   # Initialize empty summaries
   text_summaries = []
   table_summaries = []

   # Apply to text if texts are provided and summarization is requested
   if texts and summarize_texts:
       text_summaries = summarize_chain.batch(texts, {"max_concurrency": 1})
   elif texts:
       text_summaries = texts

   # Apply to tables if tables are provided
   if tables:
       table_summaries = summarize_chain.batch(tables, {"max_concurrency": 1})

   return text_summaries, table_summaries


# Get text summaries
text_summaries, table_summaries = generate_text_summaries(
   texts, tables, summarize_texts=True
)

text_summaries[0]

想定される出力は次のとおりです。

aa76e4b523d8a958.png

7. ステップ 4: 画像の概要を生成する

必要なライブラリを最初にインポートする

import base64
import os

from langchain_core.messages import HumanMessage

画像の要約を生成

def encode_image(image_path):
   """Getting the base64 string"""
   with open(image_path, "rb") as image_file:
       return base64.b64encode(image_file.read()).decode("utf-8")


def image_summarize(img_base64, prompt):
   """Make image summary"""
   model = ChatVertexAI(model_name="gemini-pro-vision", max_output_tokens=1024)

   msg = model(
       [
           HumanMessage(
               content=[
                   {"type": "text", "text": prompt},
                   {
                       "type": "image_url",
                       "image_url": {"url": f"data:image/jpeg;base64,{img_base64}"},
                   },
               ]
           )
       ]
   )
   return msg.content


def generate_img_summaries(path):
   """
   Generate summaries and base64 encoded strings for images
   path: Path to list of .jpg files extracted by Unstructured
   """

   # Store base64 encoded images
   img_base64_list = []

   # Store image summaries
   image_summaries = []

   # Prompt
   prompt = """You are an assistant tasked with summarizing images for retrieval. \
   These summaries will be embedded and used to retrieve the raw image. \
   Give a concise summary of the image that is well optimized for retrieval."""

   # Apply to images
   for img_file in sorted(os.listdir(path)):
       if img_file.endswith(".jpg"):
           img_path = os.path.join(path, img_file)
           base64_image = encode_image(img_path)
           img_base64_list.append(base64_image)
           image_summaries.append(image_summarize(base64_image, prompt))

   return img_base64_list, image_summaries


# Image summaries
img_base64_list, image_summaries = generate_img_summaries("./cj")

len(img_base64_list)

len(image_summaries)

image_summaries[0]

次のような出力が表示されます。fad6d479dd46cb37.png

8. ステップ 5: マルチベクトル取得を構築する

テキストと画像の要約を生成し、ChromaDB vectorstore に保存しましょう。

インポートにはライブラリが必要

import uuid
from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain.storage import InMemoryStore
from langchain_community.vectorstores import Chroma
from langchain_core.documents import Document

マルチベクター取得の作成

def create_multi_vector_retriever(
   vectorstore, text_summaries, texts, table_summaries, tables, image_summaries, images
):
   """
   Create retriever that indexes summaries, but returns raw images or texts
   """

   # Initialize the storage layer
   store = InMemoryStore()
   id_key = "doc_id"

   # Create the multi-vector retriever
   retriever = MultiVectorRetriever(
       vectorstore=vectorstore,
       docstore=store,
       id_key=id_key,
   )

   # Helper function to add documents to the vectorstore and docstore
   def add_documents(retriever, doc_summaries, doc_contents):
       doc_ids = [str(uuid.uuid4()) for _ in doc_contents]
       summary_docs = [
           Document(page_content=s, metadata={id_key: doc_ids[i]})
           for i, s in enumerate(doc_summaries)
       ]
       retriever.vectorstore.add_documents(summary_docs)
       retriever.docstore.mset(list(zip(doc_ids, doc_contents)))

   # Add texts, tables, and images
   # Check that text_summaries is not empty before adding
   if text_summaries:
       add_documents(retriever, text_summaries, texts)
   # Check that table_summaries is not empty before adding
   if table_summaries:
       add_documents(retriever, table_summaries, tables)
   # Check that image_summaries is not empty before adding
   if image_summaries:
       add_documents(retriever, image_summaries, images)

   return retriever


# The vectorstore to use to index the summaries
vectorstore = Chroma(
   collection_name="mm_rag_cj_blog",
   embedding_function=VertexAIEmbeddings(model_name="textembedding-gecko@latest"),
)

# Create retriever
retriever_multi_vector_img = create_multi_vector_retriever(
   vectorstore,
   text_summaries,
   texts,
   table_summaries,
   tables,
   image_summaries,
   img_base64_list,
)
 

9. ステップ 6: マルチモーダル RAG を構築する

  1. ユーティリティ関数を定義する
import io
import re

from IPython.display import HTML, display
from langchain_core.runnables import RunnableLambda, RunnablePassthrough
from PIL import Image


def plt_img_base64(img_base64):
   """Disply base64 encoded string as image"""
   # Create an HTML img tag with the base64 string as the source
   image_html = f'<img src="data:image/jpeg;base64,{img_base64}" />'
   # Display the image by rendering the HTML
   display(HTML(image_html))


def looks_like_base64(sb):
   """Check if the string looks like base64"""
   return re.match("^[A-Za-z0-9+/]+[=]{0,2}$", sb) is not None


def is_image_data(b64data):
   """
   Check if the base64 data is an image by looking at the start of the data
   """
   image_signatures = {
       b"\xFF\xD8\xFF": "jpg",
       b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A": "png",
       b"\x47\x49\x46\x38": "gif",
       b"\x52\x49\x46\x46": "webp",
   }
   try:
       header = base64.b64decode(b64data)[:8]  # Decode and get the first 8 bytes
       for sig, format in image_signatures.items():
           if header.startswith(sig):
               return True
       return False
   except Exception:
       return False


def resize_base64_image(base64_string, size=(128, 128)):
   """
   Resize an image encoded as a Base64 string
   """
   # Decode the Base64 string
   img_data = base64.b64decode(base64_string)
   img = Image.open(io.BytesIO(img_data))

   # Resize the image
   resized_img = img.resize(size, Image.LANCZOS)

   # Save the resized image to a bytes buffer
   buffered = io.BytesIO()
   resized_img.save(buffered, format=img.format)

   # Encode the resized image to Base64
   return base64.b64encode(buffered.getvalue()).decode("utf-8")


def split_image_text_types(docs):
   """
   Split base64-encoded images and texts
   """
   b64_images = []
   texts = []
   for doc in docs:
       # Check if the document is of type Document and extract page_content if so
       if isinstance(doc, Document):
           doc = doc.page_content
       if looks_like_base64(doc) and is_image_data(doc):
           doc = resize_base64_image(doc, size=(1300, 600))
           b64_images.append(doc)
       else:
           texts.append(doc)
   if len(b64_images) > 0:
       return {"images": b64_images[:1], "texts": []}
   return {"images": b64_images, "texts": texts}
  1. ドメイン固有の画像プロンプトを定義する
def img_prompt_func(data_dict):
   """
   Join the context into a single string
   """
   formatted_texts = "\n".join(data_dict["context"]["texts"])
   messages = []

   # Adding the text for analysis
   text_message = {
       "type": "text",
       "text": (
           "You are financial analyst tasking with providing investment advice.\n"
           "You will be given a mixed of text, tables, and image(s) usually of charts or graphs.\n"
           "Use this information to provide investment advice related to the user question. \n"
           f"User-provided question: {data_dict['question']}\n\n"
           "Text and / or tables:\n"
           f"{formatted_texts}"
       ),
   }
   messages.append(text_message)
   # Adding image(s) to the messages if present
   if data_dict["context"]["images"]:
       for image in data_dict["context"]["images"]:
           image_message = {
               "type": "image_url",
               "image_url": {"url": f"data:image/jpeg;base64,{image}"},
           }
           messages.append(image_message)
   return [HumanMessage(content=messages)]

  1. マルチモーダル RAG チェーンを定義する
def multi_modal_rag_chain(retriever):
   """
   Multi-modal RAG chain
   """

   # Multi-modal LLM
   model = ChatVertexAI(
       temperature=0, model_name="gemini-pro-vision", max_output_tokens=1024
   )

   # RAG pipeline
   chain = (
       {
           "context": retriever | RunnableLambda(split_image_text_types),
           "question": RunnablePassthrough(),
       }
       | RunnableLambda(img_prompt_func)
       | model
       | StrOutputParser()
   )

   return chain


# Create RAG chain
chain_multimodal_rag = multi_modal_rag_chain(retriever_multi_vector_img)

10. ステップ 7: クエリをテストする

  1. 関連ドキュメントを取得する
query = "What are the EV / NTM and NTM rev growth for MongoDB, Cloudflare, and Datadog?"
docs = retriever_multi_vector_img.get_relevant_documents(query, limit=1)

# We get relevant docs
len(docs)

docs
         You may get similar output 

74ecaca749ae459a.png

plt_img_base64(docs[3])

989ad388127f5d60.png

  1. 同じクエリで RAG を実行する
result = chain_multimodal_rag.invoke(query)

from IPython.display import Markdown as md
md(result)

出力例(コードの実行時に異なる場合があります)

e5e102eaf10289ab.png

11. クリーンアップ

この Codelab で使用したリソースについて、Google Cloud アカウントに課金されないようにする手順は次のとおりです。

  1. Google Cloud コンソールで、[リソースの管理] ページに移動します。
  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

12. 完了

お疲れさまでした。Gemini を使用してマルチモーダル RAG を開発できました。