🧾

レシートをローカルLLMでマネーフォワードCSVまで作る前に知っておきたい4つの失敗

に公開

はじめに

レシート画像をローカルLLMで読み取り、マネーフォワード(会計ソフト)のインポート用CSVまで自動生成するシステムを構築しました。

この記事では、Phase 1(OCR + テキストLLM方式)で実際にハマった4つの失敗を紹介します。同じことを試そうとしている方の参考になれば幸いです。


この記事で伝えること

  • レシート画像 → ローカルLLM(OCR + テキストLLM)→ マネーフォワードCSV を試した結果の教訓
  • 同じことをやる人向けの「やっておくべきこと/避けるべきこと」
  • 技術選定の判断基準

アンチパターン①: OCRの誤認識をLLMのプロンプトでカバーしようとする

現象

PaddleOCRでレシートを読み取った際、以下のような誤認識が発生しました:

実際の文字 OCR結果 影響
¥108 半106車王 キ106 金額が完全に崩壊
¥ 合計金額の誤認識
¥ 消費税の誤認識
¥2,717 半2,717 金額の先頭文字が誤認識

これらの誤認識テキストをそのままLLMに渡した結果:

{
  "store_name": "FamilyMart",
  "date": "2024-01-05",
  "time": "09:38",
  "items": [{"name": "青NO", "price": "999"}],
  "total": "4497",  // 実際は108円
  "tax": "5248",    // 実際は8円
  "payment_method": "クレジット"
}

実際の金額は108円(税込)だったのに、4,497円と認識されてしまいました。

なぜ失敗したか

  1. プロンプトで誤認識を修正しようとした

    • ¥の誤認識の可能性がある」とプロンプトに書いた
    • しかし、LLMは文脈から推測するしかなく、確実な修正はできない
  2. 2段階処理で誤差が累積

    画像 → OCR → テキスト → LLM → JSON
           ❌誤認識      ❌誤解釈
    
    • OCRの誤認識がLLMの誤解釈を引き起こす
    • レイアウト情報(どこに何が書かれているか)が失われる

推奨: 入力品質を先に固める

  • OCR精度を上げる: 画像の前処理(コントラスト調整、ノイズ除去)を実施
  • OCRをスキップする: Vision LLMで画像から直接構造化データを抽出(Phase 2で採用)
  • 確実な修正ルール: LLMに期待せず、プログラムで確実に修正する
# 推奨: OCR誤字の確実な修正(プロンプトに頼らない)
def correct_ocr_errors(text):
    """OCR誤字を確実に修正"""
    corrections = {
        '半': '¥',
        'ギ': '¥',
        '米': '¥',
    }
    for wrong, correct in corrections.items():
        text = text.replace(wrong, correct)
    return text

アンチパターン②: 「表示サイズ3GB」=「8GB PCで動く」と思い込む

現象

Vision LLMモデルを選定する際、以下のような誤解をしていました:

モデル 表示サイズ 実際のメモリ要求 結果
qwen2.5vl:3b 約3GB 8.4GB以上 ❌ 8GB RAMで落ちる
qwen2.5vl:7b 約7GB 12GB以上 ❌ 8GB RAMで落ちる

「3GBのモデルだから8GB PCで動く」と思い込んでいましたが、実際には実行時メモリが8GBを超えてエラーになりました。

エラーメッセージ例

RuntimeError: CUDA out of memory. Tried to allocate 2.5GB (GPU 0; 4.0GB total capacity; 1.2GB already allocated; 2.8GB free; 4.0GB reserved in total by PyTorch)

なぜ失敗したか

  1. 表示サイズ ≠ 実行時メモリ

    • モデルファイルのサイズと、実際にメモリに展開されるサイズは異なる
    • 推論時の一時メモリも必要
  2. 量子化の重要性を理解していなかった

    • llama3.2-vision:11b(Q4_K_M量子化)なら8GB RAMで動作
    • 量子化により、モデルサイズを約1/4に圧縮できる

推奨: 量子化と実機検証を必ず実施

  • 量子化モデルを使用: Q4_K_M、Q8_0などの量子化版を選ぶ
  • 実機で検証: 実際のPC環境でメモリ使用量を確認する
  • 余裕を持った選定: 表示サイズの2-3倍のメモリが必要と考える
# 推奨: 量子化モデルを使用
ollama pull llama3.2-vision:11b-q4_k_m  # 量子化版

アンチパターン③: 細長いレシートをそのままVision LLMに渡す

現象

コンビニの細長いレシート(アスペクト比が大きい)を処理した際、以下の問題が発生しました:

画像サイズ アスペクト比(高さ/幅) 結果
314×1836 5.85 ❌ JSONエラー
400×1200 3.0 ⚠️ 処理時間5分以上
600×1800 3.0 ❌ ハング(応答なし)

アスペクト比が3以上の細長いレシートで、JSONエラーまたは長時間のハングが発生しました。

エラーメッセージ例

Error: JSON parsing failed. Model output was not valid JSON.

または、処理が5分以上かかり、最終的にタイムアウト。

なぜ失敗したか

  1. Vision LLMは「普通の写真」用に学習されている

    • レシートのような細長い画像は想定外
    • アスペクト比が極端だと、モデルが混乱する
  2. 前処理をしていなかった

    • 画像をそのままモデルに渡していた
    • パディングやリサイズなどの前処理が必要

