ColPaliのシンプルなラッパー「Byaldi」を試す
ColBERTを簡単に使えるようにするRAGatouille、みたいな感じで、ColPaliを簡単に使えるようにするByaldi、という感じっぽい。
Byaldiへようこそ
ご存知ですか? 映画『レミーのおいしいレストラン』(原題: Ratatouille)の中でレミーが作る料理は、実はラタトゥイユではなく、その改良版である「コンフィ・ビャルディ」なのだ。
referered from https://github.com/answerdotai/byaldi⚠️ これはビャルディのプレリリース版です。 何か問題があったら報告してください!
Byaldi は RAGatouille のミニ姉妹プロジェクトです。 これは、ColPali リポジトリのシンプルなラッパーで、ColPali のようなレイトインタラクションのマルチモーダルモデルを使い慣れたAPIで簡単に使えるようにするものです。
はじめに
まず最初に警告ですが、これはプレリリース版ライブラリであり、非圧縮インデックスを使用しており、その他の種類の改良は施されていません。現時点では、
vidore/colpali
および更新版のvidore/colpali-v1.2
を含む、PaliGemma ベースの ColPali チェックポイントファミリーのみがサポートされています。将来的なアップデートで、その他のバックエンドもサポートされる予定です。最終的には、HNSWインデックス作成メカニズム、プール、そして、もしかしたら2ビット量子化も追加するかもしれません。前提条件
ColPaliへのアクセス
ColPaliは現在、唯一の同種のモデルです。PaliGemmaをベースとしているため、HuggingFace上のPaliGemmaに関するGoogleのライセンス契約に同意し、独自のHFトークンを使用してモデルをダウンロードする必要があります。
Poppler
フレンドリーなライセンスでPDFを画像に変換するために、pdf2imageライブラリを使用しています。このライブラリを使用するには、popplerをシステムにインストールする必要があります。popplerのインストールは、ウェブサイトの手順に従って簡単に行うことができます。
Flash-Attention
Gemmaは最新のFlash Attentionを使用しています。 できるだけスムーズに動作させるために、ライブラリをインストールした後に、これをインストールすることをお勧めします。
ハードウェア
ColPaliは、数十億のパラメータモデルを使用して文書をエンコードします。スムーズな動作のためにはGPUの使用を推奨しますが、性能の低いGPUや古いGPUでも問題なく動作します。コレクションのエンコードは、CPUやMPSではパフォーマンスが低下します。
マルチモーダルエコシステムがさらに発展するにつれ、更新されていくでしょう!
ByaldiをOpenAIと組み合わせて、RAG Structured Outputみたいなものを作った人がすでにいる
以下にサンプルのnotebookと使用するPDFが置かれている。
notebookは2つ用意されている
- quick_overview.ipynb
- chat_with_your_pdf.ipynb
とりあえず上の方からColaboratoryで試してみるとして、PDFは日本語のものを使いたいのでなにかに置き換えたいと思う(日本語が使えるかどうかはわからない)
quick_overview.ipynb
Colaboratoryのランタイムは、CPUのみだとぜんぜんインデックス作成が進まなかったので、T4に変更している。
まず以下のHuggingFaceのPaliGemmaのページで利用規約に同意しておく。
で、notebookを進めるのだが、READMEの前提条件に書かれている内容はnotebookには含まれていないようなので、以下を最初に実行する。
%%bash
apt-get update
apt-get install -y poppler-utils
pip install --upgrade byaldi
pip install flash-attn
ここからはnotebookに従って進める
モデルをロード。ColPaliの最新バージョンに変更した。
import os
from google.colab import userdata
from pathlib import Path
from byaldi import RAGMultiModalModel
os.environ["HF_TOKEN"] = userdata.get('HF_TOKEN')
# RAGMultiModalModelを初期化
model = RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2")
PDFを用意する。神戸市が公開している観光に関する統計・調査資料のうち、「令和4年度 神戸市観光動向調査結果について」を使う。
PDF直
docsディレクトリを作成してそこにダウンロード
!mkdir -p docs
!wget -P docs https://www.city.kobe.lg.jp/documents/15123/r4_doukou.pdf
インデックスを作成。ここはそこそこ時間がかかる。
index_name = "attention_index"
model.index(
input_path=Path("docs/"),
index_name=index_name,
store_collection_with_index=False,
overwrite=True
)
Indexing file: docs/r4_doukou.pdf
Added page 1 of document 0 to index.
Added page 2 of document 0 to index.
Added page 3 of document 0 to index.
Added page 4 of document 0 to index.
Added page 5 of document 0 to index.
Added page 6 of document 0 to index.
Added page 7 of document 0 to index.
Added page 8 of document 0 to index.
Added page 9 of document 0 to index.
Added page 10 of document 0 to index.
Added page 11 of document 0 to index.
Added page 12 of document 0 to index.
Added page 13 of document 0 to index.
Added page 14 of document 0 to index.
Added page 15 of document 0 to index.
Added page 16 of document 0 to index.
Added page 17 of document 0 to index.
Added page 18 of document 0 to index.
Added page 19 of document 0 to index.
Added page 20 of document 0 to index.
Added page 21 of document 0 to index.
Index exported to .byaldi/kobe_kanko_index
Index exported to .byaldi/kobe_kanko_index
{0: 'docs/r4_doukou.pdf'}
検索してみる。
query = "神戸までの主な交通機関の内訳は?"
results = model.search(query, k=5)
print(f"Search results for '{query}':")
for result in results:
print(f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}")
Search results for '神戸までの主な交通機関の内訳は?':
Doc ID: 0, Page: 17, Score: 11.4375
Doc ID: 0, Page: 10, Score: 11.25
Doc ID: 0, Page: 13, Score: 11.0625
Doc ID: 0, Page: 5, Score: 11.0
Doc ID: 0, Page: 16, Score: 10.9375
んー、今回のクエリの例では、ドキュメントの内容からは6ページ(PDFとしてのページで、文書のページではない)が最も適切、あとは2ページにその要約が一部含稀ているという感じなんだけども、検索で上がってきたページは、
- 17ページ: 神戸市以外の立ち寄り先
- 10ページ: 旅行同伴者
- 13ページ: 旅行の日程と宿泊地(のデータ)
- 5ページ: 居住地
- 16ページ: 割引制度の利用
という感じなので、これを見る限り日本語は難しそうである。実際にColPaliのモデルのページにも以下とある。
バージョン固有
このバージョンはcolpali-engine=0.2.0で学習されている。colpaliと比較すると、このバージョンはクエリ符号化における不要なトークンを修正するために、クエリの右パディングで学習されている。 また、決定論的な射影層の初期化を保証するために、vidore/colpaligemma-3b-pt-448-baseを固定している。 英語以外の言語の崩壊を減らすために、バッチ内否定とハードマイニングされた否定、1000ステップ(10倍長い)のウォームアップで5エポック学習された。 データは論文で説明されたColPaliデータと同じ
モデルトレーニグ
データセット
127,460 のクエリとページのペアからなるトレーニング データセットは、公開されている学術データセットのトレーニング セット (63%) と、Web クロールされた PDF ドキュメントのページで構成され、VLM 生成 (Claude-3 Sonnet) 疑似質問 (37%) で拡張された合成データセットで構成されています。トレーニング セットは完全に英語で設計されているため、英語以外の言語へのゼロ ショット一般化を研究できます。評価の汚染を防ぐため、 ViDoReとトレーニング セットの両方で複数ページの PDF ドキュメントが使用されていないことを明示的に検証します。ハイパーパラメータを調整するために、サンプルの 2% で検証セットが作成されます。
注: 多言語データは言語モデル (Gemma-2B) の事前トレーニング コーパスに存在し、PaliGemma-3B のマルチモーダル トレーニング中に発生する可能性があります。
ColPaliにはv1.0、v1.1もあるのだけど、そちらを見る限りデータセットは同じであり、モデルの説明からはv1.2で多言語への対応が(多少なりとも)含まれているように思えるので、他のバージョンでも厳しそう。現状日本語はちょっと難しそうな印象。
こちらの方を参考に、英語のPDFを使ってみる。
出典:「HIGHLIGHTING Japan May2024」(政府広報オンライン)(https://www.gov-online.go.jp/hlj/en/may_2024/)
“HIGHLIGHTING Japan” is an official monthly online magazine to promote understanding of Japan for the people around the world, with variety of themes.
一旦ランタイムは削除してやり直す。手順は同じなので説明は割愛。
インデックス作成までの手順
%%bash
apt-get update
apt-get install -y poppler-utils
pip install --upgrade byaldi
pip install flash-attn
import os
from google.colab import userdata
from pathlib import Path
from byaldi import RAGMultiModalModel
os.environ["HF_TOKEN"] = userdata.get('HF_TOKEN')
# RAGMultiModalModelを初期化
model = RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2")
%%bash
mkdir docs
wget -P docs https://www.gov-online.go.jp/en/assets/HIGHLIGHTING_Japan_May2024.pdf
インデックス作成
index_name = "highlighting_japan_index"
model.index(
input_path=Path("docs/"),
index_name=index_name,
store_collection_with_index=False,
overwrite=True
)
Indexing file: docs/HIGHLIGHTING_Japan_May2024.pdf
Added page 1 of document 0 to index.
Added page 2 of document 0 to index.
Added page 3 of document 0 to index.
Added page 4 of document 0 to index.
Added page 5 of document 0 to index.
Added page 6 of document 0 to index.
Added page 7 of document 0 to index.
Added page 8 of document 0 to index.
Added page 9 of document 0 to index.
Added page 10 of document 0 to index.
Added page 11 of document 0 to index.
Added page 12 of document 0 to index.
Added page 13 of document 0 to index.
Added page 14 of document 0 to index.
Added page 15 of document 0 to index.
Added page 16 of document 0 to index.
Added page 17 of document 0 to index.
Added page 18 of document 0 to index.
Added page 19 of document 0 to index.
Added page 20 of document 0 to index.
Added page 21 of document 0 to index.
Added page 22 of document 0 to index.
Added page 23 of document 0 to index.
Added page 24 of document 0 to index.
Added page 25 of document 0 to index.
Added page 26 of document 0 to index.
Added page 27 of document 0 to index.
Added page 28 of document 0 to index.
Added page 29 of document 0 to index.
Added page 30 of document 0 to index.
Added page 31 of document 0 to index.
Added page 32 of document 0 to index.
Index exported to .byaldi/highlighting_japan_index
Index exported to .byaldi/highlighting_japan_index
{0: 'docs/HIGHLIGHTING_Japan_May2024.pdf'}
検索
query = "季節を感じるアートとは?"
results = model.search(query, k=5)
print(f"Search results for '{query}':")
for result in results:
print(f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}")
Search results for '季節を感じるアートとは?':
Doc ID: 1, Page: 28, Score: 10.125
Doc ID: 1, Page: 23, Score: 9.6875
Doc ID: 1, Page: 11, Score: 8.8125
Doc ID: 1, Page: 3, Score: 8.6875
Doc ID: 1, Page: 17, Score: 8.6875
28ページはまさにそれだった。
query = "安綱の刀について"
results = model.search(query, k=5)
print(f"Search results for '{query}':")
for result in results:
print(f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}")
Search results for '安綱の刀について':
Doc ID: 1, Page: 32, Score: 10.9375
Doc ID: 1, Page: 3, Score: 10.5
Doc ID: 1, Page: 23, Score: 8.9375
Doc ID: 1, Page: 19, Score: 8.75
Doc ID: 1, Page: 28, Score: 8.0625
こちらも32ページが正解で、3ページの目次にも少し載っている。
query = "阿寒湖摩周国立公園について"
results = model.search(query, k=5)
print(f"Search results for '{query}':")
for result in results:
print(f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}")
Search results for '阿寒湖摩周国立公園について':
Doc ID: 1, Page: 10, Score: 14.5
Doc ID: 1, Page: 11, Score: 13.8125
Doc ID: 1, Page: 17, Score: 13.75
Doc ID: 1, Page: 2, Score: 13.0625
Doc ID: 1, Page: 13, Score: 12.8125
10〜11ページが正解。2は目次でここにも少し記載がある。
なるほど、今のところインデックスに入れて検索する対象は、英語のほうが良さそうではある。クエリは日本語なんだけど。
引き続き、以下のPDFを使用する。
出典:「HIGHLIGHTING Japan May2024」(政府広報オンライン)(https://www.gov-online.go.jp/hlj/en/may_2024/)
インデックスは.byaldi
に作成されていた。
$ pwd
/content
$ tree .byaldi/
.byaldi/
└── highlighting_japan_index
├── doc_ids_to_file_names.json.gz
├── embeddings
│ └── embeddings_0.pt
├── embed_id_to_doc_id.json.gz
├── index_config.json.gz
└── metadata.json.gz
2 directories, 5 files
すでに作成済みのインデックスを読み出すにはRAGMultiModalModel.from_index()
を使う
from byaldi import RAGMultiModalModel
model = RAGMultiModalModel.from_index("highlighting_japan_index")
検索すると同じ結果になっていることがわかる。
results = model.search("阿寒湖摩周国立公園について", k=5)
print(f"Search results for '{query}':")
for result in results:
print(f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}")
Search results for '阿寒湖摩周国立公園について':
Doc ID: 0, Page: 10, Score: 14.5
Doc ID: 0, Page: 11, Score: 13.8125
Doc ID: 0, Page: 17, Score: 13.75
Doc ID: 0, Page: 2, Score: 13.0625
Doc ID: 0, Page: 13, Score: 12.8125
引き続き、以下のPDFを使用する。
出典:「HIGHLIGHTING Japan May2024」(政府広報オンライン)(https://www.gov-online.go.jp/hlj/en/may_2024/)
上で、インデックスを作成した際に、store_collection_with_index=False
を指定していたが、この状態はあくまでも検索用のインデックスが作成されるだけで、どうやら実際のPDFのデータ(というか画像かな?)は保持されない様子。
index_name = "highlighting_japan_index"
model.index(
input_path=Path("docs/"),
index_name=index_name,
store_collection_with_index=False, # ここ
overwrite=True
)
マルチモーダルLLMと連携するようなケースでは、ここで実際のデータも取れると便利になる。ということで、store_collection_with_index
を有効にしてインデックスを作成してみる。
import os
from google.colab import userdata
from pathlib import Path
from byaldi import RAGMultiModalModel
os.environ["HF_TOKEN"] = userdata.get('HF_TOKEN')
model = RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2")
pdf_path = Path("docs/HIGHLIGHTING_Japan_May2024.pdf")
index_name = "highlighting_japan_index_with_collection"
model.index(
input_path=pdf_path,
index_name=index_name,
store_collection_with_index=True,
overwrite=True
)
Verbosity is set to 1 (active). Pass verbose=0 to make quieter.
Loading checkpoint shards: 100%
2/2 [00:02<00:00, 1.23s/it]
Added page 1 of document 0 to index.
Added page 2 of document 0 to index.
Added page 3 of document 0 to index.
Added page 4 of document 0 to index.
Added page 5 of document 0 to index.
Added page 6 of document 0 to index.
Added page 7 of document 0 to index.
Added page 8 of document 0 to index.
Added page 9 of document 0 to index.
Added page 10 of document 0 to index.
Added page 11 of document 0 to index.
Added page 12 of document 0 to index.
Added page 13 of document 0 to index.
Added page 14 of document 0 to index.
Added page 15 of document 0 to index.
Added page 16 of document 0 to index.
Added page 17 of document 0 to index.
Added page 18 of document 0 to index.
Added page 19 of document 0 to index.
Added page 20 of document 0 to index.
Added page 21 of document 0 to index.
Added page 22 of document 0 to index.
Added page 23 of document 0 to index.
Added page 24 of document 0 to index.
Added page 25 of document 0 to index.
Added page 26 of document 0 to index.
Added page 27 of document 0 to index.
Added page 28 of document 0 to index.
Added page 29 of document 0 to index.
Added page 30 of document 0 to index.
Added page 31 of document 0 to index.
Added page 32 of document 0 to index.
Index exported to .byaldi/highlighting_japan_index_with_collection
Index exported to .byaldi/highlighting_japan_index_with_collection
{0: 'docs/HIGHLIGHTING_Japan_May2024.pdf'}
検索。検索結果の.base64
でbase64エンコードされた画像が取り出せる。
import base64
from io import BytesIO
from PIL import Image
import IPython.display as display
query = "阿寒湖摩周国立公園について"
results = model.search(query, k=3)
print(f"Search results for '{query}':")
base_64s = []
for result in results:
print(f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}")
print(f"Base64: {result.base64[:100]}...")
base_64s.append(result.base64)
html_images = ""
for base64_data in base_64s:
image_data = base64.b64decode(base64_data)
image = Image.open(BytesIO(image_data))
new_width = 300
aspect_ratio = image.height / image.width
new_height = int(new_width * aspect_ratio)
resized_image = image.resize((new_width, new_height))
buffered = BytesIO()
resized_image.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
html_images += f'<img src="data:image/png;base64,{img_str}" style="display:inline-block; margin-right: 10px;" />'
display.display(display.HTML(f'<div style="white-space: nowrap;">{html_images}</div>'))
該当するページが取り出せていることがわかる。マルチモーダルLLMにこれを渡せば、画像をベースにQAができる。
インデックスへの追加は以下
model.add_to_index(pdf_path, store_collection_with_index=True)
chat_with_your_pdf.ipynb
ByaldiとマルチモーダルLLMと連携させた例がこちらのnotebookになる。
このnotebookでは、マルチモーダルLLMとしてAnthropic Claude 3.5 sonnetが使用されている。なお、Claudeとの接続は、byaldiの開発元であるAnswer.AIが開発したClaudetteというラッパーが使用されている。
Claudetteの使い方はこちらにまとめた。こういう用途で使うにはとてもシンプルに扱えて良いと思う。
ということで、notebookを参考に進めてみたが、非常に詳しい説明がされたnotebookなので、いちいちまとめなくても、そのまま実行するのがわかりやすいと思うw。一応、以下に日本語に翻訳したものを用意したので、参考になれば。
簡単に紹介だけしておくと、2つの例が紹介されている
"Attention is All You Need"論文中の表データに対するRAG
"Attention is All You Need"論文をByaldiでインデックス作成、評価テスト結果の図が含まれたページを検索し、LLMにそのスコアについて質問・回答を行う。
該当の箇所は8ページの以下の箇所
refererd from https://arxiv.org/pdf/1706.03762 , https://github.com/AnswerDotAI/byaldi/blob/main/examples/docs/attention_table.png and arranged in the image by kun432
クエリ
Q: TransformerのベースモデルのBLEUスコアはいくつですか?
検索結果は8ページがトップに上がっている。
results = RAG.search(query, k=1)
results
[{'doc_id': 0, 'page_num': 8, 'score': 18.0, 'metadata': {}, 'base64': 'iVBORw0KGgoAAAANSUhEUgAABqQAAAiYCAIAAAA+NVHkAAEAA...
そしてLLMの回答
A: Transformerのベースモデル(Transformer (base model))のBLEUスコアは、表2によると以下の通りです:
EN-DE (英語からドイツ語への翻訳): 27.3 EN-FR (英語からフランス語への翻訳): 38.1
財務報告書内のグラフデータに対するRAG
架空の会社の財務報告書の中にある、特定プロダクトの月次収益グラフが含まれたページを検索し、最も収益を上げた月をLLMに質問回答させる。
refererd from https://github.com/AnswerDotAI/byaldi/blob/main/examples/docs/financial_report.pdf and arranged in the image by kun432
クエリ
Q: 製品Cが最も収益を上げたのはどの月ですか?
検索結果は4ページ目がトップに上がっている。
results = RAG.search(query, k=1)
results[0].page_num
4
LLMの回答
A: この画像から、製品Cが最も収益を上げた月は6月(Jun)であることがわかります。グラフを見ると、6月の棒グラフが最も高く、約2700ドルの月間収益を示しています。1月から5月にかけて収益が徐々に増加し、6月にピークを迎えた後、7月以降は徐々に減少していく傾向が見られます。
最後にnotebookの内容をまとめたフルのコードがあるが、とてもシンプルに書ける。
import base64
import os
os.environ["HF_TOKEN"] = "YOUR_HF_TOKEN" # ColPaliモデルのダウンロードに必要
os.environ["ANTHROPIC_API_KEY"] = "YOUR_ANTHROPIC_API_KEY"
from byaldi import RAGMultiModalModel
from claudette import *
# モデルのロード
RAG = RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2", verbose=1)
# ドキュメントからインデックス作成
RAG.index(
input_path="./docs/attention.pdf",
index_name="attention",
store_collection_with_index=True,
overwrite=True
)
# クエリの定義
query = "What's the BLEU score for the transformer base model?"
# モデルに対してクエリ実行
results = RAG.search(query, k=1)
# 検索結果上位をClaudeにわたす
image_bytes = base64.b64decode(results[0].base64)
chat = Chat(models[1])
# Claudeの回答を出力
print(chat([image_bytes, query]))
まとめ
自分が試した限り、日本語PDFには対応していないように思えるが、おそらくこれはColPaliモデルが英語でのみ学習されているためだと思っている。ColPaliのレポジトリには、トレーニングについても記載されているので、頑張ればできるのかも?知らんけど。
その点については残念ではあるものの、検索精度としては触ってみた感じは良さそうであるし、Byaldiのおかげで(あとclaudetteも)、かなり少ないコードでマルチモーダルRAGができるのは非常にありがたい。
上でポストを紹介した、ゆめふく(@y_fukumochi)さんがされてたように、Qwen2-VL-2B-Instructあたりを使えば、ローカルでもなかなかの精度でマルチモーダルRAGが実現できそうに思う。
Qwen2-VL-2B-Instructに対応したモデルがでた!
日本語もいけるみたい
おおおお・・・