Open3

AutoRAGを試す

kun432kun432

このスレを読んでいて出てきた。

https://www.reddit.com/r/LangChain/comments/1bijg75/why_is_everyone_using_ragas_for_rag_evaluation/

https://github.com/Marker-Inc-Korea/AutoRAG

AutoRAG

RAG AutoMLツールは、データに最適なRAGパイプラインを自動的に見つけます。


refered from https://github.com/Marker-Inc-Korea/AutoRAG

RAGパイプラインやモジュールは数多くありますが、自分のデータやユースケースに最適なパイプラインがどれなのかはわかりません。すべてのRAGモジュールを作成し評価するのは非常に時間がかかり、難しい作業です。しかし、それなしでは、自分のユースケースに最適なRAGパイプラインがどれなのかは決してわかりません。

AutoRAGは、「あなたのデータ」に最適なRAGパイプラインを見つけるためのツールです。 お客様独自の評価データを使用して、さまざまなRAGモジュールを自動的に評価し、お客様のユースケースに最適なRAGパイプラインを見つけることができます。

AutoRAGは、多くのRAGモジュール組み合わせを簡単に評価できる方法を提供します。今すぐお試しいただき、あなたのユースケースに最適なRAGパイプラインを見つけてください。

公式のYouTube動画
https://www.youtube.com/watch?v=2ojK8xjyXAU

kun432kun432

公式が用意してくれているColaboratoryのチュートリアルnotebookがある。

ざっと内容を見てみたけども、前提としてAutoRAGで最適化を行うためには以下の2つのデータセットが必要になる。

  • コーパス(ドキュメントのチャンク)
  • QA(コーパスから作成したもの)

なのでまずこれを作る必要がある。

でチュートリアルのnotebookをみてみると、

  • ステップ1はあらかじめ用意された英語のデータセットを使用して最適化からスタート、つまりデータセットは作らないし、やるならば日本語でやりたい。
  • ステップ2が良さそうではあるのだけど、一通り進めてみた感じ、初見では何やってるのか理解しにくい箇所があって、むしろ混乱しそう。

という風に感じたので、まずはデータセット作成を以下に従ってやることにする。

https://docs.auto-rag.com/data_creation/tutorial.html

ただ、これも説明が少ないのよね・・・・ドキュメントで調べたことなどを補足しつつまとめることにする。

特に明記がない限り、Colaboratory上で進める。

kun432kun432

データセットの作成

まずデータセットを作成していく。データセット作成の流れは以下となっている。


refer from https://docs.auto-rag.com/data_creation/tutorial.html

  1. 生データからパース、これにより非構造化データが構造化データとなる。
  2. パースしたデータをチャンク分割、これが「コーパス」になる
  3. コーパスからLLMでQAを生成、これが「QAデータ」になる。
  4. チャンク戦略を更新して、別のコーパスを作成、上記で作成したQAデータをマッピングする。

1〜3までは一般的なデータセット作成の流れだと思うけど、4のところがやや理解しづらく、また4は2とも関連してくるので、より理解しづらい感じになる。なのでこの辺を少し単純化して、最後に4の部分を説明するような感じで進める。

インストール・事前準備

まずパッケージのインストールなんだけど、Colaboratoryで進める場合、OS側のPythonパッケージとコンフリクトするところがある。なのでまずOS側パッケージを一旦アンインストールする。

!apt remove -y python3-blinker

でAutoRAGのパッケージインストール。でここも確認した限り、PyPIのパッケージ(v0.3.5、2024/10/16時点)にはバグがあるようで、あとでエラーになる箇所があるので、レポジトリからインストールする。

!pip install -Uq "AutoRAG[parse] @ git+https://github.com/Marker-Inc-Korea/AutoRAG.git" datasets

OpenAIのAPIキーをセット

from google.colab import userdata
import os

os.environ["OPENAI_API_KEY"] = userdata.get('OPENAI_API_KEY')

notebookなので非同期イベントループのネストを許可しておく

import nest_asyncio

nest_asyncio.apply()

データセットの元になるドキュメントを用意。今回は以下をドキュメントとする。

https://ja.wikipedia.org/wiki/オグリキャップ

ドキュメントダウンロード用のディレクトリを作成

import os

os.makedirs('/content/raw_documents')

ダウンロード。ここは個人的な好みで、Markdownにしてゴミを消したりしてる。

from pathlib import Path
import requests
import re

def replace_heading(match):
    level = len(match.group(1))
    return '#' * level + ' ' + match.group(2).strip()

def remove_after(target: str, text: str) -> str:
    parts = text.split(target, 1)
    return parts[0] if len(parts) > 1 else text

