日本語対応で軽量なOCRモデル indra-ocr を試してみた

お疲れ様です。

indra-ocrという新たなOCRモデル・ツールが公開されていました。 CPUで実行しても高速で動作し日本語にも対応しているとの情報を見て気になったので試してみました。

公式情報

  • 公式のリリース

www.datalab.to

  • HuggingFaceのモデルページ

huggingface.co

  • 公式実装

github.com

使用方法

indra-ocrはpipで公開されています。 pip installやuv addでインストールしてすぐに使用することができます。

pip install indra-ocr

モデルはプログラムを実行したときにHuggingFaceから自動でダウンロードされます。

download

公式実装とAntigravityの力を借りてOCRの実行と検出矩形を描画するプログラムを作成しました。 下記のGitHubリポジトリから確認できます。

github.com

実行結果

上記のプログラムで実際にOCRを実行した結果を見てみます。 入力したデータはエレファントカシマシの歌詞カードをスマホで撮影した画像です。

OCR結果と検出矩形の可視化画像を載せておきます。

こちらは横書きの文字の画像です。 後半の文字が歪んでいるところ以外はうまく文字を検出できているように思います。

ocr_1

detection_1

次に縦書きの文字の画像を試してみます。 縦書きの場合はあまり精度が良くないとの情報があったのですが、実際にあまり良くなさそうです。 一応縦書きの場合は横方向になるように90度回転させる処理が走っているようです。

ocr_2

detection_2

処理速度は0.2秒ほどです。 CPUのみでこれはかなり速いのではないでしょうか…!

所感

使ってみた感想ですが、この精度で日本語OCRができて、高速なのはかなり使えるなという印象でした。 今回カメラで撮影した画像の文字を検出させてみましたが、公式の情報によるとこういう画像より印刷文字などの方が強いとのことです。 実際、請求書などのOCRに使えそうとの話もよく見たので、使いどころとしては書類関係になりそうです。

あとはライセンス周りはちょっと注意で、公式実装(コード)はApache2.0ですがモデル本体のライセンスが競合利用禁止などの条件を含んでいるため、商用利用時は使いどころの確認が必要そうです。 個人利用なら問題なく使っていけそうなのでそれは嬉しいところです。

国産LLM「Sarashina」を自作チャットボットに組み込んでみた

お疲れ様です。

少し前にSB IntuitionsからSarashinaサービスの無償ベータ版が公開されました。
国産LLMの「Sarashina」がAPIで利用できるとのことなので、自作しているチャットボットに組み込んで性能を見てみました。

使用方法

まずは以下のURLにアクセスして「Sarashinaサービス」に登録しましょう。 Googleアカウントがあればすぐに登録できます。

sarashina.ai

登録が完了するとブラウザ上で使えるチャットUIが表示されます。 ここでSarashinaを使ったチャットができますが、今回は自作チャットボットに組み込むのでAPIを使用します。 左下の「APIキー」のところからキーを発行できます。

sarashina_chat_ui

取得済みの画面にはなりますが、実際のUIは以下のようになります。 なお、keyの部分はマスクしています。
「キーの作成」ボタンから新規でAPIキーを発行できます。 APIキーは作成時に一度しか表示されないので、作成後すぐにコピーを取っておく必要があります。 もちろんAPIキーなので取り扱いには十分注意してください。

api_key

ソースコード

APIキーを取得したらプログラムに組み込みます。 組み込んだ自作チャットボットのGitHubリポジトリは以下です。 APIキーは.envに記載して環境変数として読み込む形にしています。

github.com

APIの使用方法については、公式のドキュメントを参考にしてください。 APIから使用できるモデルの種類や設定可能なパラメータなどが確認できます。

docs.sarashina.ai

プログラムとしては、自作チャットボットのプロジェクトの backend/modules/response_generator/sarashina_api.pyです。 LLMはLangChainを使って読み込むように作成しています。 SarashinaのAPIはOpenAI互換APIなのでChatOpenAIで扱うことができます。

llm = ChatOpenAI(
    model=model_name,
    base_url="https://api.sarashina.ai/v1/",
    api_key=os.getenv("SARASHINA_API_KEY", None),
    temperature=temperature,
)

出力結果

実際に自作チャットボットで動かした結果を載せておきます。 こちらのチャットではエレファントカシマシのコンテキストを与えるように設定されています。

Sarashinaの設定については、使用モデルにチャット用モデルのsarashina3-miniを選び、temperatureを0.0に設定しています。

出力は以下のようになりました。 システムプロンプトで返答を500文字程度にまとめるよう指示し、口調を調整しています。 やはり国産LLMなので日本語の表現が自然で良い感じです。 また、返答生成にかかった時間が3.46秒とかなり速いです。 アプリの仕様上、コンテキストの検索時間なども含めた計測となるため、モデルの返答生成のみであれば実際はもっと速いと考えられます。

output

一方でシステムプロンプトを無視して出力することもあるのが気になりました。 この現象はコンテキストの数を減らせば解消したため、 一度にデータを与えすぎると最初に設定したシステムプロンプトを忘れてしまうのかもしれません。

所感

使ってみた感想ですが、さすがに性能は現行のフロンティアモデルには劣るものの、日本語に限ればかなり使いやすそうな印象でした。 これを無償で公開してくれているのは非常にありがたいですね。
(もちろん無償なので入力データはプロバイダーに利用されるとは思いますが、国産LLMの改善に少しでも貢献できるなら良いかなというモチベーションです。)
翻訳用モデルや埋め込み用モデルなど、まだ試せていないSarashinaモデルもあるので個人的にはこれらも試しておこうかなと考えています。

