日本語でクエリに関連しない文章を削除できるモデル「OpenProvence」を試す
ブログ
昨今、LLMが回答するための「良い知識」を作るために、検索を行い情報を集め、さらに足りない知識を補うために多方面のさまざまな検索クエリを作り検索結果から必要な情報だけを抽出したり…といったことを、再起的に行っています。
しかしながら、大量に検索を行うと「検索結果」の情報も同時に増加していきます。そのため、本当に必要な情報の抽出をLLMが間違えたり、ハルシネーションが起きたり、入力情報の増加により処理が遅くなったり、LLM利用費用が増加したりと、大量の検索が難しかったりもします。
そこで、検索結果をLLMに渡す前に、関連しない情報は削除しちゃおう、ついでに関連度スコアもつけちゃおう、というアプローチが Provence です。
ただ研究開発として公開されている Provence 実装やモデルは非商用で、日本語のデータセットも公開されていなかったので、今回 OpenProvence というプロジェクトを作成し、で学習推論などのソースコードやモデルの重みなどを "オープン" なライセンスで公開しました。
スライド
論文
alphaXivのまとめ
Dia によるまとめ。
ProvenceはRAGのコンテキストを文レベルで賢く削って速さと精度を両立する仕組みだよ。
ざっくり言うと
ウチの言葉でまとめると、Provence は RAG のためのコンテキストプルーニング(不要な文を切る作業)を、めっちゃ効率よく・しかも堅牢にやるモデルだし。長い参照コンテキストはLLMのコストと遅延を爆上げするでしょ?そこを文単位で「役立つ文だけ残す」ように動的に削って、だいたい50–80%くらい圧縮しても品質ほぼ落とさないの。しかも再ランキングも同じモデルで同時にやるから、追加コストほぼゼロなのがウケる✨
何が新しいの?
- 固定圧縮からの卒業: RECOMPみたいに「常にN文だけ残す」じゃなく、質問ごと・文書ごとに「必要な文の量」が変わっても対応する、動的な文選択。
- プルーニング+再ランキングの統合: 1つのDeBERTa-v3クロスエンコーダにヘッドを2つ載せて、
- 再ランキングヘッド=パッセージ関連度のスコア
- プルーニングヘッド=トークンごとの二値ラベルを同時出力。だからRAGに既にある再ランキング工程に“ついで”でプルーニングが乗る感じ。計算ほぼタダだもん。
- 文レベルの意思決定: まずトークン単位で「関連」予測して、文の丸め(その文のトークンの50%以上が関連なら文を採用)で文単位に落とす。きめ細かくて意味まとまりも保てるのがマジで良い。
どう学習してるの?
- 多様データで頑健性アップ: MS MARCO(ドキュメントランキング)約37万問+Natural Questions約8.7万問。文書は1~10文の連続パッセージに動的分割して、色んな長さに適応できるよう訓練。
- シルバーラベル生成が賢い: 文ごとの正解ラベルは無いから、Llama‑3‑8B‑Instructに「与えたコンテキストだけで答えて、使った“全ての”文を引用してね」と指示するアンサーオラクル方式で文アトリビューションを作る。これが単なる関連度プロンプトよりラベル品質良いんだよね。
- 知識蒸留で安定: 事前学習済み再ランカーの振る舞いを保つため、ランキング正則化(MSE) で初期再ランカーの知識を蒸留。プルーニングを足しても再ランキングがヘタらない。
実験のポイント(刺さるとこ)
- 圧縮しても強い: Wikipedia系、BioASQ、SyllabusQA、ニュース(RGB)など7データセットで、50–80%圧縮でも品質ほぼ維持。PopQAでは逆に精度が上がるケースもある。ノイズ抜くとテンション上がるやつだし。
- 堅牢性: 文がコンテキストのどこにあっても拾える「干し草の山の針」テストで強い。関連文の数が0/1/複数でも、オラクルラベルと選択が綺麗に揃うのウケる。
- 計算効率: 典型的に生成が最大2倍速(バッチ大きいほど効く)。MFLOPS比較でもLLMベースの圧縮(LongLLM‑Linguaの重い系)より軽いか同等。
- 転移性: リトリーバ(SPLADE‑v3、RetroMAE、BM25)、再ランカー(DeBERTa‑v3、BGE‑M3)、ジェネレータ(Llama‑2/3、Mistral、SOLAR)と組み替えても強い。閾値の使い回しも効くから、データセットごとの微調整サボれて助かる。
直感的なたとえ話
RAGのコンテキストって「資料の山」だし。Provenceは「質問に答えるのに本当に必要なページだけクリップして渡す、仕事が早い秘書」みたいな感じ。しかも秘書が、資料の並べ替え(再ランキング)も同時にやってくれるから、会議(LLM生成)の準備がちょー速くなる。
実装視点のツボ(インフラ脳むけ)
- クロスエンコーダだからクエリ×文を一緒に見て意味関係をリッチに取れる。デュアルエンコーダより精度出しやすいけど、その分の計算は再ランキング工程にもうあるので“増えない”のが神。
- 文の丸め閾値(50%) はドメイン跨ぎで転移性高め。閾値調整に時間かけずすぐ回せるの助かるでしょ。
- 抽出型なので、要約生成みたいな自己回帰の遅さにハマらない。リアルタイム性や大バッチの生成TTFT/TPOTを攻めるとき相性いい。
何が嬉しい?
- スピード: 長文をスパッと削って2倍速まで狙えるし。
- 品質: ノイズでLLMがブレるのを防いで、場合によっては精度アップ。
- 汎用性: 既存RAGパイプラインにほぼ無改造でイン。チューニング地獄が減ってマジで助かる。
競合との違い(軽く)
- RECOMP: 固定で文数を残す+要約生成が遅い。Provenceは動的抽出+非自己回帰で速い。
- LongLLM‑Lingua: トークン圧縮中心でLLM依存が重いバリアントも。Provenceは文レベルで意味単位を保つのが得意。
まとめると、Provenceは「再ランカーに文削りの知能を足して、RAGの速度と精度の両取り」を現実解でやってる感じ。RAG構成でも、長尺コンテキストのボトルネックに効くから、導入のコスパはマジで高いと思うだし👌
デモが用意されている。

