いまからわかる!ChatGPT活用プログラミング

ChatGPT APIのFunction callingを使って⁠⁠請求書の構造化データを抽出する

先月、OpenAIからFunction calling(関数呼び出し)機能がリリースされました。これが何なのか、何のために使うべきなのか、ちょっと見ただけでは分かりづらいと思います。

今回は請求書から情報抽出をするというよくありがちなケースを題材に、Function callingの利便性を示してみます。

Function callingとは

OpenAIが2023年6月13日にリリースしたChat APIの追加機能です。主にできることとして以下の3つが挙げられています。

  • 外部ツールを呼び出して質問に答えるチャットボットを作成する
  • 自然言語を内部APIの呼び出しやSQLに変換する
  • テキストから構造化データを抽出する

たとえば天気予報と血液型占いという2つの機能を持ったSiriのようなシステムを構築する場合、ユーザーの発話からどちらの機能を期待しているか、またその機能の処理に必要な情報は何かを一回で判断するのは難しい問題でした。LangChainのAgentはまさにそれを解決するための仕組みでしたが、Function callingは機能判断、パラメータ判断を1回のリクエストで処理してくれます。

Function callingのすごいところ

さて、これだけでも便利な代物なのですが、個人的に一番感動したのは一番下の非構造化データから構造化データを抽出する、という作用です。これは自然言語の領域では情報抽出(Information Extraction)という分野であり、長い歴史をもちます。ChatGPTのFunction calling以前の情報抽出タスクにおける精度についての論文もあり、記事末尾の参考資料に記載しますのでぜひ読んでみてください。

この面倒な情報抽出タスクにおいて、Function callingは非常に高い精度で処理を行ってくれます。

Function callingの使い方

それでは実例に移る前に基本的な使い方を紹介します。

Function callingは先程紹介したように、内部APIや外部ツールへ渡す情報を抽出するための機構ですので、今回はOpenAIのブログと同様に天気予報APIを呼び出す関数が内部的に実装されているという仮定で構築します。処理における基本的な流れは以下のようになります。

  1. ユーザーの発話から関数を決定し、関数が必要な情報を抽出する
  2. 抽出された情報を関数に渡す

Function callingでは、どういった関数に渡すのか、ということを定義する必要があります。関数の定義は以下のようにできます。

functions = [
    {
        "name": "get_current_weather",
        "description": "指定された場所の現在の天気を取得する",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                     "description": "都市と州. 例: カリフォルニア州サンフランシスコ"
                },
            },
            "required": ["location"]
        }
    }
]

このようにして定義された関数を以下のように利用できます。

messages = [
    {"role": "user", "content": "What is the weather like in Boston?"},
]
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=messages,
    functions=functions,
    temperature=0,
    max_tokens=512,
    function_call={"name": "get_current_weather"}
)

請求書の情報抽出を行う

Function callingの基本的な使い方がわかったので、次は請求書から情報抽出をしてみましょう。

処理フローについて

基本的なフローは以下のようになります。

  1. PDFのアップロード
  2. 画像への変換
  3. OCRでのテキスト抽出
  4. 抽出されたテキストを言語モデルが理解しやすい形に変換
  5. Function callingで情報抽出

今回は3.から5.について見ていきます。

抽出項目について

今回扱う請求書は以下のようなものです。

請求書からどんな情報が抽出対象なのか簡単にまとめてみます。

- 請求概要
    - 請求日
    - 請求番号
    - 請求先
- 請求内容
    - 請求品目(array)
    - 総額
- 支払い方法
    - 振込先
        - 銀行名
        - 支店名
        - 口座種類(enum)
            - 普通, 定期, 当座, 貯蓄, 外貨
        - 口座番号
- 請求者情報
    - 住所
    - 会社名
    - 名前
    - 電話番号
    - メールアドレス
- 備考

Functionを定義する

この抽出項目に対応するようにFunctionを定義します。