uv環境でSudachiPyのユーザ辞書を作成・登録する手順まとめ【備忘録】

お疲れ様です。

形態素解析器のSudachiPyのユーザ辞書の設定をしたので手順をメモしておきます。
普段からuv環境でPythonを使用しているので、uv環境で作成することを意識した形で手順を記載しようと思います。 (基本的には通常のPython環境と大差はありませんが。)

SudachiPyはインストールされた前提で話を進めます。 インストールしていない場合は下記でインストールしてください。 SudachiDictは3種類(small、core、full)のうちいずれかでOKです。

uv add sudachipy sudachidict-core

ユーザ辞書の設定

実際にユーザ辞書作成の手順を記載します。流れとしては下記の1~3になります。

  1. 登録する単語の情報を作成する
  2. ユーザ辞書をビルドする
  3. 作成したユーザ辞書を登録する

今回使用するプログラムのソースは下記のGitHubのリポジトリに残しておきますので、併せてご参考ください。

github.com

1. 登録する単語の情報を作成する

登録したい単語の情報を特定の形式のcsvにまとめる必要があります。 専用のプログラムを書いて一括で実行するのが良いと思います。

実際にプログラムで作成したcsvは以下のようになります。 エレファントカシマシの楽曲名をいくつか使用しています。

keywords

登録に必要な情報は以下の通りです。csvでは左から順に並んでいます。 注意点としてはcsvファイルの文字コードがutf-8でなければいけません。 Windows環境の場合、デフォルトがcp932なので出力時に必ず指定するようにしてください。

  1. 見出し (TRIE 用)
  2. 左連接ID
  3. 右連接ID
  4. コスト
  5. 見出し (解析結果表示用)
  6. 品詞1
  7. 品詞2
  8. 品詞3
  9. 品詞4
  10. 品詞 (活用型)
  11. 品詞 (活用形)
  12. 読み
  13. 正規化表記
  14. 辞書形ID
  15. 分割タイプ
  16. A単位分割情報
  17. B単位分割情報
  18. ※未使用

0、4、12(見出し、正規化表記)に登録したい単語が入ります。

1(左連接ID)と2(右連接ID)は単語の前後にどのような品詞・単語が接続できるかを判定するためのIDです。いきなり調整することは難しいので固定値でも良いと思います。

3(コスト)は値が小さいほど辞書の中で優先で選択されるようになります。例えば、"風"という単語をコスト1でユーザ辞書に追加した場合、システム辞書にも同様に"風"という単語が存在します。形態素解析の際に"風"という単語が登場するとコストの小さいユーザ辞書側の情報が優先で取得されるようになります。

11(読み)は登録する単語の読み仮名を全角カタカナで記載します。登録する単語数が多い場合、1つ1つ手入力するのは大変なので私はLLM(Gemini)に読みを作成させています。

また、今回は楽曲名を辞書に追加するということで、7(品詞3)に"曲名"と設定しています。この部分は基本的にどんな文字列が入っても問題ないようです。なので、識別しやすい文字列で登録することでユーザ登録した単語を抽出するという使い方も可能になります。

ソースコード全体は下記に載せておきます(長いので折り畳み)。

import json
import os
import re

from dotenv import load_dotenv
from google import genai

load_dotenv()


PROMPT = """
あなたは日本語の楽曲タイトルの読み仮名推定を行うアシスタントです。
次の楽曲タイトルの読みを、全角カタカナで1つだけ返してください。

ルール:
- 返答は全角カタカナのみとする
- 日本語・カタカナ・英字・記号が混ざっていても、最も自然な読みを推定する
- 促音・長音・語尾の変化は自然な日本語として扱う
- 返答は JSON 形式のみとし、キーは "reading" にする
- 読みが曖昧な場合でも、最も自然な仮名表記を返す
- 余計な説明やコードブロックは付けない
- ひらがなや英字は使わず、必ず全角カタカナに変換する

例:
- "星の砂" -> {{"reading":"ホシノスナ"}}
- "さよならパーティー" -> {{"reading":"サヨナラパーティー"}}
- "DEAD OR ALIVE" -> {{"reading":"デッドオアアライブ"}}
- "ハロー New York!" -> {{"reading":"ハローニューヨーク"}}

タイトル: {keyword}
"""


