レシートをローカル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円と認識されてしまいました。
なぜ失敗したか
-
プロンプトで誤認識を修正しようとした
- 「
半は¥の誤認識の可能性がある」とプロンプトに書いた - しかし、LLMは文脈から推測するしかなく、確実な修正はできない
- 「
-
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)
なぜ失敗したか
-
表示サイズ ≠ 実行時メモリ
- モデルファイルのサイズと、実際にメモリに展開されるサイズは異なる
- 推論時の一時メモリも必要
-
量子化の重要性を理解していなかった
-
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分以上かかり、最終的にタイムアウト。
なぜ失敗したか
-
Vision LLMは「普通の写真」用に学習されている
- レシートのような細長い画像は想定外
- アスペクト比が極端だと、モデルが混乱する
-
前処理をしていなかった
- 画像をそのままモデルに渡していた
- パディングやリサイズなどの前処理が必要
推奨: 前処理で比率を抑える
- アスペクト比の調整: 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時間以上かかることが判明しました。
なぜ失敗したか
-
処理時間を軽視していた
- 「動けばいい」と思っていた
- しかし、実務では「待てる時間」が重要
-
無料APIの存在を無視していた
- Google Vision APIは月1,000枚まで無料
- 無料枠内で速度・精度が圧倒的に高い
-
「ローカル必須」の要件を再確認していなかった
- プライバシー要件が本当に必要か?
- 無料枠で足りるなら、クラウドも選択肢に入れるべき
推奨: 要件で判断基準を決める
- ベースラインを数字で取る: 店舗名・日付・金額の正解率、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方式)の検証結果も記事にする予定です。