神戸のデータ活用塾!KDL Data Blog

KDLが誇るデータ活用のプロフェッショナル達が書き連ねるブログです。

LLMを使って文献目録の作成を楽にしよう!

こんにちは、Data Intelligence チームの福岡です。今回はちょっとニッチな記事を・・ということで、「LLMを使って文献目録の作成を楽にできないか?」を検証します。

背景と目的

文献目録とは?

文献目録とは、特定のテーマに関する文献を網羅的に収集・整理し一覧化したものです。

具体的には、こんな目録が考えられます。

「日本に存在したじゃんけんの掛け声の一覧」ならこんな感じ:

「兵庫・大阪で記録のある生き物目録」ならこんな感じ:

こうした目録は学術的に非常に有用であり、広く活用されています。例えば前者は日本語表現の変遷研究に、後者は地域の生物保全活動に貢献します。 そのため、自然史博物館や大学をはじめとする研究機関では文献目録の作成が重要な業務になっています。 (上記の目録はどちらも全くの架空のデータで、説明のためかなり簡易化しています。「ふーん、文献目録というものがあると人間社会に有用で、それを作成している人がいるんだな」くらいに捉えていただければOKです。)

筆者は趣味でこうした目録を読むのですが、「作成はものすごい大変なハズ・・効率化できないか?」と考えた結果、LLM(Large Language Model;大規模言語モデル)の使用を思いついたので以下のように検証してみました!

文献目録の作成に必要なステップ

目録の作成には、ざっくり以下の3ステップが必要です:

① テーマに該当する文献を過不足なく収集

② 各文献から該当する情報を抜き出す

③ 情報の正確性をチェックする

①、③はそのテーマの専門知識が必要であり、AI等による代替は難しい*1ので今回は取り扱いません。

ここで、ステップ②の「該当する情報を抜き出す」作業を考えると、ここは人間が元の文献とにらめっこしながら手入力(もしくはコピペ)することになり、非常に面倒です。

「この面倒を自動化して、人間は専門性が必要な営みだけ専念するようにできないか?」が本連載の目的です。

方法(概要)

例として「兵庫・大阪で記録のある生き物」をテーマに、以下のステップでLLMによる抽出処理を行ってみます。

  1. テーマに該当する文献を用意
  2. Markdown形式に変換
  3. LLMで情報を抽出(←本記事のキーとなる工程)
  4. 抽出結果を表形式に整形

アーキテクチャ図

python環境構築はpyenv+poetryで行っています。なお、長くなるので環境構築の細かなやり方やAzureの各種サービスの使用方法は省略します。

順に解説します。

1. テーマに該当する文献を用意

冒頭でも触れましたが、このステップではテーマに該当する文献を抜け漏れなくピックアップする必要があります。説明のため今回は以下の文献が「大阪・兵庫で生息が確認されている生き物」のテーマを過不足なく満たすものと仮定します。これらの文献を適当なフォルダに格納します。

大阪府におけるゴホントゲザトウムシの追加記録(辻・モリス, 2023)

兵庫県におけるヒロマキミズマイマイの記録(安原, 2024)

兵庫県三田市と新温泉町におけるクロバネツリアブの記録(宇野, 2022)

兵庫県のネブトクボヒゲアシナガバエの記録(熊澤, 2020)

なお、いずれもオープンアクセスのWEB生物雑誌 ニッチェ・ライフ誌より、大阪か兵庫がタイトルに含まれる記事をいくつか選択しました。

補足:著作権的な扱い

本記事では、上述の実在する学術論文を例に実装説明を行います。また実装ではこれらの文献をクラウドサービスやLLMに引き渡しています。

いずれも、改正著作権法第47条の5に定める著作物の軽微利用もしくは正当な引用の範疇での利用であり、著作権侵害には当たらないと認識していますが、万が一何らかの問題発生が見込まれる場合や著作権侵害であると思われる場合はご連絡ください。

