G-gen 又吉です。当記事では、Cloud Vision API を用いて PDF ファイルからテキストを抽出し、Google Cloud の Generative AI モデルが利用できる Vertex AI PaLM API を呼び出して抽出したテキストの要約をやってみたので解説します。

前提知識
Generative AI Support on Vertex AI
先日 Vertex AI でも Generative AI がサポートされました。Generative AI モデル (基盤モデル) の裏側は PaLM 2 が利用されており、多言語、推論、コーディング機能が強化された最先端の大規模言語モデル (LLM) です。
Vertex AI で Generative AI がサポートされたことで、Vertex AI のエンドポイントから基盤モデルを呼び出せるようになりました。
Vertex AI の Generative AI サポートについての詳細は以下の記事をご参照下さい。
2023 年 7 月現在、Vertex AI PaLM API は日本語未対応ですが、筆者の環境は Trusted Testers プログラムに参加中のため日本語にも対応しております。
Cloud Vision API
Cloud Vision API とは、事前トレーニング済み Vision API モデル使用して、画像内のオブジェクトの検知や OCR だけでなく、PDF / TIFF ファイル中のテキスト抽出等も行なえます。
その他の機能、また詳細については以下のドキュメントをご参照下さい。
また、Cloud Vision API を用いたやってみた記事として、以下のようなものもあるので興味があれば御覧ください。
今回扱うデータ
今回扱う PDF ファイルは、ダミーで作成した日報データです。

構成図
今回の構成は以下のとおりです。

