16
9

LLMはExcel方眼紙を読めるのか?

Posted at

日本にはExcel方眼紙という文化がありますので、各企業に膨大に存在するであろうExcel方眼紙資産をLLMは読めるのか?という水と油のような事を試してみます。

Excel方眼紙としてはかなり簡易ですが、総務省の「統計表における機械判読可能なデータ作成に関する表記方法について」の悪い例を参考にして作ったもので動作を確認します。

image.png

セル結合、1セルの中に複数項目、セルの中に注釈を入れる、同じ行に別の表を入れる、などやってます。

この資料に対して、以下の質問をしてみます。

  • 複数データ表のCの費用総額は?
  • 注釈込み表において冷凍品は何月のどの商品か?
  • 結合表の東京都の市区町村を全て出力してください

テキストのみを抽出する

テキストのみを抽出する例として、Knowledge Bases for Amazon Bedrock にExcelファイルを格納し、Syncしてみます。

前回同様に pgvector に格納しSQLでチャンクのテキストを確認します。

変換後データ

select chunks from bedrock_integration.bedrock_kb

--以下、検索結果--
Sheet1 複数データ フクスウ 結合表 ケツゴウヒョウ 企業数 キギョウスウ 売上金額(費用総額) ウリアゲキンガク ヒヨウソウガク 東京都 トウキョウト 千代田区 チヨダク 総計 ソウケイ 1110 100(1000) 中央区 チュウオウク A 111 10(100) 港区 ミナトク B 222 20(200) 三鷹市 ミタカシ C 333 30(300) 青梅市 オウメシ D 444 40(400) 立川市 タチカワシ 千葉県 チバケン 千葉市 チバシ 注釈込み表 チュウシャクコ ヒョウ 柏市 カシワシ 商品A ショウヒン 商品B ショウヒン 勝浦市 カツウラシ 1月 ガツ 500 (a) 800 船橋市 フナバシシ 2月 ガツ 600 900 (b) 3月 ガツ 700 1000 (a) … 冷凍品 レイトウヒン (b) … 調理済品 チョウリスミ ヒン
--ここまで--

左から右に文字列だけを抜き出しており改行も入って無いので行列情報が失われてなかなか厳しそうです。
あと今回の確認の趣旨とは異なりますが、Excelに保存されている入力時の変換前のカナも含めてチャンクに入れているのはなるほどなと思いました。

実行結果

  • 複数データ表のCの費用総額は?
    • 複数データ表のCの費用総額は30(300)です。
  • 注釈込み表において冷凍品は何月のどの商品か?
    • 注釈付きデータ表から、冷凍品の商品名は「商品A」であり、その売上金額が記載されている月は1月であることがわかります。
  • 結合表の東京都の市区町村を全て出力してください
    • 結合表の東京都の市区町村は、千代田区、中央区、港区、三鷹市、青梅市、立川市が記載されています。
正誤
1問目
2問目
3問目

なんと概ね上手く読めました。1問目は情報は取れていますが意図が汲み取れなかったようです。

CSVに変換したデータ

次はpandasを使ってCSVに変換した後にCSVファイルに対して解析を行います。

import pandas as pd
import json
import boto3

df = pd.read_excel("./総務省.xlsx")
df.to_csv("output.csv")

prompt = "ここに質問を書く"

with open("output.csv", "r", encoding="utf-8") as file:
    text = file.read()

system_prompt="次のdocumentにはExcelから抽出したCSVデータが格納されています。\n<document>\n" + text + "\n</document>\n質問に回答してください。"

content = {"type": "text", "text": prompt}
bedrock = boto3.client('bedrock-runtime')
body = json.dumps(
    {
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 4000,
        "temperature": 0.0,
        "system": system_prompt,
        "messages": [{"role": "user", "content": [content]}]
    }   
)
response = bedrock.invoke_model(body=body, modelId='anthropic.claude-3-sonnet-20240229-v1:0')
response_body = json.loads(response.get('body').read())
answer = response_body["content"][0]["text"]
print(answer)

変換後データ

