こんにちは。エンジニアの山下です。今回は OCR について書こうと思います。
OCR は画像中の文字を文字データに変換するシステムの総称で、DX の前段階にあたるペーパーレスの推進などの文脈でしばしば見かけます。昨今の AI ブームの恩恵を受けて OCR の精度は非常に高くなっており、実際、以下のように粗悪な質の画像であってもそれなりの精度で機能します。
しかし、DX の前段階という文脈では、単に OCR の読み取り精度が高いだけでは十分とは言えません。というのは、多くの場合、OCR の出力は構造化されたデータではなく、読み取った文字列を列挙しただけのデータ片になりがちだからです。
以下に実際の OCR の出力から一部を抜粋したものを示します。
金額 NO. 数量 単価 10,000 1 1,000 2 0000002 · 用紙2 15 500
この問題は特に表データなどを含む画像において顕著で、これではデータに十分な質を要求する DX の第一歩としては不十分と言わざるを得ないでしょう。
この問題への対処方法の1つに OCR の出力を LLM を使って整理するという方法があります。非構造化データの構造化は LLM が得意とする仕事の1つなので、それなりの精度で紙データを適切な電子データに変換できそうという期待があります。
一方で、LLM を使うのであれば、マルチモーダル LLM を使って画像を直接 LLM に入力して電子データに変換するという方法も考えられます。OCR の出力を受け取るパイプラインを構築せずに済むので、開発工数上はこちらの方が有利そうですね。
こうして LLM を使った紙データの電子データ化の方法が2つ挙がったわけですが、2つ並ぶとどちらの方法がよいのかが気になるところです。というわけで、今回はこの2つの方法を比較検証してみます。
検証方法
今回は架空の納品書の紙データを構造化されたデータに変換するというシナリオで検証を行います。使用する架空の納品書はこちらです。
こちらの納品書をベースとして様々なシチュエーションを想定した画像を作成し、各手法が各画像に対してどんなデータを生成するかを観察します。
LLM には GPT-4o を使用します。マルチモーダルで動作する LLM なら何でもよいというのが本音ですが、最低限これらの手法の有効性についての感触は掴みたかったので、最も標準的なものを適当に選んでみたという感じです。
OCR には Azure AI Document Intelligence を使用します。選定理由は日本語の公式サポートを謳っているためです。GCP の Document AI も日本語サポートを謳っており、かつ OCR の精度に有意な差はなさそうだったので、そちらでもよかったのですが、Azure の方がプレイグラウンドが使いやすそうだったのでこちらを選びました。
今回の検証では架空の納品書の内容を以下の JSON フォーマットに従って整理するように LLM に依頼します。
{ "invoice_id": <納品書番号>, "invoice_date": <納品年月日>, "company": <自社情報>, "customer": <納品先情報>, "created_by": <納品書作成担当者>, "currency": <通貨単位>, "sub_total": <商品合計金額>, "tax": <税金>, "grand_total": <合計金額>, "items": [{ "item_id": <品番>, "item": <品名>, "quantity": <数量>, "unit_price": <単価>, "total": <金額> }, { "item_id": <品番>, "item": <品名>, "quantity": <数量>, "unit_price": <単価>, "total": <金額> }] }
それぞれの方法で LLM に与えるプロンプト全文を掲載すると長くなるので、プロンプト全文はこの記事の末尾に Appendix として掲載します。
最も簡単な納品書で試してみる
まずは軽いジャブからということで、理想状態の納品書から始めます。要するに印刷前の納品書の元データのことで、1つ前の章の冒頭で掲載した画像です。以下に再掲しておきます。
この画像を対象にマルチモーダルと OCR + LLM それぞれの手法を使って JSON データを取得してみます。結論から述べると、この画像については特に差は生じず、どちらの手法からも以下の内容の出力が得られました。
{ "invoice_id": "1-23456", "invoice_date": "2024/09/01", "company": "株式会社〇〇機器", "customer": "株式会社〇〇商事", "created_by": "山田 太郎", "currency": "JPY", "sub_total": 35500, "tax": 3550, "grand_total": 39050, "items": [{ "item_id": "0000001", "item": "用紙1", "quantity": 1000, "unit_price": 10, "total": 10000 }, { "item_id": "0000002", "item": "用紙2", "quantity": 500, "unit_price": 15, "total": 7500 }, { "item_id": "0000003", "item": "用紙3", "quantity": 800, "unit_price": 10, "total": 8000 }, { "item_id": "0000004", "item": "インク", "quantity": 20, "unit_price": 500, "total": 10000 }] }
内容も完璧です。とはいえ、ここで失敗するようでは実用に耐えないという感じもします。
納品書を傾けてみる
先ほどの画像を少し傾けてみます。
まあ、少し傾けたくらいなら楽勝でしょう。というわけで、これを OCR + LLM で整理してみます。
{ "invoice_id": "1-23456", "invoice_date": "2024/09/01", "company": "株式会社〇〇機器", "customer": "株式会社〇〇商事", "created_by": "山田太郎", "currency": "JPY", "sub_total": 35500, "tax": 3550, "grand_total": 39050, "items": [{ "item_id": "0000001", "item": "用紙1", "quantity": 1000, "unit_price": 10, "total": 10000 }, { "item_id": "0000002", "item": "用紙2", "quantity": 15, "unit_price": 500, "total": 7500 }, { "item_id": "0000003", "item": "用紙3", "quantity": 800, "unit_price": 10, "total": 8000 }, { "item_id": "0000004", "item": "インク", "quantity": 20, "unit_price": 500, "total": 10000 }] }
楽勝でしたね。と見せかけて、実は間違っています。以下の箇所です。
}, { "item_id": "0000002", "item": "用紙2", "quantity": 15, "unit_price": 500, "total": 7500 }, {
数量と単価の値が逆になっていますね。何が起こったのかと思い OCR の出力を覗いてみると、以下のようになっていました。
No. : 1-23456 発行日: 2024年9月1日 納品書 株式会社〇〇商事 御中 〒 123-4567 東京都文京区〇〇〇 1-2-3 担当者:田中 花子 様 下記の通り納品申し上げます。 合計金額 ¥39,050- 株式会社〇〇機器 〒 123-4567 東京都荒川区〇〇〇 1-2-3 ○○○ビルディング 1F 123 ☎ 03-1234-5678 码 03-1234-5679 info@example.com 担当者:山田 太郎 No. 商品名 / 品名 1,000 1 0000001 · 用紙1 2 0000002 ·用紙2 3 0000003 · 用紙3 4 0000004·インク 5 6 ...(中略)... 23 24 小 計(税抜) ¥35,500 ¥3,550 消費税(10%) 合 計(税込) ¥39,050 備考欄: 数 量 単 価 金 額 10,000 10 15 500 800 10 8,000 10,000 20 500 7,500
文字としては一通り読めているのですが、格納順序がバラバラですね ... 。特に表の構造が崩れてしまっているので、さすがの LLM も元の情報を適切に復元することができなかったようです。むしろ数量と単価が1箇所逆転しただけで済んでいることが奇跡的に思えるレベルです。
実は OCR では出力する文字列は読み込んだ順で列挙することがほとんどで、必ずしも人が自然に感じる順序で出力されるとは限りません。Azure もその例に漏れず、API Reference に読み込んだ順で出力されることが明記されています。従って、OCR の機嫌如何で LLM の出力に誤りが生じてしまうということがしばしば発生します。
一応、Azure AI Document Intelligence の出力には、上記の文字情報以外に読み込んだ文字が書かれていた画像中の位置などの情報も含まれているので、そちらを LLM に渡すことで改善しないか確認してみましたが、残念ながら数量と単価の逆転現象を解消することはできませんでした。
一方で、マルチモーダル LLM に同じ作業を依頼したところ、1つ前の章で掲載した JSON と同じ内容のものが返ってきました。この時点でマルチモーダルの方が一歩リードという感じですね。
納品書を印刷して撮影してみる
さらに難易度をアップして、納品書を一度紙データに起こして、それを撮影したものを入力にしてみます。実際の写真が以下になります。
結果ですが、やはり OCR + LLM はダメでした。生成された JSON のうち、納品書の表の部分に関する箇所のみを抜粋して以下に示します。
"items": [{ "item_id": "0000001", "item": "用紙1", "quantity": 10, "unit_price": 1000, "total": 10000 }, { "item_id": "0000002", "item": "用紙2", "quantity": 15, "unit_price": 500, "total": 7500 }, { "item_id": "0000003", "item": "用紙3", "quantity": 1, "unit_price": 8000, "total": 8000 }, { "item_id": "0000004", "item": "インク", "quantity": 1, "unit_price": 10000, "total": 10000 }]
数量が1で単価が金額カラムの値になっている箇所が散見されますね。やはり OCR の出力順序に振り回されているようです。
一方でマルチモーダル LLM の方は相変わらず完璧な内容でした。
くしゃくしゃに丸めてみる
最後に、より高い難易度を求めて、一度くしゃくしゃに丸めた紙を広げて撮影してみます。写真は以下です。
もはや人間も読むのが難しいレベルですが、OCR を見ていると結構な精度で読めています。というわけで、OCR + LLM の結果がこちらです。
{ "invoice_id": "1-23456", "invoice_date": "2024年9月1日", "company": "株式会社〇〇機器", "customer": "株式会社〇〇商事", "created_by": "山田 太郎", "currency": "JPY", "sub_total": 35500, "tax": 3550, "grand_total": 39050, "items": [{ "item_id": "0000001", "item": "用紙1", "quantity": 1000, "unit_price": 10, "total": 10000 }, { "item_id": "0000002", "item": "用紙2", "quantity": 500, "unit_price": 15, "total": 7500 }, { "item_id": "0000003", "item": "用紙3", "quantity": 10, "unit_price": 800, "total": 8000 }, { "item_id": "0000004", "item": "インク", "quantity": 500, "unit_price": 20, "total": 10000 }] }
文字自体の視認はできているものの、例によって 用紙3
と インク
の数量と単価が逆転してます。
やはりマルチモーダル LLM の圧勝か ... と思いきや、マルチモーダルの結果がこちらです。
{ "invoice_id": "1-23456", "invoice_date": "2024年9月1日", "company": "株式会社〇〇機器", "customer": "株式会社〇〇商事", "created_by": "田中花子", "currency": "JPY", "sub_total": 35500, "tax": 3550, "grand_total": 39050, "items": [{ "item_id": "00000001", "item": "商品A", "quantity": 1000, "unit_price": 10, "total": 10000 }, { "item_id": "00000002", "item": "商品B", "quantity": 500, "unit_price": 5, "total": 2500 }, { "item_id": "00000003", "item": "商品C", "quantity": 800, "unit_price": 10, "total": 8000 }, { "item_id": "00000004", "item": "商品D", "quantity": 1000, "unit_price": 10, "total": 10000 }, { "item_id": "00000005", "item": "商品E", "quantity": 20, "unit_price": 50, "total": 1000 }] }
もはや一々指摘するのも嫌になるくらい色々間違っています。
- 納品書の担当者がお客様側の担当者になっている
- 4つのはずの納品物が5つある
- 合計金額が個々の納品物の金額の合計と一致していない
- etc.
たまたま重度の幻覚を引いただけかと思い何回か試してみましたが、結果はそれほど変わりませんでした。画像の視認性が悪化してくるとマルチモーダルは回答が破綻してしまうようです。これはなかなか興味深い現象ですね。
OCR + LLM も間違っているとはいえ、マルチモーダルの間違いに比べると些細なものに思えます。ということで、極端に視認性が低い環境下においては OCR + LLM がマルチモーダルを超えることがわかりました。が、そんなユースケースがあるのだろうか ... 。
まとめ
今回の検証により、紙データを電子データ化する際にはマルチモーダルを使った方が圧倒的に精度がよいということがわかりました。唯一、視認性の悪い画像を入力された場合は OCR + LLM の方が安定した結果を出せましたが、そもそもほとんどのユースケースではそのような画像の入力は行われないでしょう。というわけで、結果は紛れもないワンサイドゲームでした。
マルチモーダル LLM については AI ブームの最中にあってもあまり話題を耳にしなかったのですが、いざ使ってみると想像以上のパフォーマンスで驚きました。まだまだ有効なユースケースが埋まっていそうでワクワクしますね。
逆に OCR は LLM との相性が意外と良くないということがわかり、少々残念でした。単に画像から文字を読み出す目的であればマルチモーダル LLM を使った方が圧倒的に便利なので、OCR は市場の活気から少し遠ざかりそうな雰囲気を感じています。
以上です。最後までお読みいただき、ありがとうございました。
Appendix: LLM のプロンプト
おまけとして LLM に与えたプロンプトの全文を掲載しておきます。
マルチモーダル LLM のプロンプト
(先に画像を入力しておく) あなたはドキュメントの情報整理のための AI アシスタントです。納品書の情報を整理し、指定された形式のデータを作成するのがあなたの仕事です。出力は作成したデータのみとし、それ以外の文章は出力に含めないでください。 作成するデータは JSON 形式で、以下のフォーマットに従ってください。 *** { "invoice_id": <納品書番号>, "invoice_date": <納品年月日>, "company": <自社情報>, "customer": <納品先情報>, "created_by": <納品書作成担当者>, "currency": <通貨単位>, "sub_total": <商品合計金額>, "tax": <税金>, "grand_total": <合計金額>, "items": [{ "item_id": <品番>, "item": <品名>, "quantity": <数量>, "unit_price": <単価>, "total": <金額> }, { "item_id": <品番>, "item": <品名>, "quantity": <数量>, "unit_price": <単価>, "total": <金額> }] } *** 上記の JSON フォーマットのプレースホルダに実際の値を適用した場合のサンプルが以下になります。 *** { "invoice_id": "order-01", "invoice_date": "2024/08/01", "company": "株式会社ほげほげ", "customer": "ほげほげ商事", "created_by": "山田太郎", "currency": "USD", "sub_total": 15000, "tax": 1500, "grand_total": 16500, "items": [{ "item_id": "0001", "item": "商品1", "quantity": 200, "unit_price": 50, "total": 10000 }, { "item_id": "0002", "item": "商品2", "quantity": 50, "unit_price": 100, "total": 5000 }] } *** 与えられた画像に写っている納品書からデータを生成してください。
OCR + LLM のプロンプト
あなたはドキュメントの情報整理のための AI アシスタントです。納品書の PDF に OCR を適用して得られた情報を整理し、指定された形式のデータを作成するのがあなたの仕事です。出力は作成したデータのみとし、それ以外の文章は出力に含めないでください。 作成するデータは JSON 形式で、以下のフォーマットに従ってください。 *** { "invoice_id": <納品書番号>, "invoice_date": <納品年月日>, "company": <自社情報>, "customer": <納品先情報>, "created_by": <納品書作成担当者>, "currency": <通貨単位>, "sub_total": <商品合計金額>, "tax": <税金>, "grand_total": <合計金額>, "items": [{ "item_id": <品番>, "item": <品名>, "quantity": <数量>, "unit_price": <単価>, "total": <金額> }, { "item_id": <品番>, "item": <品名>, "quantity": <数量>, "unit_price": <単価>, "total": <金額> }] } *** 上記の JSON フォーマットのプレースホルダに実際の値を適用した場合のサンプルが以下になります。 *** { "invoice_id": "order-01", "invoice_date": "2024/08/01", "company": "株式会社ほげほげ", "customer": "ほげほげ商事", "created_by": "山田太郎", "currency": "USD", "sub_total": 15000, "tax": 1500, "grand_total": 16500, "items": [{ "item_id": "0001", "item": "商品1", "quantity": 200, "unit_price": 50, "total": 10000 }, { "item_id": "0002", "item": "商品2", "quantity": 50, "unit_price": 100, "total": 5000 }] } *** 納品書の PDF に OCR を適用して得られた情報は以下の通りです。 *** (ここに OCR の出力データが入る) *** データを生成してください。