Wikipediaの「オグリキャップ」の冒頭部分の文章を使って試してみる。
テキスト
オグリキャップ(欧字名:Oguri Cap、1985年3月27日 - 2010年7月3日)は、日本の競走馬、種牡馬[1]。
1987年5月に岐阜県の地方競馬・笠松競馬場でデビュー。8連勝、重賞5勝を含む12戦10勝を記録した後、1988年1月に中央競馬へ移籍し、重賞12勝(うちGI4勝)を記録した。1988年度のJRA賞最優秀4歳牡馬[† 1]、1989年度のJRA賞特別賞[† 1]、1990年度のJRA賞最優秀5歳以上牡馬および年度代表馬[† 1]。1991年、JRA顕彰馬に選出。愛称は「オグリ」「芦毛の怪物」など多数。「スーパー・スター」[2]と評された。
中央競馬時代はスーパークリーク、イナリワンの二頭とともに「平成三強」と総称され、自身と騎手である武豊の活躍を中心として起こった第二次競馬ブーム期において[3]、第一次競馬ブームの立役者とされるハイセイコーに比肩するとも評される高い人気を得た[4]。その人気は競馬ファンのみならず一般の大衆にも波及し、社会現象を巻き起こした。日本競馬史上において、特に人気を博した競走馬の一頭である。
競走馬引退後は北海道新冠町の優駿スタリオンステーションで種牡馬となったが、産駒から中央競馬の重賞優勝馬を出すことができず、2007年に種牡馬を引退。種牡馬引退後は同施設で功労馬として繋養されていたが、2010年7月3日に右後肢脛骨を骨折し、安楽死の処置が執られた。
クエリ: オグリキャップの主な勝鞍は?
結果
8連勝、重賞5勝を含む12戦10勝を記録した後、1988年1月に中央競馬へ移籍し、重賞12勝(うちGI4勝)を記録した。1988年度のJRA賞最優秀4歳牡馬[† 1]、1989年度のJRA賞特別賞[† 1]、1990年度のJRA賞最優秀5歳以上牡馬および年度代表馬[† 1]。


