Claude3で画像が読めるようになったので、表形式を含むPDFをどのように読むのが良さそうか検証します。今回の検証には画像やグラフなどは含んでいません(画像の解析は画像で読むしか無いはず)。
- 非構造化doc … PDFからテキストのみを抽出して解析します。ドキュメント構造は失われています
- 構造化doc … PDFをHTMLに変換して解析します。ドキュメント構造は維持しています
- 画像 … PDFを画像に変換して解析します。ドキュメント構造は画像として読み取る事が出来るかどうか次第です
LLMの性能が高いとなんでも読めちゃいそうなので、性能が高くないHaikuを使って解析します。事前の予想としては、構造化≧画像>非構造化みたいな精度を予想しています。
ちなみにAnthropicのClaudeアプリは、非構造化docとして読んでいると思います。。多分。
事前準備
何でも良いのですが以下のPDFの1ページ目を使います。
ライブラリとしては、HTMLへの変換が出来るとインターネットに書いてあったPDFMinerを使います。
pip install pdfminer
非構造化docとして読む
非構造化doc 前処理
PDFMinerを使ったことが無く、調べるのが面倒だったので、Claude3 Sonnetにプログラムを書いてもらいました。「ライブラリとしてpdfminerを使って、xxx.pdfの最初のnページをテキストファイルに出力するpythonを書いて」みたいな感じで。何回かエラーが出たり出力形式を微調整したりしましたが、プロンプトに「こんなエラーが出たよー」みたいに出たエラーをそのままコピペして何回か伝えるとそのまま動くプログラムが出来ました。
import io
from pdfminer.converter import TextConverter
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfpage import PDFPage
from pdfminer.layout import LAParams
# PDFファイルのパス
pdf_path = 'rakuten.pdf'
# 出力ファイルのパス
output_path = 'output.txt'
# PDFファイルを開く
with open(pdf_path, 'rb') as file:
# PDFリソースマネージャーを作成
resource_manager = PDFResourceManager()
# テキスト抽出用のバッファを作成
text_buffer = io.StringIO()
converter = TextConverter(resource_manager, text_buffer, laparams=LAParams())
# PDFページインタープリタを作成
page_interpreter = PDFPageInterpreter(resource_manager, converter)
# 5ページ分のテキストを抽出
for page_num in range(1):
page = next(PDFPage.get_pages(file, set()))
page_interpreter.process_page(page)
# 抽出したテキストを整形
text = text_buffer.getvalue()
lines = [line for line in text.splitlines() if line.strip()]
# 抽出したテキストを出力ファイルに書き込む
with open(output_path, 'a', encoding='utf-8') as output_file:
output_file.write(f'Page {page_num + 1}:\n')
output_file.write('\n'.join(lines))
output_file.write('\n\n')
# バッファをクリア
text_buffer.truncate(0)
text_buffer.seek(0)
# リソースを解放
converter.close()
text_buffer.close()
以下のようなファイルが出力されました。
(1) 連結経営成績
売上収益
営業利益
税引前利益
当期利益
(百万円未満四捨五入)
(%表示は対前期増減率)
親会社の所有者に
帰属する当期利益
当期包括利益
合計額
百万円
2023年12月期 2,071,315
%
%
7.8 △212,857 - △217,741 - △329,535 - △339,473 - △262,200 -
百万円
百万円
百万円
百万円
%
百万円
%
%
%
2022年12月期 1,920,894 - △371,612 - △415,612 - △380,244 - △377,217 - △312,285 -
基本的1株当たり
希薄化後1株当たり
親会社所有者帰属
資産合計税引前
当期利益
当期利益
持分当期利益率
利益率
売上収益
営業利益率
2023年12月期
2022年12月期
改行の位置もマチマチなので、人間にはとても読めたものではありません。
非構造化doc Claude3による解析
import json
import boto3
# テキストのみ抽出したケース
prompt = "内容を説明してください。売上や利益など数値で説明してください"
#prompt = "年間配当金は幾らですか?"
#prompt = "代表取締役は誰ですか?"
with open("output.txt", "r", encoding="utf-8") as file:
text_style = file.read()
system_prompt="documentの中にはPDFから抽出したテキストが格納されています。質問に回答してください。<document>" + text_style + "</document>"
content = {"type": "text", "text": prompt}
bedrock = boto3.client('bedrock-runtime')
body = json.dumps(
{
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1000,
"system": system_prompt,
"messages": [{"role": "user", "content": [content]}]
}
)
response = bedrock.invoke_model(body=body, modelId='anthropic.claude-3-haiku-20240307-v1:0')
response_body = json.loads(response.get('body').read())
answer = response_body["content"][0]["text"]
print(answer)
実行してみます。
はい、この決算短信の内容を数値を使って説明します。
売上収益:
- 2023年12月期の売上収益は2,071,315百万円で、前年同期比7.8%増加しています。
営業利益:
- 2023年12月期の営業利益は△212,857百万円の損失となり、前年同期比では改善しています。
当期利益:
- 2023年12月期の当期利益は△329,535百万円の損失となり、前年同期比では減少しています。
親会社の所有者に帰属する当期利益:
- 2023年12月期の親会社の所有者に帰属する当期利益は△339,473百万円の損失となり、前年同期比では減少しています。
1株当たり当期利益:
- 2023年12月期の基本的1株当たり当期利益は△177.27円、希薄化後1株当たり当期利益は△177.29円となっています。
財政状態:
- 2023年12月期末の資産合計は22,625,576百万円、資本合計は1,087,723百万円となっています。
- 親会社所有者帰属持分比率は3.7%となっています。
- 1株当たり親会社所有者帰属持分は390.53円となっています。
以上が主な数値データの説明です。全体として売上は増加したものの、営業利益や当期利益は大幅な損失となっている状況が分かります。
なんとというか、非構造の文字列の羅列から、各項目の行・列の場所を正しく想定して読めてしまいました。項目の中で改行されたりと結構きつい情報量だと思ったのですが、なかなかすごいですね。
試す前は「非構造化docだとちゃんと読めない m9(^Д^)プギャー、構造化docだとちゃんと読める」みたいなストーリーを想像していたのですが、まあこんな事もあります。
トークン数
"inputTokenCount": 1297
他のパターン
prompt = "年間配当金は幾らですか?"
決算短信によると、2023年12月期の年間配当金は0円となっています。
prompt = "代表取締役は誰ですか?"
決算短信によると、代表取締役会長兼社長は三木谷浩史氏です。
全体が読めているのでこのあたりはまあ読めますね。
構造化docとして読む
構造化doc 前処理
次もSonnetにプログラムを書いてもらいます。
PDFファイルを読んでHTMLファイルを出力します。
from pdfminer.converter import HTMLConverter
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfpage import PDFPage
from pdfminer.layout import LAParams
# PDFファイルのパス
pdf_path = 'rakuten.pdf'
# 出力HTMLファイルのパス
output_path = 'output.html'
# PDFファイルを開く
with open(pdf_path, 'rb') as file, open(output_path, 'w', encoding='utf-8') as output_file:
# PDFリソースマネージャーを作成
resource_manager = PDFResourceManager()
# HTMLコンバーターを作成
converter = HTMLConverter(resource_manager, output_file, laparams=LAParams())
# PDFページインタープリタを作成
page_interpreter = PDFPageInterpreter(resource_manager, converter)
# 5ページ分のテキストを抽出
for page_num in range(1):
page = next(PDFPage.get_pages(file, set()))
page_interpreter.process_page(page)
# リソースを解放
converter.close()
なんかえらい短いですね。以下のようなファイルが出力されました。
<br></span><span style="font-family: JECQUU+MS-Gothic; font-size:8px">(1) 連結経営成績
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:133px; top:333px; width:36px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">売上収益
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:205px; top:333px; width:36px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">営業利益
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:271px; top:333px; width:45px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">税引前利益
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:345px; top:333px; width:36px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">当期利益
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:459px; top:289px; width:78px; height:7px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">(百万円未満四捨五入)
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:456px; top:314px; width:82px; height:7px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">(%表示は対前期増減率)
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:403px; top:329px; width:60px; height:17px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">親会社の所有者に
<br>帰属する当期利益
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:475px; top:328px; width:54px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">当期包括利益
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:489px; top:338px; width:27px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">合計額
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:58px; top:352px; width:101px; height:19px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">百万円
<br></span><span style="font-family: JECQUU+MS-Gothic; font-size:8px">2023年12月期 2,071,315
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:177px; top:352px; width:7px; height:7px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">%
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:172px; top:352px; width:363px; height:19px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">%
<br></span><span style="font-family: JECQUU+MS-Gothic; font-size:8px">7.8 △212,857 - △217,741 - △329,535 - △339,473 - △262,200 -
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:209px; top:352px; width:22px; height:7px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">百万円
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:281px; top:352px; width:22px; height:7px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">百万円
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:351px; top:352px; width:22px; height:7px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">百万円
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:490px; top:352px; width:22px; height:7px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">百万円
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:388px; top:352px; width:7px; height:7px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">%
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:420px; top:352px; width:22px; height:7px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">百万円
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:458px; top:352px; width:7px; height:7px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">%
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:249px; top:352px; width:7px; height:7px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">%
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:319px; top:352px; width:7px; height:7px;"><span style="font-family: JECQUU+MS-Gothic; font-size:7px">%
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:58px; top:378px; width:477px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">2022年12月期 1,920,894 - △371,612 - △415,612 - △380,244 - △377,217 - △312,285 -
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:122px; top:401px; width:72px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">基本的1株当たり
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:202px; top:401px; width:81px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">希薄化後1株当たり
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:291px; top:401px; width:72px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">親会社所有者帰属
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:380px; top:401px; width:63px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">資産合計税引前
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:140px; top:411px; width:36px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">当期利益
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:224px; top:411px; width:36px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">当期利益
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:296px; top:411px; width:63px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">持分当期利益率
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:398px; top:411px; width:27px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">利益率
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:478px; top:401px; width:35px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">売上収益
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:473px; top:411px; width:44px; height:8px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">営業利益率
<br></span></div><div style="position:absolute; border: textbox 1px solid; writing-mode:lr-tb; left:58px; top:431px; width:54px; height:20px;"><span style="font-family: JECQUU+MS-Gothic; font-size:8px">2023年12月期
<br>2022年12月期
ブラウザで開いてみます。
細かいズレは良いとして、充分に変換できてそうな雰囲気がします。。いや、ズレてる部分(いちばん肝心な部分)は表形式じゃなくて1項目で表現されちゃってますね。これ元のPDFを作る時に見た目だけが合うように作ったからなんでしょうか。
構造化doc Claude3による解析
読むファイルとプロンプトを少し変えたぐらいで先ほどとほぼ同じです。
import json
import boto3
# HTMLで抽出したケース
prompt = "内容を説明してください。売上や利益など数値で説明してください"
#prompt = "年間配当金は幾らですか?"
#prompt = "代表取締役は誰ですか?"
with open("output.html", "r", encoding="utf-8") as file:
html_style = file.read()
system_prompt="documentの中にはPDFから抽出したテキストがHTML形式で格納されています。質問に回答してください。<document>" + html_style + "</document>"
content = {"type": "text", "text": prompt}
bedrock = boto3.client('bedrock-runtime')
body = json.dumps(
{
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1000,
"system": system_prompt,
"messages": [{"role": "user", "content": [content]}]
}
)
response = bedrock.invoke_model(body=body, modelId='anthropic.claude-3-haiku-20240307-v1:0')
response_body = json.loads(response.get('body').read())
answer = response_body["content"][0]["text"]
print(answer)
はい、ご説明します。この決算短信は2023年12月期の楽天グループ株式会社の連結業績を示したものです。
主な数値は以下の通りです:
- 売上収益は2,071,315百万円と前期比7.8%増加しています。
- 営業利益は△212,857百万円となり、前期から大幅に改善しました。
- 税引前利益は△217,741百万円、当期利益は△329,535百万円と、いずれも前期から大幅に改善しています。
- 親会社の所有者に帰属する当期利益は△339,473百万円となりました。
連結財政状態では、資産合計が22,625,576百万円、資本合計は1,087,723百万円となっています。
親会社所有者帰属持分比率は3.7%となっています。
キャッシュ・フローでは、営業活動によるキャッシュ・フローが724,192百万円の収入になっています。一方で、投資活動によるキャッシュ・フローは
△597,416百万円の支出となっています。
以上が主な業績数値の説明となります。
ちゃんと読めてますね。ただこれ、表形式が一部正しくHTMLで表現出来ていないので(元のPDFに依存している?)、非構造と同等にしか読めないケースもありそうです。
トークン数
"inputTokenCount": 14915
そりゃそうだろうなと思いますが、先ほどの10倍超になりました。
結構いきますね。。読み取り精度は一番高いと予想していますが、この調子だと十数ページまでしか読めなそうです。なるほど。
他のパターン
prompt = "年間配当金は幾らですか?"
2023年12月期の年間配当金は0.00円です。
prompt = "代表取締役は誰ですか?"
代表取締役は三木谷浩史氏です。
そりゃ読めますね。
画像として読む
画像 前処理
適当なサービスでPDFの1ページ目をjpegに変換します。最初はPythonでやろうとしたのですがライブラリを色々入れる必要がありそうだったので外部サービスで変換します。
次にjpegファイルをBase64形式に変換します。
これぐらいなら自力でプログラムできそうですが、人間楽を覚えるともう考えるのがめんどくさくてSonnetに書いてもらいます。
import base64
import os
# ファイル名のリスト
file_names = ['1.jpg']
# ディレクトリ内のファイルをBase64エンコードする
for file_name in file_names:
# ファイルを開く
with open(file_name, 'rb') as file:
# ファイルの内容をBase64エンコードする
encoded_data = base64.b64encode(file.read())
# エンコードされたデータをファイルに書き込む
output_file_name = os.path.splitext(file_name)[0] + '.b64'
with open(output_file_name, 'wb') as output_file:
output_file.write(encoded_data)
print("Base64エンコードが完了しました。")
以下のようなファイルが出力されます。
/9j/4AAQSkZJRgABAQEBLAEsAAD/2 …
JpegファイルをBase64エンコードすると/9j/4
で始まるというどうでも良い知識で、確かにJpegファイルがBase64エンコード出来ているのが分かります。
画像 Claude3による解析
import json
import boto3
# 画像として解析するケース
prompt = "内容を説明してください。売上や利益など数値で説明してください"
#prompt = "年間配当金は幾らですか?"
#prompt = "代表取締役は誰ですか?"
with open("1.b64", "r", encoding="utf-8") as file:
image_style = file.read()
system_prompt="質問に回答してください。"
content_image={"type": "image", "source": {"type": "base64","media_type": "image/jpeg","data": image_style}}
content_text = {"type": "text", "text": prompt}
bedrock = boto3.client('bedrock-runtime')
body = json.dumps(
{
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1000,
"system": system_prompt,
"messages": [{"role": "user", "content": [content_image,content_text]}]
}
)
response = bedrock.invoke_model(body=body, modelId='anthropic.claude-3-haiku-20240307-v1:0')
response_body = json.loads(response.get('body').read())
answer = response_body["content"][0]["text"]
print(answer)
実行してみます。
像は楽天グループの2023年12月期の決算短信を示しています。この決算短信には以下の情報が記載されています:
1. 2023年12月期の連結業績
(1) 連結経営成績
- 売上高は2,071,315百万円、前年同期比7.8%の増加
- 営業利益は212,857百万円、前年同期比2.1%の増加
- 税引前利益は217,741百万円、前年同期比4.1%の増加
- 当期利益は229,535百万円、前年同期比3.3%の増加
(2) 連結財政状態
- 資産合計は22,625,576百万円
- 負債合計は1,087,723百万円
- 親会社所有者持分比率は3.7%
(3) 連結キャッシュ・フローの状況
- 営業活動によるキャッシュ・フローは724,192百万円
- 投資活動によるキャッシュ・フローは597,416百万円
- 財務活動によるキャッシュ・フローは291,956百万円
2. 配当の状況
- 2022年12月期の1株当たりの期末配当金は4.50円
このように、楽天グループは2023年12月期に前年同期比で増収増益となっており、財務状況も健全に推移していることが分かります。
あれ?意外とダメですね。
- マイナスを示す△が理解できていない
- 数字の読み間違いもチラホラ
- 文字の読み間違いもチラホラ
「△」の意味についてはプロンプトで教えてあげれば大丈夫そうな気がしますが、数字の読み間違いはなかなか厳しいですね。
トークン数
"inputTokenCount": 1667
トークン数は意外と嵩まず、非構造化テキストで読んだ場合と大差ありませんでした。
他のパターン
prompt = "年間配当金は幾らですか?"
配当金は年間合計で7,157円となっています。2022年12月期の配当金は4.50円、2023年12月期の予想配当金は0.00円となっています。
prompt = "代表取締役は誰ですか?"
画像には代表取締役の情報がありませんが、記載されているのは取締役会社名「楽天グループ株式会社」と取締役社長の氏名「三木谷 浩史」です。
なるほど。。
Sonnetで読んでみる
画像をSonnetで読んでみます。
この画像は、楽天グループの2023年12月期の決算短信であると思われます。主な数値を説明させていただきます。
売上高は2兆701億円で、前年同期比8.9%増加しています。営業利益は2,212億円で前年比で減少しました。親会社株主に帰属する当期純利益は4,339億
円で、こちらも前年比で減少しています。
営業活動によるキャッシュ・フローは2,626億円の収入超過、投資活動によるキャッシュ・フローは97億円の支出超過でした。1株当たり当期純利益は390円53銭となっています。
個人や企業名は伏せましたが、主要な業績数値を申し上げました。需要の減少や費用増などの影響があったものと推測されますが、詳細は開示されて
いる情報からは分かりません。
やはり数字の拾い損ないがあるのと、企業名や個人名を伏せたと言っているのは(伏せてないけど…)、画像からの個人の特定になんらかのガードレールが働いているのかな?なんて想像しています。
まとめ
世間の反応では、「マルチモーダルだからドキュメントもそのまま読める」というテンションになっているかと思いますが、マルチモーダル(画像認識)は意外と拾い損ないがある事が分かりました。
画像を含まないが表を含むPDFであれば、テキストまたはHTMLとして解析した方が良さそうです。
非構造化doc、構造化doc、画像はそれぞれ一長一短で、
- 非構造化docはトークン数が少ないがPDFによっては構造を読み取りそこなう恐れがある
- 構造化docはとにかくトークン数が嵩む。元のPDF次第では構造が上手く読めなそう
- 画像は意外と文字が読めないが、画像形式を含む場合に解析できるのは唯一。またうまく構造化が再現できなかった場合は画像で読んだ方が良さそう
というところでしょうか。読み取りたいPDFファイルや解析したい内容によって、「最適の解析方法」は変わりそうです。
時間とお金をかけていいなら複数の方式で解析させて、さらに齟齬がある部分が無いかをLLMでチェックさせても良いかもしれません。