技術 約16分で読めます

エンコーダーモデル+ローカルLLMでOCR誤字を自動検出・修正する

背景

OCRで読み取ったテキストの欠損・誤認識を修正するのに、QwenやSwallowなどのローカルLLMを使っていた。当たるっちゃ当たるが、文脈を無視して埋めたり(Qwen)、勝手にリライトしたり(Swallow)という問題がある。

そこで、生成AI(デコーダー系)ではなくエンコーダー系の小型モデル(LUKE/BERT)を使うアプローチを試してみる。エンコーダー系は文章の左右両方の文脈を見て穴埋めするため、「原文を壊さずピンポイントで欠損を推測する」用途に向いている。

京都大学のWikipediaAnnotatedCorpusは、形態素解析・構文解析・格解析・照応解析がアノテーションされた約9,000記事のコーパス。これを使ってLUKEを追加学習(ファインチューニング)させれば、「てにをは」の整合性や省略された主語の補完精度が上がる可能性がある。

実験環境

項目スペック
GPUNVIDIA GeForce RTX 4060 Laptop(VRAM 8GB)
メインメモリ32GB
OSWindows 11(WSL2 Ubuntu 22.04で作業)
Python3.10.12(WSL2側)
CUDA12.9

環境の確認

まずWindows側の状態を確認する。

$ nvidia-smi
NVIDIA-SMI 576.02    Driver Version: 576.02    CUDA Version: 12.9
RTX 4060 Laptop GPU  |  0MiB / 8188MiB  |  0%

$ python --version
Python 3.14.0

$ wsl --list
Ubuntu-22.04 (既定)

Windows側のPython 3.14はPyTorch未対応なので使えない。WSL2のUbuntu 22.04側で作業する。

WSL2内の確認:

$ python3 --version
Python 3.10.12

$ nvidia-smi
NVIDIA-SMI 575.51.02    Driver Version: 576.02    CUDA Version: 12.9

WSL2からもGPUが見えている。Python 3.10でPyTorchもOK。pip3とvenvが未インストールだったので入れる。

WSL2環境のセットアップ

# pip3, venvのインストール
sudo apt update && sudo apt install -y python3-pip python3-venv

# 仮想環境の作成
python3 -m venv ~/luke-ocr
source ~/luke-ocr/bin/activate

# PyTorch(CUDA 12.4対応)
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu124

# transformers 4.x系(5.xはMLukeTokenizerにバグあり)
pip install 'transformers>=4.40,<5' sentencepiece protobuf tiktoken

transformers 5.2.0を最初に入れたが、MLukeTokenizer の初期化で TypeError: argument 'vocab': 'dict' object cannot be converted to 'Sequence' が出る。4.57.6にダウングレードしたら解消した。

インストール後の動作確認:

import torch
print(torch.__version__)        # 2.6.0+cu124
print(torch.cuda.is_available()) # True
print(torch.cuda.get_device_name(0))  # NVIDIA GeForce RTX 4060 Laptop GPU

import transformers
print(transformers.__version__)  # 4.57.6

LUKEのfill-maskベースラインテスト

まずは追加学習なしの素のLUKEで穴埋め精度を確認する。使用モデルは studio-ousia/luke-japanese-base-lite。VRAM使用量はたった512MB。

from transformers import MLukeTokenizer, LukeForMaskedLM
import torch

model_name = "studio-ousia/luke-japanese-base-lite"
tokenizer = MLukeTokenizer.from_pretrained(model_name)
model = LukeForMaskedLM.from_pretrained(model_name).to("cuda")

基本テスト

入力Top1予測確信度評価
織田信長は京都の本能寺で<mask>した。死去32.0%近い(「自害」が理想)
東京は日本の<mask>である。首都60.1%正解
太郎はパンを<mask>べた。64.7%正解(「食」べた)
彼女は大学で<mask>を学んでいる。数学7.6%妥当(確信度は低い)

OCR欠損シミュレーション

実際のOCRで起きやすいパターン(助詞・送り仮名・固有名詞の欠損)をテスト。