functions = [
    {
        "name": "invoice_information_extraction",
        "description": """これは請求書のpdfをOCRにかけたものから情報を抽出するための処理です。
        OCRで抽出されたテキストは以下の形式に従います
        (x座標, y座標): {OCRで抽出されたテキスト}

        また、請求書は以下の配置ルールがあります。
        - 銀行名、支店名、口座種類、口座番号は近い位置にある
        - 住所、会社名、名前、電話番号、メールアドレスは近い位置にある
        - 郵便番号、住所は近い位置にある
        - 請求先は「御中」や「様」などの左に書かれる
        """,
        "parameters": {
            "type": "object",
            "properties": {
                "invoice_date": {
                    "type": "string",
                    "description": "請求日"
                },
                "invoice_number": {
                    "type": "string",
                    "description": "請求番号"
                },
                "invoice_to": {
                    "type": "string",
                    "description": "請求先 (御中とか様は除外する)"
                },
                "invoice_items": {
                    "type": "array",
                    "description": "請求品目",
                    "items": {
                        "type": "object",
                        "properties": {
                            "item_name": {
                                "type": "string",
                                "description": "請求品目名"
                            },
                            "item_unit_name": {
                                "type": "string",
                                "description": "品目単位"
                            },
                            "item_count": {
                                "type": "number",
                                "description": "品目数量"
                            },
                            "item_unit_price": {
                                "type": "number",
                                "description": "品目単価"
                            },
                            "item_price": {
                                "type": "number",
                                "description": "品目料金"
                            }
                        }
                    }
                },
                "total_amount": {
                    "type": "number",
                    "description": "総額"
                },
                "bank_name": {
                    "type": "string",
                    "description": "銀行名"
                },
                "branch_name": {
                    "type": "string",
                    "description": "支店名"
                },
                "account_type": {
                    "type": "string",
                    "enum": ["普通", "定期", "当座"],
                    "description": "口座種類"
                },
                "account_number": {
                    "type": "string",
                    "description": "口座番号"
                },
                "zipcode": {
                    "type": "string",
                    "description": "郵便番号"
                },
                "address": {
                    "type": "string",
                    "description": "住所"
                },
                "company_name": {
                    "type": "string",
                    "description": "会社名"
                },
                "name": {
                    "type": "string",
                    "description": "名前"
                },
                "phone_number": {
                    "type": "string",
                    "description": "電話番号"
                },
                "email_address": {
                    "type": "string",
                    "description": "メールアドレス"
                },
                "remarks": {
                    "type": "string",
                    "description": "備考"
                }
            },
            "required": ["invoice_date"]
        }
    }
]

OCRについて

OCRは今回の記事の範疇ではないので軽く触れるだけに留めますが、選択肢としては以下があります。

  • GoogleやMicrosoftが提供している有償のAPIを使う
  • 無償の日本語対応しているOCRを使う

今回はFunction callingの能力をそのまま評価したいので、シンプルなOCRだと嬉しいです。そこで、日本語対応していてCPUでも動作するPaddleOCRを使うことにします。

OCRの処理からFunction callingまで

それでは、PaddleOCRを使ってテキスト抽出し、Function callingに渡します。

from paddleocr import PaddleOCR, draw_ocr
from pdf2image import convert_from_path
from google.colab import files
from PIL import Image
# PDFを画像に変換
images = convert_from_path(pdf_path)

# OCRモデルの初期化
ocr = PaddleOCR(lang='japan')
results = []
for i, image in enumerate(images, start=1):
    print(f"Processing page {i}")
    image_path = f'{i}.png'
    image.save(image_path, 'PNG')
    result = ocr.ocr(image_path)
    results.append(result)
ocr_info = ""
for r in results[0][0]:
    ocr_info += f"({r[0][0][0]}, {r[0][0][1]}): {r[1][0]}\n"

ここでChatGPT APIに渡すための情報であるocr_infoには以下のような文字列が入ります。