,Unnamed: 0,Unnamed: 1,Unnamed: 2,Unnamed: 3,Unnamed: 4,Unnamed: 5,Unnamed: 6,Unnamed: 7,Unnamed: 8,Unnamed: 9,Unnamed: 10,Unnamed: 11,Unnamed: 12,Unnamed: 13,Unnamed: 14,Unnamed: 15,Unnamed: 16
0,,複数データ,,,,,,,,,,,,結合表,,,
1,,,,企業数,,売上金額(費用総額),,,,,,,,東京都,,,千代田区
2,,総計,,1110,,100(1000),,,,,,,,,,,中央区
3,,A,,111,,10(100),,,,,,,,,,,港区
4,,B,,222,,20(200),,,,,,,,,,,三鷹市
5,,C,,333,,30(300),,,,,,,,,,,青梅市
6,,D,,444,,40(400),,,,,,,,,,,立川市
7,,,,,,,,,,,,,,千葉県,,,千葉市
8,,注釈込み表,,,,,,,,,,,,,,,柏市
9,,,,商品A,,,商品B,,,,,,,,,,勝浦市
10,,1月,,500 (a),,,800,,,,,,,,,,船橋市
11,,2月,,600,,,900 (b),,,,,,,,,,
12,,3月,,700,,,1000,,,,,,,,,,
13,,(a) … 冷凍品,,,,,,,,,,,,,,,
14,,(b) … 調理済品,,,,,,,,,,,,,,,

テキスト抽出が読めたんだからまあきっと読めるんだろうなという気がしますね。

実行結果

  • 複数データ表のCの費用総額は?
    • 複数データ表のCの費用総額(売上金額)は30(300)です。
  • 注釈込み表において冷凍品は何月のどの商品か?
    • 具体的には、13行目の"(a) … 冷凍品"という注釈が、10行目の"1月"の"500"という数値を修飾しているため、1月の商品Aが冷凍品を表し
      ていると解釈できます。
  • 結合表の東京都の市区町村を全て出力してください
    • 東京都の市区町村は以下の通りです。- 千代田区- 中央区- 港区- 三鷹市- 青梅市- 立川市
正誤
1問目
2問目
3問目

概ね正しく読めました。1問目は情報は取れていますが意図が汲み取れなかったようです。

JSONに変換したデータ

同じくpandasでJSONに変換します。

import pandas as pd
import json
import boto3

df = pd.read_excel("./総務省.xlsx")
df.to_json("output.json")

prompt = "ここに質問を書く"

with open("output.json", "r", encoding="utf-8") as file:
    text = file.read()

system_prompt="次のdocumentにはExcelから抽出したJSONデータが格納されています。\n<document>\n" + text + "\n</document>\n質問に回答してください。"

content = {"type": "text", "text": prompt}
bedrock = boto3.client('bedrock-runtime')
body = json.dumps(
    {
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 4000,
        "temperature": 0.0,
        "system": system_prompt,
        "messages": [{"role": "user", "content": [content]}]
    }   
)
response = bedrock.invoke_model(body=body, modelId='anthropic.claude-3-sonnet-20240229-v1:0')
response_body = json.loads(response.get('body').read())
answer = response_body["content"][0]["text"]
print(answer)

変換後データ