パターン入力正解Top1確信度当たり
助詞日本語<mask>文法は複雑である。:16.0%x(2位に「の」10.2%)
助詞<mask>東京に住んでいる。もまた11.4%x(2位に「は」10.0%)
送り仮名彼は会議に出<mask>した。21.6%x(2位に「席」14.8%)
送り仮名この問題を解<mask>するのは難しい。こうと20.0%x
固有名詞<mask>島県は四国にある。-5.4%x
固有名詞ノーベル<mask>学賞を受賞した。物理等生理34.1%△(「生理学賞」も正解の一つ)
文脈依存彼は毎朝6時に<mask>きる。起きて5.6%△(トークン単位の差)
文脈依存この薬は食後に<mask>んでください。84.6%o

ベースラインの所見

  • 文脈が強い場合(「薬→飲む」)は追加学習なしでも高精度
  • 助詞の推測は方向性は合っているが、Top1に来ないケースが多い
  • 固有名詞(地名など)は苦手。文頭の1文字欠損は特に弱い
  • 「解決する」のような複合語の途中欠損は、サブワードトークナイズとの相性が悪い

ここから京大コーパスで追加学習させると、助詞や格関係の精度がどう変わるかが見どころ。

京大コーパスの準備

WikipediaAnnotatedCorpusをcloneしてデータを確認する。

git clone https://github.com/ku-nlp/WikipediaAnnotatedCorpus.git

ディレクトリ構成:

  • knp/: KNP形式のアノテーション付きデータ(3,979ファイル)
  • org/: 生テキスト
  • id/: train/dev/test分割(train 3,679, dev 100, test 200)

KNP形式の中身

足利尊氏のWikipedia記事(wiki00010002.knp)の冒頭:

# S-ID:wiki00010002-00-01
* 3D
+ 1D
足利 あしかが 足利 名詞 6 人名 5 * 0 * 0 NIL <NE:PERSON:head>
+ 8D <NE:PERSON:足利 尊氏>
尊氏 たかうじ 尊氏 名詞 6 人名 5 * 0 * 0 NIL <NE:PERSON:tail>
は は は 助詞 9 副助詞 2 * 0 * 0 NIL
...
+ -1D <rel type="ガ" target="尊氏" .../>
武将 ぶしょう 武将 名詞 6 普通名詞 1 * 0 * 0 NIL

各行に形態素(表層形・読み・原形・品詞)、+ 行に格解析(ガ格・ヲ格・ニ格…)、<NE:...>に固有表現タグが付いている。

rhoknpでパース

READMEで推奨されているrhoknpライブラリを使う。pyknpは古い。

pip install rhoknp
from rhoknp import Document

with open("knp/wiki0001/wiki00010002.knp") as f:
    doc = Document.from_knp(f.read())

for sent in doc.sentences:
    text = "".join(m.text for m in sent.morphemes)
    print(text)
    # => 足利 尊氏は、鎌倉時代末期から室町時代前期の武将。

格解析情報も取れる:

文: 足利 尊氏は、鎌倉時代末期から室町時代前期の武将。
  [末期から] --ノ--> 時代
  [武将。]   --ガ--> 尊氏
  [武将。]   --カラ--> 末期

テキスト抽出

全KNPファイルから原文テキストを抽出し、10文字未満の短文(括弧内の読みがなど)は除外。

Split文数
train10,243
dev312
test545

ファインチューニング

学習設定

BATCH_SIZE = 8
EPOCHS = 3
LR = 5e-5
MAX_LEN = 128
MASK_PROB = 0.15  # 15%のトークンをランダムにマスク

標準的なMLM(Masked Language Modeling): 入力文の15%をランダムに<mask>に置換し、元のトークンを予測させる。そのうち80%をマスク、10%をランダムトークン、10%をそのまま保持。

学習結果

EpochTrain LossDev Loss時間VRAM Peak
12.0501.943203s2,677 MB
21.9191.933200s2,677 MB
31.6921.853202s2,677 MB

3エポック合計約10分。VRAMは8GBのうち2.7GBしか使っていない。バッチサイズを16や32に上げる余裕もある。

ベースライン vs ファインチューニングの比較

同じテスト文で、追加学習前後の精度を比較する。Top3に正解が含まれていれば「o」。

