AIに文章を直させたら、内容が消えた
前回の記事で、YomiTokuによるOCRでテキスト抽出の精度は大幅に上がった。でも「95%正確」は「5%が壊れている」ということでもある。
「力ード」が「カード」じゃなく漢字の「力」になっている。「夕イトル」が片仮名の「タ」じゃなく漢字の「夕」になっている。OCR特有の「形が似た文字の取り違え」が残っていた。
これを手作業で直すのは現実的じゃない。504ページの小説、26チャンクに分割したテキスト。1チャンクあたり数千文字。全部目視で確認するくらいなら、自分で読んだほうが早い。
だからLLM(大規模言語モデル)に直させることにした。ローカルで動くOllamaに、OCR誤字だけを修正するよう指示を出す。AIが得意そうな仕事だと思っていた。
やったこと
LLMにOCR誤字修正を任せる:llm_fixステップ
パイプラインに llm_fix というステップを追加した。やることは単純で、OCRテキストをチャンク(6,000文字ずつ)に分割して、OllamaのLLMに「OCRエラーだけを直してください」と送る。
システムプロンプトはこう書いた:
あなたはOCR誤り訂正の専門家です。以下のテキストのOCRエラーのみを修正してください。
修正対象:
- 類似形状の誤認識(例: 目↔日, 力↔カ, 口↔ロ, 夕↔タ)
- 濁点・半濁点の欠落(例: は↔ば↔ぱ)
- OCRによる行途中の不自然な改行
- 文字化け・ゴミ文字
制約:
- 入力テキストの95%以上をそのまま保持すること
- 確信が持てない箇所は修正しない
禁止事項:
- 意味や内容を変えない
- 説明やコメントを追加しない
- 段落構造を変えない
「95%以上をそのまま保持」「意味を変えない」と何度も釘を刺した。これで大丈夫だと思った。
最初の惨事:LLMが文章を要約し始めた
結果は想定外だった。
LLMに渡したテキストが、修正されて返ってくる——のではなく、要約されて返ってきた。3ページ分の文章が1段落に圧縮されていたり、会話文がまるごと消えていたり。「修正」ではなく「書き直し」をしていた。
原因を調べると、OCR直後のテキストの行構造がLLMを混乱させていたことがわかった。
OCRは物理的な列幅で改行を入れる。つまり、原文では1つの文なのに、こうなる:
彼女は窓の外を
眺めていた。空はあくま
でも青かった。
LLMはこの不自然な改行を「別々の文」と解釈する。そして「整理」しようとして、元の文脈を壊す。OCR誤字の修正よりも行構造の解釈にトークン予算を消費してしまい、本来の仕事ができなくなっていた。
前処理の順番地獄
ここから「前処理の順番」との戦いが始まった。
失敗1:normalize_ocrだけでLLMに渡した
最初は normalize_ocr(HTMLタグ除去・基本的な整形)だけやってLLMに渡していた。行構造はOCRのまま。LLMは行結合と誤字修正の両方をやろうとして、どちらも中途半端になった。
失敗2:sentence_structをLLMの後に入れた
「じゃあ行結合をやればいいのか」と sentence_struct(文構造化)を追加した。ただし最初はLLMの後に配置してしまった。すでにLLMが行構造を壊した後では、sentence_structが正しく動かない。壊れた出力をさらに壊す結果になった。
正解:sentence_structをLLMの前に入れる
最終的にたどり着いた順番はこうだった:
YomiToku OCR出力
→ normalize_ocr :HTMLタグ除去 + ルビ抽出
→ sentence_struct :段落バッファ方式で一文一行化
→ llm_fix :OCR誤字修正のみに専念
sentence_struct がOCRの不自然な改行を先に直すことで、LLMは「きれいな一文一行のテキスト」を受け取る。こうなると、LLMは本来の仕事——OCR誤字の修正——に集中できる。
sentence_structの段落バッファ方式
sentence_struct は最初「ペア結合方式」だった。隣り合う2行を見て、結合するかどうかを判定する。しかしOCRの改行は「任意の文字」で終わるので、ペアだけ見ても判断できず、結合数がほぼゼロだった。
そこで「段落バッファ方式」に切り替えた。文末記号(。!?」など)が現れるまで行をバッファに蓄積し続ける。引用符の中のピリオドでは分割しない。これにより、1チャンクあたり6〜7箇所の結合が正しく行われるようになった。
LLMの暴走防止:Over-correction Gate
もう一つ追加したのが、LLMの出力を検証する「Over-correction Gate」だ。
入力と出力のCER(文字エラー率)を計算し、5%以上変わっていたらLLMが暴走したとみなしてリジェクトする。「95%以上をそのまま保持」というプロンプトの制約を、コードでも強制する仕組みだ。
結果
パイプライン精度の推移
PDF原文を手で書き起こした「マスターテキスト(M0)」と比較した精度:
OCR生出力(raw) → 40.2%
deep_clean 適用後 → 52.9%
ocr_confusion_fix 適用後 → 56.9%
sentence_struct 適用後 → 79.4%
punct_normalize 適用後 → 81.4% (102文中83文が正確)
目標だった60%を大幅に超え、81.4%の精度を達成した。
LLM処理の安定化
前処理パイプラインを整えたことで:
failed_chunks = 0(失敗チャンクゼロ)- 品質チェック全項目PASS
- 奥付パターン残留ゼロ
- 孤立ルビ行ゼロ
504ページ・26チャンクの小説を通しで処理しても、LLMが暴走しなくなった。
うまくいった点
- 前処理の順番が決定的に重要だった — 同じLLM、同じプロンプトでも、入力テキストの品質で結果が劇的に変わる。sentence_structをLLMの前に入れただけで、LLMの暴走がほぼ消えた
- OCR混同辞書(confusion dict)が効いた —
力→カ、夕→タ、口→ロのような形状類似の誤認識パターンを30件以上辞書化した。LLMに頼る前にルールベースで直せるものは直す。これでLLMの負荷が下がった - Over-correction Gateの安心感 — LLMが暴走しても検出できる仕組みがあると、大量処理を回しても怖くない。5%閾値は厳しめだが、OCR校正の用途にはちょうどいい
- 段落バッファ方式への切り替え — ペア結合方式からの切り替えは正解だった。OCR改行は2行セットで解決できるほど単純じゃない
失敗・課題
- LLMへの過信 — 「AIに文章を渡せば勝手に直してくれる」と思っていた。実際には、LLMは入力の品質に非常に敏感で、汚いテキストを渡すと「修正」ではなく「書き直し」を始める。LLMは魔法じゃない。きれいな入力を用意するのは人間の仕事だ
- 前処理パイプラインの順番を3回間違えた — normalize_ocrだけ、sentence_structを後ろに、deep_cleanとの順序……正しい組み合わせにたどり着くまでに何度もパイプライン全体を再設計した
- deep_cleanのボトルネック — M0(マスター原本)との比較でdeep_clean段階の精度は65.4%。ルビ残留や#記号の処理が最大のボトルネックで、まだ改善の余地がある
- 精度測定の基準づくりが大変だった — 「何が正しいか」の基準(M0マスターテキスト)は、結局PDFを手で読んで書き起こすしかなかった。11〜20ページを手読みしてM0を作成し、Claudeが生成していたM0との差分から12箇所以上のエラーを発見した
次にやること
テキストの前処理とLLM校正は安定した。次の課題はLLMモデルそのものだ。
最初はデフォルトの llama3.2:3b を使っていたが、処理速度と品質のバランスが悪い。qwen3:32b に切り替えたら安定したが、今度はthinking機能が暴走して処理時間が14倍に膨れ上がった。
次回は、LLMモデルの選定で何が起きたか——そしてthinking抑制で処理速度が劇的に改善した話を書く。
この実験で使った機材 【PR】
ZOTAC GAMING GeForce RTX 5090 SOLID — Ollama + qwen3:32b(20GB)をローカルで動かすのに必要なVRAM 32GB