1 概要
2025年1月15日~2月12日で、SIGNATEのLLMコンペ (金融庁共催)第3回金融データ活用チャレンジ が開催されました。
本コンペティションでは、ESGレポートや統合報告書に関連する質問に対し、自動的かつ正確に回答できるRAG(Retrieval-Augmented Generation)システムの構築 を目指しました。提供されたJ-LAKEデータ(DATAZORAのKIJIサービス提供データを含む)を活用し、質問に対する適切な回答を生成する仕組みを設計し、精度を競います。
今回の取り組みでは、RAGに初めて触れる状態からスタート し、 LlamaIndexのチュートリアル(公式ドキュメント)を参考にしながらシステムの基盤を構築しました。知識的に不足している部分も多くありましたが、試行錯誤を重ねながら実装を進める中で、検索・生成のプロセスやLLMの活用方法について多くの学びを得ることができました。
本記事では、RAGの基礎的な実装から、検索精度を向上させるための試行錯誤、質問処理の工夫、最終的な検証結果の分析 までを詳しく解説します。特に、質問をそのまま使用するパターンと、質問を細かく分割して段階的に検索・回答生成を行うパターンの比較 を行い、それぞれのメリット・デメリットについて考察しました。
初めてのRAG構築ということもあり、まだ最適化の余地が多く残る状態ですが、実装を通じて得た知見や今後の改善策についても共有 し、より精度の高い情報検索・回答生成システムの開発につながればと思います。
2. 処理フロー
本システムの全体的な処理フローは以下の通りです。
- ドキュメントの読込
- 埋め込み処理+ベクトルストアへの格納
-
検索・回答作成
- パターン①: 質問をそのまま使用
- パターン②: 質問を分割(主語・目的・条件)
2.1 ドキュメントの読込
RAGの基盤となる、ESGレポートのドキュメントデータを読み込みます。
処理内容
-
テキスト・表・画像の抽出:
PyMuPDF
- テキスト:不要な改行を削除
- 表:表データをMarkdown化
-
画像:
OCR処理(Tesseract)+LLMによる補正
コード詳細
from pathlib import Path
from llama_index.core.schema import Document
import fitz # PyMuPDF
import io
from PIL import Image
import pytesseract
import requests
import json
import urllib3
from llama_index.core import PromptTemplate
# 🔇 **urllib3の警告を無効化**
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# プロンプトテンプレート
prompt_text = """
"以下のOCRで取得したテキストを意味の通る文章に修正してください。\n"
"修正後の文章のみを出力してください。\n"
"OCR結果が空白の場合は空白を出力してください。\n"
"【OCR結果】\n"{element}
"""
# LlamaIndexのプロンプトテンプレートとして設定
prompt_template = PromptTemplate(prompt_text)
# APIを使ってモデルを呼び出す関数
def call_custom_api(prompt):
headers = {
"X-Api-Key": API_KEY,
"Content-Type": "application/json",
}
data = {
"prompt": prompt,
"temperature": 1.0,
}
try:
response = requests.post(API_URL, headers=headers, data=json.dumps(data), verify=False)
response.raise_for_status()
return response.text
except requests.exceptions.RequestException as e:
print(f"LLM API Error: {e}")
return ocr_text # APIエラー時は元のOCR結果をそのまま使用
def clean_text(text):
"""
Remove unnecessary newlines from the text while preserving paragraph breaks.
"""
# Replace newline characters that are not followed by another newline
cleaned_text = " ".join(line.strip() for line in text.splitlines() if line.strip())
return cleaned_text
def table_to_markdown(table_data):
"""
2Dリストのテーブルデータをマークダウン形式の文字列に変換する関数。
Noneを空文字""に置き換えて処理する。
"""
if not table_data or not isinstance(table_data, list) or not isinstance(table_data[0], list):
return "データなし"
# ヘッダー行を取得(最初の行をヘッダーと仮定)
header = [str(cell) if cell is not None else "" for cell in table_data[0]]
body = [[str(cell) if cell is not None else "" for cell in row] for row in table_data[1:]]
# ヘッダーをMarkdown形式に変換
markdown_table = ["| " + " | ".join(header) + " |"]
markdown_table.append("|" + " | ".join(["---"] * len(header)) + "|") # 区切り線
# 本文をMarkdown形式に変換
for row in body:
markdown_table.append("| " + " | ".join(row) + " |")
return "\n".join(markdown_table) # 改行で結合
# まとめる関数
def summarize(element):
prompt = prompt_template.format(element=element)
summary = call_custom_api(prompt)
return summary
documents=[]
pdf_directory = "/ESG-RAG-System/data/processed"
pdf_files = list(Path(pdf_directory).glob("*.pdf"))
for pdf_file in tqdm(pdf_files):
pdf = fitz.open(pdf_file)
for page_num in range(len(pdf)):
page = pdf[page_num]
# 📌 **通常のテキストを取得**
page_text = page.get_text("text")
cleaned_page_text = clean_text(page_text)
# ✅ **Documentオブジェクト作成**
cleaned_doc = Document(
text=cleaned_page_text,
metadata={"file": str(pdf_file), "page": page_num + 1}
)
documents.append(cleaned_doc)
# 📌 **テーブルを抽出**
cleaned_tables=[]
tables = page.find_tables()
for table in tables.tables:
# ✅ **Documentオブジェクト作成**
cleaned_doc = Document(
text=table_to_markdown(table.extract()),
metadata={"file": str(pdf_file), "page": page_num + 1}
)
documents.append(cleaned_doc)
# 📌 **画像のOCR処理**
images = page.get_images(full=True)
ocr_texts = []
for img_index, img in enumerate(images):
xref = img[0]
base_image = pdf.extract_image(xref)
image_bytes = base_image["image"]
image = Image.open(io.BytesIO(image_bytes))
# 🔄 **画像前処理(OCR精度向上)**
image = image.convert("L")
image = image.point(lambda x: 0 if x < 180 else 255)
# 📝 **OCRを適用**
ocr_text = pytesseract.image_to_string(image, lang="jpn").strip()
if not ocr_text:
continue
# ✅ **Documentオブジェクト作成**
cleaned_doc = Document(
text=summarize(ocr_text),
metadata={"file": str(pdf_file), "page": page_num + 1}
)
documents.append(cleaned_doc)
2.2 埋め込み処理+ベクトルストアへの格納
読み込んだドキュメントを 埋め込みモデルを用いてベクトル化し、ベクトルデータベースに保存 します。
埋め込みモデルには、cl-nagoya/ruri-large
を採用しました。これは JMTEBのランキングを参考に、検索精度が高く、実装のしやすさを考慮して選定 しています。
1. テキストのチャンク化(分割)
PDFなどのドキュメントをそのままベクトル化すると、検索の精度が低下する可能性があります。そのため、適切なサイズでテキストを分割(チャンク化)し、検索効率を向上 させます。
次に、ドキュメントごとにチャンク化を適用 し、text_chunks
リストに保存します。
2. チャンクをノード化
検索システムで適切に処理できるように、チャンク化したテキストを TextNode
オブジェクトに変換 し、メタデータ(ドキュメント情報)を付与 します。
3. ベクトル化
検索システムがテキストの意味を理解できるように、埋め込みモデル(cl-nagoya/ruri-large
)を使用して、各ノードのテキストを高次元のベクトルに変換 します。
4. ベクトルストアへの格納
最終的に、ベクトル化が完了したノードをデータベースに格納 し、類似検索を可能な状態にします。
コード詳細
#1.テキストの分割
from llama_index.core.node_parser import SentenceSplitter
text_parser = SentenceSplitter(
chunk_size=1024,
chunk_overlap=500
)
text_chunks = []
doc_idxs = []
for doc_idx, doc in enumerate(documents):
cur_text_chunks = text_parser.split_text(doc.text)
text_chunks.extend(cur_text_chunks)
doc_idxs.extend([doc_idx] * len(cur_text_chunks))
# 2.分割したテキストをノード化
nodes = []
for idx, text_chunks in enumerate(text_chunks):
node = TextNode(
text=text_chunks,
)
src_doc = documents[doc_idxs[idx]]
node.metadata = src_doc.metadata
nodes.append(node)
# 3.テキストのベクトル化
embed_model = HuggingFaceEmbedding(model_name="cl-nagoya/ruri-large")
for node in tqdm(nodes):
node_embedding = embed_model.get_text_embedding(
node.get_content(metadata_mode="all")
)
node.embedding = node_embedding
# 4.ベクトルストアへの格納
vector_store.add(nodes)
検索・回答作成
質問に関連するコンテキストを検索し、回答を生成するプロセスについて説明します。
共通部分
質問に対して適切な情報を検索し、その結果をもとにLLM(大規模言語モデル)が回答を生成 します。
今回は、以下の手順で 検索・生成の処理 を実施しました。
-
検索処理(Retrieverの実装)
- ベクトルデータベース(PGVector)を活用し、質問に関連する情報を検索
-
Retrieverの初期化
- 検索対象のデータやパラメータを設定し、最適な検索結果を取得
-
LLMの設定(Azure OpenAI)
- Azure OpenAI の GPT-4o を使用し、検索結果をもとに適切な回答を生成
-
検索と生成を統合(RetrieverQueryEngine)
- 検索エンジン(Retriever)と生成AI(LLM)を組み合わせ、質問に対する応答を自動的に生成
このプロセスにより、高精度な情報検索と回答生成 を目指しました。
コード詳細
1.検索処理(Retrieverの実装)
まずは検索機能を担当するVectorDBRetrieverクラスを実装します。このクラスは、PostgreSQL + PGVector に格納されたベクトルデータを活用し、質問に関連する情報を検索 する役割を持ちます。
class VectorDBRetriever(BaseRetriever):
"""Retriever over a postgres vector store."""
#vector_store: PostgreSQL(PGVector)を使用したベクトルデータベース
#embed_model: クエリ(質問)を埋め込みベクトルに変換するためのモデル
#query_mode: デフォルトの検索モード(類似検索の方法)
#similarity_top_k: 上位 K件 の類似データを取得する設定
def __init__(
self,
vector_store: PGVectorStore,
embed_model: Any,
query_mode: str = "default",
similarity_top_k: int = 2,
) -> None:
"""Init params."""
self._vector_store = vector_store
self._embed_model = embed_model
self._query_mode = query_mode
self._similarity_top_k = similarity_top_k
super().__init__()
def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]:
"""Retrieve."""
query_embedding = embed_model.get_query_embedding(
query_bundle.query_str
)
vector_store_query = VectorStoreQuery(
query_embedding=query_embedding,
similarity_top_k=self._similarity_top_k,
mode=self._query_mode,
)
query_result = vector_store.query(vector_store_query)
nodes_with_scores = []
for index, node in enumerate(query_result.nodes):
score: Optional[float] = None
if query_result.similarities is not None:
score = query_result.similarities[index]
nodes_with_scores.append(NodeWithScore(node=node, score=score))
return nodes_with_scores
# 2.Retrieverの初期化
retriever = VectorDBRetriever(
vector_store, embed_model, query_mode="default", similarity_top_k=10
)
# 3.LLMの設定
from llama_index.llms.azure_openai import AzureOpenAI
# Azure OpenAI の設定
llm = AzureOpenAI(
model="gpt-4o-mini",
api_key=APIKEY,
deployment_name="",
azure_endpoint=ENDPOINT,
api_version="2024-10-21",
)
# 4. 検索と生成を統合(RetrieverQueryEngine)
from llama_index.core.query_engine import RetrieverQueryEngine
query_engine = RetrieverQueryEngine.from_args(
retriever, llm=llm, enable_cache=False
)
パターン①:質問をそのまま使用
本パターンでは、質問をそのまま使用し、ベクトル検索(Retriever)を用いて関連情報を取得し、LLMを活用して適切な回答を生成 します。以下の手順で処理を行いました。
1. クエリデータ(質問リスト)の読み込み
まず、query.csv
から質問を読み込み、回答(response
)と検索結果(context
)を保存するための列を追加します。
2. 各質問に対する検索&回答生成
質問ごとに以下の処理を実行します。
2.1 ベクトル検索による関連情報の取得
質問をもとに Retriever を活用し、ESGレポートの関連情報を取得します。
2.2 LLMへのプロンプト作成
取得したコンテキスト(検索結果)をもとに、LLMに問い合わせるための プロンプトを作成 します。
2.3 LLMによる回答生成
作成したプロンプトを RetrieverQueryEngine
に渡し、LLMが最適な回答を生成 します。
3. 回答結果の保存
最後に、検索・生成した回答を CSVファイルとして出力 し、結果を記録します。
コード詳細
# 1. クエリデータ(質問リスト)の読み込み
# 入力と出力のCSVファイルパス
input_csv_path = "/ESG-RAG-System/data/raw/query.csv"
output_csv_path = "/ESG-RAG-System/data/raw/predictions_par_test.csv"
output_csv_path_2 = "/ESG-RAG-System/data/raw/prompt_par_test.csv"
# query.csvを読み込み
query_df = pd.read_csv(input_csv_path)
# 'response'列を追加
query_df["response"] = ""
query_df["context"] = ""
# 2. 各質問に対する検索&回答生成
for idx, row in tqdm(query_df.iterrows()):
query_str = row["problem"]
# 2.1 ベクトル検索による関連情報の取得
query_bundle = QueryBundle(query_str=query_str)
retrieved_nodes = retriever.retrieve(query_bundle)
doc_context = "\n".join([node.node.get_content() for node in retrieved_nodes])
# 2.2 LLMへのプロンプト作成
prompt = (
"あなたは優秀なAIアシスタントです。\n"
"ユーザーが与えた情報だけをもとに回答してください。\n"
"情報がコンテキストに含まれない場合は『わかりません』と答えてください。\n"
"以下のコンテキストを参考に回答をしてください。回答は1行でお願いします。\n"
"回答は必ず50トークン以下にしてください。50トークンを超える場合は『わかりません』と答えてください。\n\n"
f"質問:\n{query_str}\n\n"
f"コンテキスト:\n{doc_context}"
)
# 2.3 LLMによる回答生成
response = query_engine.query(prompt)
query_df.loc[idx, "response"] = str(response)
query_df.loc[idx, "context"] = str(doc_context)
# 3. 回答結果の保存
output_df = query_df[["index","response"]]
# predictions.csvとして出力
output_df.to_csv(output_csv_path, index=False, header=False)
# prompt.csvとして出力(コンテキスト情報含む)
query_df.to_csv(output_csv_path_2, index=False, header=True)
パターン②:質問を分割して段階的に検索・回答生成
本パターンでは、ESGレポートに対する質問を複数のサブ質問へ分解し、各サブ質問ごとに情報検索を行い、段階的に回答を生成・統合するプロセス を実装しました。
このアプローチにより、複雑な質問にも対応可能な高精度な検索・回答生成システムの実現 を目指しました。
1. クエリデータ(質問リスト)の読み込み
まず、query.csv
から質問を読み込み、回答(response
)と検索結果(context
)を保存するための列を追加します。
2. 質問を複数のサブ質問へ分解
LLMを活用し、質問を複数のサブ質問に分割 することで、検索と回答生成の精度向上を図ります。
サブ質問の順序は 「基本情報 → 詳細情報」 の流れを意識し、段階的に情報を取得します。
3. 段階的な検索&回答生成
分割したサブ質問ごとに、以下のプロセスを実行します。
3.1 ベクトル検索による関連情報の取得
各サブ質問に対し、Retrieverを活用してESGレポートの関連情報を検索 します。
3.2 LLMへのプロンプト作成
取得したコンテキスト(検索結果)と、前のサブ質問の回答を考慮しながら、適切なプロンプトを作成 します。
3.3 LLMによる回答生成
作成したプロンプトを RetrieverQueryEngine
に渡し、LLMが回答を生成 します。
このプロセスを繰り返し、最終的な回答を統合します。
4. 回答結果の保存
検索・生成した最終回答を CSVファイルとして出力 し、結果を記録します。
このアプローチにより、複雑な質問に対しても、検索の精度を向上させつつ、段階的な回答生成を行うことで、より正確な情報提供 を目指しました。
コード詳細
# 1. クエリデータ(質問リスト)の読み込み
# パターン①と同様のため割愛
query_gen_str = """\
あなたは、与えられた質問を適切な検索クエリに変換する高精度なアシスタントです。
次の入力クエリを分析し、**検索エンジンで効果的に検索できるように、複数のサブ質問へ分解してください。**
以下の**厳格なルール**を必ず守って回答してください。\n\n
【ルール】
1. **質問の要素を論理的に分解し、それぞれの情報を取得するために必要なサブ質問を作成してください。**
- 検索の順序を意識し、**基本情報 → 詳細情報** の流れで分解する。
- 例えば、「X会社のAという特徴を持つ〇〇のBをCで答えてください」の場合:
- **サブ質問1:** 「X会社のAという特徴を持つ〇〇は何か?」
- **サブ質問2:** 「【サブ質問1】の答えのBは何か?」
- **サブ質問3:** 「【サブ質問2】の答えをCでいうと何か?」
- 例えば、「Y会社の〇〇年のCの指標は△△年のDの指標の何パーセントですか?」の場合:
- **サブ質問1:** 「Y会社の〇〇年のCの指標は?」
- **サブ質問2:** 「Y会社の△△年のDの指標は?」
- **サブ質問3:** 「【サブ質問1】の答えは【サブ質問2】の答えの何パーセントですか?」
2. **サブ質問は検索のしやすさを意識し、適切な順序で並べる。**
- 先に基本情報を取得し、その後に条件を検証する流れにする。
3. **不要なサブ質問は作成しない。**
- 最小限のステップで回答にたどり着けるようにする。
4. **サブ質問はシンプルかつ検索に適した形式にする。**
- 「〇〇の△△は?」のような形にする。
- 「〇〇について教えてください」などの曖昧な表現は避ける。
5. **特定の条件(単位、表記法、正式名称を答えるなど)が含まれる場合、段階的に処理する。**
例
- **サブ質問1:**「〇〇は何か」
- **サブ質問2:**「質問1を△△(単位、表記法、正式名称)で表すと何か
入力クエリ: {query}
検索クエリ:
"""
query_gen_prompt = PromptTemplate(query_gen_str)
def generate_queries(query: str, llm, ):
response = llm.predict(
query_gen_prompt, query=query,
)
queries = response.split("\n")
return queries
# 3. 段階的な検索&回答生成
for idx, row in query_df.iterrows():
query_str = row["problem"]
sub_answers = []
doc_contexts = []
three_query = generate_queries(query_str, Settings.llm)
for query in three_query:
docserch = hyde.run(f"{query} 前の質問の答え:{sub_answers}")
query_bundle = QueryBundle(query_str=docserch)
retrieved_nodes = retriever.retrieve(query_bundle)
doc_context = "\n".join([node.node.get_content() for node in retrieved_nodes])
extracted_parts = extract_query_parts(query)
prompt = (
"あなたは優秀なAIアシスタントです。\n"
"ユーザーが与えた情報だけをもとに回答してください。\n"
"情報がコンテキスト・前の質問の答えに含まれない場合は『わかりません』と答えてください。\n"
"回答は1行でお願いします。\n"
f"質問:\n{extracted_parts}\n\n"
f"コンテキスト:\n{doc_context}"
f"前の質問の答え:\n{sub_answers}"
)
sub_answer = query_engine.query(prompt)
sub_answers.append(f"{query}:{sub_answer}")
doc_contexts.append(f"context:{doc_context}")
# 4.最終回答の作成
final_prompt = (
"あなたは優秀なAIアシスタントです。\n"
"ユーザーが与えた情報だけをもとに回答してください。\n"
"情報がコンテキスト・サブ質問の答えに含まれない場合は『わかりません』と答えてください。\n"
"回答は1行でお願いします。\n"
f"質問:\n{query_str}\n\n"
f"質問のコンテキスト:\n{doc_context}"
f"【サブ質問の答え】\n"
f"{sub_answers}"
)
# 5. 結果の保存
# パターン①と同様のため割愛
3. 結果
本システムでは、以下の2つのアプローチで質問応答を実施し、正解・不正解・無回答の割合を比較 しました。
アプローチ
- パターン①:質問をそのまま使用
- パターン②:質問を分割
結果の比較
パターン | 正解数 | 不正解数 | 無回答数 |
---|---|---|---|
① そのまま | 58 | 29 | 13 |
② 分割 | 60 | 32 | 8 |
結果の考察
- 質問を分割すると正解数はわずかに増加 したが、不正解数も増加した。
- 正答率には大きな差はなく、質問分割によって 検索の精度は向上するが、生成の精度が低下する傾向 が見られた。
4. 失敗ケースの分析
各パターンにおける 不正解・無回答の原因を分類し、その割合を算出 しました。
失敗要因別の割合
失敗要因 | ① そのまま | ② 分割 |
---|---|---|
ドキュメント読込失敗 | 10 | 10 |
検索失敗 | 13 | 5 |
生成失敗(検索結果の読み取りミス) | 9 | 15 |
生成失敗(計算・論理ミス) | 10 | 11 |
失敗の詳細
① ドキュメント読込失敗
- 概要:ドキュメントを適切に読み取れず、情報を取得できなかったケース。
-
原因:
- テキストの構造が崩れてしまう(例:グラフや表に補足テキストがある場合、正しく解析できない)。
- OCRの精度が低く、誤読が発生。
② 検索失敗
- 概要:検索エンジンが適切な情報を取得できなかったケース。
-
原因:
- コンテキストに該当する情報が含まれていない。
- 質問の分割により検索精度は向上したが、元の質問の検索精度は低かった。
③ 生成失敗(検索結果の読み取りミス)
- 概要:検索結果の中に答えが含まれていたが、LLMが適切に解釈できなかったケース。
-
原因:
- 検索結果の重要部分を正しく抽出できなかった。
- 質問を分割すると検索範囲が明確になるが、その分、回答を組み立てる難易度が上がった。
④ 生成失敗(計算・論理ミス)
- 概要:検索結果の中に答えがあったにも関わらず、計算ミスや論理的な誤りにより不正解になったケース。
-
原因:
- 数値計算が必要な質問でミスが発生。
- 複数のサブ質問の答えを統合する際に、論理的なミスが生じた。
考察
- 質問を分割すると、検索の精度は向上 するが、その一方で 生成の精度が低下 する傾向が見られた。
- 分割された質問ごとに個別に回答を生成するため、生成の回数が増加し、その分誤読や論理ミスの発生率が上昇 したと考えられる。
5. 改善策
① ドキュメント読込失敗
✅ 対策
-
画像からテキストを抽出するLLMを活用
- 現在の PyMuPDF + OCR の組み合わせでは、補助情報の構造を保持できない ため、より高度なテキスト解析技術を導入。
- GPT-4 Vision などのマルチモーダルモデルを試行し、画像とテキストの統合解析を強化。
② 検索失敗
✅ 対策
-
チャンク化の処理を最適化
- 現在は
SentenceSplitter
を使用 → **SemanticSplitter
などの 意味単位での分割 を試す。 - チャンクサイズやオーバーラップを調整し、適切な範囲の情報を取得。
- 現在は
-
検索クエリの改善
- 質問の分割ルールを調整(検索に適した形にする)。
- HyDE(仮回答生成)を活用し、検索精度をさらに向上。
③ 生成失敗(検索結果の読み取りミス)
✅ 対策
-
より高性能なLLMの使用
- GPT-4 や Claude 3 などの検索結果を正確に理解する能力が高いモデルを導入。
-
コンテキストの再構成
- 検索結果のリランキング(Re-Ranking)を行い、最も関連度の高い情報を優先する。
④ 生成失敗(計算・論理ミス)
✅ 対策
-
段階的な計算・論理構築を促すプロンプト設計
- 現在のプロンプトでは 一度に計算・論理推論を求める 仕様 → 逐次的な回答を促す設計に変更。
- 例:「ステップ1: 〇〇の数値を取得 → ステップ2: 前回の答えを用いて計算」のような 段階的なプロンプト構造 にする。
-
より高性能なLLMの使用
- 計算・論理推論能力が強い GPT-4 TurboやClaude 3 への切り替え。
6. まとめ
🔹 検証結果
- 質問を分割すると、検索の精度は向上 したが、その分 生成のミスが増加。
- 検索結果の精度向上には貢献したが、生成時のミス(読み取り・論理ミス)が目立つ結果に。
🔹 改善策
✅ ドキュメント読込の改善
- 画像解析を強化(GPT-4 Visionなどの導入)
✅ 検索精度の向上
- チャンク化手法の最適化(SentenceSplitter → SemanticSplitterの試行)
- 検索クエリの改善(HyDE処理の活用)
✅ 生成精度の向上
- 高性能なLLM(GPT-4/Claude 3)の使用
- 検索結果のリランキング
- 段階的なプロンプト設計(計算・論理的思考を促す)
この検証を通じて、検索・生成の精度向上のための課題と解決策を明確化 することができました。
他の方の解法等を拝見させていただき、上記課題の解消を目指していきたいと思います。
最後に、本コンペティションを主催し、貴重なデータと機会を提供してくださった SIGNATE様、パートナー企業の皆様に深く感謝申し上げます。