しかしながら、文章形式のデータは、参照するべき情報が十分に反映されておらず、表構造のデータは項目と数値の対応が取れていませんでした。
そこで本記事では
1. Google Cloud Vision API を用いたOCRでテキスト処理+LLMで生成する2段階の手法
2. LLMのみで画像から直接情報抽出する1段階の手法
を比較し、文章形式のデータと表構造データの精度が高いLLMシステムの設計・実装を行います。
また、最終的にはGradioを使用してWebインターフェースも構築します。
今回OCRにGoogle Cloud Visionを使用した理由は、2つあります。
第1は、Google Cloudを登録すると初期に1000ユニットまで無料で利用できるためです。
多くの方に実装を試していただきたいので、なるべくAPIコストのかからないものを選択しました。
第2に、日本語の読み取り性能が高いという記事が数多く出ていたからです。
参考元1 : https://data.gunosy.io/entry/ocr-tesseract-deeplearning
参考元2 : https://dev.classmethod.jp/articles/using-document-ai-and-google-cloud-vision-api-for-ocr/
設計フローはほとんど同じで、画像からのテキスト化をGoogle Cloud Visionを使用するか、GPT-4o-miniを使用するかの違いです。
OSバージョン
開発環境
言語
主要ライブラリ
PDF文書
実際に企業特化LLMシステムを作る上で、50ページを超えるようなPDFを扱う場合を想定して、76ページのPDFを採用しています。
設計フローをもとにコードを肉付けしていきます。
import os
import io
import gradio as gr
from google.cloud import vision
from google.oauth2 import service_account
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.vectorstores import Chroma
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
from langchain.schema.runnable import RunnablePassthrough
from langchain.prompts import PromptTemplate
from langchain.retrievers import BM25Retriever, EnsembleRetriever
from pdf2image import convert_from_path
service_account
: Google Cloud サービスの認証に必要なクレデンシャルを管理RecursiveCharacterTextSplitter
: 長いテキストを小さなチャンクに分割ConversationalRetrievalChain
: 会話型の検索と質問応答を組み合わせたチェーンConversationBufferMemory
: 会話の履歴を保持するためのメモリRunnablePassthrough
: LangChainの実行可能なコンポーネントを作成PromptTemplate
: LLMへの入力プロンプトを定義するためのテンプレートBM25Retriever, EnsembleRetriever
: BM25アルゴリズムを使用したキーワード検索と、複数の検索手法を組み合わせたアンサンブル検索(=ハイブリッド検索)convert_from_path
: PDFファイルを画像に変換するためのライブラリGoogle Cloud Vision APIにアクセスするためのクライアントを作成します。認証情報のパスを受け取り、環境変数を設定した後、認証済みのクライアントを返します。
def create_vision_client(credentials_path):
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = credentials_path
return vision.ImageAnnotatorClient(credentials=service_account.Credentials.from_service_account_file(credentials_path))
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = credentials_path
参考元 : API Keyを発行する手順 for Google Cloud API
https://zenn.dev/tmitsuoka0423/articles/get-gcp-api-key
credentials = service_account.Credentials.from_service_account_file(credentials_path)
return vision.ImageAnnotatorClient()
PDFファイルからテキストを抽出して、検証用にPDFのページ番号もテキストに追加できる関数を作成します。
def extract_text_from_pdf(vision_client, pdf_path):
text = ""
for page_num, image in enumerate(convert_from_path(pdf_path), start=1):
image_bytes = io.BytesIO()
image.save(image_bytes, format='PNG')
try:
response = vision_client.document_text_detection(image=vision.Image(content=image_bytes.getvalue()))
text += f"--- ページ {page_num} ---\n{response.full_text_annotation.text}\n\n"
except Exception as e:
print(f"OCRプロセスでエラーが発生しました (ページ {page_num}): {str(e)}")
return text
def extract_text_from_pdf(vision_client, pdf_path)
は、Google Cloud Vison APIのクライアントオブジェクトであるvision_client
とPDFファイルへのパスpdf_path
を2つの引数とします。text
= ""で空の文字列text
を初期化して、ここの各ページのテキストを追加していきます。convert_from_path(pdf_path)
でPDFの各ページを画像に変換してから、ページ番号が1から始まるので、画像とページ数が対応するように、start = 1
とします。io.BytesIO()
でメモリ内にバイトストリームを作成して、image.save()
メソッドで画像をPNG形式でバイトストリームに保存します。try-except
ブロックでエラーハンドリングを行いつつ、vision_client.document_text_detection()
メソッドで画像からテキストを抽出します。text
に追加します。PDFファイルからテキストを抽出し、質問応答(QA)システムを初期化する関数を作ります。
def initialize_qa_system(pdf_path, vision_client):
text = extract_text_from_pdf(vision_client, pdf_path)
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=250, separators=["\n\n", "\n", "| ", " | ", "-+-", " ", ""])
split_docs = text_splitter.create_documents([text])
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
vectorstore = Chroma.from_documents(split_docs, embedding=embeddings, persist_directory=".")
bm25_retriever = BM25Retriever.from_documents(split_docs)
bm25_retriever.k = 3
ensemble_retriever = EnsembleRetriever(retrievers=[vectorstore.as_retriever(), bm25_retriever], weights=[0.8, 0.2])
llm = RunnablePassthrough() | ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True, output_key="answer")
prompt = PromptTemplate(
template="""あなたは与えられた文書の内容に基づいて質問に答える専門家です。
以下の文脈情報と質問に対して、文書の情報のみを使用して詳細かつ正確に回答してください。
文書に関連する情報がない場合は、「申し訳ありませんが、この文書にはその情報が含まれていません」と答えてください。
表や図表の情報が含まれている場合は、その構造や関係性を考慮して回答してください。
文脈情報:
{context}
質問: {question}
回答:""",
input_variables=["context", "question"]
)
return ensemble_retriever, llm, memory, prompt, vectorstore
extract_text_from_pdf()
関数でPDFからテキストを抽出します。RecursiveCharacterTextSplitter
クラスを使用してテキストを小さなチャンクに分割します。separators
は、自分が読み込むPDFが何の記号を用いてテキストの意味を分断しているのかを考えて指定すると良いと思います。create_documents()
メソッドで分割されたテキストからドキュメントのリストを作成します。OpenAIEmbeding
クラスでモデルを初期化するChroma.from_documents()
メソッドでChromaベクトルストアに保存するBM25Retriever
を使用してキーワード検索システムを初期化するbm25_retriever.k = 3
でBM25検索をするときは、上位3件の結果を取得するように設定するEnsembleRetriever
を使用して、ベクトル検索とBM25検索(キーワード検索)を組み合わせたハイブリッド検索システムを作成weights=[0.8, 0.2]
: ベクトル検索に80%、BM25検索に20%の重みを付与。以下の参照元より、この割合が最も精度が高いようなので採用した。引用元:ハイブリッド検索の融合方法の検討と Azure AI Search への実装
https://qiita.com/nohanaga/items/91cc6fb0eff06af94a19
RunnablePassthrough()
と ChatOpenAI
を組み合わせて言語モデルを設定。ConversationBufferMemory
を使用して会話履歴を保持するメモリを初期化。def answer_question(question, k_val, retriever, llm, memory, prompt, vectorstore):
try:
retriever.retrievers[0].search_kwargs["k"] = k_val
retriever.retrievers[1].k = k_val
qa = ConversationalRetrievalChain.from_llm(
llm=llm, retriever=retriever, memory=memory, return_source_documents=True,
verbose=True, combine_docs_chain_kwargs={"prompt": prompt}, chain_type="stuff",
return_generated_question=False, output_key="answer"
)
result = qa.invoke({"question": question, "chat_history": []})
vector_results = vectorstore.similarity_search_with_relevance_scores(question, k=len(result["source_documents"]))
doc_score_map = {doc.page_content: score for doc, score in vector_results}
sources = [f"ページ: {doc.metadata.get('page_number', 'N/A')}\n類似度: {doc_score_map.get(doc.page_content, 0):.4f}\nテキスト:\n{doc.page_content}\n\n"
for doc in result["source_documents"][:k_val]]
return result["answer"], "\n".join(sources)
except Exception as e:
return f"エラーが発生しました: {str(e)}", ""
ConversationalRetrievalChain.from_llm()
メソッドを使用して、会話型のreturn_source_documents = True
に設定chain_type="stuff"
を採用qa.invoke()
メソッドを呼び起こして、質問に対する回答を生成します。chat_history
を使用しています。similarity_search_with_relevance_scores()
メソッドを使用して、質問に:.4f
フォーマット指定子を使用することで、類似度スコアを小数点以下4桁までの表示にします。Gradioライブラリを使用して、質問応答システムのためのウェブベースのユーザーインターフェースを実行可能な関数を作成します。
def create_gradio_interface(qa_system):
with gr.Blocks(title="化学薬品等取扱いの手引き Q&Aシステム") as demo:
gr.Markdown("# 化学薬品等取扱いの手引き Q&Aシステム")
gr.Markdown("化学薬品等取扱いの手引きに関する質問に答えます。")
chatbot = gr.Chatbot(label="会話履歴")
msg = gr.Textbox(label="質問", placeholder="ここに質問を入力してください...", lines=3)
k_val_slider = gr.Slider(minimum=1, maximum=10, value=3, step=1, label="抽出数")
with gr.Row():
submit_button = gr.Button("送信")
clear_button = gr.Button("クリア")
sources_output = gr.Textbox(label="参照元ドキュメント", lines=20, visible=True)
def user(user_message, chat_history, k_val):
return "", chat_history + [[user_message, None]]
def bot(chat_history, k_val):
user_message = chat_history[-1][0]
answer, sources = qa_system(user_message, k_val)
chat_history[-1][1] = answer
return chat_history, sources
def clear_chat():
return None, 3, ""
submit_button.click(
user,
inputs=[msg, chatbot, k_val_slider],
outputs=[msg, chatbot],
queue=False
).then(
bot,
inputs=[chatbot, k_val_slider],
outputs=[chatbot, sources_output]
)
clear_button.click(
clear_chat,
outputs=[chatbot, k_val_slider, sources_output]
)
return demo
gr.Blocks()
を使用すると、Gradio UIの骨組みを作成することができます。title
でWebページのタイトルを、'with'ステートメントを使うと、このブロック内のすべてのコードが demo オブジェクトと同期できます。gr.Row()
で要素を横並びに配置できます。gr.Textbox()
で質問入力用のテキストボックスを作成しますgr.Slider()
で調整可能です。gr.button()
でボタンを作成できます。簡単すぎて笑ってしまいました。UIのコーディングを一度もしたことないので、よくわかりませんが、このGradioは簡単すぎて良いですね。笑gr.Textbox
で回答と参照元ドキュメントのテキストボックスが表示できます。submit_button.click()
で送信ボタンをクリックしたときの動作が定義できます。qa_system
関数を呼び出して、入力と出力を指定できます。clear_button.click()
でクリアボタンがクリックしたとき、入力欄や表示欄がクリアされ、スライダーが初期位置に戻る動作が定義できます。多少ややこしいのでコードを抜き出して解説します。最後に、アプリケーション全体のフローを制御/実行するメイン関数を定義します。
def main():
credentials_path = "/content/drive/MyDrive/third-strategy-435210-h5-192ddf08752c.json"
pdf_path = "/content/drive/MyDrive/化学薬品等取扱いの手引き.pdf"
vision_client = create_vision_client(credentials_path)
retriever, llm, memory, prompt, vectorstore = initialize_qa_system(pdf_path, vision_client)
qa_system = lambda question, k_val: answer_question(question, k_val, retriever, llm, memory, prompt, vectorstore)
demo = create_gradio_interface(qa_system)
demo.launch()
credentials_path
で、Google Cloud Vision APIの認証情報(サービスアカウントキー)のJSONファイルへのパスを指定します。pdf_path
で質問応答の対象となるPDFファイル(今回は「化学薬品等取扱いの手引き」)へのパスを指定します。create_vision_client()
関数を呼び出して、Google Cloud Vision APIのクライアントを作成する。これは、PDFファイルからテキストを抽出するときに使います。initialize_qa_system()
関数を呼び出して、QAシステムの主要コンポーネントを初期化します。qa_system
関数を定義します。この関数は質問(question
)と抽出数(k_val
)を引数として受け取って、answer_question()
関数を呼び出します。create_gradio_interface()
関数を呼び出して、ウェブベースのユーザーインターフェースを作成し、demo
変数に格納します。launch()
メソッドでGradioアプリケーションを起動する。半分くらいはOCR+LLMと内容が同じなので、差分のみ説明します!
import gradio as gr
import base64
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.vectorstores import Chroma
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
from langchain.schema.runnable import RunnablePassthrough
from langchain.prompts import PromptTemplate
from langchain.retrievers import BM25Retriever, EnsembleRetriever
from pdf2image import convert_from_path
import requests
import os
import time
import random
今回、APIコールで何回も失敗してしまいました。OpenAIのAPIはレート制限を設けているので、短時間に送るリクエストの制限しなければならない or レート制限にかかったら場合待機時間を増やしてレート上限に達しないように設定しなければなりません。
def api_call_with_retry(func, max_retries=5):
for i in range(max_retries):
try:
return func()
except Exception as e:
if 'rate_limit_exceeded' in str(e):
wait_time = (4 ** i) + random.random()
print(f"Rate limit exceeded. Waiting for {wait_time:.4f} seconds.")
time.sleep(wait_time)
else:
raise
raise Exception("Max retries exceeded")
レート制限に達した場合のために以下を設定します。
4**i
により、レート制限エラーが起きた後の最試行回数が増えるごとに、待機時間が指数関数的に増加するように設定することで、連続的なレートエラーを防ぎます。引用元 : OpenAI Cookbook examples : API 使用方法 : レート制限の操作 (翻訳/解説)
https://tensorflow.classcat.com/2023/08/09/openai-cookbook-examples-how-to-handle-rate-limits/
time.sleep()
で指定時間だけ処理を一時停止させます。OCRではなく、画像の内容を理解/解釈してテキスト化できることを期待して、gpt-4o-miniを使用し、画像からテキストを抽出する関数を実装します。
def extract_text_from_image(image_path):
with open(image_path, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode('utf-8')
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}"
}
payload = {
"model": "gpt-4o-mini",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "この画像から全てのテキストを抽出してください。以下の点に注意してください:\n"
"1. 表形式のデータは、その構造を維持してください。各セルの内容を正確に抽出し、セルの区切りには '|' を使用してください。\n"
"2. 図や図表に含まれるテキストは、その配置や関係性を考慮して抽出してください。\n"
"3. 段落や文章の構造を維持し、改行や段落の区切りを適切に表現してください。\n"
"4. 箇条書きやリストの形式は保持してください。\n"
"5. ヘッダー、フッター、ページ番号なども含めて抽出してください。\n"
"6. テキストの前後関係や文脈を維持することに注意してください。\n"
"出力は純粋なテキスト形式で、マークダウンや特殊な書式は使用しないでください。"
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}"
}
}
]
}
],
"max_tokens": 8192
}
def make_api_call():
response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
response_json = response.json()
print("API Response:", response_json) # レスポンスの内容を出力
if 'error' in response_json:
raise Exception(f"API Error: {response_json['error']}")
return response_json['choices'][0]['message']['content']
return api_call_with_retry(make_api_call)
with open(image_path, "rb") as image_file: base64_image = base64.b64encode(image_file.read()).decode('utf-8')
headers = { "Content-Type": "application/json", "Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}"}
content-Type
: JSONデータの送信Authorization
: OpenAI APIキーを使用した認証role
: ユーザーからの入力content
: テキストと画像を含む配列PDFファイルからテキストを抽出し、質問応答(QA)システムを初期化する関数を作ります。
def initialize_qa_system():
# PDFをイメージに変換
images = convert_from_path("/content/drive/MyDrive/化学薬品等取扱いの手引き.pdf")
# イメージからテキストを抽出
text = ""
for i, image in enumerate(images):
image_path = f"temp_image_{i}.jpg"
image.save(image_path)
text += extract_text_from_image(image_path)
os.remove(image_path)
time.sleep(2) # APIコールの間に2秒の待機時間を追加
# テキストを分割
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1024,
chunk_overlap=256,
length_function=len,
separators=["\n\n", "\n", " ", ""]
)
split_docs = text_splitter.create_documents([text])
# OpenAIのチャットモデルを初期化
base_llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
llm = RunnablePassthrough() | base_llm
# OpenAIの埋め込みモデルを初期化
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# Chromaベクトルストアを作成
vectorstore = Chroma.from_documents(split_docs, embedding=embeddings, persist_directory=".")
# BM25Retrieverを初期化
bm25_retriever = BM25Retriever.from_documents(split_docs)
bm25_retriever.k = 3
# EnsembleRetrieverを作成
ensemble_retriever = EnsembleRetriever(
retrievers=[vectorstore.as_retriever(), bm25_retriever],
weights=[0.8, 0.2]
)
# メモリを初期化
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True,
output_key="answer"
)
# プロンプトテンプレート
template = """あなたは与えられた文書の内容に基づいて質問に答える専門家です。
以下の文脈情報と質問に対して、文書の情報のみを使用して詳細かつ正確に回答してください。
文書に関連する情報がない場合は、「申し訳ありませんが、この文書にはその情報が含まれていません」と答えてください。
文脈情報:
{context}
質問: {question}
回答:"""
PROMPT = PromptTemplate(template=template, input_variables=["context", "question"])
return ensemble_retriever, llm, memory, PROMPT, vectorstore
time.sleep(2)
: PDFを画像に変換して、テキスト化する際にもAPIのレート制限が数回かかったので、抽出処理の間に2秒の待機時間を設定しています。初期化されたQAシステムを使用して、ユーザーからの質問に対して応答を生成する機能を実装します。
# QAシステムを初期化
retriever, llm, memory, PROMPT, vectorstore = initialize_qa_system()
def answer_question(question, k_val):
try:
# 抽出数を動的に設定
retriever.retrievers[0].search_kwargs["k"] = k_val
retriever.retrievers[1].k = k_val
qa = ConversationalRetrievalChain.from_llm(
llm=llm,
retriever=retriever,
memory=memory,
return_source_documents=True,
verbose=True,
combine_docs_chain_kwargs={"prompt": PROMPT},
chain_type="stuff",
return_generated_question=False,
output_key="answer"
)
result = qa.invoke({"question": question, "chat_history": []})
answer = result["answer"]
source_documents = result["source_documents"]
# ベクトル検索を使用してスコアを取得
vector_results = vectorstore.similarity_search_with_relevance_scores(question, k=len(source_documents))
# ドキュメントとスコアのマッピングを作成
doc_score_map = {doc.page_content: score for doc, score in vector_results}
# 参照元ドキュメントのリストを作成
sources = []
for i, doc in enumerate(source_documents[:k_val]):
score = doc_score_map.get(doc.page_content, 0) # スコアが見つからない場合は0とする
source_info = f"ページ: {doc.metadata.get('page_number', 'N/A')}\n"
source_info += f"類似度: {score:.4f}\n"
source_info += f"チャンク(取得テキスト): {doc.page_content}\n\n"
sources.append(source_info)
return answer, "\n".join(sources)
except Exception as e:
return f"エラーが発生しました: {str(e)}", ""
OCR+LLMのコード説明を参照してください。
with gr.Blocks(title="化学薬品等取扱いの手引き Q&Aシステム") as demo:
gr.Markdown("# 化学薬品等取扱いの手引き Q&Aシステム")
gr.Markdown("化学薬品等取扱いの手引きに関する質問に答えます。")
with gr.Row():
question_input = gr.Textbox(label="質問", lines=3, placeholder="ここに質問を入力してください...")
k_val_slider = gr.Slider(minimum=1, maximum=10, value=3, step=1, label="抽出数")
with gr.Row():
submit_button = gr.Button("Submit")
clear_button = gr.Button("Clear")
answer_output = gr.Textbox(label="回答", lines=10)
sources_output = gr.Textbox(label="参照元ドキュメント", lines=20)
def clear_inputs():
return "", 3, "", ""
submit_button.click(
answer_question,
inputs=[question_input, k_val_slider],
outputs=[answer_output, sources_output]
)
clear_button.click(
clear_inputs,
outputs=[question_input, k_val_slider, answer_output, sources_output]
)
# Gradioアプリの起動
demo.launch()
OCR+LLMのコード説明を参照してください。
Gradioアプリが起動すると、Google colab上で下図のようなUIが表示されます。
今回は検証として、ユーザーの質問に対しての回答箇所が、「1.文章, 2.特定のキーワード, 3.表データのキーワード, 4.表データの数値」の4つパターン調査して、OCR+LLMとLLMのみの場合の精度(出力の確からしさ)を測っていきたいと思います。
詳細の結果の前に、1~4の結果をまとめておきます。
結論としては、LLMはPDFの表データの構造化はできますが、漢字の誤認識が非常に多かったため、精度はかなり低い結果になりました。
OCR+LLMは表構造データが少し崩れていましたが、文字の認識率が見たところ100%で回答精度が高く、実用性はOCR+LLMに軍配があがりました。
PDFの検索対象 | OCR+LLM | LLMのみ |
---|---|---|
1.文章 | ⭕️ | ❌ |
2.特定のキーワード | ⭕️ | ❌ |
3.表データのキーワード | ⭕️ | ❌ |
4.表データの数値 | ⭕️ | ❌ |
■ 検索クエリ
劇薬や毒物を扱うときは、どのように保存すればいいですか
劇薬や毒物を扱う際の保存方法は以下の通りです。
保管場所 : 劇薬や毒物は専用の金属製保管庫で保管し、常時施錠する必要があります。
要管理物質 : 要管理物質は施錠可能な保管場所で保管します。
保管庫の固定 : 保管庫は床等に固定し、安定した状態で保管します。
表示 : 保管庫には「医薬用外毒物」や「医薬用外劇物」の表示を行います。
保管庫の点検 : 保管庫に腐食、亀裂、破損等がないかを定期的に確認します。
劇薬や毒物を扱う際の保存方法は以下の通りです。
3-5.毒物・劇物・要管理物質の取扱い
毒物及び劇物取締法により毒物又は劇物として指定されている化学物質 (p.58)については、以
下のように適切な取扱いをしてください。また、特定毒物については、 特定毒物研究者の許可を受け
た者でなければ使用できません。要管理物質(p.59) は、毒劇物に準じた管理をお願いします。
■ 容器について
• 環境安全管理室発行のバーコードラベルを貼る
• 飲食物用に使用される容器に移し替えてはならない
別な容器に移し替えた場合はその容器にも 医薬用外毒物」(赤地に白字で)、「医薬用外
劇物」(白地に赤字で)と表示する
■ 保管について
毒劇物は専用金属製保管庫で、常時施錠し保管する
要管理物質は施錠可能な保管場所での保管する
保管庫は、床等に固定する
保管庫に「医薬用外毒物 」「 医薬用外劇物」の表示をする
薬品棚には転落防止用の措置を講じる
保管庫に腐食・亀裂・破損等がないか定期的に確認する
3-5. 毒物・劇物・要管理物質の取扱い
毒物及び劇物取締法による毒物は劇物として指定されている化学物質 (p.58) については、以下のように適切に取扱ってください。また、特定毒物については、特定毒物研究者の許可を受けた者でなければ使用できません。要管理物質 (p.59) は、毒劇物に準じて管理をおこないます。
■ 容器について
- 環境安全管理室発行のバーコードラベルを貼る
- 飲食物用に使用される容器に移し替えてはならない
- 別な容器に移し替えた場合はその容器にも「医薬用外毒物」(赤地に白字で)、「劇物」と表示する
■ 保管について
- 毒物は専用の痛床及び管理室で、常時施錠し保管する
- 要管理物質は施錠可能な保管場所での保管する
- 保管場所は、常に定定する
- 薬品棚には管理者の指示を記し
- 保管庫に棚番・電源・破損等がないか定期的に確認する
結果としては、OCR+LLMが正解、LLMのみが50%程度正解になります。
ここの50%の差は、表示 : 保管庫には「医薬用外毒物」や「医薬用外劇物」の表示があるかないかに加え、PDF内の文字をそのままテキスト化できているかどうかです。
PDF内の該当箇所には、容器/保管についての2箇所で赤文字で「"医薬用外毒物"と”医薬用外劇物”の表示が必要」と書かれているので、これを書いていないのは割と致命的だと思うので、評価を下げました。
また、LLMの参考箇所とPDF内の該当箇所を比較して見てみると、OCR+LLMでは完全にテキスト化できている一方で、LLMのみでは、保管のところで1行テキスト化できていない部分があったり、誤字がかなり目立ちます。
PDFの該当箇所 | LLMが参考にした文脈情報(OCR+LLM) | LLMが参考にした文脈情報(LLMのみ) |
---|---|---|
保管庫は、床等に固定する | 保管庫は、床等に固定する | 保管場所は、常に定定する |
保管庫に「医薬用外毒物」「医薬用外劇物」の表示をする | 保管庫に「医薬用外毒物」「医薬用外劇物」の表示をする | 記載なし |
保管庫に腐食・亀裂・破損等がないか定期的に確認する | 保管庫に腐食・亀裂・破損等がないか定期的に確認する | 保管庫に棚番・電源・破損等がないか定期的に確認する |
上記の結果を踏まえると、テキストを文字通りに書き起こせるOCR+LLMの方が優秀であると判断できます。おそらく、gpt-4o-miniで画像処理を行うと、認識した文字を文脈に沿って最適だと判断した文字に直してしまうのかな?と思います。
7.1の実験は、頻出するワードに対して検索をかけました。この7.2ではこの文章でおそらく1回しか出現しない"エネルギー分散型蛍光X線分析装置"について検索してみたいと思います。
エネルギー分散型蛍光X線分析装置について知ってることを教えてください
エネルギー分散型蛍光X線分析装置(島津製作所: EDX-8000)の用途は、固体・液体試料中の元素の定性及び半定量分析です。以下はその仕様です:
エネルギー分散型紫外線X線分析装置(島津製作所:EDX-8000)
用途:固体・液体試料中の元素の定性及び定量分析
【仕様】
検出器性能:6C~92U
X線高電圧:Rhターゲット(4~50 kV、1~1000 μA)
照射時間:1.3,5,10 mm φ
検出器:SDD(シリコンディスリフト)検出器
検出限度:数百 ppm ~ %(測定元素、分析条件により異なる)
定量限度:数%(測定元素、分析条件により異なる)
・エネルギー分散型蛍光X線分析装置 (島津製作所:EDX-8000)
用途: 固体・液体試料中の元素の定性及び半定量分析
[仕様]
検出可能元素:6C~92U
X線管球 : Rh ターゲット(4~50kV、 1~1000 μA)
照射面積:1,3,5,10mmΦ
検出器 SDD(シリコンドリフト)検出器
検出下限:数百 ppm~数%(測定元素、分析条件により異なる)
定量範囲:数%~ (測定元素、 分析条件により異なる)
- エネルギー分散型紫外線X線分析装置(島津製作所:EDX-8000)
用途:固体・液体試料中の元素の定性及び定量分析
【仕様】
検出器性能:6C~92U
X線高電圧:Rhターゲット(4~50 kV、1~1000 μA)
照射時間:1.3,5,10 mm φ
検出器:SDD(シリコンディスリフト)検出器
検出限度:数百 ppm ~ %(測定元素、分析条件により異なる)
定量限度:数%(測定元素、分析条件により異なる)
OCR+LLMの方が圧倒的に正確であるという結果になりました。どちらにも数値ミスはほとんどありませんでしたが、LLMのみの方にやはり説明が足りない部分と誤字が沢山ありました。1行の文章に1ミスはあるので、結構実務利用には辛いですね。ここまでで、社内文書や契約書、名刺など誤字が絶対にあってはいけないものにはOCRが必須であることがわかりました。
PDFの該当箇所 | LLMが参考にした文脈情報(OCR+LLM) | LLMが参考にした文脈情報(LLMのみ) |
---|---|---|
エネルギー分散型蛍光X線分析装置(島津製作所: EDX-8000)用途 : 固体・液体試料中の元素の定性及び半定量分析 | エネルギー分散型蛍光X線分析装置(島津製作所: EDX-8000)の用途は、固体・液体試料中の元素の定性及び半定量分析 | エネルギー分散型紫外線X線分析装置(島津製作所:EDX-8000)用途:固体・液体試料中の元素の定性及び定量分析 |
検出可能元素:6C~92U | 検出可能元素 : 6C~92U | 検出器性能:6C~92U |
X線管球 : Rhターゲット | X線管球 : Rhターゲット | X線高電圧:Rhターゲット |
照射面積:1,3,5,10mmφ | 照射面積:1,3,5,10mmΦ | 照射時間:1.3,5,10 mm φ |
検出器 SDD(シリコンドリフト)検出器 | 検出器 SDD(シリコンドリフト)検出器 | 検出器:SDD(シリコンディスリフト)検出器 |
検出下限:数百 ppm~数%(測定元素、分析条件により異なる) | 検出下限:数百 ppm~数%(測定元素、分析条件により異なる) | 検出限度:数百 ppm ~ %(測定元素、分析条件により異なる) |
定量範囲:数%~(測定元素、分析条件により異なる) | 定量範囲:数%~(測定元素、分析条件により異なる) | 定量限度:数%(測定元素、分析条件により異なる) |
重金属系の廃液を分類するときの注意点について教えてください
重金属系の廃液を分類する際の注意点は以下の通りです:
含有する重金属の種類: 重金属系廃液には、鉄、ニッケル、銅、亜鉛、マンガン、鉛、カドミウムなどの重金属類が含まれます。
有害元素の取り扱い: ベリリウム、オスミウム、タリウム、ヒ素、セレンなどの有害元素を含む廃液は、別途回収する必要があります。
混入防止: 無機廃液には極力有機物が混入しないように注意することが求められています。
重金属系の廃液を分類する際の注意点は以下の通りです:
4-2. 実験廃液の分類
当機構では、実験廃液を以下のように分類しています。次ページの早見表も参考にしてください。
(手続きについては p.9 参照)
区分
できるだけ重金属類など他の成分が混入しないようにしてく
ださい。
無機水銀を含む水溶液 (金属水銀は除く。)
水銀系
廃液
クロム系
クロムを含む水溶液
重金属系
鉄、ニッケル、銅、亜鉛、マンガン、鉛、カドミウムなどの重
金属類を含む水溶液
※ベリリウム、オスミウム、 タリウム、ヒ素、セレンなどの有
害元素を含むものは、 別途回収してください。
表になった途端に、構造が壊れてしまうので、単語や数字の前後関係が崩れてしまい、ハルシネーションが起きる原因となりそうです。
類似度: 0.5132
チャンク(取得テキスト): 区分 | 内容・注意点など
----|----
シアン系 | シアンを含む水溶液 ※必ずアルカリ性で保存し、酸などと絶対に混合しないこと。(酸性にすると猛毒のシアン化水素ガスを発生し、大変危険です。) できるだけ重金属類など他の成分が混入しないようにしてください。
無機系廃液 | 水銀系 | 無機水銀を含む水溶液(金属水銀は除く。)
| フッ素系 | フッ素を含む水溶液
| クロム系 | クロムを含む水溶液
| 重金属系 | 鉄、ニッケル、銅、亜鉛、マンガン、鈴、カドミウムなどの重金属類を含む水溶液 ※ベリリウム、オスミウム、タリウム、セレンなどの有毒元素を含むものは、別途申請してください。
Open AIのAPIは画像処理ができるので、やはり表構造をある程度は保持できています。
見事OCR+LLMが完答しており、LLMのみも鉛を鈴と間違えたのみで他は正解していました。今回は回答箇所にベリリウムやオスミウム、タリウムなどカタカナが多かったので、理解しやすかったため、LLMのみも正答率が高いように感じられます。
うーん、GPTは漢字が苦手なのでしょうか?
再度、異なる表の検証を行います。
酸化性固体の推定数量は?
酸化性固体の推定数量は以下の通りです:
回答:
酸化性固体の推定数量は、以下の通りです。
第一類酸化性固体:
第二類酸化性固体:
| | | 4 | 鉱酸 | 第一種酸化性固体 | 1,000 kg |
| | | 5 | よう素類 | 第二種酸化性固体 | 1,000 kg |
| | | 6 | 問和酸類 | 第二種酸化性固体 | 1,000 kg |
| | | 7 | 重クロム酸類 | 第二種酸化性固体 | 1,000 kg |
| | | 8 | その他、食べて定めるもの | 第一種酸化性固体 | 1,000 kg |
| | | 9 | 1-10のいずれかを含有するもの | 第一種酸化性固体 | 1,000 kg |
| 種 別 | 性 質 | 名 名 | 性 質 | 代表的な物質 | 指定数量 |
|-------|-----------|-----------|-------------------|----------------|----------|
| 第一類 | 酸化性固体 | 1 | 硫酸塩類 | 第一種酸化性固体 | 50 kg |
| | | 2 | 海水酸化物 | 第一種酸化性固体 | 300 kg |
| | | 3 | 無機酸化物 | 第一種酸化性固体 | 300 kg |
| | | 4 | 鉱酸 | 第一種酸化性固体 | 1,000 kg |
流石に誤字が酷すぎますね。後ほど比較します。
危険物(消防法及び危険物の規制に関する政令)
類別 性質
品名
性質
代表的な物質
指定数量
1 塩素酸塩類
2 過塩素酸塩類
3 無機過酸化物
4 亜塩素酸塩類
第一種酸化性固体
50kg
5 臭素酸塩類
第一類 酸化性固体
6 硝酸塩類
第二種酸化性固体
300kg
7
よう素酸塩類
8 過マンガン酸塩類
第三種酸化性固体
1,000 kg
表がすべて縦で配列されていますが、誤字はありません。さすがGoogle Vison。
今回の酸化性固体の推定数量は?とういう質問に対しての期待する回答は、第一種、第二種、第三種の指定数量をただいく回答してもらいたいというものでした。LLMのみでは、誤字&数字ミスが多く見られました。一方でOCR+LLMでは、完答しています。OCRで表構造が崩れても文字:値のセットが近くにあれば、1対1対応をしてくれるため、今回は正解していたと考えられます。
1.Google Cloud Vision API を用いたOCRでテキスト処理+LLM で生成する2段階の手法と、2.LLMのみで画像から直接情報抽出する1段階の手法を比較し、最も精度の高いLLMシステムの検討を行いました。
結果としては、OCRでテキスト化による誤字がなかったことから、1.Google Cloud Vision API を用いたOCRでテキスト処理+LLM で生成の方が精度が高いことがわかりました。
今回は、OCRでテキスト化したものをLLMで成形せずにベクトル化しましたが、成形してからベクトル化してみると、OCRで精度高くテキスト化できている状態を活かすことができそうです。
長い記事になりましたが、ここまで読んでいただき誠にありがとうございました!!!
弊社ではLLM側のご相談や構築も対応しておりますので、お気軽にお問合せください。
また、インターンや採用の問い合わせもお待ちしています!