推奨: 前処理で比率を抑える

  • アスペクト比の調整: 3:4や4:3などの標準比率にパディング
  • 画像の分割: 細長いレシートは上下に分割して処理
  • ドメイン特化のファインチューニング: レシート専用モデルを作る(Phase 3で予定)
# 推奨: アスペクト比の調整
from PIL import Image

def adjust_aspect_ratio(image_path, target_ratio=0.75):
    """アスペクト比を調整(パディング追加)"""
    img = Image.open(image_path)
    width, height = img.size
    current_ratio = height / width
    
    if current_ratio > target_ratio:
        # 高さが長すぎる場合、左右にパディング
        new_width = int(height / target_ratio)
        new_img = Image.new('RGB', (new_width, height), (255, 255, 255))
        new_img.paste(img, ((new_width - width) // 2, 0))
        return new_img
    return img

アンチパターン④: 「ローカル=無料=正義」で終わりにしない

現象

「全部ローカルで完結させたい」という理想を追い求めましたが、現実は以下の通りでした:

項目 ローカルVision LLM Google Vision API(無料枠)
処理時間 1枚30秒〜5分 1枚1-3秒
月100枚の処理時間 約8時間以上 約3-5分
速度差 - 約500倍速
コスト 電気代のみ 月1,000枚まで無料
精度 金額30-40%(素のモデル) 95%以上

月100枚のレシートを処理するのに、ローカルでは8時間以上かかることが判明しました。

なぜ失敗したか

  1. 処理時間を軽視していた

    • 「動けばいい」と思っていた
    • しかし、実務では「待てる時間」が重要
  2. 無料APIの存在を無視していた

    • Google Vision APIは月1,000枚まで無料
    • 無料枠内で速度・精度が圧倒的に高い
  3. 「ローカル必須」の要件を再確認していなかった

    • プライバシー要件が本当に必要か?
    • 無料枠で足りるなら、クラウドも選択肢に入れるべき

推奨: 要件で判断基準を決める

  • ベースラインを数字で取る: 店舗名・日付・金額の正解率、1枚あたり時間を測定
  • 要件を明確にする: 「ローカル必須か」「無料枠で足りるか」を要件で決める
  • 表で比較する: コスト・速度・精度を表にして、判断材料にする
判断基準 ローカルVision LLM クラウドAPI
プライバシー要件が厳しい ✅ 推奨 ❌ 不向き
処理速度を重視 ❌ 不向き ✅ 推奨
月100枚以下 ⚠️ 検討 ✅ 推奨
月1,000枚以上 ⚠️ 検討 ⚠️ 有料

やるべきこと・判断の目安

1. ベースラインを数字で取る

測定すべき項目:

  • 店舗名の正解率
  • 日付の正解率
  • 金額の正解率(最重要)
  • 1枚あたりの処理時間

測定結果の例(Phase 1の結果):

項目 正解率 備考
店舗名 80% 候補から選択できている
日付 95% ほぼ正確
金額 30-40% 実用不可
消費税 70% 誤字が残る

2. プロンプトとファインチューニングの役割を分けて考える

  • プロンプト = 応急処置: 簡単な調整は可能だが、根本的な精度向上は難しい
  • ファインチューニング = 根本治療: 実用的な精度を出すには、専用モデルの学習が必要

3. 「ローカル必須か」「無料枠で足りるか」を要件で決める

判断フローチャート:

プライバシー要件が厳しい?
├─ Yes → ローカル必須(Vision LLM + ファインチューニング)
└─ No → 無料枠で足りる?
    ├─ Yes → クラウドAPI(Google Vision API等)
    └─ No → ローカル + クラウドのハイブリッド

まとめ: 技術選定チェックリスト

レシートをローカルLLMで処理する前に、以下を確認してください:

ハードウェア要件

  • メモリ: 8GB以上(量子化モデル使用時)
  • GPU: 必須ではないが、あると処理速度が向上
  • ストレージ: モデルファイル用に5-10GBの空き容量

処理要件

  • 処理枚数: 月何枚を想定しているか
  • 待てる時間: 1枚あたり何秒まで許容できるか
  • 精度要件: 金額の正解率は何%必要か

プライバシー要件

  • ローカル必須か: データを外部に送信できないか
  • 無料枠で足りるか: 月1,000枚以下なら無料APIが使える

技術選定の選択肢

選択肢 向いているケース
ローカルVision LLM プライバシー要件が厳しい、月100枚以下
クラウドOCR + ローカルLLM プライバシー要件は緩いが、LLMはローカルで
クラウドAPI中心 処理速度を重視、無料枠で足りる

おわりに

Phase 1(OCR + テキストLLM方式)では、上記の4つの失敗を経験しました。

現在はGoogle Vision API(OCR) + Ollama(ローカルLLM)のハイブリッド構成に移行し、実用的な速度と精度を実現しています。

「全部ローカルで」という理想は素晴らしいですが、ユーザー体験を優先して技術選定することが重要だと学びました。

同じことを試そうとしている方の参考になれば幸いです。


参考リンク


次回予定: Phase 2(Vision LLM方式)の検証結果も記事にする予定です。

Discussion