長年の開発現場で培った知見から言えることですが、金融領域、特に「決算書(財務諸表)」のデータ化ほど、エンジニアを悩ませ、同時にビジネスのボトルネックになりやすいタスクは少ないかもしれません。多くの企業が「AI OCR」という魔法の杖に期待し、そして導入後の精度に失望してきた歴史があるからです。
なぜでしょうか? 答えはシンプルです。
「文字が読めること」と「意味がわかること」は、全く別の次元の話だからです。
従来のOCR(光学的文字認識)のアプローチは、あくまで「画像上の黒い点を文字コードに変換する」技術に過ぎません。しかし、エンジニアが審査システムに求めているのは、「売上高」という文字そのものではなく、「この数値がPL(損益計算書)のトップラインであり、昨対比でどう変化したか」という構造化された情報ですよね。
今回は、既存のパッケージソフトに頼らず、PythonとLLM(大規模言語モデル)を組み合わせて、自前で「真に使える決算書自動審査パイプライン」を構築する実践的なアプローチを解説します。特に、最大の難関である「勘定科目の正規化(表記ゆれの統一)」を、コードベースでどう解決するか。技術の本質を見抜き、ビジネスへの最短距離を描く視点で焦点を当てていきましょう。
なぜ従来のOCRでは「決算書」を攻略できないのか
まず、敵を知ることから始めましょう。なぜ多くのOCRプロジェクトが、PoC(概念実証)止まりで終わってしまうのか。その原因を技術的な視点で整理します。特に、2025年から2026年にかけてのOCR技術の進化を踏まえた上で、それでも残る課題に焦点を当てます。
「読み取れる」ことと「理解できる」ことの決定的違い
近年のAI-OCRの進化は目覚ましいものがあります。最新のトレンドでは、手書き文字や不規則なPDFの読み取り精度が飛躍的に向上し、ETL(抽出・変換・格納)機能を統合してデータの加工まで行う製品も登場しています。しかし、決算書は依然として「非定型」の極みであり、単なる文字認識精度の向上だけでは解決できない壁が存在します。
中小企業が税理士によって作成する決算書は、使用する会計ソフト(弥生会計、freee、勘定奉行など)によってレイアウトが異なり、独自のExcelフォーマットであることも珍しくありません。行の高さ、フォント、インデント、全てがバラバラです。
ここでOCR(たとえ最新のAI-OCRであっても)が吐き出すデータは、往々にして「意味を持たない文字列の羅列」になりがちです。
例えば、「現金及び預金」と「現預金」を同一の勘定科目として認識することや、ヘッダーとデータの主従関係を正しく解釈することは、本来OCRエンジンの役割ではありません。これを後処理のルールベース(正規表現など)で直そうとすると、依然として無限の if-else 地獄に陥ることになります。
非定型帳票における「座標指定」の限界
かつての主流であった「座標定義」というアプローチは、事実上限界を迎えています。数千、数万パターンのレイアウトに対してテンプレートを定義し続けるのは、運用コスト的に破綻しているからです。
もちろん、最新の商用OCR製品では、位置合わせロジックの改善(特徴点マッチングなど)により、ある程度のレイアウト変動に対応できるようになっています。しかし、決算書のように「表の構造自体が可変」であり、「ページをまたぐ論理構造」を持つドキュメントに対しては、座標ベースのアプローチでは柔軟性に欠けます。レイアウトが変わるたびにエンジニアが設定を修正するのでは、自動化の恩恵は薄れてしまいます。
LLMによる「セマンティック・マッピング」という解決策
ここで登場するのがLLMです。現在のAIエージェント開発や高速プロトタイピングにおいて、LLMは単なるテキスト生成ツールではなく、高度な推論エンジンとして機能します。LLMの強みは、「曖昧なレイアウトの中から、文脈を理解して必要な情報を抽出・変換できる」点にあります。
最新のトレンドである「生成AI連携」のアプローチは以下の通りです。
- OCR: 文字情報は全てテキスト化し、ドキュメントの構造(Markdown形式など)を保持する役割に徹する(位置情報は参考程度)。
- LLM: テキストの塊から、「これは資産の部の流動資産にある『現金及び預金』だな」と推論し、自社のデータベース定義に合わせてデータを整形(正規化)する。
この「OCRで読み取り、LLMで理解して正規化する」という役割分担こそが、複雑な決算書を攻略するための最適解だと断言できます。
環境構築とアーキテクチャ設計
では、実際に動くものを作って検証していきましょう。今回は、機密性の高い財務データを扱うことを前提に、セキュアかつ実用的なPython環境を構築します。
技術スタックの選定:Python, LangChain, OpenAI API
コアとなる技術スタックは以下の通りです。
- Python 3.10+: データ処理のエコシステムが最強であるため一択です。
- LangChain: LLMアプリケーション構築のフレームワーク。現在はv0.2.x系が標準となっており、中核機能(
langchain-core)と外部連携(langchain-community)にパッケージが分割され、安定性と拡張性が向上しています。特に構造化出力(Structured Output)の取り回しに使います。 - OpenAI API (最新モデル): かつての主力であったChatGPT世代はレガシーな位置付けとなり、2026年時点ではChatGPTの最新モデル(ChatGPTの最新モデルシリーズなど)への移行が進んでいます。これらは複雑なレイアウト解析や推論能力に加え、エージェント機能が大幅に強化されており、非定型データの処理において圧倒的なパフォーマンスを発揮します。Azure OpenAI経由であれば、エンタープライズレベルのセキュリティを担保しつつ、これらの最新モデルを利用可能です。
- Pydantic v2: データのバリデーションとスキーマ定義に必須。LLMの出力を厳格な型にはめるために使います。
開発環境のセットアップ手順
まずは必要なライブラリをインストールします。LangChainのパッケージ構成変更に伴い、langchain-communityを含める点がポイントです。仮想環境(venvやconda)の中で行うのがマナーですね。
pip install langchain langchain-community langchain-openai openai pydantic pdfplumber pandas python-dotenv
.env ファイルにAPIキーを設定するのを忘れずに。ソースコードに直接キーを書くのは、エンジニアとして絶対にやってはいけないアンチパターンです。
# .env
OPENAI_API_KEY=sk-...
基本的なセットアップコードは以下のようになります。ここでは最新モデルを活用する前提で設定を行います。
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
load_dotenv()
# モデルの初期化
# temperature=0 は必須。創造性よりも再現性を重視します。
# ※modelパラメータには、公式ドキュメントで確認した最新のモデルID(例: ChatGPTの最新モデルシリーズやその時点の最新安定版)を指定してください
llm = ChatOpenAI(
model="ChatGPTの最新モデル", # 利用時点での最新安定版モデルを指定(例: ChatGPTの最新モデルなど)
temperature=0,
model_kwargs={"response_format": {"type": "json_object"}}
)
参考リンク
Step 1: ハイブリッドアプローチによるテキスト抽出の実装
最初のステップは、PDFから「LLMが理解しやすいテキスト」を取り出すことです。ここでの品質が最終的な精度を左右します。
PDFからRawテキストへの変換処理
ここでは pdfplumber を使用します。単純なテキスト抽出だけでなく、表(Table)構造を認識して抽出する機能があるため、決算書のような表形式データと相性が良いのが特徴です。
import pdfplumber
def extract_text_from_pdf(pdf_path: str) -> str:
full_text = ""
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
# 表抽出モードとテキスト抽出モードを併用するアプローチ
# extract_text() はレイアウトをある程度保持してくれます
text = page.extract_text(x_tolerance=2, y_tolerance=2)
if text:
full_text += text + "\n--PAGE BREAK--\n"
return full_text
表形式データの構造を維持するための前処理テクニック
PDFから単純にテキストを抜くと、左右のカラムが混ざって意味不明になることがあります(特に2段組みのBSなど)。
これを防ぐため、実務では商用のOCRエンジン(Azure AI Document Intelligence や Google Cloud Vision API)を前段に噛ませることも多いです。これらは「Markdown形式」や「HTML形式」でレイアウト情報を保持したまま出力してくれるため、LLMへの入力として最適です。
もし商用APIを使わず pdfplumber だけで戦うなら、上記のコードにある x_tolerance(横方向の許容誤差)パラメータを調整し、視覚的に離れている数値を別の列として認識させるチューニングが必要になります。
ノイズ除去とトークン節約のためのテキストクリーニング
LLMは課金がトークン(文字数)ベースです。不要な情報はコスト増と精度低下を招きます。以下の情報は正規表現で削除しておきましょう。
- ページ番号(「1 / 20」など)
- 印刷日時
- 税理士事務所のロゴや連絡先
- 長い罫線(「-------」など)
import re
def clean_text(text: str) -> str:
# ページ番号のようなパターンを削除
text = re.sub(r'\d+\s*/\s*\d+', '', text)
# 連続する記号を削除
text = re.sub(r'[-=]{3,}', '', text)
return text.strip()
Step 2: PydanticとLLMを用いた「勘定科目」の正規化と構造化
ここからが本記事のハイライトです。抽出したテキストを、「自社の審査システムが理解できるJSON」に変換します。
出力スキーマの定義:財務諸表(PL/BS)のデータモデル設計
LLMに「いい感じでJSONにして」と頼むだけでは、業務システム設計としては不十分です。実運用を見据えるなら、Pydantic でスキーマを定義し、その型を守るようLLMに強制することが重要です。
ここで重要なのは、「科目マッピング(正規化)」のロジックを含めることです。例えば、入力PDFに「旅費」とあっても「交通費」とあっても、システム上は travel_expense という統一コードで扱いたいですよね。
from typing import List, Optional
from pydantic import BaseModel, Field
class AccountItem(BaseModel):
original_name: str = Field(..., description="PDFに記載されている元の勘定科目名")
standard_code: str = Field(..., description="標準タクソノミーに基づく統一コード(例: sales, cogs, sga)")
amount: int = Field(..., description="金額。単位は円に統一すること")
class FinancialStatement(BaseModel):
company_name: str = Field(..., description="企業名")
fiscal_year: str = Field(..., description="決算年度(例: 2023年度)")
balance_sheet: List[AccountItem] = Field(..., description="貸借対照表の項目リスト")
profit_loss: List[AccountItem] = Field(..., description="損益計算書の項目リスト")
Function Callingによる確実なJSON出力の強制
OpenAIの Function Calling (Tools API) を使うと、LLMは定義されたJSONスキーマに従う確率が飛躍的に高まります。LangChainを使えば、このPydanticモデルを直接LLMに渡すことができます。
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
# プロンプトの定義
system_prompt = """
あなたは熟練した財務アナリストです。
提供された決算書のテキストデータから、重要な財務情報を抽出し、指定されたフォーマットで出力してください。
特に以下の点に注意すること:
1. 金額の単位(千円、百万円)を検知し、全て「円」単位の整数に変換すること。
2. 勘定科目は、文脈を判断して最も適切な標準コード(standard_code)にマッピングすること。
- 「売上」「売上高」「完成工事高」 -> sales
- 「給料手当」「役員報酬」「雑給」 -> labor_cost
- 「旅費交通費」「交通費」 -> travel_expense
- 「現預金」「現金及び預金」 -> cash_and_deposits
"""
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("user", "{input_text}")
])
# 構造化出力の設定
# 最新のLangChainでは with_structured_output を推奨
structured_llm = llm.with_structured_output(FinancialStatement)
# チェーンの構築
chain = prompt | structured_llm
独自の勘定科目を標準タクソノミーへマッピングするプロンプト設計
上記の system_prompt にあるように、マッピングのルール(タクソノミー)をプロンプト内で例示するのがコツです。これをFew-Shotプロンプティングと呼びます。
さらに精度を高めるなら、マッピングの候補リストを全てプロンプトに埋め込むのではなく、RAG(検索拡張生成)の要領で、関連しそうな科目コードだけを動的にプロンプトに挿入するテクニックもあります。ですが、一般的な中小企業の決算書であれば、主要な50〜100項目程度の定義をプロンプトに含めるだけで十分機能します。
Step 3: 財務指標の自動算出と審査判定ロジックの統合
データが構造化されれば、あとはPythonの世界です。ここで一つ忠告しておきます。LLMに計算をさせてはいけません。
LLMは計算が苦手です。「1+1」はできても、複雑な財務比率計算では平気で嘘をつく(ハルシネーション)リスクがあります。計算は必ずロジックで実装しましょう。
構造化データからの財務指標(流動比率、自己資本比率)計算
抽出した FinancialStatement オブジェクトを使って、Pandasで計算を行います。
import pandas as pd
def calculate_kpis(data: FinancialStatement) -> dict:
# BSデータをDataFrame化
df_bs = pd.DataFrame([item.model_dump() for item in data.balance_sheet])
# 特定のstandard_codeを持つ項目の金額を合計するヘルパー関数
def get_total(code_list):
return df_bs[df_bs['standard_code'].isin(code_list)]['amount'].sum()
# 流動資産と流動負債の定義(実際はもっと細かいコードリストになります)
current_assets = get_total(['cash_and_deposits', 'accounts_receivable', 'inventory'])
current_liabilities = get_total(['accounts_payable', 'short_term_loans'])
# 指標算出
# ゼロ除算エラーを防ぐロジックを入れること
current_ratio = (current_assets / current_liabilities * 100) if current_liabilities else 0
return {
"current_ratio": round(current_ratio, 2),
"total_assets": df_bs['amount'].sum() # 簡易的な総資産計算
}
異常値(バランス不一致など)の検知とアラート機能
決算書には「貸借一致の原則」があります。資産の合計と、負債・純資産の合計は必ず一致しなければなりません。これをバリデーションとして実装することで、OCRの読み取りミスやLLMの抽出ミスを検知できます。
def validate_balance(data: FinancialStatement):
# 資産コードと負債・純資産コードのリスト定義が必要
assets = sum(item.amount for item in data.balance_sheet if item.standard_code in ASSET_CODES)
liabilities_equity = sum(item.amount for item in data.balance_sheet if item.standard_code in LIABILITY_EQUITY_CODES)
# 1%程度の誤差はOCRの読み取りミス(端数処理など)として許容するか、エラーとするか
# 厳密な審査なら許容誤差は0にすべきです
if abs(assets - liabilities_equity) > assets * 0.01:
raise ValueError(f"Balance Sheet not balanced! Assets: {assets}, L&E: {liabilities_equity}")
トラブルシューティングと精度向上のためのTips
実際にこのパイプラインを本番運用すると、様々なエッジケースに遭遇します。考えられる対策をいくつか共有しましょう。
LLMのハルシネーション対策と検証方法
LLMは自信満々に嘘をつくことがあります。特に数値の「0」の数や、桁区切りのカンマの解釈ミスは致命的です。
対策として、「元のテキストのどこからその数値を引用したか」をLLMに出力させるフィールド(evidence_snippet)をPydanticモデルに追加すると良いでしょう。これがあれば、人間が後で検証する際に、PDFの該当箇所と突き合わせることができます。
処理速度とコストの最適化(トークン削減)
決算書全体を一度にLLMに投げると、トークン数が膨大になり、APIコストがかさみます。また、コンテキストウィンドウが溢れると精度が落ちます。
- 分割処理: BSとPLをページ単位で分割し、それぞれ個別にLLMで処理してから統合する。
- 不要語句の削除: 前述のクリーニング処理を徹底する。
Human-in-the-loop(人間による確認)UIの設計指針
完全自動化を目指してはいけません。目指すべきは「90%の自動化と、残り10%の効率的な人間による補正」です。
信頼スコア(Confidence Score)を算出し、スコアが低い場合や、貸借が一致しない場合のみ、人間にアラートを飛ばすワークフローを組みましょう。Slackに通知を飛ばし、修正用の簡易Web画面(Streamlitなどでサクッと作ったもの)へ誘導するのが、今のトレンドであり、現場もハッピーになれる設計です。
まとめ
今回紹介したアプローチは、高価なSaaS製品を導入せずとも、エンジニアの知恵と最新のライブラリだけで、実用的な金融データ解析基盤が作れることを示しています。
- OCRは「目」、LLMは「脳」: 役割を明確に分ける。
- Pydanticは「共通言語」: LLMの出力をシステムが扱える形に強制する。
- Pythonは「実行部隊」: 計算やバリデーションは堅実なコードで行う。
このパイプラインを一度構築してしまえば、決算書だけでなく、請求書、契約書、登記簿謄本など、あらゆる非定型ドキュメントに応用が可能です。
コメント