{
    "Unnamed: 0": {
        "0": null,
        "1": null,
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": null,
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 1": {
        "0": "複数データ",
        "1": null,
        "2": "総計",
        "3": "A",
        "4": "B",
        "5": "C",
        "6": "D",
        "7": null,
        "8": "注釈込み表",
        "9": null,
        "10": "1月",
        "11": "2月",
        "12": "3月",
        "13": "(a) … 冷凍品",
        "14": "(b) … 調理済品"
    },
    "Unnamed: 2": {
        "0": null,
        "1": null,
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": null,
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 3": {
        "0": null,
        "1": "企業数",
        "2": 1110,
        "3": 111,
        "4": 222,
        "5": 333,
        "6": 444,
        "7": null,
        "8": null,
        "9": "商品A",
        "10": "500 (a)",
        "11": "600",
        "12": "700",
        "13": null,
        "14": null
    },
    "Unnamed: 4": {
        "0": null,
        "1": null,
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": null,
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 5": {
        "0": null,
        "1": "売上金額(費用総額)",
        "2": "100(1000)",
        "3": "10(100)",
        "4": "20(200)",
        "5": "30(300)",
        "6": "40(400)",
        "7": null,
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 6": {
        "0": null,
        "1": null,
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": null,
        "8": null,
        "9": "商品B",
        "10": "800",
        "11": "900 (b)",
        "12": "1000",
        "13": null,
        "14": null
    },
    "Unnamed: 7": {
        "0": null,
        "1": null,
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": null,
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 8": {
        "0": null,
        "1": null,
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": null,
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 9": {
        "0": null,
        "1": null,
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": null,
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 10": {
        "0": null,
        "1": null,
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": null,
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 11": {
        "0": null,
        "1": null,
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": null,
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 12": {
        "0": null,
        "1": null,
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": null,
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 13": {
        "0": "結合表",
        "1": "東京都",
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": "千葉県",
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 14": {
        "0": null,
        "1": null,
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": null,
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 15": {
        "0": null,
        "1": null,
        "2": null,
        "3": null,
        "4": null,
        "5": null,
        "6": null,
        "7": null,
        "8": null,
        "9": null,
        "10": null,
        "11": null,
        "12": null,
        "13": null,
        "14": null
    },
    "Unnamed: 16": {
        "0": null,
        "1": "千代田区",
        "2": "中央区",
        "3": "港区",
        "4": "三鷹市",
        "5": "青梅市",
        "6": "立川市",
        "7": "千葉市",
        "8": "柏市",
        "9": "勝浦市",
        "10": "船橋市",
        "11": null,
        "12": null,
        "13": null,
        "14": null
    }
}

変換の仕方によると思いますが、CSVの逆で列方向の情報が集まっているので列方向の同一性は間違いにくそうです。

実行結果

  • 複数データ表のCの費用総額は?
    • 複数データ表のC列の"売上金額(費用総額)"は"30(300)"となっています。つまり、Cの費用総額は300です。
  • 注釈込み表において冷凍品は何月のどの商品か?
    • 注釈込み表の中で、冷凍品に関する記述は以下のとおりです。"Unnamed: 1"列の13行目に "(a) … 冷凍品" と記載されています。そして、"Unnamed: 6"列の10行目に "商品A" が500、11行目に "(b) … 調理済品" と商品Bが900と記載されています。つまり、冷凍品は1月の商品Aを指していると考えられます。
  • 結合表の東京都の市区町村を全て出力してください
    結合表の東京都の市区町村は以下の通りです。- 千代田区- 中央区- 港区
正誤
1問目
2問目
3問目 ×

おー、あからさまに間違えました(不足している)。
列方向にまとまっており1問目は意図を正しく読めた反面、行方向の区切りが上手く取れず3問目は間違えました。

Markdownに変換したデータ

同じくpandasでMarkdownに変換します。

import pandas as pd
import json
import boto3

df = pd.read_excel("./総務省.xlsx")
df.to_markdown("output.md")

prompt = "ここに質問を入れる"

with open("output.md", "r", encoding="utf-8") as file:
    text = file.read()

system_prompt="次のdocumentにはExcelから抽出したMarkdownデータが格納されています。\n<document>\n" + text + "\n</document>\n質問に回答してください。"

content = {"type": "text", "text": prompt}
bedrock = boto3.client('bedrock-runtime')
body = json.dumps(
    {
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 4000,
        "temperature": 0.0,
        "system": system_prompt,
        "messages": [{"role": "user", "content": [content]}]
    }   
)
response = bedrock.invoke_model(body=body, modelId='anthropic.claude-3-sonnet-20240229-v1:0')
response_body = json.loads(response.get('body').read())
answer = response_body["content"][0]["text"]
print(answer)

変換後データ