def main() -> None:
    client = genai.Client(api_key=os.getenv("GOOGLE_API_KEY"))
    keywords = []

    # 例として曲名のリストを与える
    examples = [
        "今宵の月のように",
        "風",
        "悪魔メフィスト",
        "漂う人の性",
        "FLYER",
        "Hello. I love you",
        "ヒトコイシクテ、アイヲモトメテ",
    ]

    for title in examples:
        # LLMでキーワードの読みを取得する
        response = client.models.generate_content(
            model="gemini-3.5-flash-lite", contents=PROMPT.format(keyword=title)
        )

        reading = title
        try:
            result = json.loads(response.text.strip())
            if isinstance(result, dict) and "reading" in result and result["reading"]:
                reading = result["reading"]
        except Exception:
            text = response.text.strip()
            match = re.search(r'"reading"\s*:\s*"([^"]+)"', text)
            if match:
                reading = match.group(1)
            elif text:
                reading = text.strip().strip("\"'`")

        print(f"{title} -> {reading}")
        keyword_info = []
        keyword_info.append(title)  # - 0 見出し (TRIE 用)
        keyword_info.append("4786")  # - 1 左連接ID
        keyword_info.append("4786")  # - 2 右連接ID
        keyword_info.append("1")  # - 3 コスト
        keyword_info.append(title)  # - 4 見出し (解析結果表示用)
        keyword_info.append("名詞")  # - 5 品詞1
        keyword_info.append("固有名詞")  # - 6 品詞2
        keyword_info.append("曲名")  # - 7 品詞3
        keyword_info.append("*")  # - 8 品詞4
        keyword_info.append("*")  # - 9 品詞 (活用型)
        keyword_info.append("*")  # - 10 品詞 (活用形)
        keyword_info.append(reading)  # - 11 読み
        keyword_info.append(title)  # - 12 正規化表記
        keyword_info.append("*")  # - 13 辞書形ID
        keyword_info.append("*")  # - 14 分割タイプ
        keyword_info.append("*")  # - 15 A単位分割情報
        keyword_info.append("*")  # - 16 B単位分割情報
        keyword_info.append("*")  # - 17 ※未使用

        keywords.append(keyword_info)

    with open("./keywords.csv", mode="w", encoding="utf-8") as f:
        for keyword in keywords:
            f.write(",".join(keyword) + "\n")


if __name__ == "__main__":
    main()

2. ユーザ辞書をビルドする

csvが作成できたら次はユーザ辞書をビルドします。

uv環境なのでuv runで実行したパターンで記載します。 -sの後にインストール済みのSudachiPyのシステム辞書のパスを指定します。 これもuv環境なので、プロジェクトフォルダ内の「.venv」フォルダからSudachiPyのフォルダを探し、その中から「system.dic」を探してください。 「system.dic」のパスの後に1で作成したcsvファイルのパスを指定します。

uv run sudachipy ubuild -s .venv\Lib\site-packages\sudachidict_core\resources\system.dic .\keywords.csv

system_dict

ビルドに成功するとこのような表示が出ます。 実行したときのカレントディレクトリに「user.dic」というファイルが作成されていることを確認してください。 これがビルドしたユーザ辞書になります。

build

3. 作成したユーザ辞書を登録する

最後に作成したユーザ辞書をSudachiPyで使えるように登録します。 SudachiPyの設定ファイル「sudachi.json」がライブラリ内のフォルダにあるので探してください。 それを複製してカレントディレクトリに配置して編集するか、ファイルをそのまま編集するかどちらでもOKです。 個人的には複製して別ファイルで管理した方がユーザ辞書の設定なしの状態とで使い分けられるのでおすすめです。

setting

「sudachi.json」に下記の内容を追記してください。 リスト内には作成した「user.dic」のパスを記載します。今回はカレントディレクトリに設置しているのでこの書き方です。

"userDict": ["user.dic"]

これで設定を追加できましたが、複製した「sudachi.json」を使用する場合はPythonプログラムでの読み込み時に引数config_pathで設定ファイルのパスを指定する必要があるのでご注意ください。 これを設定しない場合はライブラリのフォルダ内のデフォルトの「sudachi.json」が読み込まれるので、ユーザ辞書を使用しない場合はconfig_pathの指定なしとすることで使い分けができます。

dictionary = sudachipy.Dictionary(config_path="sudachi.json")

動作確認

実際に動作確認してみます。 比較のため、ユーザ辞書の設定をする前と後の両方の出力を示します。 入力する文章は以下で"今宵の月のように"と"風"の部分が今回ユーザ辞書で追加した単語になります。

今宵の月のようにと風という曲が好きです。

設定前後を比較すると、設定後の方で"今宵の月のように"が1つの単語として取得できていることがわかります。 また、"風"はシステム辞書にもある単語なので設定前は普通名詞として取得されていますが、設定後はユーザ辞書に登録した曲名としての"風"が取得できています。

  • ユーザ辞書の設定前

result_before

  • ユーザ辞書の設定後

result_after

参考サイト

ユーザ辞書の作成や設定方法について参考にしたサイトが以下になります。

Sudachiの詳細については下記の記事が網羅的に書かれておりわかりやすかったです。

RF-DETRとRT-DETRを実装して物体検出の精度・速度を比較してみた

お疲れ様です。

前回までの記事で、DETR系の物体検出モデル2種について調べました。
今回は実際にコードを実装し、モデルの学習と結果の出力を試してみましたので、その内容をまとめます。

使用するモデルはRT-DETRとRF-DETRです。
各モデルの詳細は、以下の過去記事をご参照いただければと思います。

fallpoke-tech.hatenadiary.jp

fallpoke-tech.hatenadiary.jp

実行条件

過去に作成したDETRの学習・予測スクリプトにRT-DETRとRF-DETRを追加しました。
Hugging Faceで公開されているモデルの使用を前提としたスクリプトです。 RF-DETRには公式ライブラリもありますが、使ってみたところカスタマイズが難しそうだったため、自作して使い慣れているHugging Faceのコードを使用しています。 ソースコード自体が古くなっていたので、プロジェクトのメンテナンスも同時に行いました。

github.com

データセットには、以前作成した人間の顔検出データを使用します。 学習と検証の内訳は100:23で、データ数としてはかなり少なめです。