2. 文献をMarkdown形式に変換

文献をMarkdown形式に変換することで、目録の作成には不必要な情報(図表や謝辞、引用文献など)の削除が可能になり、情報抽出精度が良くなります。また費用も安くなります。

変換にはAzure Document Intelligenceのレイアウトモデルを使用しました。Markdown変換用のノートブックを用意し、以下を記述してください。

import os
import re
from glob import glob

from azure.ai.documentintelligence import DocumentIntelligenceClient
from azure.ai.documentintelligence.models import DocumentContentFormat
from azure.core.credentials import AzureKeyCredential

DI_ENDPOINT = "実際の値に置き換えてください"
DI_APIKEY = "実際の値に置き換えてください"

credential = AzureKeyCredential(DI_APIKEY)
document_intelligence_client = DocumentIntelligenceClient(DI_ENDPOINT, credential)


# === 処理対象のディレクトリ ===
target_dir = "./docs"  # 必要に応じて変更
output_dir = "./results/markdown"

# === 全文献を処理 ===
for filename in os.listdir(target_dir):
    doc_path = os.path.join(target_dir, filename)

    # 拡張子を除いたファイル名
    file_name = os.path.splitext(filename)[0]
    print(f"\n📄 処理中: {filename}")

    try:
        with open(doc_path, "rb") as f:
            poller = document_intelligence_client.begin_analyze_document(
                "prebuilt-layout",
                body=f,
                content_type="application/octet-stream",
                output_content_format=DocumentContentFormat.MARKDOWN, # この指定でMarkdown形式になる
            )
            result = poller.result()

        # === Markdown処理 ===
        markdown_text = result.content

        # <figure> や <table> を削除
        markdown_text = re.sub(r"<figure[\s\S]*?</figure>", "", markdown_text, flags=re.IGNORECASE)
        markdown_text = re.sub(r"<table[\s\S]*?</table>", "", markdown_text, flags=re.IGNORECASE)

        # 「参考文献」「謝辞」などの後を削除 
        patterns_to_remove = [
            r"(#+\s*(参考文献|引用文献|文献|謝辞|Acknowledg?ments?)\s*[\s\S]*)",
            r"(参考文献|引用文献|文献|謝辞|Acknowledg?ments?)\s*\n[\s\S]*"
        ]

        for pattern in patterns_to_remove:
            markdown_text = re.sub(pattern, "", markdown_text, flags=re.IGNORECASE)

        # 書き出し
        output_path = os.path.join(output_dir, f"{file_name}.md")
        with open(output_path, "w", encoding="utf-8") as f_out:
            f_out.write(markdown_text)

        print(f"✅ 保存完了: {output_path}")

    except Exception as e:
        print(f"⚠️ エラー: {filename} - {e}")

これで、文献はMarkdown形式に変換できるようになりました。

実行結果

3. LLMで情報を抽出

ここまでで文献をLLMに引き渡す用意ができました。では、目録作成に情報を抽出してみましょう。

Azure OpenAI Serviceを使用します。使うモデルはgpt-4oです。

まずは情報抽出用の関数を定義します。LLMにどのようにふるまってほしいかをここで設定しています。

import json
import os

from openai import AzureOpenAI