クエリ: オグリキャップのライバルは?
こちらは、少ししきい値を下げた(0.05)
結果
中央競馬時代はスーパークリーク、イナリワンの二頭とともに「平成三強」と総称され、自身と騎手である武豊の活躍を中心として起こった第二次競馬ブーム期において[3]、第一次競馬ブームの立役者とされるハイセイコーに比肩するとも評される高い人気を得た[4]。


おー、これはすごいね。
モデルは以下の4つ。なお、デモで使用されているのは open-provence-reranker-xsmall-v1。
open-provence-reranker-xsmall-v1のモデルカードから抜粋して翻訳(PLaMo翻訳)
✂️ OpenProvence: 検索拡張生成向け効率的かつ堅牢な文脈フィルタリングのオープンソース実装
⚡️ 軽量なProvenceスタイルの再ランク付けモデルで、回答を保持しつつノイズを除去する検索拡張生成向けソリューション
OpenProvenceは、Provenceの手法に基づき、質問応答ワークフローにおいて無関係なパッセージをフィルタリングするとともに、再ランク付けスコアを同時生成するアプローチを採用しています。現代のエージェントシステム(DeepResearchループ、自律型検索パイプライン、文脈エンジニアリングシステムなど)では、LLMのトークン予算を圧迫する関連性の低い段落が蓄積されがちです。OpenProvenceのチェックポイントをLLMの前に配置することで、重要なパッセージのみを抽出することが可能になります。
本プロジェクトでは、MITライセンスの下で学習済みモデルの重みデータと、学習・推論・データセット構築用のツール群を公開しており、汎用ハードウェア上で再現性のあるワークフローを実現できます。
主な特徴
- 強力なフィルタリング機能 – トピックから外れた文章の約99%を除去しつつ、関連するテキストの80~90%を圧縮可能。MLDR評価結果からも、回答内容が正確に保持されていることが確認されています。
- 本番環境対応チェックポイント – Hugging Face上で利用可能な4つのバイリンガルモデル(パラメータ数30M~310M)をMITライセンスで提供。特に30MパラメータのxsmallモデルはCPU環境で快適に動作し、GPU環境では高速に処理できます。
- 再現性のある学習環境 – 学習ガイドに従って、16GB以上のNVIDIA GPU1台で全てのチェックポイントを学習可能です。
- データセット構築ツール – データセット作成ガイドを使用して、独自のデータからOpenProvence形式のコーパスを構築できます。
- 評価ユーティリティ – データセット保持率のスイープやMLDRの長文ドキュメントベンチマーク用のCLIランナーを提供しており、回帰追跡を容易に行えます。
- ドキュメントファースト設計 – エンドツーエンドのレポート、ガイド、設定ファイルを通じて、学習・評価・データセット構築の各プロセスを網羅的にサポートしています。
- 教師モデル – 多言語スパンアノテーターであるquery-context-pruner-multilingual-Qwen3-4Bを提供しており、カスタムラベルパイプラインの基盤として活用できます。
モデルラインアップ
レイテンシ要件と対応言語に合わせて最適なチェックポイントを選択してください。すべてのチェックポイントはHugging Face上で提供されており、柔軟なライセンス条件を採用しています。
モデル 対応言語 Hugging Face ID パラメータ数 備考 base 英語・日本語 hotchpotch/open-provence-reranker-v1 130M バイリンガル処理向けの精度と速度のバランスが取れたモデル xsmall 英語・日本語 hotchpotch/open-provence-reranker-xsmall-v1 30M 最速のオプション。GPUがなくても実用的な性能を発揮 large 英語・日本語 hotchpotch/open-provence-reranker-large-v1 310M 同等のF2スコアにおいて最高の圧縮率を実現 en-gte 英語 hotchpotch/open-provence-reranker-v1-gte-modernbert-base 149M 英語専用チェックポイントで、最高の再ランキング精度を実現
学習データについて
OpenProvence v1のチェックポイントは、多言語QAコーパスから作成されており、Qwen3-4B教師モデルを用いて再ラベル付けされています。英語データの範囲はhotchpotch/msmarco-context-relevance、hotchpotch/gooaq-context-relevance-130k、およびhotchpotch/natural-questions-context-relevanceに及びます。日本語データはhotchpotch/japanese-context-relevanceから提供されており、MS >MARCO JAデータセットとネイティブQAソースを含んでいます。すべてのデータセットでは、文単位の保持/削除ラベルと教師リランキングモデルのスコアが提供されているため、これらのデータセットを組み合わせて独自のドメイン向けに再現・拡張することが可能です。
学習レシピ
本モデルファミリーはオープンソースのOpenProvenceスタックを使用して学習されており、16GB以上のメモリを搭載したNVIDIA GPU1台で再現可能です。
1. 教師モデルによるラベル生成(DeepSeek-V3)
DeepSeek-V3を使用して質問-文脈の関連性をアノテーションし、多言語14万サンプルのデータセットqa-context-relevance-multilingual-140kを作成しました。
2. 教師モデルによる文脈関連性SFT(Qwen3-4B)
Qwen3-4Bをファインチューニングして多言語教師モデルquery-context-pruner-multilingual-Qwen3-4Bを構築し、高速かつ一貫した文範囲レベルのアノテーションを可能にしました。
3. 文脈関連性データセットの構築
以下のコーパスから文範囲ラベルと教師モデルスコアを生成しました:
英語データ:
- hotchpotch/msmarco-context-relevance
- hotchpotch/gooaq-context-relevance-130k
- hotchpotch/natural-questions-context-relevance
日本語データ:
ほぼ同一のネガティブサンプルは重複排除し、データセット作成ガイドに記載されている前処理のポイントを参照してください。
4. 最終モデルの学習
既存の再ランク付けスコアを統合し、クロスエンコーダ型再ランク付けヘッドと文脈剪定ヘッドを組み合わせた統一モデルを構築しました。設定詳細やベースラインコマンドについてはdocs/train.mdを参照してください。
ライセンス
MITライセンス
モデルカードなどに従って、Colaboratoryで試す。GPUを効かせたいのでランタイムはT4で。
パッケージインストール。 fast-bunkai ってのは高速な文境界判定ライブラリらしい。
!uv pip install transformers torch tokenizers sentencepiece protobuf
!uv pip install fast-bunkai nltk
!uv run python -c "import nltk; nltk.download('punkt'); nltk.download('punkt_tab')"
モデルをロード。largeを使ってみる。
from transformers import AutoModel
model_name = "hotchpotch/open-provence-reranker-large-v1"
model = AutoModel.from_pretrained(
model_name,
trust_remote_code=True,
)
VRAM消費は約1.4GB。
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.54.15 Driver Version: 550.54.15 CUDA Version: 12.4 |
|-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 Tesla T4 Off | 00000000:00:04.0 Off | 0 |
| N/A 43C P0 27W / 70W | 1374MiB / 15360MiB | 30% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
コンテキストは、wikipediaの記事を雑に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 get_wiki_content(title):
response = requests.get(
"https://ja.wikipedia.org/w/api.php",
# ref: https://foundation.wikimedia.org/wiki/Policy:Wikimedia_Foundation_User-Agent_Policy
headers={
"User-Agent": "SampleBot/0.0 (https://zenn.dev/kun432/) requests/2.32.5"
},
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)
return wiki_text
print(len(get_wiki_content("オグリキャップ")))
print(get_wiki_content("オグリキャップ")[:1000])
37306
# オグリキャップ
## 概要
オグリキャップ(欧字名:Oguri Cap、1985年3月27日 - 2010年7月3日)は、日本の競走馬、種牡馬。
1987年5月に岐阜県の地方競馬・笠松競馬場でデビュー。8連勝、重賞5勝を含む12戦10勝を記録した後、1988年1月に中央競馬へ移籍し、重賞12勝(うちGI4勝)を記録した。1988年度のJRA賞最優秀4歳牡馬、1989年度のJRA賞特別賞、1990年度のJRA賞最優秀5歳以上牡馬および年度代表馬。1991年、JRA顕彰馬に選出。愛称は「オグリ」「芦毛の怪物」など多数。「スーパー・スター」と評された。
中央競馬時代はスーパークリーク、イナリワンの二頭とともに「平成三強」と総称され、自身と騎手である武豊の活躍を中心として起こった第二次競馬ブーム期において、第一次競馬ブームの立役者とされるハイセイコーに比肩するとも評される高い人気を得た。その人気は競馬ファンのみならず一般の大衆にも波及し、社会現象を巻き起こした。日本競馬史上において、特に人気を博した競走馬の一頭である。
競走馬引退後は北海道新冠町の優駿スタリオンステーションで種牡馬となったが、産駒から中央競馬の重賞優勝馬を出すことができず、2007年に種牡馬を引退。種牡馬引退後は同施設で功労馬として繋養されていたが、2010年7月3日に右後肢脛骨を骨折し、安楽死の処置が執られた。
## デビューまで
### 誕生に至る経緯
オグリキャップの母・ホワイトナルビーは競走馬時代に馬主の小栗孝一が所有し、笠松競馬場の調教師鷲見昌勇が管理した。ホワイトナルビーが繁殖牝馬となった後はその産駒の競走馬はいずれも小栗が所有し、鷲見が管理していた。
1984年のホワイトナルビーの交配相手には、小栗によると当初はトウショウボーイが種付けされる予定だったが、種付け予定に空きがなかったため断念した。そこで小栗の意向により、笠松競馬で優れた種牡馬成績を残していたダンシングキャップが選ばれた。鷲見はダンシングキャップの産駒に気性の荒い競走馬が多かったことを理由に反対したが、小栗は「ダンシングキャップ産駒は絶対によく走る」という確信と、ホワイトナルビーがこれまでに出産していた5頭の産駒が大人しい性格だったため大丈夫だろうと感じ、最終的に提案が実現した。
なお、オグリキャップは仔分けの馬で、出
では推論。
question:str = "オグリキャップの主な勝鞍は?"
context:str = get_wiki_content("オグリキャップ")[:1000]
result = model.process(
question=question,
context=context,
threshold=0.1,
show_progress=True,
)
print("削除済みコンテキスト:\\n" + result["pruned_context"])
print("リランキングスコア:", round(result["reranking_score"], 4))
print("圧縮率:", round(result["compression_rate"], 2))
削除済みコンテキスト:\n8連勝、重賞5勝を含む12戦10勝を記録した後、1988年1月に中央競馬へ移籍し、重賞12勝(うちGI4勝)を記録した。
リランキングスコア: 0.7011
圧縮率: 94.0
コンテキストを少し増やしてみる。
question:str = "オグリキャップの馬主は誰?"
context:str = get_wiki_content("オグリキャップ")[:10000]
result = model.process(
question=question,
context=context,
threshold=0.1,
show_progress=True,
)
print("削除済みコンテキスト:\\n" + result["pruned_context"])
print("リランキングスコア:", round(result["reranking_score"], 4))
print("圧縮率:", round(result["compression_rate"], 2))
削除済みコンテキスト:\nオグリキャップの母・ホワイトナルビーは競走馬時代に馬主の小栗孝一が所有し、笠松競馬場の調教師鷲見昌勇が管理した。ホワイトナルビーが繁殖牝馬となった後はその産駒の競走馬はいずれも小栗が所有し、鷲見が管理していた。
なお、オグリキャップは仔分けの馬で、出生後に小栗が稲葉牧場に対してセリ市に出した場合の想定額を支払うことで産駒の所有権を取得する取り決めがされていた。食欲は稲葉牧場にいた頃と変わらず旺盛で、その点に惹かれた馬主が鷲見に購入の申し込みをするほどであった。
1988年1月、馬主の小栗はオグリキャップを2000万円で佐橋五十雄に売却し、佐橋は中央競馬への移籍を決定した。
オグリキャップは初代馬主の小栗孝一が中央で走らせるつもりがなかったことでクラシック登録をしていなかったため、前哨戦である毎日杯を優勝して本賞金額では優位に立ったものの皐月賞に登録できず、代わりに京都4歳特別に出走した。このレースでは翌1989年の全戦に騎乗することとなる南井克巳が鞍上を務め、オグリキャップ一頭だけが58キロの斤量を背負ったが第3コーナーで後方からまくりをかけ、優勝した。
天皇賞(秋)の結果を受け、馬主の佐橋がタマモクロスにリベンジを果たしたいという思いを強く抱いたことからオグリキャップの次走にはタマモクロスが出走を決めていたジャパンカップが選ばれ、オグリキャップは引き続き東京競馬場で調整された。前述のように、オグリキャップは初代馬主の小栗孝一が中央で走らせるつもりがなかったことでクラシック登録をしていなかったため、中央競馬クラシック三冠競走には出走できなかった。1988年9月、オグリキャップの2代目の馬主であった佐橋五十雄に脱税容疑がかかり、将来馬主登録を抹消される可能性が浮上した。これを受けて多くの馬主から購入の申し込みがあり、最終的に佐橋は翌1989年2月22日、近藤俊典へオグリキャップを売却した。ただしこの契約には、オグリキャップが競走馬を引退した後には所有権を佐橋に戻すという条件が付けられており、実態は名義貸しであり、実質的な権限は佐橋に残されているのではないかという指摘がなされた。オグリキャップ売却と同時に佐橋の馬主登録は抹消されたが、近藤は自らの勝負服の色と柄を、佐橋が用いていたものと全く同じものに変更した。
#### 5歳(1989年)
リランキングスコア: 0.7286
圧縮率: 90.12
いいねぇ。
ただし、ある程度大きなコンテキストを与えると瞬間的にVRAM消費は増えるので、バッチサイズを減らしたり、入力コンテキストのサイズに制限かけておくほうがいいかも。