学習条件は以下の通りです。 予測時はモデルごとに検出閾値を変えています。 なお、ウォームアップ処理を入れ忘れたため、1枚目の画像の検出速度が遅くなっています。

epoch数: 1000 (50epochベスト更新無でEarlyStopping)
バッチサイズ: 8
入力画像サイズ: 512x512
初期学習率: 1e-4

また、実行環境のスペックは以下の通りです。

CPU: 13th Gen Intel(R) Core(TM) i7-13700F 2.10 GHz
メモリ: 32 GB
GPU: NVIDIA GeForce RTX 4060 Ti
VRAM: 16GB
OS: Windows11 Pro

RT-DETR

まずはRT-DETRから試しました。使用したモデルは以下の通りです。

huggingface.co

検証結果は以下の通りです。
学習時のmAPの推移が不安定で、実際の予測精度についても微妙な結果となりました。 検出閾値を0.1まで下げないと顔部分を検出できず、過検出も目立ちます(データセット側にも問題はありそうですが)。
検出速度は1枚あたり約0.020〜0.025秒でした。

  • 学習

rtdetr_train

  • 予測

検出閾値: 0.1

rtdetr_result

rtdetr_time

RF-DETR

次にRF-DETRです。使用したモデルは以下の通りです。

huggingface.co

検証結果は以下の通りです。
学習も安定しており、精度面でも検出閾値0.5でたまに見逃しがある程度と、良好な感触を得られました。 検出速度は1枚あたり約0.020秒前後でした。 RT-DETRより後発のモデルということもあり、順当に改善されていると感じます。

  • 学習

rfdetr_train

  • 予測

検出閾値: 0.5

rfdetr_result

rfdetr_time

DETR(比較用)

比較のため、同条件で通常のDETRについても学習と予測を実行しました。 データセットの問題か実装の問題かは特定できていませんが、今回の条件では上記2つのモデルよりも精度・予測速度ともに良好な結果となりました。 なお、モデル学習時の収束が遅い点や、予測時に1枚目の画像以外でもたまに処理が遅くなるケース(1枚だけ約0.04秒かかった画像あり)は見られました。

  • 学習

detr_train

  • 予測

検出閾値: 0.5

detr_result

detr_time

所感

今回の検証結果を見る限り、RF-DETRの方が扱いやすい印象を受けました。 実は業務でもすでに活用しており、かなり実用的な結果が出ています。 RT-DETRもデータセットが変われば活躍する場面があるかもしれませんし、今回試しておいてよかったと思います。

検証に使ったデータセットについては、もう少しデータ数を増やすか、別のものを用意したいところです。 少し適当に作りすぎてしまった感があります。 先日VLMを使ったアノテーション補助ツールを作成したので、そちらも活用して準備を進めたいと考えています(いつになるかは分かりませんが)。

物体検出モデル「RF-DETR」について調べたまとめ

お疲れ様です。

今回は物体検出モデルのRF-DETRについて調査し、簡易的な動作確認を行ってみました。

前回の記事で「RT-DETR」というモデルについて調べた内容をまとめています。 DETRをベースにしている点や、YOLO系のようなリアルタイム推論を目指している点など、開発思想が近いモデルです。 下記、前回記事のリンクです。併せてご参照ください。(名前が似ているので混同しそうですね…。)

fallpoke-tech.hatenadiary.jp

論文

arxiv.org

要約(from ChatGPT)

RF-DETRとは

RF-DETRは、Roboflowが開発したリアルタイム物体検出向けのTransformerベースのモデルです。論文「RF-DETR: Neural Architecture Search for Real-Time Detection Transformers」では、Neural Architecture Search(NAS)を利用して、物体検出モデルの精度と推論速度のバランスを自動的に最適化する手法が提案されています。

RF-DETRの大きな特徴は、単一の固定されたモデル構成を利用するのではなく、Weight-Sharing NASによって複数のモデル構成を一つの学習プロセスで探索する点です。Patch Size、入力画像の解像度、Transformer Decoderの層数、Query Token数、AttentionのWindow数などを探索対象とし、精度とレイテンシの異なるモデル構成を生成します。

また、異なるアーキテクチャを学習中に切り替える仕組みは、単にNASのためだけではなく、Architecture Augmentationと呼ばれる正則化の効果も持つことが示されています。これにより、モデルの汎化性能向上にもつながることが報告されています。

バックボーンにはDINOv2を利用し、リアルタイム物体検出に適したモデル構成を探索します。さらに、対象データセットに対してNASを行うことで、COCOなどの一般的なデータセットだけでなく、特定のデータセットに適したモデル構成を探索できる点も特徴です。

論文では、COCOにおいてRF-DETR-Nanoが48.0 AP、RF-DETR-2XLが60 APを超える性能を達成したことが報告されています。

このようにRF-DETRは、単に高精度な物体検出モデルを提案するだけでなく、「対象データセットと推論環境に対して、どのようなモデル構成が適しているか」をNASによって探索することで、精度と速度のトレードオフを柔軟に最適化することを目指したモデルです。

ライセンスは「Apache-2.0 license」なので商用利用も可能です。

実装

公式実装

公式実装は以下のGitHubリポジトリです。

github.com

利用方法

Hugging Faceからモデルをダウンロードして使用する方法と、Roboflow社が公開している公式ライブラリを使用する方法の2通りがあります。
公式ライブラリは pip install で手軽に導入でき、RF-DETR専用ではあるものの学習・評価・学習済みモデルの利用まで幅広く対応しているようです。 手軽に使い始めたい場合は、まず公式ライブラリを試してみるのが良さそうです。

  • HuggingFace