def extract_info_from_text(user_message: str) -> dict:
    client = AzureOpenAI(
        azure_endpoint=os.getenv("実際に値に置き換えてください"),
        api_key=os.getenv("実際に値に置き換えてください"),
        api_version="2024-05-01-preview",
    )

    system_message = (
        "あなたは生物目録作成アシスタントです"
        "あなたの役割は、入力された文章(生物の発見記録や論文)から次の情報を抽出してjson形式で出力することです:\n"
        "・論文タイトル\n"
        "・発表年\n"
        "・著者(複数名いる場合はリスト形式)\n"
        "・生物ごとの分類情報(目、科、属、和名、学名)\n"
        "発見場所、記録された場所"
        "出力はjson形式、以下を参考にしてください。指示の復唱や補足説明は不要です。\n\n"
        
        "{\n"
        '  "タイトル": "〜",\n'
        '  "年": 2022,\n'
        '  "著者": ["著者1", "著者2"],\n'
        '  "生物情報": [\n'
        "    {\n"
    '          "目": "Helicida",\n'
    '          "科": "オナジマイマイ科",\n'
    '          "属": "Aegista",\n'
    '          "和名": "マヤサンマイマイ",\n'
    '          "学名": "Aegist mayasana",\n'
    '          "記録場所": "兵庫県神戸市摩耶山"\n'
        "    },\n"
        "    {\n"
        '      "目": "...",\n'
        '      "科": "...",\n'
        '      "属": "...",\n'
        '      "和名": "...",\n'
        '      "学名": "..."\n'
        '      "記録場所": "..."\n'
        "    }\n"
        "  ]\n"
        "}"
        
        "文章中に該当する情報がない場合は決して補わず、空白としてください"
    )

    response = client.chat.completions.create(
        model="gpt-4o",
        response_format={"type": "json_object"},
        messages=[
            {"role": "system", "content": system_message},
            {"role": "user", "content": user_message},
        ],
        max_tokens=1000,
    )

    result = json.loads(response.to_json())["choices"][0]["message"]["content"]
    return result

では、Markdown化した文献に対して関数を適用しましょう。以下を実行します。

# markdownディレクトリのパス(必要に応じて変更)
markdown_dir = "results/markdown"
output_results = {}

# すべてのMarkdownファイルに対して処理
for filename in os.listdir(markdown_dir):
    file_path = os.path.join(markdown_dir, filename)
    with open(file_path, "r", encoding="utf-8") as f:
        markdown_text = f.read()
        result = extract_info_from_text(markdown_text)
        output_results[filename] = result

4. 抽出結果を表形式に整形

ここまで来たらあと一息です。抽出結果を表に整形します。

import json
import csv

# CSV に書き出す行のリスト
rows = []

for filename, result_str in output_results.items():
    result = json.loads(result_str)
    title = result.get("タイトル", "")
    year = result.get("年", "")
    authors = "/".join(result.get("著者", []))  # 著者リストを「/」区切りに

    for info in result.get("生物情報", []):
        rows.append({
            "ファイル名": filename,
            "タイトル": title,
            "年": year,
            "著者": authors,
            "目": info.get("目", ""),
            "科": info.get("科", ""),
            "属": info.get("属", ""),
            "和名": info.get("和名", ""),
            "学名": info.get("学名", ""),
            "記録場所": info.get("記録場所", ""),
        })

# CSV 出力
with open("./results/文献調査目録.csv", "w", encoding="utf-8-sig", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=[
        "ファイル名", "タイトル", "年", "著者", "目", "科", "属", "和名", "学名", "記録場所"
    ])
    writer.writeheader()
    writer.writerows(rows)

print("✅ 文献目録.csv に書き出しました。")

作成した目録を確認

結果を確認します。なかなかいい感じではないでしょうか??

抽出結果を整理した表

一部取得できていない情報もありますが、これは手動や追加プログラム(例えば、あらかじめ用意した学名と分類群の対応リストを当てはめるとか)によって補ってあげればOKです!少なくとも1からこの表を作るより、負担は少ないのではないでしょうか。

また繰り返しますが、この情報が本当に正しいか?の確認は人間の仕事です!

まとめ

いかがだったでしょうか。今回は「LLMを使って文献目録の作成を楽にできないか?」を検証し、「すべての作業を人間が行うより、負担を減らせる」という結果になりました。

次回更新もお楽しみに!

*1:なぜなら、AIも学習していない&検索でヒットしない文献も沢山あるからです。またそもそも元の文献が間違っていたり最新知見がアップデートされていることは頻繁にあります。さらに、AI使用の有無によらず作成した目録の責任は人間(作成者)が負います。