テスト正解ベースラインTop1FT後Top1改善
本能寺で<mask>した死去死去(32.0%)修行(20.7%)x 悪化
日本の<mask>である首都首都(60.1%)首都(85.6%)o 大幅改善
パンを<mask>べたた(64.7%)た(69.9%)o 微改善
大学で<mask>を学んでいる数学等数学(7.6%)数学(16.5%)o 改善
日本語<mask>文法は:(16.0%)では(13.7%)x 変わらず
<mask>東京にもまた(11.4%)自身は(38.2%)x 悪化
<mask>した発(21.6%)席(82.6%)o 大幅改善
<mask>するのはこうと(20.0%)こうと(25.2%)x 変わらず
<mask>島県は四国-(5.4%)(24.3%)x 変わらず
ノーベル<mask>学賞物理等生理(34.1%)生理(75.5%)△ 確信度UP
6時に<mask>きる起きて(5.6%)起(27.8%)o 大幅改善
食後に<mask>んで飲(84.6%)飲(79.0%)o 維持

比較の所見

改善したケース:

  • 「出席した」: 14.8% → 82.6%(Top1に浮上)
  • 「首都」: 60.1% → 85.6%(確信度UP)
  • 「起きる」: Top3圏外 → 27.8%でTop1(大幅改善)
  • 「数学」: 7.6% → 16.5%(確信度2倍)

悪化したケース:

  • 「死去した」: 32.0% → 5.5%に下がり、「修行」がTop1に
  • 「彼は」: 助詞単体の推測が苦手なまま

変わらないケース:

  • 固有名詞の1文字欠損(「徳島県」)は依然として当たらない
  • 複合語の途中欠損(「解決する」)も改善なし

京大コーパスはWikipedia百科事典的な文体なので、百科事典的な穴埋め(「首都」「出席」など)は大きく改善した。一方、助詞単体や固有名詞の部分欠損はこのコーパスだけでは不十分で、別のアプローチ(文字レベルのモデルやOCR特化の学習データ)が必要。

WikipediaモデルにWikipediaを食わせている問題

ここで気づいたが、LUKEの事前学習データはWikipedia + BookCorpus。そして京大コーパスもWikipediaから抽出した約4,000記事。つまりLUKEがすでに学習済みのドメインのテキストを、アノテーションを活かさずにもう一回食わせていた。改善が限定的だったのはこれが大きい。

今回の学習ではコーパスの原文テキストだけを使い、格解析(ガ格・ヲ格・ニ格の関係)、照応解析(代名詞の指し先)、固有表現タグ(人名・地名の区別)といったアノテーション情報は活用していない。京大コーパスの本当の強みはこれらのアノテーションにあるので、活かすなら格ラベル予測や固有表現分類のマルチタスク学習にする必要がある。

効果を出すための方向性:

  1. アノテーションを活かしたタスク設計にする
  2. Wikipedia以外のドメイン(古文書、役所文書など)のテキストで学習する
  3. ベースモデルをWikipedia以外で学習されたものに変える

東北大BERT v3での比較実験

LUKEと同じくWikipediaベースだが、アーキテクチャとトークナイザが異なる cl-tohoku/bert-base-japanese-v3 でも試す。

  • LUKE: SentencePieceトークナイザ(サブワード分割)
  • 東北大BERT: MeCab + WordPiece(形態素ベース分割)

BERT v3もWikipedia + CC-100で事前学習されているため、Wikipedia被り問題は同様に存在する。ただしトークナイザの違いが結果に影響するかが見どころ。

追加で fugashi(MeCabのPythonバインディング)と unidic-lite が必要。

pip install fugashi unidic-lite

学習結果

EpochTrain LossDev Loss時間VRAM Peak
11.5031.295201s2,417 MB
21.3701.395198s2,417 MB
31.2281.419198s2,417 MB

LUKEより全体的にLossが低い。ただしdev lossがEpoch2→3で微増しており、若干の過学習傾向。

4モデル比較

LUKE(ベースライン / FT後)と東北大BERT(ベースライン / FT後)の4パターンを並べる。

