2023.10.05

請求書OCR自動化: Document AI + ChatGPT API で非構造化データを JSON で出力させる

はじめに

こんにちは。グループ研究開発本部 次世代システム研究室のT.D.Qです。
2023年10月よりインボイス制度が開始されます。この制度に対応するため、請求書のOCR自動化はますます重要となっています。今回は、ChatGPTとDocumentAIの力を結集し、インボイス制度への対応を加速する請求書OCR自動化の方法について探ってみたいと思います。

1.やりたいこと

  • 目的: 非構造化データを OCR で取得して、指定の JSON 形式で出力させたい
  • 使用する技術: OpenAIのChatGPT及びGoogleのDocumentAI
  • 実現手段:
    1. DocumentAIで請求書(PDF形式)を読み取り
    2. ChatGPTでDocumentAIのレスポンスから必要な情報を抽出
    3. OCRの結果はJsonファイルに出力

    2.環境構築

    2-1.DocumentAIの設定

    Document AIとは、非構造化データ(請求書、レシート、議事録、など)を構造化データに変換できる AI OCRツールです。機械学習を活用することで、体系化されていないドキュメントからインサイトを引き出すことができます。Google Cloud Platformでサービスとして提供されています。
    Google Cloudでデモサイトは用意されているので、ひとまずこのページでDocument AIの性能を確認できると思います。Document AI の料金については、こちらで確認可能です。プロセッサによって金額は変わりますが、「Form Parser」は1,000 ページあたり$65になっています。
    早速、次の手順でAPIでDocument AIを利用できるようにしていきましょう。

    Cloud Document AI APIの有効化

    Google Cloud コンソール、またはコマンドにてDocument AIのAPIを有効化できます。Google Cloud コンソールでAPIの有効化しやすいので今回はこの方法を使いました。
    コマンドでAPIを有効化の場合には、以下を実行してみて下さい。
    gcloud services enable documentai.googleapis.com
    

    APIリクエストの認証設定

    サービスアカウントはプロジェクトに属するもので、PythonクライアントライブラリでAPIリクエストを行うために使用されます。
    Google Cloud コンソールで、メニュー[IAMと管理]-[サービスとアカウント]から、サービスアカウントを作成を選択します。
    作成されたサービスアカウントを選択して、キータブから鍵を追加をクリックして、新しい鍵を作成を選択します。
    JSONを選択して作成されたファイルをダウンロードします。
    後ほど、環境変数 GOOGLE_APPLICATION_CREDENTIALS をサービスアカウントキーが含まれるJSONファイルのパスに設定しますので、分かりやすいところに保存しておきましょう。

    プロセッサの作成

    OCRプロセッサを作成します。
    [Document AI]メニューの[プロセッサギャラリー]メニューから、プロセッサを確認を選択します。
    今回は、請求書の中に表形式のデータもちゃんと取得したいので「Form Parser」を選択しました。
    フォームパーサーAPIの使用方法について詳しくは、こちらのガイドをご覧ください。
    プロジェクトIDも含むて作成したプロセッサの詳細画面でプロセッサのID、リージョン及びバージョン情報を確認しましょう。

    2-2.ChatGPT APIの設定

    前回のブログを書きましたが、ChatGPT APIを使うため、OpenAIのAPI公式サイトからAPIキーを取得する必要があります。

    2-3.envファイルの作成

    開発プロジェクトフォルダ内に.envファイルを作成し、以下の内容を追加しましょう。
    PROJECT_ID=12xxxxxxxxx
    PROCESSOR_LOCATION=us
    PROCESSOR_ID=1a83xxxxxx
    PROCESS_VERSION=pretrained-form-parser-v2.0-2022-11-10
    GOOGLE_APPLICATION_CREDENTIALS=./gcp/xxxxxxx-xxxxxx.json
    OPENAI_API_KEY=sk-xxxxxxx
    

    3.アプリの開発

    3-1.Python開発環境の準備

    Pythonの仮想環境の設定

    Pythonで仮想環境を作成する方法は比較的簡単で、Python3.3以降は標準モジュールの「venv」を使用すると仮想環境を作成できます。

    python3 -m venv invoice_ocr
    source invoice_ocr/bin/activate
    

    必要なライブラリーを用意する

    今回のアプリ開発に必要となるPythonライブラリはrequirements.txtファイルに定義しましょう。
    typing
    python-dotenv
    pandas
    openai
    langchain
    google-api-core
    google-cloud-documentai
    

    必要なライブラリーをインストールする

    以下のコマンドで今回のアプリ開発に必要なライブラリをインストールしましょう。
    pip install -r requirements.txt
    

    3-1.OCR機能の実装

    次のPythonクラス「InvoiceOCR」はGoogle CloudのDocumentAIサービスを利用して請求書のOCRを自動化します。請求書(PDF形式)からテーブルやフォームフィールドの正確な情報を抽出するため、このクラスはプロジェクトIDや「Form Parser」プロセッサ設定を読み込み、指定のファイルを処理してOCR結果を返します。OCR結果からレイアウト情報・テーブル情報を基づいて必要なテキストを抽出し、整形して返却します。
    # ocr_process.py
    
    import os
    from typing import Optional, Sequence
    from google.api_core.client_options import ClientOptions
    from google.cloud import documentai
    
    class InvoiceOCR(object):  
    
        def __init__( self ):
            self.name = 'InvoiceOCR'
            self.project_id =os.environ.get("PROJECT_ID")
            self.location = os.environ.get("PROCESSOR_LOCATION") 
            self.processor_id = os.environ.get("PROCESSOR_ID")  
            self.processor_version = os.environ.get("PROCESS_VERSION")
    
        def process_invoice_ocr(
            self,
            file_path: str,
            mime_type: str,
        ) -> documentai.Document:
            # Online processing request to Document AI
            document = self.process_document(
                self.project_id, self.location, self.processor_id, self.processor_version, file_path, mime_type
            )
    
            # Read the table and form fields output from the processor
            # The form processor also contains OCR data. For more information
            # on how to parse OCR data please see the OCR sample.
    
            text = document.text
            print(f"Full document text: {repr(text)}\n")
            
            ocr_result = ""
    
            # Read the form fields and tables output from the processor
            for page in document.pages:
                print(f"\n\n**** Page {page.page_number} ****")
    
                print(f"\nFound {len(page.tables)} table(s):")
                for table in page.tables:
                    num_columns = len(table.header_rows[0].cells)
                    num_rows = len(table.body_rows)
                    print(f"Table with {num_columns} columns and {num_rows} rows:")
    
                    # Print header rows
                    #print("Columns:")
                    header_data = self.print_table_rows(table.header_rows, text)
                    ocr_result += header_data + "\n"
    
                    # Print body rows
                    #print("Table body data:")
                    table_data = self.print_table_rows(table.body_rows, text)
                    ocr_result += table_data + "\n"
    
                print(f"\nFound {len(page.form_fields)} form field(s):")
                for field in page.form_fields:
                    name = self.layout_to_text(field.field_name, text)
                    value = self.layout_to_text(field.field_value, text)
                    form_data = f"    * {repr(name.strip())}: {repr(value.strip())}"
                    ocr_result += form_data + "\n"
    
            return ocr_result
    
    
        def print_table_rows(
            self,
            table_rows: Sequence[documentai.Document.Page.Table.TableRow], text: str
        ) -> str:
            result = ""
            for table_row in table_rows:
                row_text = ""
                for cell in table_row.cells:
                    cell_text = self.layout_to_text(cell.layout, text)
                    row_text += f"{repr(cell_text.strip())} | "
                result += row_text + "\n"
            
            return result 
    
        def process_document(
            self,
            project_id: str,
            location: str,
            processor_id: str,
            processor_version: str,
            file_path: str,
            mime_type: str,
            process_options: Optional[documentai.ProcessOptions] = None,
        ) -> documentai.Document:
            # You must set the `api_endpoint` if you use a location other than "us".
            client = documentai.DocumentProcessorServiceClient(
                client_options=ClientOptions(
                    api_endpoint=f"{location}-documentai.googleapis.com"
                )
            )
    
            # The full resource name of the processor version, e.g.:
            # `projects/{project_id}/locations/{location}/processors/{processor_id}/processorVersions/{processor_version_id}`
            # You must create a processor before running this sample.
            name = client.processor_version_path(
                project_id, location, processor_id, processor_version
            )
    
            # Read the file into memory
            with open(file_path, "rb") as image:
                image_content = image.read()
    
            # Configure the process request
            request = documentai.ProcessRequest(
                name=name,
                raw_document=documentai.RawDocument(content=image_content, mime_type=mime_type),
                # Only supported for Document OCR processor
                process_options=process_options,
            )
    
            result = client.process_document(request=request)
    
            # For a full list of `Document` object attributes, reference this page:
            # https://cloud.google.com/document-ai/docs/reference/rest/v1/Document
            return result.document
    
    
        def layout_to_text(self, layout: documentai.Document.Page.Layout, text: str) -> str:
            """
            Document AI identifies text in different parts of the document by their
            offsets in the entirety of the document"s text. This function converts
            offsets to a string.
            """
            # If a text segment spans several lines, it will
            # be stored in different text segments.
            return "".join(
                text[int(segment.start_index) : int(segment.end_index)]
                for segment in layout.text_anchor.text_segments
            )
    

    3-2.OCRレスポンスの正規化

    次のソースコードは、OCRレスポンスの正規化を実現します。請求書のOCR結果に基づいて特定のフォーマットに従ってテキストを生成するためのクラス「InvoiceGPT」を定義しています。
    このクラスはLangchainでOpenAIのChatGPT(gpt-3.5-turboモデル)を初期化します。また、入力変数とテンプレートを使って動的にPromptを生成するため、LangchainのPromptTemplateを使います。ChatGPTを使って、OCRで取得したテキストを指定のフォーマット及び項目に基づいてレスポンスを生成し、JSON形式で出力フォーマットを返します。
    # invoice_gpt.py
    
    import json
    from langchain.llms import OpenAI
    from langchain.prompts import PromptTemplate 
    
    
    class InvoiceGPT(object):
        # gpt-3.5-turbo
        def __init__(self, temperature = 0 , model_name = "gpt-3.5-turbo"):
            self.name = 'InvoiceGPT'
            self.model_name = model_name
            self.temperature = temperature
    
        def create_prompt_template(self, input_variables, validate_template):
        
            template = """以下の[テキスト]を[制約]に従って[出力フォーマット]で出力してください。
            [制約]
            * 出力は[出力フォーマット]のみ出力してください。
            * [出力フォーマット]以外の余計な文章は出力しないでください。
            [出力フォーマット]
            {sample_json} 
            [テキスト] {ocr_text}"""
            
            prompt_template = PromptTemplate(
                                input_variables   = input_variables,   # 入力変数 
                                template          = template,          # テンプレート
                                validate_template = validate_template, # 入力変数とテンプレートの検証有無
                            )
            return prompt_template
    
        def create_llm(self, model_name, temperature = 0, n = 1):
            LLM = OpenAI(
                model_name        = model_name,  # OpenAIモデル名
                temperature       = temperature, # 出力する単語のランダム性(0から2の範囲) 0であれば毎回返答内容固定
                n                 = n,           # いくつの返答を生成するか           
            )
            return LLM
    
        def process_invoice_ocr(self, ocr_text: str) -> str:
            # ====================================================================================
            # Prompt Templateを適用しLLM実行
            # ====================================================================================
            input_variables=["ocr_text", "sample_json"]
            prompt_template = self.create_prompt_template(input_variables, validate_template=True)
            # プロンプトテンプレートにフォーマット適用
            sample_json = self.get_sample_output_format()
            prompt_format = prompt_template.format(ocr_text=ocr_text, sample_json=sample_json)
            llm = self.create_llm(self.model_name, self.temperature)
            # LLMにPrompt Templateを指定
            response = llm(prompt_format)
    
            # 出力
            return response
    
        def get_sample_output_format(self):
            sample_json = json.dumps("{ '明細': [{  '適用': 'テスト', '数量': 2,'単価': 1000, '合計': 2000}],'合計金額': 10000,'振込期限': '2023/10/04','振込先':'A銀行 B支店' }", ensure_ascii=False)
            return sample_json
    

    3-3.エントリポイントの実装

    次はエントリポイントの実装になります。以下の「app.py」スクリプトは、請求書のOCRとテキスト生成を行います。.envファイルから環境変数を読み込み、指定のPDFファイルをOCR処理してテキストに変換し、生成されたテキストから必要な情報を抽出しJson形式に出力します。結果はoutput/ocr_result.jsonファイルに保存します。
    import os
    from os.path import join, dirname
    from dotenv import load_dotenv
    from ocr_process import InvoiceOCR
    from chatgpt import InvoiceGPT
    
    if __name__ == '__main__' :
        dotenv_path = join(dirname(__file__), '.env')
        load_dotenv(dotenv_path)
    
     
        file_path = "./data/sample_invoice.pdf"
        mime_type = "application/pdf" 
    
        invoiceOcr = InvoiceOCR()
        ocr_response = invoiceOcr.process_invoice_ocr(file_path, mime_type)
    
         invoiceGPT = InvoiceGPT()
        result = invoiceGPT.process_invoice_ocr(ocr_text=ocr_response)
    
        print(result)
        
        path_w = 'output/ocr_result.json'
    
        with open(path_w, mode='w') as f:
            f.write(result)
    

    4.検証実施

    以下の請求書で検証を実施したいと思います。この請求書は複数な箇所で情報が記載されていて、非構造化データになっています。今まで実装したアプリでこの請求書のレイアウトを理解し、ラベルのない請求書文書から詳細な項目や請求情報を読み取ることができないか検証したいと思います。

    以下のコマンドを実行して実際の請求書データで検証を行います。
    (invoice_ocr) usr0101836@TINN0031 invoice_ocr % python app.py
    
    Cloud Document AI APIの出力結果を確認しましょう。このAPIは、非構造化データであるPDF形式の請求書をテキストに変換することができます。特に、PDF内のテーブルも正しく読み取ることができます。目で見たときには少し分かりにくいかもしれませんが、人間の感覚ではだいたい理解できる内容が得られました。
    Full document text: '御請求書\nNo.12345\n2025年12月1日\nミントメイドファッション 御中\nT123-4567\n東野市西平5丁目12-34\n01-2345-6789\nデュアリティ\nアパレル\nT123-4567\n北山市西区南通り1-2-3\n01-2345-6789\[email protected]\nアパレル\nティ\nデュア\n下記の通りご請求申し上げます。\nご請求金額:\n¥54,120 (税込)\n摘要\n数量\n単価\n合計\nブルーラインコレクション\n1\n¥12,300\n¥12,300\nガラスボタン式\n2\n\\12,300\n¥24,600\n赤ニットセーター\n12枚セット\n1\n\\12,300\n¥12,300\n小計\n:\n¥49,200\n消費税 ( 10%)\n:\n¥4,920\n合計\n:\n¥54,120\nお支払いは下記銀行口座へ振込くださいますようお願い申し上げます。\n振込先情報\n振込先:ゼニー中央銀行 港支店\n口座番号:(普通) 1234567\n口座名義: デュアリティアパレル\n恐れ入りますが、 振込手数料は貴社にてご負担ください。\nお振込期限:2026年1月15日\n'
    
    **** Page 1 ****
    
    Found 1 table(s):
    Table with 4 columns and 5 rows:
    
    Found 9 form field(s):
    '摘要' | '数量' | '単価' | '合計' | 
    
    'ブルーラインコレクション' | '1' | '¥12,300' | '¥12,300' | 
    'ガラスボタン式' | '2' | '\\12,300' | '¥24,600' | 
    '赤ニットセーター' | '1' | '\\12,300' | '¥12,300' | 
    '12枚セット' | '' | '小計\n消費税 ( 10%)' | ':\n¥49,200\n:\n¥4,920' | 
    '' | '' | '合計' | ':\n¥54,120' | 
    
        * '番号:(普通) 1234567': ''
        * '口座名義: デュアリティアパレル': ''
        * 'ご請求金額:': '¥54,120 (税込)'
        * 'お振込期限:': '2026年1月15日'
        * '振込先:': 'ゼニー中央銀行 港支店'
        * '合計\n:': '¥54,120'
        * '消費税 ( 10%)\n:': '¥4,920'
        * '小計\n:': '¥49,200'
        * '恐れ入ります': 'が、 振込手数料は貴社にてご負担ください'
    
    上記のテキストから「項目:値」という形式の情報を抽出する必要があります。これは、「OCRレスポンスの正規化」フェーズで行う作業で、通常は正規表現などを使用して対応します。しかし、実際にはこの処理は非常に難しいものです。そこで、ChatGPTのパワーを活用して、自動的にJSON形式のデータを生成したいと考えています。
    ChatGPTの処理結果は以下の感じです。
    { '明細': [{  '適用': 'ブルーラインコレクション', '数量': 1,'単価': 12300, '合計': 12300},{  '適用': 'ガラスボタン式', '数量': 2,'単価': 12300, '合計': 24600},{  '適用': '赤ニットセーター', '数量': 1,'単価': 12300, '合計': 12300}],'合計金額': 54120,'振込期限': '2026/01/15','振込先':'ゼニー中央銀行 港支店' }
    
    アプリの出力結果から見ると、非構造化データから正確に情報を抽出できることを確認し、結果がJSON形式で出力され、期待通りの結果が得られました。良さそうですね。

    5.まとめ

    いかがでしょうか?Google CloudのDocumentAIを使用した請求書のOCR自動化手法と、ChatGPTを組み合わせたOCRデータ正規化及びJsonフォーマット形成の方法について説明しました。結果から見ると複雑な請求書データの正確な抽出と効率的な文書生成が可能になり、請求書のデータ入力業務プロセスの最適化が期待できます。OCR機能から得られたデータをChatGPTで正規化することで、シンプルな実装で高度な精度が得られました。緊急性が高いインボイス制度および電子帳簿保存法への対応ですが、インボイスのチェックや入力がとても負荷になる点は、AI-OCRを活用することで解決できます。このソリューションのソースコードはGithubに公開していますので、請求書処理の自動化に興味を持つ方はぜひ、検討してみてください。

    宣伝

    次世代システム研究室では、最新のテクノロジーを調査・検証しながらインターネットのいろんなアプリケーションの開発を行うアーキテクトを募集しています。募集職種一覧 からご応募をお待ちしています。

    • Twitter
    • Facebook
    • はてなブックマークに追加

    グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

     
    • AI研究開発室
    • 大阪研究開発グループ

    関連記事