huggingface.co

  • 公式ライブラリ

rfdetr.roboflow.com

動作確認

HuggingFaceのモデルページにあるデモ用コードをベースに動作確認用のコードを作成しました。 コード全体は下記のGitHubリポジトリに公開していますので、必要に応じてご参照ください。

github.com

動作確認用コード

import os
import time

os.environ["HF_HOME"] = "./pretrained"

import torch
from PIL import Image
from transformers import AutoImageProcessor, RfDetrForObjectDetection
from visualize import show_detections

image = Image.open("./2008_002610.jpg")

processor = AutoImageProcessor.from_pretrained("stevenbucaille/rf-detr-base")
model = RfDetrForObjectDetection.from_pretrained("stevenbucaille/rf-detr-base")

inputs = processor(images=image, return_tensors="pt")

start_time = time.perf_counter()

with torch.no_grad():
    outputs = model(**inputs)

end_time = time.perf_counter()
print("Elapsed time:", end_time - start_time)

target_sizes = torch.tensor([image.size[::-1]])
results = processor.post_process_object_detection(
    outputs, target_sizes=target_sizes, threshold=0.5
)[0]

for score, label, box in list(
    zip(results["scores"], results["labels"], results["boxes"])
)[:8]:
    box = [round(i, 2) for i in box.tolist()]
    print(
        f"Detected {model.config.id2label[label.item()]} with confidence "
        f"{round(score.item(), 3)} at location {box}"
    )

# バウンディングボックスを描画して表示
show_detections(image, results, model.config.id2label)

実際の検出結果は以下のようになりました。
画像1枚の推論にかかった時間は約0.2秒でした。 RT-DETRが約0.4秒だったので、2倍近く速くなっていますね。 精度に関しては画像1枚だけでは判断できないため、何かしら同一条件でモデルを学習・検証して比較してみたいところです。

result_vis

result_bbox

参考URL

物体検出モデル「RT-DETR」について調べたまとめ

お疲れ様です。

久しぶりにモデルアーキテクチャの調査です。 今回は物体検出モデルの「RT-DETR」について調べた内容と、簡易的な動作チェックの結果を記事に残しておこうと思います。

論文

要約(from ChatGPT)

RT-DETRとは

RT-DETR(Real-Time DEtection Transformer)は、Transformerをベースとしたリアルタイム物体検出モデルです。DETR(DEtection TRansformer)をベースに、従来のDETRが抱えていた推論速度の問題を改善し、リアルタイムでの物体検出を実現することを目的として開発されました。

物体検出では、画像の中から「何が」「どこに」存在するのかを検出します。RT-DETRでは、画像から特徴を抽出した後、Transformerの仕組みを利用して物体のクラスとBounding Box(矩形領域)を直接予測します。

RT-DETRの特徴

RT-DETRの大きな特徴は、NMS(Non-Maximum Suppression)を必要としないEnd-to-Endな物体検出であることです。

YOLOなどの一般的な物体検出モデルでは、同じ物体に対して複数のBounding Boxを予測するため、推論後にNMSを使用して重複したBounding Boxを除去します。

一方、DETR系のモデルでは「Object Query」と呼ばれる仕組みを利用して、画像中の物体を直接予測します。そのため、NMSに依存せずに検出結果を得ることができます。

画像
  ↓
Backbone
  ↓
特徴抽出
  ↓
Hybrid Encoder
  ↓
Transformer Decoder
  ↓
Object Query
  ↓
クラス + Bounding Box

RT-DETRの構成

RT-DETRは、大きく分けると以下の要素で構成されています。

  • Backbone 入力画像から物体検出に必要な特徴を抽出します。
  • Hybrid Encoder Backboneから得られた複数スケールの特徴量を効率的に処理します。RT-DETRの高速化における重要な要素の一つです。
  • Transformer Decoder Object Queryを利用して、画像中の物体を検出します。
  • Detection Head 検出した物体のクラスとBounding Boxを出力します。

YOLOとの違い

RT-DETRとYOLOはどちらもリアルタイム物体検出を目的としていますが、検出方法が異なります。

YOLOは画像上に多数の候補を生成して物体を検出する方式が基本で、一般的にはNMSなどの後処理を利用します。

一方、RT-DETRはDETRの仕組みを利用し、Object Queryによって物体を直接予測します。そのため、NMSを必要としないEnd-to-Endな検出が可能です。

YOLO

画像
 ↓
特徴抽出
 ↓
大量の検出候補
 ↓
NMS
 ↓
最終的な検出結果


RT-DETR

画像
 ↓
特徴抽出
 ↓
Transformer
 ↓
Object Query
 ↓
最終的な検出結果

RT-DETRのメリット

RT-DETRには、主に次のようなメリットがあります。

  • DETRのEnd-to-Endな検出方式を利用できる
  • NMSなどの後処理を必要としない
  • Transformerによって画像全体の情報を利用できる
  • リアルタイム物体検出に対応できる
  • Decoderの構成を調整することで、速度と精度のバランスを調整できる

また、RT-DETRには複数のモデルサイズが用意されており、用途や利用するGPUに応じてモデルを選択できます。

RT-DETRv2

RT-DETRには改良版であるRT-DETRv2も存在します。