PDF 用バケットに PDF ファイル (日報) がアップロードされたことをトリガーに、 PDF からテキストを抽出する Cloud Functions 関数が起動し、抽出したテキストデータをテキスト用バケットに格納します。
次に、テキスト格納用バケットにデータがアップロードされたことをトリガーに、文章を要約する Cloud Functions 関数が起動し、要約したデータが要約された文章用バケットに格納します。
プロンプト設計
概要
プロンプトとは、簡単に言うと大規模言語モデル (LLM) に送信するリクエストのことです。
Vertex AI PaLM API からより良い回答を生成してもらうためには、プロンプトの設計が非常に重要です。
参考:プロンプト設計
今回は、自然な会話やチャットボット等のユースケースに特化した chat-bison@001 (チャット用言語モデル) を使用します。
チャット用言語モデルのプロンプトには、次の 3 つのコンポーネントで構成されます。
- メッセージ [必須]
- コンテキスト [オプション]
- 入出力例の追加 [オプション]
コンテキスト
コンテキスト とは、モデルの応答方法を指示したり、モデルのペルソナを指定したりできます。
よって今回は、以下のようなコンテキストを設定します。
あなたはプロの編集者です。以下の制約条件に従って、入力する文章を要約してください。 制約条件 ・300文字以内にまとめて要約した文章として出力。 ・文章の意味を変更しない。 ・架空の表現や言葉を使用しない。
入出力例の追加
入出力例の追加 とは、特定の入力例と、その入力に対するモデルの出力例、つまり入出力のペアリストのことです。
PDF から抽出されるテキストデータのサンプルとして、以下を入力例とします。
日付名前2023年7月24日営業 太郎日報業務報告お疲れ様です。 本日の活動について報告いたします。本日は、 当社の製品に関心を示していただいた新規のお客様への訪問を中心に行いました。 まず、 午前中にはA社を訪問し、当社製品の特徴と価格競争力についてプレゼンテーションを行いました。 彼らは特に当社の製品のコストパフォーマンスに感心していましたが、 具体的な購入の意志は明らかになりませんでした。 引き続き 情報提供を行い、 ビジネスを進展させるべく、 交渉を続けます。午後は、新たに興味を示してくださったB社とのミーティングを行いました。 当社の製品についての詳細な質問や、予算に関する議論が交わされました。B社は即決ではありませんでしたが、 当社の製品に強い興味を持っていることが伺え、有望な見込み客と判断しています。その他、進行中のC社との契約交渉については、一部微調整が必要な部分が見つかりました。 来週中には改めて会議を設定し、これをクリアにする予定です。全体として、本日は新規顧客開拓と既存顧客との継続的な関係強化に重点を置いた一日となりました。 明日はさらなる顧客訪問と情報収集、ならびに製品のプレゼンテーションを行う予定です。 ご支援のほど、よろしくお願い申し上げます。以上、本日の報告となります。
モデルの出力例としては、「日付 : 」「名前 : 」「要約 : 」を分けて出力してもらえるよう、以下のように設定します。
日付:2023年7月24日
名前:営業 太郎
要約:本日は、当社の製品に関心を示していただいた新規のお客様への訪問を中心に行いました。午前中にはA社を訪問し、当社製品の特徴と価格競争力についてプレゼンテーションを行いました。午後は、新たに興味を示してくださったB社とのミーティングを行いました。その他、進行中のC社との契約交渉については、一部微調整が必要な部分が見つかりました。明日はさらなる顧客訪問と情報収集、ならびに製品のプレゼンテーションを行う予定です。
準備
ディレクトリ構成
開発環境は Cloud Shell を用いて行います。ディレクトリ構造は以下のとおりです。
terraform
ディレクトリ配下は、以下のとおりです。
terraform|-- gcf_source_code| |-- pdf_to_text| | |-- main.py| | `-- requirements.txt| `-- sammarize| |-- main.py| `-- requirements.txt`-- main.tf
main.tf
main.tf には Terraform のコードを記述しています。
locals {terraform_service_account = ${Terraform 実行に使われるサービスアカウントのメールアドレス}project_name = ${プロジェクト名}project_id = ${プロジェクト ID}folder_id = ${フォルダ ID}billing_account_id = ${請求先アカウント ID}}# terraform & provider の設定terraform {required_providers {google = {source = "hashicorp/google"version = ">= 4.0.0"}}required_version = ">= 1.3.0"backend "gcs" {bucket = ${tfstate ファイルを格納する Cloud Storage バケット名}impersonate_service_account = ${Terraform 実行に使われるサービスアカウントのメールアドレス}}}# サービスアカウント権限借用の設定provider "google" {alias = "impersonation"scopes = ["https://www.googleapis.com/auth/cloud-platform","https://www.googleapis.com/auth/userinfo.email",]}data "google_service_account_access_token" "default" {provider = google.impersonationtarget_service_account = local.terraform_service_accountscopes = ["userinfo-email", "cloud-platform"]lifetime = "1200s"}# Google プロバイダの設定provider "google" {project = local.project_idregion = "asia-northeast1"access_token = data.google_service_account_access_token.default.access_tokenrequest_timeout = "60s"}######################################### プロジェクトの作成と API の有効化 ########################################## プロジェクトの作成resource "google_project" "poc" {name = local.project_nameproject_id = local.project_idfolder_id = local.folder_idbilling_account = local.billing_account_id}# API の有効化module "tenant_a_project_services" {source = "terraform-google-modules/project-factory/google//modules/project_services"version = "14.2.1"project_id = google_project.poc.project_idenable_apis = trueactivate_apis = ["iam.googleapis.com","cloudbuild.googleapis.com","run.googleapis.com","cloudfunctions.googleapis.com","pubsub.googleapis.com","eventarc.googleapis.com","artifactregistry.googleapis.com","storage.googleapis.com","vision.googleapis.com","aiplatform.googleapis.com"]disable_services_on_destroy = false}########################################## サービスアカウントの作成と権限の付与 ########################################## Cloud Functions 用サービスアカウントの作成と権限付与resource "google_service_account" "sa_gcf" {project = google_project.poc.project_idaccount_id = "sa-gcf"display_name = "Cloud Functions 用サービスアカウント"}resource "google_project_iam_member" "invoke_gcf" {project = google_project.poc.project_idrole = "roles/run.invoker"member = "serviceAccount:${google_service_account.sa_gcf.email}"}resource "google_project_iam_member" "storage_admin" {project = google_project.poc.project_idrole = "roles/storage.admin"member = "serviceAccount:${google_service_account.sa_gcf.email}"}resource "google_project_iam_member" "event_receiving" {project = google_project.poc.project_idrole = "roles/eventarc.eventReceiver"member = "serviceAccount:${google_service_account.sa_gcf.email}"depends_on = [google_project_iam_member.invoke_gcf]}resource "google_project_iam_member" "artifactregistry_reader" {project = google_project.poc.project_idrole = "roles/artifactregistry.reader"member = "serviceAccount:${google_service_account.sa_gcf.email}"depends_on = [google_project_iam_member.event_receiving]}resource "google_project_iam_member" "vertex_ai_user" {project = google_project.poc.project_idrole = "roles/aiplatform.user"member = "serviceAccount:${google_service_account.sa_gcf.email}"}# Eventarc のサービスアカウントに権限付与resource "google_project_iam_member" "serviceAccount_token_creator" {project = google_project.poc.project_idrole = "roles/iam.serviceAccountTokenCreator"member = "serviceAccount:service-${google_project.poc.number}@gcp-sa-pubsub.iam.gserviceaccount.com"depends_on = [module.tenant_a_project_services]}# Cloud Storage のサービスアカウントに権限付与data "google_storage_project_service_account" "gcs_account" {project = google_project.poc.project_iddepends_on = [ module.tenant_a_project_services ]}resource "google_project_iam_member" "gcs_pubsub_publishing" {project = google_project.poc.project_idrole = "roles/pubsub.publisher"member = "serviceAccount:${data.google_storage_project_service_account.gcs_account.email_address}"}################################### バケットとオブジェクトの作成 #################################### Cloud Functions のソースコード格納用バケットの作成resource "google_storage_bucket" "source_gcf" {project = google_project.poc.project_idlocation = "asia-northeast1"name = "${google_project.poc.project_id}-source-gcf"force_destroy = true}# Cloud Functions で使うソースコードを ZIP 化data "archive_file" "pdf_to_text" {type = "zip"source_dir = "./gcf_source_code/pdf_to_text"output_path = "./zip_source_code/pdf_to_text.zip"}data "archive_file" "sammarize" {type = "zip"source_dir = "./gcf_source_code/sammarize"output_path = "./zip_source_code/sammarize.zip"}# ZIP 化したソースコードをバケットに追加resource "google_storage_bucket_object" "pdf_to_text" {name = "pdf-to-text.${data.archive_file.pdf_to_text.output_md5}.zip"bucket = google_storage_bucket.source_gcf.namesource = data.archive_file.pdf_to_text.output_path}resource "google_storage_bucket_object" "sammarize" {name = "sammarize.${data.archive_file.sammarize.output_md5}.zip"bucket = google_storage_bucket.source_gcf.namesource = data.archive_file.sammarize.output_path}# pdf_data バケットの作成resource "google_storage_bucket" "pdf_data" {project = google_project.poc.project_idlocation = "asia-northeast1"name = "${google_project.poc.project_id}-pdf-data"force_destroy = true}# text_data バケットの作成resource "google_storage_bucket" "text_data" {project = google_project.poc.project_idlocation = "asia-northeast1"name = "${google_project.poc.project_id}-text-data"force_destroy = true}# summarized_text_data バケットの作成resource "google_storage_bucket" "summarized_text_data" {project = google_project.poc.project_idlocation = "asia-northeast1"name = "${google_project.poc.project_id}-summarized-text-data"force_destroy = true}############################### Cloud Functions 作成 ################################ pdf_to_text 関数resource "google_cloudfunctions2_function" "pdf_to_text" {depends_on = [google_project_iam_member.event_receiving,google_project_iam_member.artifactregistry_reader,google_project_iam_member.serviceAccount_token_creator]name = "pdf-to-text"location = "asia-northeast1"description = "PDF からテキストを取得し text_data バケットに格納する関数"build_config {runtime = "python310"entry_point = "main" # Set the entry point in the codesource {storage_source {bucket = google_storage_bucket.source_gcf.nameobject = google_storage_bucket_object.pdf_to_text.name}}}service_config {max_instance_count = 3min_instance_count = 1available_memory = "256M"timeout_seconds = 60environment_variables = {DESTINATION_BUCKET_NAME = google_storage_bucket.text_data.name}service_account_email = google_service_account.sa_gcf.email}event_trigger {trigger_region = "asia-northeast1"event_type = "google.cloud.storage.object.v1.finalized"retry_policy = "RETRY_POLICY_DO_NOT_RETRY"service_account_email = google_service_account.sa_gcf.emailevent_filters {attribute = "bucket"value = google_storage_bucket.pdf_data.name}}}# sammarize 関数resource "google_cloudfunctions2_function" "sammarize" {depends_on = [google_project_iam_member.event_receiving,google_project_iam_member.artifactregistry_reader,google_project_iam_member.serviceAccount_token_creator]name = "sammarize"location = "asia-northeast1"description = "Vertex AI PaLM API でテキストを要約し summarized_text_data バケットに格納する関数"build_config {runtime = "python310"entry_point = "main" # Set the entry point in the codesource {storage_source {bucket = google_storage_bucket.source_gcf.nameobject = google_storage_bucket_object.sammarize.name}}}service_config {max_instance_count = 3min_instance_count = 1available_memory = "256M"timeout_seconds = 60environment_variables = {PROJECT_ID = google_project.poc.project_idDESTINATION_BUCKET_NAME = google_storage_bucket.summarized_text_data.name}service_account_email = google_service_account.sa_gcf.email}event_trigger {trigger_region = "asia-northeast1"event_type = "google.cloud.storage.object.v1.finalized"retry_policy = "RETRY_POLICY_DO_NOT_RETRY"service_account_email = google_service_account.sa_gcf.emailevent_filters {attribute = "bucket"value = google_storage_bucket.text_data.name}}}
gcf_source_code/pdf_to_text
main.py
gcf_source_code/pdf_to_text には、PDF ファイルからテキストを抽出する Cloud Functions 関数のソースコードを格納しています。
import jsonimport reimport osfrom cloudevents.http import CloudEventimport functions_frameworkfrom google.cloud import visionDESTINATION_BUCKET_NAME = os.environ.get("DESTINATION_BUCKET_NAME")# クライアントの初期化vision_client = vision.ImageAnnotatorClient()def async_detect_document(gcs_source_uri,gcs_destination_uri,):# 入力設定を構成mime_type = "application/pdf"gcs_source = vision.GcsSource(uri=gcs_source_uri)input_config = vision.InputConfig(gcs_source=gcs_source, mime_type=mime_type)# 出力設定を構成feature = vision.Feature(type_=vision.Feature.Type.DOCUMENT_TEXT_DETECTION)gcs_destination = vision.GcsDestination(uri=gcs_destination_uri)output_config = vision.OutputConfig(gcs_destination=gcs_destination)# 非同期リクエストを実行async_request = vision.AsyncAnnotateFileRequest(features=[feature],input_config=input_config,output_config=output_config)# operations リソースでステータスを確認operation = vision_client.async_batch_annotate_files(requests=[async_request])print("Waiting for the operation to finish.")# 非同期処理が完了していない場合、最大 timeout 秒まで待機operation.result(timeout=420)return "ok"@functions_framework.cloud_eventdef main(cloud_event: CloudEvent):# CloudEvent から渡されたデータを取得data = cloud_event.databucket_name = data["bucket"]file_name = data["name"]no_extension_file_name = file_name.split('.')[0]gcs_source_uri = f"gs://{bucket_name}/{file_name}"gcs_destination_uri = f"gs://{DESTINATION_BUCKET_NAME}/{no_extension_file_name}/"async_detect_document(gcs_source_uri = gcs_source_uri,gcs_destination_uri = gcs_destination_uri,)return "ok"
PDF からテキストを取得する際、 AsyncBatchAnnotateFilesRequest メソッドを利用していますが、こちらは非同期でリクエストが行われます。 operations リソースを用いることで、そのステータスが確認でき、尚タイムアウトの指定も可能となります。
また、今回は検証のため対応しておりませんが、出力されるテキストが大きくなると複数ファイルに分かれて Cloud Storage に書き込まれる可能性がある為、本番運用時はこのあたりの考慮も必要となります。本検証は、1 ファイルのインプットにつき 1 ファイルのアウトプットのみに対応した構成となっています。
requirements.txt
functions-framework==3.* cloudevents==1.9.0 google-cloud-vision==3.4.4
gcf_source_code/sammarize
main.py
gcf_source_code/sammarize には、テキストを要約する Cloud Functions 関数のソースコードを格納しています。
import jsonimport osfrom cloudevents.http import CloudEventimport functions_frameworkimport vertexaifrom vertexai.preview.language_models import ChatModel, InputOutputTextPairfrom google.cloud import storagePROJECT_ID = os.environ.get("PROJECT_ID")DESTINATION_BUCKET_NAME = os.environ.get("DESTINATION_BUCKET_NAME")# 基盤モデルとストレージクライアントの初期化vertexai.init(project=PROJECT_ID, location="us-central1")chat_model = ChatModel.from_pretrained("chat-bison@001")storage_client = storage.Client()def download_json_from_bucket(bucket_name, blob_name):# バケットを取得bucket = storage_client.get_bucket(bucket_name)# オブジェクトを取得blob = bucket.blob(blob_name)json_string = blob.download_as_bytes().decode("utf-8")response = json.loads(json_string)return responsedef summarize_text(text):# 言語モデルのリクエストパラメラータを指定parameters = {"temperature": 0.2,"max_output_tokens": 1024,"top_p": 0.8,"top_k": 40}# コンテキストと INPUT / OUTPUT の例を追加chat = chat_model.start_chat(context="""あなたはプロの編集者です。以下の制約条件に従って、入力する文章を要約してください。制約条件・300文字以内にまとめて要約した文章として出力。・文章の意味を変更しない。・架空の表現や言葉を使用しない。#出力形式日付:名前:要約した文章:""",examples=[InputOutputTextPair(input_text="""日付名前2023年7月24日営業 太郎日報業務報告お疲れ様です。 本日の活動について報告いたします。本日は、 当社の製品に関心を示していただいた新規のお客様への訪問を中心に行いました。 まず、 午前中にはA社を訪問し、当社製品の特徴と価格競争力についてプレゼンテーションを行いました。 彼らは特に当社の製品のコストパフォーマンスに感心していましたが、 具体的な購入の意志は明らかになりませんでした。 引き続き 情報提供を行い、 ビジネスを進展させるべく、 交渉を続けます。午後は、新たに興味を示してくださったB社とのミーティングを行いました。 当社の製品についての詳細な質問や、予算に関する議論が交わされました。B社は即決ではありませんでしたが、 当社の製品に強い興味を持っていることが伺え、有望な見込み客と判断しています。その他、進行中のC社との契約交渉については、一部微調整が必要な部分が見つかりました。 来週中には改めて会議を設定し、これをクリアにする予定です。全体として、本日は新規顧客開拓と既存顧客との継続的な関係強化に重点を置いた一日となりました。 明日はさらなる顧客訪問と情報収集、ならびに製品のプレゼンテーションを行う予定です。 ご支援のほど、よろしくお願い申し上げます。以上、本日の報告となります。""",output_text="""日付:2023年7月24日名前:営業 太郎要約:本日は、当社の製品に関心を示していただいた新規のお客様への訪問を中心に行いました。午前中にはA社を訪問し、当社製品の特徴と価格競争力についてプレゼンテーションを行いました。午後は、新たに興味を示してくださったB社とのミーティングを行いました。その他、進行中のC社との契約交渉については、一部微調整が必要な部分が見つかりました。明日はさらなる顧客訪問と情報収集、ならびに製品のプレゼンテーションを行う予定です。""")])# 基盤モデルにリクエストを実行response = chat.send_message(text, **parameters)return response.textdef upload_text_to_bucket(bucket_name, blob_name, text_data):# バケットを取得bucket = storage_client.get_bucket(bucket_name)# バケットにアップロードするためのblobを作成blob = bucket.blob(blob_name)# テキストデータをアップロードblob.upload_from_string(text_data)return "OK"@functions_framework.cloud_eventdef main(cloud_event: CloudEvent):# CloudEvent から渡されたデータを取得data = cloud_event.dataprint(data)bucket_name = data["bucket"]blob_name = data["name"] # "Sheet2/output-1-to-1.json"folder_name = blob_name.split('/')[0]response = download_json_from_bucket(bucket_name = bucket_name,blob_name = blob_name)# json からテキストデータの取得first_page_response = response["responses"][0]response_text = first_page_response["fullTextAnnotation"]["text"]# 改行と「|」の削除corrected_text = response_text.replace('\n', '').replace('|', '')print(f"=====原文=====")print(corrected_text)print(f"=====サマリ=====")summarized_text = summarize_text(corrected_text)print(summarized_text)upload_text_to_bucket(bucket_name = DESTINATION_BUCKET_NAME,blob_name = f"{folder_name}.txt",text_data = summarized_text.encode("shift_jis"))return "ok"
Vertex AI PaLM API エンドポイントへのリクエストには、先程のプロンプト設計で作成したコンテキストと入出力例の追加を行っています。
requirements.txt
functions-framework==3.* cloudevents==1.9.0 google-cloud-vision==3.4.4 google-cloud-storage==2.10.0 google-cloud-aiplatform==1.28.1
動作検証
検証データ
以下の PDF ファイルで動作検証を行います。尚、業務報告内の文字数は 736 文字となります。

実行
PDF 格納バケットに daily_report.pdf をアップロードします。

直後に Cloud Storage トリガー経由で Cloud Functions が起動し、最終的に要約されたテキスト格納バケットにオブジェクトが生成されました。

中身は以下のようになってました。

入力コンテキストの制限事項には 300 文字以内で要約するよう記載していましたが、要約後のテキストでは 411 文字で出力されました 。圧縮率 (または要約率) 56%。
何度か実行してみましたが 300 文字以内で要約することはできなかったため、制限事項に正確に従うことは現状難しいのかと思います。
しかし要約内容について、重要な箇所は漏れなく記載されており、文章全体にも違和感なく要約されています。
又吉 佑樹(記事一覧)
クラウドソリューション部
はいさい、沖縄出身のクラウドエンジニア!
セールスからエンジニアへ転身。Google Cloud 全 11 資格保有。Google Cloud Champion Innovator (AI/ML)。Google Cloud Partner Top Engineer 2024。Google Cloud 公式ユーザー会 Jagu'e'r でエバンジェリスト。好きな分野は生成 AI。
Follow @matayuuuu