テスト正解LUKE素LUKE FTBERT素BERT FT
本能寺で○した死去死去(32%)修行(21%)自害(46%)自害(34%)
日本の○である首都首都(60%)首都(86%)地名(51%)首都(67%)
パンを○べたた(65%)た(70%)お(2%)焼く(23%)
大学で○を学ぶ数学等数学(8%)数学(17%)哲学(8%)哲学(14%)
日本語○文法:(16%)では(14%)の(99%)の(99%)
彼○東京にもまた(11%)自身は(38%)は(81%)は(86%)
出○した発(22%)席(83%)##頭(62%)##頭(34%)
解○するのはこうと(20%)こうと(25%)と(34%)##法(48%)
○島県は四国-(5%)(24%)淡路(4%)十(17%)
ノーベル○学賞物理等生理(34%)生理(76%)物理(80%)物理(51%)
6時に○きる起きて(6%)起(28%)しゃべり(7%)始まり(6%)
食後に○んで飲(85%)飲(79%)飲ま(44%)お(34%)

モデル特性の違い

東北大BERTはMeCab形態素解析ベースのトークナイザなので、助詞(「の」「は」)の推測が圧倒的に強い。形態素単位でトークンが切れるため、助詞が独立した1トークンとして扱われる。

一方、LUKEはSentencePieceによるサブワード分割なので、文字レベルの穴埋め(「出○した」→「席」)ではLUKEの方が柔軟に対応できている。