RT-DETRv2では、学習方法やデータ拡張、モデル構成などに改良が加えられており、RT-DETRをベースとして検出精度や性能の向上が図られています。

そのため、RT-DETRを利用する場合は、基本的な仕組みを理解した上でRT-DETRv2についても確認するとよいでしょう。

まとめ

RT-DETRは、DETRのEnd-to-Endな物体検出方式と、リアルタイム処理を両立することを目指したTransformerベースの物体検出モデルです。

特に、

  • Transformerを利用した物体検出
  • Object Queryによる直接的な物体予測
  • NMSを必要としないEnd-to-End構成
  • リアルタイム処理への対応

といった点が特徴です。

YOLOとは異なるアプローチでリアルタイム物体検出を実現しているため、「CNNベースのYOLO」と「TransformerベースのDETR」の違いを理解する上でも重要なモデルです。

DETRのアーキテクチャでYOLO系のようなリアルタイム物体検出を目指したモデルという感じですね。

ライセンスは「Apache license 2.0」なので商用利用も問題ありません。 実運用での精度次第ですが業務内で使うことも検討できそうです…!

実装

公式実装

公式実装は以下のGitHubリポジトリです。 v1とv2の両方のソースが入っています。

github.com

利用方法

利用方法としては、Hugging Faceを利用する方法とUltralyticsを使用する方法の2通りあります。

huggingface.co

docs.ultralytics.com

動作確認

HuggingFaceのモデルページにあるデモ用コードをベースに動作確認用のコードを作成しました。 改良版のRT-DETRv2を使用しています。

ソースコードは下記に記載しておきます。展開してご確認ください。
GitHubに動作可能な構成でプログラム一式を置いていますので、こちらもご参照ください。

学習スクリプトについても今後作成したいと思います。

github.com

動作確認用コード

import os
import time

os.environ["HF_HOME"] = "./pretrained"

import torch
from PIL import Image
from transformers import RTDetrImageProcessor, RTDetrV2ForObjectDetection
from visualize import show_detections

image = Image.open("./2008_000847.jpg")

image_processor = RTDetrImageProcessor.from_pretrained("PekingU/rtdetr_v2_r50vd")
model = RTDetrV2ForObjectDetection.from_pretrained("PekingU/rtdetr_v2_r50vd")

inputs = image_processor(images=image, return_tensors="pt")

start_time = time.perf_counter()

with torch.no_grad():
    outputs = model(**inputs)

end_time = time.perf_counter()
print("Elapsed time:", end_time - start_time)

results = image_processor.post_process_object_detection(
    outputs, target_sizes=torch.tensor([(image.height, image.width)]), threshold=0.5
)

for result in results:
    for score, label_id, box in zip(
        result["scores"], result["labels"], result["boxes"]
    ):
        score, label = score.item(), label_id.item()
        box = [round(i, 2) for i in box.tolist()]
        print(f"{model.config.id2label[label]}: {score:.2f} {box}")

# バウンディングボックスを描画して表示
show_detections(image, results, model.config.id2label)

検出結果は以下のようになりました。
推論にかかった時間は私の実行環境で画像1枚あたり約0.4秒でした。

result_img

result_info

参考URL

【コンペ参加記】SignateのRAGコンペ「AI Engineering Challenge」に挑戦した話

お疲れ様です。

7~8月の期間にSignateのコンペティションに参加していました。 先日表彰式があり区切りがつきましたので、今回はコンペ参加の振り返りや感想を書いていきたいと思います。 上位入賞したわけではないので、一参加者の感想として気軽に読んでいただければ幸いです。

「AI Engineering Challenge ~煩雑な社内ドライブをハックせよ~」と題したコンペで、 RAGを使用して社内共有サーバにあるファイルを検索してユーザが質問した内容に答えるシステムを作成するという内容です。 決められたユーザの質問文に対し、作成したシステムを使って得られた回答の正解率をスコアとして競い合います。

title

user.competition.signate.jp

順位は178位でした。ランクインが416チームあるので全体の約43%(上位半分弱)くらいの位置です。

alt text

alt text

参加のきっかけと背景

まず、こういったコンペに参加するのは今回が初めてでした。 コンペの雰囲気とか自分のレベル感とかを知りたいなと思い参加を決めた形です。 RAGはベクトルDBを使う方法で実装したことがあるので、仕組み自体は作れるなと考えていました。 ちなみに、普段の業務では画像系のディープラーニングモデルの学習・検証や社内向けのアプリ・ツールの作成を主に行っています。 LLMやRAGの実装については個人レベルでしかやっていないという感じです。

参加した感想

ベクトル検索型を使用する方向で実装を進めていましたが、この方法だと実際の返答生成の部分よりベクトルDBを作成する前処理段階に難しさがあるなと感じました。
様々な形式のファイルを扱う課題で、単純なテキストファイルばかりではないのでそこはかなり苦労したポイントです。 ユーザの質問はテキスト形式ですが、検索するデータ側はPDFなどのバイナリの情報だったり画像や図にしかない情報だったりがある感じです。 ユーザが入力した質問内容に対応した情報を受け取れるようにベクトルDBにデータを格納する段階で前処理をする必要があり、ファイル形式ごとに処理を実装して返答生成を試すという繰り返しでなかなか大変でした。