# Wikipediaからのデータ読み込み
wiki_titles = ["オグリキャップ"]
for title in wiki_titles:
    response = requests.get(
        "https://ja.wikipedia.org/w/api.php",
        params={
            "action": "query",
            "format": "json",
            "titles": title,
            "prop": "extracts",
            # 'exintro': True,
            "explaintext": True,
        },
    ).json()
    page = next(iter(response["query"]["pages"].values()))
    wiki_text = f"# {title}\n\n## 概要\n\n"
    wiki_text += page["extract"]

    wiki_text = re.sub(r"(=+)([^=]+)\1", replace_heading, wiki_text)
    wiki_text = re.sub(r"\t+", "", wiki_text)
    wiki_text = re.sub(r"\n{3,}", "\n\n", wiki_text)
    wiki_text = remove_after("## 脚注", wiki_text)

    # markdown(.md)ファイルとして出力
    with open(f"/content/raw_documents/{title}.md", "w") as fp:
        fp.write(wiki_text)

ドキュメントのパース

https://docs.auto-rag.com/data_creation/parse/parse.html

ダウンロードしたドキュメントをパースしてAutoRAGで扱えるようにする。AutoRAGのパース用モジュールは大きく分けると以下の4つ。

  • LangChain Parse: LangChainを使ったパース
  • Llama Parse: LlamaIndexのSaaSであるLlamaParseを使ったパース
  • Clova: NaverのClova OCRを使ったパース
  • Table Hybrid Parse: ドキュメント内の表のパース。複数のモジュールを組み合わせて使う様子

で、さらに各モジュールごとに対応しているファイルフォーマットに違いがあったり、同じファイル形式でも内部で使用するライブラリの違いによってモジュールが別れていたりする。以下にまとまっている。

https://edai.notion.site/Supporting-Parsing-Modules-e0b7579c7c0e4fb2963e408eeccddd75

上記を参考に、使用するドキュメントに合わせてモジュールタイプとパース用のメソッドを指定する設定ファイルを用意する。今回はMarkdownなので、モジュールはlangchain_parse、パース用メソッドはunstructuredmarkdownを指定する。

%%writefile parse.yaml

modules:
  - module_type: langchain_parse
    parse_method: [unstructuredmarkdown]

パース結果を出力するディレクトリを作成

import os
os.makedirs('/content/parse_project_dir')

どこで使っているかわからないけど、PyArrowのインストールが必要らしいのでインストール

!pip install pyarrow==15.0.2

では、生ドキュメントを保持しているディレクトリと出力ディレクトリを指定して、ドキュメントファイルをパースする。ただし、そのまま実行するとNLTKのデータが見つからないというエラーが起きたので、そのコードを追加している。

# エラーになるので追加
import nltk
nltk.download('punkt_tab')
nltk.download('averaged_perceptron_tagger_eng')

# AutoRAGのパース処理
from autorag.parser import Parser
parser = Parser(
    data_path_glob="/content/raw_documents/*.md",
    project_dir="/content/parse_project_dir"
)
parser.start_parsing("/content/parse.yaml")

パース結果はこんな感じで出力される。

$ parse_project_dir
├── 0
│   ├── 0.parquet
│   ├── parse_config.yaml
│   └── summary.csv
└── trial.json

1 directory, 4 files

summary.csv0/0.parquetの中身を見てみる。

import pandas as pd

parse_summary_df = pd.read_csv("/content/parse_project_dir/0/summary.csv")
parse_summary_df

こちらはパース処理自体に関する情報が含まれていて、使用したモジュールやパース処理にかかった時間などが含まれている模様。

pdfminer_raw_df = pd.read_parquet("/content/parse_project_dir/0/0.parquet")
pdfminer_raw_df.head()

こちらにはパースされたデータが含まれている様子で、各カラムの意味は以下。

生のデータフレームでは、4つの列があります。

  • texts: パース結果。元のドキュメントからのパースされた内容が含まれる。
  • path: 元のファイルのパス
  • page: ドキュメントのページ。-1の場合はドキュメント全体を意味します。
  • last_modified_datetime: ドキュメントが最後に変更された日時。

今回はMarkdownファイルが1つだけなので上記の様な結果になっているが、PDFとか、複数のファイルとかの場合だとまた変わってくるのだろうと思う。

以降の処理はこのParquetファイルを使って進めることになる。

チャンク分割

ドキュメントをパースしたらこれをチャンク分割する。チャンク用のモジュールは大きく分けて以下の2つ。

  • LangChain Chunk: LangChainを使ったチャンクモジュール
  • LlamaIndex Chunk: LlamaIndexを使ったチャンクモジュール

パース用モジュールと同様に、こちらもチャンク手法の違いや、モジュールごとに使えるメソッドが異なるので、そのあたりを踏まえて選択することになる。