得意分野LUKE東北大BERT
助詞の推測弱い非常に強い
文字レベルの穴埋め柔軟サブワードの影響あり(##付き)
固有名詞弱い弱い(どちらも)
FTの改善幅一部で大幅改善限定的(Wikipedia被り)

どちらのモデルも、京大コーパスでの追加学習による改善は限定的だった。Wikipedia被りの問題は東北大BERTでも同様に存在する。

実際のOCR出力で校正してみる

ここまでのfill-maskテストは人工的に<mask>を入れた穴埋め問題だった。実際のOCRでは「ここが欠損です」と教えてくれないので、まったく違うアプローチが必要になる。

NDLOCR-Liteのセットアップ

国立国会図書館が公開した軽量OCR NDLOCR-Lite をWSL2にインストール。セットアップ手順の詳細はNDLOCR-LiteをWindowsで動かしてみたを参照。

git clone https://github.com/ndl-lab/ndlocr-lite.git
cd ndlocr-lite
pip install -r requirements.txt

サンプル画像(1963年の国会図書館スタッフ・マニュアル)でOCRを実行。CPUオンリーで1.5秒。

cd src
python ocr.py --sourceimg ../resource/digidepo_2531162_0024.jpg --output /root/ocr-output

OCR出力と誤認識

出力テキストの一部:

(z)気送子送付管
気送子送付には、上記気送管にて送付するものと、空
気の圧縮を使用せず,直接落下させる装置の二通りがあ
る。後者の送付雪は出納台左側に設置されており.5
3.1の各層ステーションに直接落下するよう3本の管
が通じ投入ロのフタに層表示が記されている。取扱いに
当っては気送子投入優すみやかにフタを閉め速度を調整

目視で確認できる誤認識:

  • 「送付」→ 正しくは「送付
  • 「投入」→ 正しくは「投入
  • 「投入」→ 正しくは「投入」(カタカナのロ → 漢字の口)
  • 「待する」→ 正しくは「待する」
  • 「(z)」→ 正しくは「(ヱ)」

Perplexityベースの校正

fill-maskでは<mask>の位置を知っている前提だが、OCR校正では「どこが間違っているか」を先に見つける必要がある。

そこで、各トークンを1つずつマスクして「その位置にその文字が来る確率」を計算する。確率が極端に低いトークン(閾値1%未満)は、文脈に合わない=誤認識の可能性が高い。

def check_line(text, tokenizer, model, threshold=0.01):
    encoding = tokenizer(text, return_tensors="pt")
    input_ids = encoding["input_ids"][0]
    suspects = []

    for i in range(len(input_ids)):
        # i番目のトークンをマスクに置換
        masked_ids = input_ids.clone().unsqueeze(0).to("cuda")
        original_id = masked_ids[0, i].item()
        masked_ids[0, i] = tokenizer.mask_token_id

        with torch.no_grad():
            outputs = model(input_ids=masked_ids, ...)

        probs = torch.softmax(outputs.logits[0, i], dim=-1)
        original_prob = probs[original_id].item()

        if original_prob < threshold:
            # 怪しいトークン → 修正候補を出す
            top_tokens = probs.topk(3)
            suspects.append(...)
    return suspects

4モデル共通で検出できた誤認識

LUKE素 / LUKE FT / BERT素 / BERT FTの4モデルで同じOCRテキストを校正し、主要な誤認識箇所の検出結果を比較。

OCRテキスト正解LUKE素LUKE FTBERT素BERT FT
送付箱(38%)機(29%)機(17%)ポスト(9%)
投入後(38%)後(44%)後(73%)後(84%)
投入口(61%)口(67%)口(84%)口(56%)

3箇所とも4モデル全てが「このトークンは怪しい」(確率1%未満)と判定できた。再現性が高い。

→後」はBERT FTが84%で最も確信度が高く、「→口」はBERT素が84%。いずれも正解候補をTop1で出している。

→管」は「管」がTop候補に来ず「箱」「機」が出ている。正解ではないが「雪はおかしい」という検出自体はできている。元の文書が「気送子送付管」という専門用語なので、一般的な言語モデルでは「管」より「機」「箱」を推すのは妥当。

校正の所見

  • ファインチューニングの有無よりも、「perplexityベースの検出」という手法自体が効いている
  • 4モデルとも同じ箇所を疑わしいと判定するため、モデル間の差は小さい
  • 正解候補の精度は東北大BERTの方が全体的に高い(「後」73-84%、「口」84%)
  • 専門用語の修正候補は的外れになりがち(「送付管」は一般語彙にない)
  • false positive(正しいのに怪しいと判定される箇所)が多い。「気送子」「ステーション」のような専門用語や固有名詞は軒並み引っかかる

3段パイプライン: BERT検出 → 小型LLM修正

BERTの検出精度は十分だが、「怪しい」と判定された箇所を人間がいちいち確認するのは現実的でない。小型のローカルLLMを組み合わせて、検出→修正の自動パイプラインを組めないか試す。

ollamaのセットアップ

WSL2にollamaをインストールし、小型モデルをプルする。

curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen2.5:1.5b  # 986MB
ollama pull qwen2.5:3b    # 1.9GB

VRAMはBERTが約2.4GB使うため、推論を分離して使い回す。BERT検出後にアンロードし、LLM推論時にollamaがGPUを使う形。

v1: 行ごとに修正依頼

最初のアプローチ: BERTで怪しい箇所を検出し、その情報と一緒に行全体をLLMに渡して修正させる。

結果: 惨敗

  • 1.5B: 原文を無視して完全にリライト。「気送管」→「真空管」、「気送管による」→「気圧管による」など、別の文書を生成してしまう
  • 3B: 1.5Bよりはマシだが、やはり勝手に文を変える。「目的層の」→「目的地の」、「消える」→「点灯する」(逆の意味に修正)

小型LLMに「行全体を修正して」と依頼すると、コンテキストウィンドウの中で自由に生成するため、原文の保持ができない。これは以前Qwen/Swallowで経験したのと同じ問題。

v2: 1文字ずつYES/NO判定

BERTが検出した各トークンについて、個別に「この修正は正しいですか?」とYES/NO形式で3Bに聞く。

結果: 全部NO

行[5] 「ロ」→「口」(84%): LLM=NO -> 却下
行[6] 「優」→「後」(73%): LLM=NO -> 却下

正しい修正(「優→後」「ロ→口」)まで含めて35件全てNO。3Bモデルには「よくわからないから変えない」という保守バイアスがある。YES/NOの二値分類は小型モデルにとって「責任が重い」タスクで、NOに倒れやすい。

v3: A/B選択式 + フィルタリング強化

YES/NOではなく、原文の行と修正した行を並べて「AとBどちらが正しいか」選ばせる。また、BERTの検出結果を以下の条件でフィルタリング:

  • BERTのTop1候補が句読点・助詞・汎用動詞の場合は除外(行跨ぎの誤検出が多い)
  • Top1の確率が30%未満は除外
  • ##付きサブワードトークンは除外

フィルタ後14箇所 → LLM検証で5箇所を修正:

元の文字修正後BERT確率LLM判定正誤
673%B(修正採用)正解
17交通通行87%B(修正採用)正解
4落下接続32%B(修正採用)誤修正
3##納53%B(修正採用)意味なし
16##子47%B(修正採用)意味なし

正しい修正が2件、誤修正が1件、サブワードの問題で意味をなさない修正が2件。

見逃したもの

  • 「投入」: BERTは84%で検出していたが、LLMはA(原文維持)を選択。カタカナ「ロ」と漢字「口」はUnicode上も見た目も類似しており、3Bモデルには区別が難しい
  • 「二」: BERTが100%で検出したが、LLMが却下
  • 「送付」: BERTのTop1が「管」ではなく別の候補だったため、修正候補自体がずれた

パイプラインの評価

3段パイプラインの方向性自体は正しいが、3Bクラスのローカルモデルでは判定精度に限界がある。

検出(BERT):

  • perplexityベースの検出は安定して動く。4モデルとも同じ箇所を検出する再現性がある
  • 閾値とフィルタの調整でfalse positiveはある程度絞れる
  • ただし専門用語(「気送子」「ステーション」)は構造的に引っかかり続ける

修正(小型LLM):

  • 1.5B: 原文破壊。校正には使えない
  • 3B(行全体修正): 原文をリライトする傾向
  • 3B(YES/NO): 全部NOの保守バイアス
  • 3B(A/B選択): 一部は正しく判定できるが、精度は5割程度

実用的には、BERTの検出結果(怪しい箇所 + Top候補)を人間に提示して、最終判断は人間がやるのが現時点での最適解。完全自動化には7B以上のモデルか、OCR特化のファインチューニングが必要。

7Bモデルでの敗者復活戦

3Bで力不足なら7Bではどうか。Qwen2.5:7bは4bit量子化で4.7GB。BERTをアンロードすればVRAM 8GBに収まる。

「気送子」を知っているか

まず語彙力テスト。

Q: 「気送子」とは何ですか?

3B: 「気送子」は、中国語の俗語で、他人に対して優しく配慮する人を指します。
7B: 「気送子」は、空気を用いて粉体や微粉末状の物質を搬送する装置やシステムのことです。

3Bは完全にハルシネーション。7Bは正確に理解している。パラメータ数の差が語彙力に直結する典型例。

3B vs 7B: 同一テストでの正答率

BERTが検出した14箇所の修正候補に対して、3Bと7Bの判定を比較。人間が正解を付けた上での正答率:

元の文字修正候補BERT確率正解3B7B
0##子63%KEEPxo
0送付真空35%KEEPxo
2空気78%KEEPxo
3##納53%KEEPxo
4落下接続32%KEEPxo
584%FIXox
673%FIXoo
10呼ぶなる31%KEEPxo
1284%KEEPxo
16##子47%KEEPxo
17落下バス32%KEEPxo
17交通通行87%FIXoo
21請求59%KEEPxx
21100%FIXoo
正答率29%86%

7Bは14問中12問正解。3Bの29%から86%への跳躍。

7Bが正しく判定できたケース

3Bが全滅したKEEP判定(原文を保持すべき箇所)で、7Bは10件中9件を正しく保持した。「気送子」「出納台」「落下」といった専門用語を知っているため、BERTが「怪しい」と言っても「いや、これは正しい」と判断できる。

FIX判定でも「優→後」「交通→通行」「っ→つ」の3件を正しく修正採用。

7Bでも突破できなかった壁

カタカナ「ロ」vs 漢字「口」:

7BでもKEEP(原文維持)と判定。テキストベースのLLMでは、カタカナ「ロ」(U+30ED)と漢字「口」(U+53E3)の区別は原理的に難しい。トークナイザが別トークンとして扱っていても、意味的には「投入ロ」も「投入口」もどちらもありえると判断してしまう。

これはLLMの限界というより、OCRの「視覚→テキスト」変換で失われた情報をテキストだけで復元しようとする構造的な問題。画像とテキストの両方を扱うマルチモーダルモデルなら解決できる可能性がある。

「図書請求票」→「図書館票」:

7Bが「図書館」への引っ張りで誤判定。「図書請求票」という図書館固有の専門用語は、7Bの学習データにも十分には含まれていなかったと思われる。

モデルサイズと判断力の関係

モデルサイズ語彙力保守バイアス正答率
Qwen2.5 1.5B986MB低い(原文破壊)なし(何でも変える)測定不能
Qwen2.5 3B1.9GB低い(気送子を知らない)プロンプト依存で不安定29%
Qwen2.5 7B4.7GB高い(気送子を知っている)適度(正しく保守的)86%

3Bから7Bへの跳躍が大きいのは、7Bが昭和の図書館用語を語彙として持っているため。モデルが修正候補の意味を理解できなければ、正しく判定しようがない。

エスカレーション付きパイプライン

7Bの正答率86%は高いが、「ロ→口」のように7Bが見逃すケースがある。一方でBERTはこれを84%の確信度で検出できている。

そこで、7BとBERTの意見が食い違う場合に人間へ差し戻すロジックを入れる。

3段階の判定ルール

  1. AUTO-FIX: 7BがFIX判定 → 自動修正
  2. ESCALATE: 7BがKEEPだがBERTの確信度が50%以上 → 人間に差し戻し
  3. AUTO-KEEP: 7BがKEEPでBERTの確信度も50%未満 → 自動保持

実行結果

修正候補BERT確率7B判定処理正誤
673%FIXAUTO-FIX正解
17交通通行87%FIXAUTO-FIX正解
21100%FIXAUTO-FIX正解
21請求59%FIXAUTO-FIX誤修正
584%KEEPESCALATE人間が拾える
0##子63%KEEPESCALATE人間がKEEP
2空気78%KEEPESCALATE人間がKEEP
3##納53%KEEPESCALATE人間がKEEP
1284%KEEPESCALATE人間がKEEP
0送付真空35%KEEPAUTO-KEEP正解
4落下接続32%KEEPAUTO-KEEP正解
10呼ぶなる31%KEEPAUTO-KEEP正解
16##子47%KEEPAUTO-KEEP正解
17落下バス32%KEEPAUTO-KEEP正解

結果

  • 自動処理: 14件中9件(64%)を自動判定。精度89%(8/9正解)
  • 人間に差し戻し: 5件。うち本当に修正が必要だったのは1件(「ロ→口」)
  • 見逃し: 0件。7Bが見逃した「ロ→口」もESCALATEで人間に渡る

唯一の自動誤修正は「図書請求票→図書館票」。BERT確信度59%で、閾値を60%にすればこれもESCALATEに回せる。

エスカレーションの中身

人間に渡る5件の内訳:

  • 行[5]「ロ→口」: 修正が必要。人間なら一目でわかる
  • 行[0]「##子→管」: 行跨ぎのノイズ → KEEP
  • 行[2]「気→空気」: 行跨ぎ(空\n気の改行) → KEEP
  • 行[3]「##納→雪」: 「送付雪」は実は誤OCR(正しくは送付管)だがBERTの提案がずれている → 人間が別途修正
  • 行[12]「層→地」: 「目的層」はこの文書では正しい(層=フロア) → KEEP

BERTの検出結果を元の131箇所からフィルタで14箇所に絞り、さらに9件を自動処理して、人間が確認するのは5件だけ。131箇所を全部目視するのに比べれば大幅な省力化になる。


BERT perplexityスキャン → 7B LLM判定 → 不一致時エスカレーションのパイプラインは、VRAM 8GBのRTX 4060 Laptopで動かせる現実的な構成で、完全自動化ではないが人間の作業量を大幅に減らせることがわかった。

残る壁は2つ。「視覚的類似文字」(ロ/口)はテキストベースのアプローチでは構造的に限界があり、マルチモーダルモデルか文字レベルのOCR確信度が必要。「ドメイン固有の専門用語」(図書請求票)は7Bでもカバーしきれず、辞書や用語リストとの照合が現実的な解決策になる。

閉域ネットワークへの展開を考えると、NVIDIA Jetson Orin Nano(統合メモリ8GB、CUDA対応、消費電力7-15W)あたりなら今回の構成がそのまま載る。BERTと7B LLMの切り替え運用、NDLOCR-LiteのONNX Runtime、ollamaのARM64+CUDA対応、いずれもJetsonのUbuntuベース環境で動く。Developer Kitは$249だが、2026年2月時点の円安で実売5-6万円程度。Raspberry Pi 5(8GB)もフル装備すると4万円前後にはなるので、価格差はそこまで大きくない。RPi 5はCUDAが使えないのが致命的で、eGPU増設してもROCmがARM非対応なので汎用計算に使えない。

NDLOCRシリーズの他の記事: