LLMの推論を薄くして書類解析の精度を改善する設計
こんにちは。スマートラウンドのinouehiです。最近はもっぱらLLMと向き合っています。
はじめに
本ポストではLLMを使った書類解析において直面した、解析対象の情報量が多くなると精度が著しく低下するという問題を解決したアプローチを紹介します。
LLMを組み込む開発を行なっていると、LLMを使って非決定論的に解決することとプログラムで決定論的に解決することの使い分けが設計の論点になります。様々な理由から、極力非決定論を薄くする方針をとっています。
この方針を書類解析に適用すると「LLMには書類からの情報抽出に極力専念させて、それ以外の諸々を決定論的に解決する」と捉えることができそうです。
今回改善した旧方式は確かにLLMが書類からの情報抽出に極力専念していたのですが、書類の多様な書式、書き方に対応するために表の解釈の仕方を指示しており、これが推論を要する複雑さの要因になっていそうでした。新方式も同様に情報抽出に専念するものですが、より推論を減らすことに成功しています。
直面した課題
課題1: LLMのキャパシティ超過
前提として、二桁ページ程度の多種多様なテキストや表形式の明細を含む書類を解析します。その中から、数種類の特定の明細を識別し、その明細から特定の情報を抽出するという作業において、解析対象の明細数が多くなると以下の様な問題が発生しました。
- 出力が途中で切れる(明細の一部しか抽出されない)
- LLMのレスポンスがエラーになる
課題2: プロンプト分割の難しさ
プロンプトの肥大化を回避する定石としてPrompt Chainingの様な手法があります。すなわち、問題を細分化してステップバイステップで推論を進めることで1つ1つのプロンプトを小さくするという手法です。
今回は、既に推論ステップが細分化されており、これ以上の分割は困難に感じられたのと、仮に分割できたとして結果の統合が課題になりそうなのと、分割・統合により煩雑になり処理全体で見たときに非効率になることが懸念されました。
課題3: 表構造の多様性
書類の画像をそのままLLMに渡して解析する方式では、表の構造認識に限界がありました。一口に表と言っても様々な記載様式があります。
- 列結合、行結合等により構造が曖昧になった表
- 複数ページに跨る表。頁単位でみた時にヘッダーのない表
- 正規化された表と非正規化された表
- 結合された表(表と表が隙間なくくっついて1つになった表)
- データ行の中に現れる小計・合計行
などなど
結果として、行の対応関係がずれる(明細Aのデータと明細Bのデータが混同して出力される)という問題がありました。
解決のアプローチ
課題1・2から、プロンプトを細分化するという視点だけでなく、対象の捉え方を変えて推論の量を減らすというアプローチを考えました。加えて、LLMの出力をスリムにすることも意識しました。
ステップ分割の視点を変える
旧方式は、最終的に欲しい出力を定義したスキーマをLLMに示し、書類の情報を直接的に構造化させるものでした。
一方で新方式は、書類に記載された構造のまま抽出し、最終出力へのマッピング(構造化)を後続処理に委ねるようにしました。
旧方式
①抽出・構造化出力 ──▶ ②後処理(無効データ排除等)
新方式
①抽出(生データ取得) ──▶ ②スキーマ変換(構造化) ──▶ ③後処理(無効データ排除等)
これを実現するため、キー・バリュー方式で表を捉えるようにしました(加えて、出力をスリムにするためそれをヘッダー+行形式に変形しています)。例えば、行番号、費目、金額というカラムがある場合、以下の様な形式で出力するようにします。
| 行番号 | 費目 | 金額 |
|---|---|---|
| 1 | a | 100 |
| 2 | b | 200 |
"header": ["行番号", "費目", "金額"],
"rows": [
["1", "a", "100"],
["2", "b", "200"]
],
ただし、書類によって表の書き方が様々であるため、愚直に表を抽出するだけでは使い物になりません。
ある書類では1つの表で表現されるものが、別の書類では複数の表に分かれていることがあります。カラムの書き方も様々で、"金額"と書かれていることもあれば"費用"と書かれていることもあるかもしれません。そこで、後続の構造化処理の手掛かりとなる情報をこのステップで抽出するようにしました。例えば、表のカラムと最終出力のマッピングをここで推論させます。
見たままを忠実に
最初のステップでは、LLMに「変換」を求めず、表を見たまま抽出させます。そして、後続処理でデータの構造化に用いる情報をとるために最低限の推論を行います。加えて、LLMの作業に分岐や繰り返しがなるべく生じないように、なるべく一本道の処理になるようにルールを作ることを心がけます。このようにプロンプトをシンプルにしてLLMの推論負荷を減じます。
// 出力スキーマイメージ(簡略化しています)
const tableSchema = z.object({
tableName: z.string().describe('表の名前'),
tableType: z.array(z.enum(TABLE_TYPE)).describe('表の種類'),
columnMapping: columnMappingSchema.describe('最終出力と表のマッピング'),
header: z.array(z.string()).describe('表のカラム'),
rows: z.array(rowSchema).describe('表のデータ'),
});
OCRテキストの併用
ここまでのところで、ひとまず出力が途中で切れたりエラーになることはなくなりました。しかし、行が欠損する(ずれる)という問題が残されていました。
そこで補助データとして書類をOCR処理したテキストも添付するようにしました。このテキストにおいて表はHTMLのテーブル形式で表現されます。小計・合計行、空行のようなノイズが依然として含まれるものの高い確度で行を認識できるようになることが期待されます。なお、OCR処理したテキストは文字列の認識精度が低いため書類と置き換えるわけにはいかず、併用するという判断に至りました。文字の認識と構造の認識の異なる強みを持つ機構を組み合わせた形です。
構造化
抽出したデータを、最終出力のスキーマに変換します。このステップはLLMを使わず、プログラムで処理します。
表の構造(カラムの数、内容、セル結合の有無、ヘッダ・データ・サマリー行のあり方など)が多種多様ということ以外にも以下のような要件がありました。
- 1つの書類には様々な種類の表が含まれる。
- 同時に複数の書類を解析にかけることがある。
- 最終出力は様々な表から抽出したデータを組み合わせて作る。
そのため、書類から抽出したデータを統合する際にも工夫が必要で、以下の様に対策しました。
- 表の種類をenumで定義して抽出ステップにおいて識別し、不要な表を除外する。
- キーを設計し名寄せ機構を適用する。
- 重複や無効なデータを検知し、排除する。
- そうはいっても統合が困難な場合にはユーザーに知らせる。
出力をスリムにする
課題1は重い推論を要するということの他に、出力サイズの超過を示唆しています。事実として、旧方式の出力には以下のような非効率がありました。
- 要素と別の要素を掛け合わせた組み合わせの数だけフィールドが存在する。
- オブジェクトの配列という形式(フィールドの値だけでなくキーも繰り返される)。
新方式では以下の様にして出力をスリムにしました。
- 出力の構成要素の最小単位で抽出し、それらを掛け合わせて最終出力を組み立てる処理を設ける。
- 上記の通り
headerとrowsというフィールドに分け、headerにキーを、rowsに値を出力する。
まとめ
LLMに複雑なタスクを一度に任せると、結果が安定しない、処理に時間がかかる、データ量が増えたときに破綻しやすいという問題がありました。
旧方式ではエラーないし半分未満の抽出精度でしたが、新方式では90〜99%程度の抽出精度に達しました(揺れや、新しいパターンの書類で精度が落ちうるので引き続きモニタリングが必要です)。
LLMに情報抽出に極力専念させるのみならず、極力シンプルな思考回路で実現することによってデータ量が増えても処理できるようになりました。また、強みの異なる処理系を組み合わせること、補助データを与えることで精度を改善することもできました。
以上、お読みいただきありがとうございました。
株式会社スマートラウンドは『スタートアップの可能性を最大限に発揮できる世界を作る』というミッションを掲げています。スタートアップと投資家の実務を効率化するデータ作成・共有プラットフォーム『smartround』を開発・提供しています。 採用ページはこちら-> jobs.smartround.com/
Discussion