Unnamed: 0 Unnamed: 1 Unnamed: 2 Unnamed: 3 Unnamed: 4 Unnamed: 5 Unnamed: 6 Unnamed: 7 Unnamed: 8 Unnamed: 9 Unnamed: 10 Unnamed: 11 Unnamed: 12 Unnamed: 13 Unnamed: 14 Unnamed: 15 Unnamed: 16
0 nan 複数データ nan nan nan nan nan nan nan nan nan nan nan 結合表 nan nan nan
1 nan nan nan 企業数 nan 売上金額(費用総額) nan nan nan nan nan nan nan 東京都 nan nan 千代田区
2 nan 総計 nan 1110 nan 100(1000) nan nan nan nan nan nan nan nan nan nan 中央区
3 nan A nan 111 nan 10(100) nan nan nan nan nan nan nan nan nan nan 港区
4 nan B nan 222 nan 20(200) nan nan nan nan nan nan nan nan nan nan 三鷹市
5 nan C nan 333 nan 30(300) nan nan nan nan nan nan nan nan nan nan 青梅市
6 nan D nan 444 nan 40(400) nan nan nan nan nan nan nan nan nan nan 立川市
7 nan nan nan nan nan nan nan nan nan nan nan nan nan 千葉県 nan nan 千葉市
8 nan 注釈込み表 nan nan nan nan nan nan nan nan nan nan nan nan nan nan 柏市
9 nan nan nan 商品A nan nan 商品B nan nan nan nan nan nan nan nan nan 勝浦市
10 nan 1月 nan 500 (a) nan nan 800 nan nan nan nan nan nan nan nan nan 船橋市
11 nan 2月 nan 600 nan nan 900 (b) nan nan nan nan nan nan nan nan nan nan
12 nan 3月 nan 700 nan nan 1000 nan nan nan nan nan nan nan nan nan nan
13 nan (a) … 冷凍品 nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan
14 nan (b) … 調理済品 nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan

実行結果

  • 複数データ表のCの費用総額は?
    • 複数データ表のCの費用総額は300です。
  • 注釈込み表において冷凍品は何月のどの商品か?
    • 注釈込み表において、冷凍品は1月の商品Aを指しています。データを見ると、(略)と記載があり、(a)が冷凍品を示す注釈です。そして10行目に、(略)
      とあり、1月の商品Aの数値が500 (a)と注釈付きで記載されています。したがって、注釈込み表において冷凍品は1月の商品Aを指しています。
  • 結合表の東京都の市区町村を全て出力してください
    • 結合表の東京都の市区町村は以下の通りです。- 千代田区- 中央区- 港区- 三鷹市- 青梅市- 立川市
正誤
1問目
2問目
3問目

全部正解しました。

まとめ

結論としてはそこそこちゃんと読めてます。

テキスト CSV JSON Markdown
1問目
2問目
3問目 ×

かなり小さいサンプルですが、行方向のデータを連続させているテキスト・CSVは列方向の情報を読み損ねる(1問目)、列方向のデータを連続させているJSONは行方向の情報を読み損ねる(2問目)、Markdownは正しく読める、という結果になりました。

  • テキスト抽出
    • 今回の例では概ね正しく動いたが、行列の情報が失われ過ぎているので難しそう
  • CSV変換
    • 行列の情報は失われていないので、Excel方眼紙がどういうものかを教えてあげれば大丈夫そうな気がする
  • JSON変換
    • 行列の情報は失われていないが、実際に存在するExcel方眼紙は縦長が多いと想定されるので、データ次第ではあるが微妙かも
  • Markdown変換
    • Excel方眼紙を一番正しく再現していると言え、結果も一番優れていたが、CSVより情報量が多いわけでは無いので(むしろ余計な情報が多い)たまたまのような気もする

ヘイトを集めがちなExcel方眼紙ですが、プレーンテキストにすると構造化情報が失われがちなWordやPDFと比べて、行列位置で構造を表現できる(しがちな)Excel方眼紙はプレーンテキストにしても構造化情報を比較的保持できるのでLLMでの解析に向いているような気がしました。
(セルに塗った色で構造を表現されると難しいかも。あとオートシェイプでのお絵描きは無理)

16
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
9