(718.0, 161.0): 請求書
(170.0, 278.0): 株式会社クロ
(403.0, 280.0): 一ズドエーアイ
(711.0, 275.0): 御中
(1036.0, 280.0): 請求No.
(1333.0, 283.0): 202306300
(1033.0, 361.0): 請求日
(1353.0, 363.0): 2023/06/30
(199.0, 458.0): 件名:
(307.0, 460.0): Kawaiiコンサルのニ請求
(957.0, 463.0): 逆瀬川ささみ
(236.0, 524.0): 下記の通り
(394.0, 521.0): ご請求申し上げます
(962.0, 524.0): 111-1234
(959.0, 575.0): 東京都文京区左京
(1213.0, 577.0): 1-1-1
(957.0, 626.0): 美少女ハウス101号室
(1033.0, 682.0): TEL:
(1114.0, 680.0): 090-0000-0000
(187.0, 767.0): 合計金額
(480.0, 772.0): 399,459
(716.0, 772.0): (税込)
(927.0, 777.0): お支払期限
(1237.0, 775.0): 2023/07/3-
(153.0, 880.0): No.
(514.0, 877.0): 品名
(918.0, 877.0): 数量
(1119.0, 877.0): 単価
(1363.0, 877.0): 金額
(197.0, 933.0): IKawaiiコンサル
(957.0, 936.0): 時間
(1149.0, 936.0): 100,000
(1409.0, 936.0): 400,000
(194.0, 984.0): 2おやつ代
(1203.0, 987.0): 300
(1464.0, 989.0): 300
(1215.0, 1518.0): 小計
(1405.0, 1515.0): 400,300
(1188.0, 1569.0): 消費税
(1424.0, 1569.0): 40,030
(1068.0, 1620.0): 源泉徴収・復興税
(1422.0, 1623.0): 40,871
(1122.0, 1674.0): 合計
(1407.0, 1676.0): 399,459
(143.0, 1725.0): お振込先
(221.0, 1774.0): さかせ銀行
(467.0, 1776.0): 東京支店
(226.0, 1830.0): 普通
(231.0, 1886.0): 11922960
(229.0, 1935.0): サカセガワササミ
(182.0, 2066.0): 備考

ここで表現している座標はOCRで取得されたものです。言語モデル側では座標を加味した処理をしてくれることを期待しています。

messages = [
    {"role": "user", "content": ocr_info}
]
import openai
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=messages,
    functions=functions,
    temperature=0,
    max_tokens=512,
    function_call={"name": "invoice_information_extraction"}
)

OCRが6秒(CPU処理⁠⁠、OpenAIのFunction calling処理が4秒で合計10秒で請求書情報抽出が終わります。

抽出された情報

それでは抽出された情報を見てみましょう。以下に見るように、構造化された情報になっています。

{
  "invoice_date": "2023/06/30",
  "invoice_number": "202306300",
  "invoice_to": "株式会社クロ一ズドエーアイ",
  "invoice_items": [
    {
      "item_name": "Kawaiiコンサルのニ請求",
      "item_count": 1,
      "item_unit_name": "時間",
      "item_unit_price": 100000,
      "item_price": 100000
    },
    {
      "item_name": "おやつ代",
      "item_count": 2,
      "item_unit_name": "個",
      "item_unit_price": 150,
      "item_price": 300
    }
  ],
  "total_amount": 399459,
  "bank_name": "さかせ銀行",
  "branch_name": "東京支店",
  "account_type": "普通",
  "account_number": "11922960",
  "zipcode": "111-1234",
  "address": "東京都文京区左京1-1-1",
  "company_name": "株式会社クロ一ズドエーアイ",
  "name": "逆瀬川ささみ",
  "phone_number": "090-0000-0000",
  "remarks": ""
}

今回はそもそもOCRで数量が取れていないのと品名が若干間違っていましたが、ほぼ正しい抽出処理が行えました。こうしたプログラムを作り込めば請求書を自動読み込みして反映するシステムを容易に作れそうです。

参考文献

補足情報[7月19日追記]

この記事の応用で、Function callingを使ってレシートや請求書、発注書など、画像/PDFからどのような要素があるか推測し、自動で情報抽出をしてくれるシステムが、著者のQiitaページで公開されています。ぜひご覧ください。

おすすめ記事

記事・ニュース一覧