返答生成の段階も単純にベクトルDBから得られた情報をコンテキストにして生成させるだけでなく、より精度を上げるための手法をいろいろ実践できたことは個人的に良かったかなと思います。 Rerankerモデルの導入やLLMを使用した検索クエリの自動生成、類似文書検索とBM25を使用した検索の併用など今後にも生かせそうな様々な知見を得られました。 様々な改善を試してスコアを伸ばすことはできましたが、テキストベースでデータを取得するベクトル検索型のアプローチではどうしてもスコアが頭打ちになっていきました。 ベクトル検索型で進めてきましたがコンペの期間の後半はスコアを伸ばすことができず、そのまま終了という形になりました。

先日の表彰式では上位3名の発表を聞くことができました。 3名はそれぞれアプローチが違いましたが、共通していたのが「エージェント型RAG」を使っていたことでしょうか。 Codexなどのツールを使用したやり方もあれば、実際にエージェントの仕組みから実装したという方までいました。 個人的には1位の方が使用されていたPydantic AIは気になっています…! 私はエージェントを実際に組むということは今のところ経験がないので試してみたくなりました。 こういった上位陣のハイレベルな知見に触れられたのも今回コンペに参加してよかったと感じる点の1つです。

作成したプログラム

プログラムは下記のリポジトリに置いています。

github.com

上述の通りベクトル検索型のRAGを構築しました。 ChromaDBとLangChainを主に使用してプログラムは作成しています。

LLMについては、メインの返答生成の部分を「Gemini 3.5 flash lite」、 検索クエリの生成など各種細かい処理をLLMでさせる場合は「Gemma 4 31B」を使用しました。 それぞれGoogle AI StudioのAPIの無料枠で使用しています。

プログラムの処理フローを以下に載せておきます。 ちょっとチャートが大きくなりすぎたので折りたたんでいます。見たい場合は展開して確認してもらえればと思います。

  • ベクトルDBの作成処理

処理フロー

flowchart TD
    %% -------------------------------------------------------------
    %% 1. 初期化フェーズ
    %% -------------------------------------------------------------
    subgraph S_INIT ["初期化・準備フェーズ"]
        Start(["開始"]) --> InitEnv["環境設定とモデル準備<br/>DATASET_DIR, TARGET_DIR<br/>Embedding, スプリッタ用意"]
        InitEnv --> LoadCache["既存キャッシュの読み込み<br/>file_info_list.csv (処理済み判定)<br/>all_documents.joblib (Doc キャッシュ)<br/>Chroma 4コレクション初期化"]
        LoadCache --> GetFiles["TARGET_DIR 配下の全ファイル一覧を取得"]
    end

    GetFiles --> LoopFiles

    %% -------------------------------------------------------------
    %% 2. ループ制御
    %% -------------------------------------------------------------
    subgraph S_LOOP ["ファイルループ制御"]
        LoopFiles{"未処理ファイルあり?"}
        LoopFiles -->|Yes| CheckProcessed{"処理済みリストに存在?"}
        CheckProcessed -->|Yes| SkipProcessed["スキップして次へ"] --> LoopFiles
        CheckProcessed -->|No| CheckSupported{"対応拡張子?"}
        CheckSupported -->|No| SkipUnsupported["未対応拡張子の判定<br/>スキップして次へ"] --> LoopFiles
    end

    %% -------------------------------------------------------------
    %% 3. ロード・分割フェーズ (縦型集約)
    %% -------------------------------------------------------------
    subgraph S_LOAD ["ロードとテキスト分割フェーズ"]
        CheckSupported -->|Yes| CheckEncrypt{"Office または PDF ?<br/>.xlsx, .docx, .pptx, .pdf"}
        
        %% 暗号化対象
        CheckEncrypt -->|Yes| EncryptJudge{"パスワード保護<br/>されている?"}
        EncryptJudge -->|保護あり| EncryptedDoc["暗号化フラグのみ保持<br/>(本文抽出はスキップ)"]
        EncryptJudge -->|保護なし| LoadOffice["専用ローダーでロード<br/>Excel, Word, PPT, PDF"]
        
        %% 暗号化対象外
        CheckEncrypt -->|No| LoadOthers["各ファイルローダーでロード<br/>CSV, TSV, Py, ipynb, MD, TXT, 画像"]
        
        LoadOffice --> SplitText
        LoadOthers --> SplitText
        EncryptedDoc --> PostProcessDocs

        SplitText["テキスト分割処理<br/>- Python: py_splitter<br/>- テキストや文書: text_splitter<br/>- 画像: 分割なし"]
        
        SplitText --> PostProcessDocs["docs_ids 前処理<br/>- ファイル名ヘッダ付与<br/>- ドキュメント一意ID付与<br/>- all_docs リストに追加"]
    end

    %% -------------------------------------------------------------
    %% 4. ベクトルストア保存フェーズ
    %% -------------------------------------------------------------
    subgraph S_STORE ["Chroma コレクションへの登録"]
        PostProcessDocs --> InsertSwitch{"格納先コレクションの振分"}
        
        InsertSwitch -->|csv| StoreCSV["shared_folder_csv_documents<br/>(バッチ追加: 500件)"]
        InsertSwitch -->|xlsx| StoreExcel["shared_folder_excel_documents<br/>(バッチ追加: 500件)"]
        InsertSwitch -->|その他| StoreAll["shared_folder_all_documents<br/>(tsv: バッチ500件 / 他: 一括)"]
    end

    %% -------------------------------------------------------------
    %% 5. メタデータ & キャッシュ保存
    %% -------------------------------------------------------------
    subgraph S_SAVE ["メタデータ記録とキャッシュ永続化"]
        StoreCSV --> MetaSave
        StoreExcel --> MetaSave
        StoreAll --> MetaSave

        MetaSave["file_info_list.csv に追記保存"]
        MetaSave --> FileInfoDoc["FileInfoCreator でメタ要約を生成<br/>shared_folder_file_info_list に登録"]
        FileInfoDoc --> DumpJoblib["all_documents.joblib にキャッシュ保存"]
    end

    DumpJoblib --> LoopFiles

    %% -------------------------------------------------------------
    %% 6. エラーハンドリング
    %% -------------------------------------------------------------
    subgraph S_ERROR ["例外ハンドリング"]
        CheckSupported -.->|処理中エラー| ErrorHandle["例外キャッチ (try-except)<br/>traceback を error_log.json に追記"]
    end
    ErrorHandle --> LoopFiles

    %% -------------------------------------------------------------
    %% 7. 完了 & 検索テスト
    %% -------------------------------------------------------------
    LoopFiles -->|No| FinishMain["全ファイル完了"]
    FinishMain --> SearchTest["テスト実行<br/>Chroma (all_documents) 類似度検索 (k=10)"]
    SearchTest --> EndNode(["終了"])

  • 返答生成処理

