PDFファイルを使ったRAGに挑戦(1)

今ではLLMを使う場合にRAG(Retrieval-augmented generation)の技術は必須となってきています。理屈は理解できているのですが、本当に正しく知識をつかえて言えるのか心配ですよね。

今回、SIGNATEのコンペに挑戦しながら、いろんな手法を試してみました。

SIGNATE

SIGNATEでは、Kaggleのようなデータ分析のコンペが開催されています。日本語なので、初心者にはとっつきやすいのと、特に”日本語のデータ分析”ができるので、何度か参加させていただいています。

今回参加したのは、以下のコンペです。

簡単に言うと、企業のレポート(pdfファイル)をデータとして読み取り、そのデータを使って質問にどれだけ正確に答えられるかを競うものです。企業のレポートは19社分あり、質問は固定で100問です。

アプローチ方法

コンペのルールとして、RAGの構築について、以下のように定められています。

  • 使用可能なLLMや計算環境、ライブラリ等に制約はありません。
  • 手動で回答を作成することは禁止です。コンペで提供された質問を入力として、提供データ(documents.zip)を利用し、自動で結果(質問に対する回答)が出力されるプログラムのみが評価対象となります。
  • 1回答あたりの受付可能な最大文字数は54トークンです。これを超えた場合はエラーになりますのでご注意ください。
  • 応答時間の制限は80時間です。

自動で回答するプログラムを作れば、それ以外の制約はあまりなさそうです。

実際は試行錯誤をしていたのですが、大きく以下の方法を試しています。

  • PDFから単純にテキスト部分を抽出して、コンテキストとしてLLMに渡す。
  • PDFを読み取り可能なLLMに対して、直接質問文とpdfファイルとを渡す。
  • PDFを一旦、MarkDown形式に変換して、直接質問文と、mdファイルとを渡す。
  • LangChainを使って、mdファイルをベクトルデータベースに保存する。
  • LangChainを使って、pdfファイルをベクトルデータベースに保存する。

質問内容の分類やファイルの紐付けなどの”前処理”、結果を整形したり統合したりする”後処理”などもありますが、ここでは与えられた知識(ファイル)をどうやってLLMに渡すかについてのみ言及いたします。

RAGの入力となるデータによりますが、今回のような企業レポートはグラフや模式図、表が多く含まれていますので、単純なテキスト抽出はほとんど役に立ちませんでしたので、1については省略いたします。

PDF対応LLM

PDFを直接入力できるLLMは、2025年1月時点で以下がありました。

  • gemini-1.5-pro
  • claude-3-5-sonnet-20241022

Geminiは入力token数が、2,097,152 tokensと桁違いに大きいので、今回のpdfをそのまま入力することができました。しかし、Claudeは入力token数の上限が200k tokensとGeminiの約1/10であったため、処理することができませんでした。

なお今回は、質問ごとにどのファイルを見るべきかは、前処理でわかっているものとします。

Gemini 1.5 Pro

geminiを使うためのライブラリをインストールします。

pip install google-generativeai

Geminiの場合は、ファイルを一旦アップロードして蓄積(キャッシュ)しておくことができます。今回は全てのファイルをploadしておきました。

ちなみに、キャッシュは2日後に削除されます。

import google.generativeai as genai

API_TOKEN = <Your API key>
genai.configure(api_key=API_TOKEN)

model = genai.GenerativeModel("gemini-1.5-pro")

#ファイルのアップロード
pdf = genai.upload_file("filename.pdf")

#ファイル取得方法
for f in genai.list_files():
  if f.display_name=="poi.pdf":  # "poi.pdf"というファイルのファイルオブジェクトを取得する
    break

質問とファイル名とを入力して回答を得るコードは以下のようにしました。


import google.generativeai as genai
import os
import threading
import time


MODEL_NAME = "gemini-1.5-pro"
MODEL_NAME_FOR_OUTPUT = "gemini-1-5-pro"
API_TOKEN = "<Your API key>"

def analyze_pdf_with_gemini(pdf_filepath: str,prompt: str):

    SYSTEM_PROMPT = f"""

   <<< システムプロンプト >>>

  質問:{prompt}"""


    # LLMの設定
    genai.configure(api_key=API_TOKEN)
    model = genai.GenerativeModel(MODEL_NAME)

    # 対象のファイルを検索
    for f in genai.list_files():
      if f.display_name==pdf_filepath:
        break

    response = model.generate_content([
        SYSTEM_PROMPT,
        f
    ])

    # 消費したトークンの表示
    print(f"input token: {response.usage_metadata.prompt_token_count}")
    print(f"output token: {response.usage_metadata.candidates_token_count}")
    print(f"total token: {response.usage_metadata.total_token_count}")
    return ''.join([chunk.text for chunk in response])

システムプロンプトと、ファイルオブジェクトを渡すだけなので、非常に簡単に使うことができます。

成績あまり良くなかったです。 表や模式図内のデータをうまく解釈できていなかったように思います。