https://edai.notion.site/Supporting-Chunk-Modules-8db803dba2ec4cd0a8789659106e86a3

で、これも設定をYAMLで定義して実行できるのだが、上の方で記載した通り、4と関連すると少しややこしくなるので、まずはベタに定義することにする。ここでは、LlamaIndex Chunkモジュールを使って、文単位・チャンクサイズ300文字、オーバーラップ50文字でチャンク分割する。

from autorag.data.qa.schema import Raw

# パース済ドキュメントを読み込み
initial_raw_df = pd.read_parquet("/content/parse_project_dir/0/0.parquet")

# パース済ドキュメントをチャンク分割。
initial_raw = Raw(initial_raw_df)
initial_corpus = initial_raw.chunk(
    "llama_index_chunk", chunk_method="sentence", chunk_size=300, chunk_overlap=50
)

中のデータを見てみる。

initial_corpus.data

ドキュメントがチャンク分割されているのがわかる。こちらの各カラムはこうなっている。

  • doc_id: 各チャンクのユニークなID
  • contents : チャンクの内容
  • path : 元のドキュメントのパス。
  • start_end_idx: チャンクがドキュメントのどこに位置しているか
  • metadata : last_modified_datetimenext idprev idなどのメタデータ

これが「コーパス」になる。

QA生成

ではチャンクからQAデータを作成する。

from llama_index.llms.openai import OpenAI

from autorag.data.qa.filter.dontknow import dontknow_filter_rule_based
from autorag.data.qa.generation_gt.llama_index_gen_gt import (
    make_basic_gen_gt,
    make_concise_gen_gt,
)
from autorag.data.qa.query.llama_gen_query import factoid_query_gen
from autorag.data.qa.sample import random_single_hop
from autorag.data.qa.schema import Raw
import pandas as pd

# コーパスからQAを作成
initial_qa = (
    initial_corpus.sample(random_single_hop, n=50)     # チャンク数や要件等にQA生成数を設定。
    .map(
        lambda df: df.reset_index(drop=True),  # インデックスを削除
    )
    .make_retrieval_gt_contents() # gt(ground truth)の文章を含む質問を作成するために不可欠
    .batch_apply(
        factoid_query_gen,  # 質問の生成
        llm=llm,
        lang="ja"
    )
    .batch_apply(
        make_basic_gen_gt,  # 通常の回答の生成
        llm=llm,
        lang="ja"
    )
    .batch_apply(
        make_concise_gen_gt,  # 簡潔で短い回答の生成
        llm=llm,
        lang="ja"
    )
    .filter(
        dontknow_filter_rule_based,  # 「わからない」となった回答をフィルタ
        lang="ja",
    )
)

少し長いので補足

  • コーパスに対して、.sample()メソッドでGround Truthとなるチャンクのサンプリングを行う。
    • random_single_hopはコーパスからランダムに1ホップをサンプリングする設定。もう一つの設定として、コーパス内の範囲を指定して1ホップをサンプリングするrange_single_hopというのもある。
    • n=でサンプリングするコーパス数を設定する。
  • あとは.batch_apply()をメソッドチェーンで繋いで、生成するクエリおよび回答の設定を行っていく。
    • クエリの生成
      • factoid_query_genは事実に基づいた簡潔な質問を生成する。
      • 他にも、概念的な質問を生成するconcept_completion_query_genや、マルチホップの質問を生成するtwo_hop_incrementalというものもある。
    • 回答の生成
      • make_basic_gen_gtは特に制約のない普通の回答を生成する。
      • 他に簡潔でも次回回答を生成するmake_concise_gen_gtというものもある。
    • フィルタ
      • 生成された結果をルールベースでフィルタ(削除)することができる。
      • ここでは「わかりません」というような回答に文字列一致した結果をフィルタしている。
      • 他にも、「わかりません」という回答に合致しているかをLLMで判定させるdontknow_filter_openaiや、チャンクの内容によって回答が変わるもの(例えば「その表には…」みたいなもの等)を除くpassage_dependency_filter_openaiなどがある。

上記を実行するとQAデータが作成される。中身を見てみる。

initial_qa.data

質問と回答が生成されているのがわかる。

ではこれらをファイルに保存する。

# 作成したQAおよびQA作成に使用したコーパスをParquetファイルに保存
initial_qa.to_parquet(
    "./initial_qa.parquet",
    "./initial_corpus.parquet"
)

これでこのファイルを最適化プロセスに使えるようになった。

この初回に作成したコーパス及びQAデータを「初期コーパス」「初期QAデータ」と呼ぶ。この「初期」というのが、このあとのステップに関連してくる。