処理フロー

flowchart TD
    %% -------------------------------------------------------------
    %% 1. 初期化フェーズ
    %% -------------------------------------------------------------
    subgraph S_INIT ["初期化・準備フェーズ (generate_response.py)"]
        Start(["開始"]) --> Args["引数解析 (ArgumentParser)<br/>num_top_docs, lambda_mult, output_path, resume"]
        Args --> InitComp["ResponseGenerator 初期化<br/>LLM, Chroma 4コレクション, BM25<br/>Reranker, QueryGenerator, FilePathFilter, 画像キャッシュ"]
        InitComp --> LoadQ["質問データ読み込み (questions_test.csv)"]
        LoadQ --> ResumeCheck{"resume 引数の指定あり?"}
        ResumeCheck -->|Yes| LoadExisting["既存 result_generated.csv を読込<br/>処理済み index を保持"]
        ResumeCheck -->|No| NewDict["新規回答辞書を初期化"]
    end

    LoadExisting --> LoopCheck
    NewDict --> LoopCheck

    %% -------------------------------------------------------------
    %% 2. ループ制御
    %% -------------------------------------------------------------
    subgraph S_LOOP ["質問ループ制御"]
        LoopCheck{"未処理の質問あり?"}
        LoopCheck -->|Yes| SkipCheck{"対象 index は回答済み?"}
        SkipCheck -->|Yes| SkipItem["スキップして次の質問へ"] --> LoopCheck
    end

    %% -------------------------------------------------------------
    %% 3. コンテキスト検索パイプライン
    %% -------------------------------------------------------------
    subgraph S_SEARCH ["1. コンテキスト検索パイプライン (search_context)"]
        SkipCheck -->|No| QGen["QueryGenerator で検索クエリ生成"]
        QGen --> FISearch["vectorstore_file_info から類似度検索 (k=20)"]
        FISearch --> FIRerank["CrossEncoder でファイル概要情報をリランク"]
        FIRerank --> PFilter["FilePathFilter (LLM) で対象ファイルパス絞り込み"]
        PFilter --> MultiMMR["拡張子別に Chroma から MMR 検索<br/>all (k=50) / csv (k=30) / excel (k=30)"]
        MultiMMR --> BM25Merge["BM25 キーワード検索結果を合流 (ハイブリッド化)"]
        BM25Merge --> Dedup["重複コンテンツ除外 (seen_contents)"]
        Dedup --> ChunkRerank["CrossEncoder で全チャンクを質問と照合してリランク<br/>スコア降順で reranked_docs を保持"]
    end

    %% -------------------------------------------------------------
    %% 4. 回答生成パイプライン
    %% -------------------------------------------------------------
    subgraph S_GEN ["2. 回答生成パイプライン (genrate_answer)"]
        ChunkRerank --> Slice0["ドキュメント抽出 (retry=0: Top 1〜20件)"]
        Slice0 --> BuildPrompt0["メッセージ構築<br/>テキスト + 画像 base64 + 質問 + 制約プロンプト"]
        BuildPrompt0 --> LLM0["LLM 呼出 (Ollama / Gemini)<br/>JSON (reason, answer) 取得"]
        LLM0 --> RetryCheck{"回答に わかりません が含まれる?"}
        
        RetryCheck -->|Yes| Slice1["次点ドキュメント抽出 (retry=1: Top 21〜40件)"]
        Slice1 --> BuildPrompt1["メッセージ再構築 (前回の返答履歴含む)"]
        BuildPrompt1 --> LLM1["LLM 再推論 (retry=1)"]
        
        RetryCheck -->|No| FinalAnswer["回答確定"]
        LLM1 --> FinalAnswer
    end

    %% -------------------------------------------------------------
    %% 5. 記録・保存フェーズ
    %% -------------------------------------------------------------
    subgraph S_SAVE ["3. 記録・逐次保存"]
        FinalAnswer --> Record["answers 辞書へ結果格納<br/>回答、思考根拠、参照ファイルパス"]
        Record --> SaveFiles["CSV へ逐次保存<br/>result_generated.csv / predictions.csv"]
    end

    SaveFiles --> LoopCheck
    LoopCheck -->|No| EndNode(["全処理終了"])