model.processのパラメータ
| パラメータ名 | 型 | デフォルト | 説明 |
|---|---|---|---|
| question | str / Sequence[str] |
なし | クエリ文。リスト指定でバッチ処理。contextとインデックスを対応させる。 |
| context | str / Sequence[str] / Sequence[Sequence[str]] |
なし | 対応するコンテキスト。文字列は単一文書、リストは各クエリに1文書、リストのリストは複数文書または文単位の事前分割。 |
| title | str / Sequence[str] / Sequence[Sequence[str]] / None |
"first_sentence" | 各コンテキストの任意のタイトル。デフォルトの"first_sentence" は先頭文をタイトル扱いにするためのセンチネル。always_select_title=True や first_line_as_title=True と併用すると先頭文を必ず保持できる。Noneでタイトル機能を無効化。 |
| threshold | float | 0.1 | 文章の削除閾値。大きいほど多くを削除。推奨範囲は 0.05–0.5。 |
| batch_size | int | 32 | 推論バッチ内で処理するコンテキスト数。メモリに合わせて調整。 |
| language | str / None |
auto相当 | 分割器の選択。"auto"(英日自動判定)、"ja"、"en"。 |
| reorder | bool | False |
Trueでリランキングスコア順に並べ替え。 |
| top_k | int / None |
None | 並べ替え時に上位k件のみ残す。reorder=Trueと併用。 |
| first_line_as_title | bool | False | 最初の非空行をタイトルとして抽出。 |
| always_select_title | bool | False | タイトル文を必ず残す(プルーニングから保護)。 |
| return_sentence_metrics | bool | False | 各文の確率などのメトリクスを返す。分析向け。 |
| return_sentence_texts | bool | False | 残した文・削除した文のテキストリストを返す。 |
APIサーバにしてみた。
mkdir open-provence-api/ && cd $_
uv venv -p 3.12 --seed
uv pip install torch --torch-backend=auto
uv pip install transformers tokenizers sentencepiece protobuf fast-bunkai nltk
uv pip install gunicorn uvicorn[standard] fastapi
トークナイザーやモデルは事前にダウンロードしておくと良い。
uv run python -c "import nltk; nltk.download('punkt'); nltk.download('punkt_tab')"
uv run python -c "from transformers import AutoModel; \
AutoModel.from_pretrained('hotchpotch/open-provence-reranker-xsmall-v1', trust_remote_code=True)"
サーバのスクリプト。ChatGPTに注文して書いてもらった。モデルとコンテキスト長の上限は環境変数で指定できるようにして、questionとcontextの組み合わせをチェックするようにした。
import os
import gc
import nltk
import torch
from contextlib import asynccontextmanager
from typing import List, Optional, Union, Sequence
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from transformers import AutoModel
# モデル名
MODEL_ID = os.getenv(
"OPEN_PROVENCE_MODEL_ID",
"hotchpotch/open-provence-reranker-xsmall-v1",
)
# コンテキスト合計の上限
MAX_CONTEXT_TOTAL_CHARS = int(os.getenv("MAX_CONTEXT_TOTAL_CHARS", "10000"))
@asynccontextmanager
async def lifespan(app: FastAPI):
try:
nltk.data.find("tokenizers/punkt")
nltk.data.find("tokenizers/punkt_tab")
except LookupError:
nltk.download("punkt", quiet=True)
nltk.download("punkt_tab", quiet=True)
model = AutoModel.from_pretrained(
MODEL_ID,
trust_remote_code=True,
)
if torch.cuda.is_available():
model = model.cuda()
model.eval()
app.state.model = model
yield
del app.state.model
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
app = FastAPI(lifespan=lifespan)
QuestionType = Union[str, List[str]]
ContextType = Union[str, List[str], List[List[str]]]
class RerankRequest(BaseModel):
question: QuestionType
context: ContextType
threshold: float = 0.1
batch_size: int = 32
language: Optional[str] = None
reorder: bool = False
top_k: Optional[int] = None
first_line_as_title: bool = False
always_select_title: bool = False
return_sentence_metrics: bool = False
return_sentence_texts: bool = False
class RerankResponse(BaseModel):
data: dict
def _flatten_context_total_chars(context: ContextType) -> int:
"""contextをどの形で渡されても合計文字数を返す"""
# str
if isinstance(context, str):
return len(context)
# list[str]
if isinstance(context, list) and (len(context) == 0 or isinstance(context[0], str)):
return sum(len(c) for c in context)
# list[list[str]]
total = 0
for sub in context: # type: ignore[assignment]
for s in sub:
total += len(s)
return total
def _same_len(a: Sequence, b: Sequence) -> bool:
return len(a) == len(b)
def _validate_shape_and_size(req: RerankRequest):
"""questionとcontextのshapeをチェックする"""
q = req.question
c = req.context
# question: str
if isinstance(q, str):
# context: str
if isinstance(c, str):
pass
# context: list[str]
elif isinstance(c, list) and (len(c) == 0 or isinstance(c[0], str)):
pass
# context: list[list[str]]
elif isinstance(c, list):
pass
else:
raise HTTPException(400, detail="invalid context shape for question:str")
# question: list[str]
else:
if not isinstance(q, list):
raise HTTPException(400, detail="question must be str or list[str]")
# context: list[str]
if isinstance(c, list) and (len(c) == 0 or isinstance(c[0], str)):
if not _same_len(q, c):
raise HTTPException(
400,
detail=f"question and context must have same length (got {len(q)} and {len(c)})",
)
# context: list[list[str]]
elif isinstance(c, list):
if not _same_len(q, c):
raise HTTPException(
400,
detail=f"question and context must have same length (got {len(q)} and {len(c)})",
)
else:
raise HTTPException(400, detail="invalid context shape for question:list[str]")
# 合計文字数チェック
total = _flatten_context_total_chars(c)
if total > MAX_CONTEXT_TOTAL_CHARS:
raise HTTPException(
400,
detail={
"message": "total context too long",
"total": total,
"max_total": MAX_CONTEXT_TOTAL_CHARS,
},
)
@app.middleware("http")
async def cleanup_after_request(request, call_next):
resp = await call_next(request)
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
return resp
@app.post("/rerank", response_model=RerankResponse)
def rerank(req: RerankRequest):
_validate_shape_and_size(req)
model = app.state.model
with torch.inference_mode():
out = model.process(
question=req.question,
context=req.context,
threshold=req.threshold,
batch_size=req.batch_size,
language=req.language,
reorder=req.reorder,
top_k=req.top_k,
first_line_as_title=req.first_line_as_title,
always_select_title=req.always_select_title,
return_sentence_metrics=req.return_sentence_metrics,
return_sentence_texts=req.return_sentence_texts,
)
return RerankResponse(data=out)
APIサーバ起動。
export OPEN_PROVENCE_MODEL_ID=hotchpotch/open-provence-reranker-large-v1
export MAX_CONTEXT_TOTAL_CHARS=5000
uv run gunicorn app:app \
-k uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--workers 4 \
--max-requests 1000 \
--max-requests-jitter 100
リクエストを送ってみる。
curl -s -X POST http://localhost:8000/rerank \
-H "Content-Type: application/json" \
-d '{
"question": "オグリキャップの主な勝鞍は?",
"context": "オグリキャップ(欧字名:Oguri Cap、1985年3月27日 - 2010年7月3日)は、日本の競走馬、種牡馬[1]。1987年5月に岐阜県の地方競馬・笠松競馬場でデビュー。8連勝、重賞5勝を含む12戦10勝を記録した後、1988年1月に中央競馬へ移籍し、重賞12勝(うちGI4勝)を記録した。1988年度のJRA賞最優秀4歳牡馬[† 1]、1989年度のJRA賞特別賞[† 1]、1990年度のJRA賞最優秀5歳以上牡馬および年度代表馬[† 1]。1991年、JRA顕彰馬に選出。愛称は「オグリ」「芦毛の怪物」など多数。「スーパー・スター」[2]と評された。中央競馬時代はスーパークリーク、イナリワンの二頭とともに「平成三強」と総称され、自身と騎手である武豊の活躍を中心として起こった第二次競馬ブーム期において[3]、第一次競馬ブームの立役者とされるハイセイコーに比肩するとも評される高い人気を得た[4]。その人気は競馬ファンのみならず一般の大衆にも波及し、社会現象を巻き起こした。日本競馬史上において、特に人気を博した競走馬の一頭である。競走馬引退後は北海道新冠町の優駿スタリオンステーションで種牡馬となったが、産駒から中央競馬の重賞優勝馬を出すことができず、2007年に種牡馬を引退。種牡馬引退後は同施設で功労馬として繋養されていたが、2010年7月3日に右後肢脛骨を骨折し、安楽死の処置が執られた。",
"reorder": true,
"top_k": 2,
"batch_size": 4
}' | jq -r .
結果
{
"data": {
"pruned_context": "8連勝、重賞5勝を含む12戦10勝を記録した後、1988年1月に中央競馬へ移籍し、重賞12勝(うちGI4勝)を記録した。",
"reranking_score": 0.6971461176872253,
"compression_rate": 90.08264462809917,
"title": null,
"timing": {
"preprocess_seconds": 0.00035514775663614273,
"assembly_seconds": 3.791693598031998e-05,
"inference_seconds": 1.8919517369940877,
"postprocess_seconds": 0.00022390950471162796,
"total_seconds": 2.01486146915704,
"sentence_collect_seconds": 0,
"sentence_normalize_seconds": 0,
"tokenize_seconds": 0,
"fragment_split_seconds": 1.1714175343513489e-05,
"fragment_decode_seconds": 0.00034343358129262924
},
"performance_trace": {
"preprocess_seconds": 0.00035514775663614273,
"assembly_seconds": 3.791693598031998e-05,
"inference_seconds": 1.8919517369940877,
"postprocess_seconds": 0.00022390950471162796,
"total_seconds": 2.01486146915704,
"sentence_collect_seconds": 0,
"sentence_normalize_seconds": 0,
"tokenize_seconds": 0,
"fragment_split_seconds": 1.1714175343513489e-05,
"fragment_decode_seconds": 0.00034343358129262924
}
}
}
まとめ
素晴らしいな。自分はこれまでリランカーには触れてこなかったのだが、リランカーでこういうことができるのだね。改めてリランカーをきちんと触ってみようという気になった。
全然関係ないけど、だいぶ前に以下を触っていたのを思い出した。発端とかはぜんぜん違うのだけども、なんとなく今回とは考え方やアプローチとは逆だなーと感じた。
どもども、試していただきありがとうございます。大元の検索はシステム的に手を入れるのがいささか面倒なことも多いんですが、後段の reranker やこの provence などは、後ろにフィルタ的に挟むだけなので、意外と簡単い使えるケースも多いので面白いです。またalphaXivなんてあるんですね、知りませんでした(勉強になります)。
process () の title について open_provence github の方の README には書いてあったんですが、huggingface README(model_card)の方に、title が書いていなかったことを、kun432 さんが試された内容を見て気づきました。現在は修正してあります。ありがとうございます。
長い文章コンテキストのものは、title が適切に入ると精度が上がることもあるため、もしよければお試しください。
いつも有用な記事やモデルをありがとうございます!
rerankerはCohereのものを以前試した程度で、トータルの処理時間が伸びるのもあって、あまり活用してなかったのですが、とても興味が湧きました。改めて試してみるきっかけを頂いてありがとうございます。
titleについてもご指摘ありがとうございます。こちらも試してみたいと思います。(上のパラメータの箇所は追記しておきました。)