🏺

Azure OpenAI Service の GPT-4 を使ってギリシア神話に登場する神々の関係性を抽出してグラフデータベースに格納する

2023/04/27に公開

はじめに

以前のモデルに比べて大きく性能が向上してより高い論理的思考能力を獲得した GPT-4 を使えば、自然言語で書かれたテキストを読解して、登場する人物や物事の関係性を抽出してグラフデータベース化できるのではと思い、実際にやってみました。
OpenAI の言語モデルを使ってテキストから key-value の形式でデータを抽出する事はすでに広く行われていると思いますが、より応用的な利用案の位置づけです。
つまるところ、やろうとしている事は非構造化データ (テキスト) から半構造化データ (グラフデータベース) への変換で、いわゆる データエンリッチメント (洞察を得られる価値のある情報を付加する事) です。

方法

GPT-4 を含めて、全て Microsoft Azure のサービスを使います。

1. 環境準備

1.1. Azure OpenAI Service (GPT-4)

Azure OpenAI Service で GPT-4 を使う方法に関しては別の記事でまとめましたので、そちらを参照してください。

1.2. Azure Cosmos DB

グラフデータベースには Azure Cosmos DB を使います。Cosmos DB は高可用性と低レイテンシーを兼ね備えた NoSQL データベースで、用途に応じた 6 種類の API が存在しています。今回はグラフを扱うため、この中から Azure Cosmos DB for Apache Gremlin を選択してリソースを作成します。Gremlin とは、Apache Tinkerpop プロジェクトに含まれるグラフトラバーサル言語 (グラフに対するクエリ言語) です。
また、この文脈におけるグラフとは、ノード (頂点)、エッジ (関係)、プロパティ (属性) で表現されるデータ構造のことです。

グラフの例

参考

1.2.1. リソース作成

以下のドキュメントを参考にして Azure Cosmos DB のリソースを作成します。API の種類は グラフ を選択します。今回はヘヴィな処理は行わないため、容量モードServerless を選択します。

1.2.2. データベースとグラフの作成

リソースを作成したら、データエクスプローラー からデータベースグラフを作成します。データベース名とグラフ名は後で使うので控えておきます。





※とりあえず論理パーティションキーは name としました。

1.3. Python パッケージ

処理は Python で実行します。実行環境にあらかじめ以下のパッケージをインストールしておく必要があります。

2. データ取得

タイトルにもあるとおり、Wiki-40B英語データセットに含まれる Greek mythology (ギリシア神話)」を使います。ギリシア神話は Wiki40b 英語データセットの train スプリットの 96,5171 番目 (インデックスは 96,5170) のレコードに含まれています。(全てのレコードをフルスキャンして調べました。)
なお、内容は Wiki-40B データセットが公開された当時のものですので、現在の記事とは異なる可能性があります。

データを取得するクエリは以下のとおりです。

import tensorflow_datasets as tfds

text = ""
ds = tfds.load("wiki40b/en", split="train")
for idx, record in enumerate(ds):
    if idx == 965170:
        text = record["text"].numpy().decode("utf-8")
        break

# テキストファイルに保存
with open("greek_mythology_wiki_en.txt", "w") as f:
    f.write(text)

3. Gremlin クエリ生成

以下のコードで、テキストから Gremlin クエリを生成します。Azure OpenAI Service のキーとエンドポイントは事前に取得して環境変数に登録しておきます。

import os
import time
import openai
import tiktoken

openai.api_type = "azure"
openai.api_base = os.getenv("AOAI_API_BASE")  # 事前に環境変数に設定しておく
openai.api_version = "2023-03-15-preview"
openai.api_key = os.getenv("AOAI_API_KEY")  # 事前に環境変数に設定しておく

# テキストファイルに保存しておいた記事を読み込む
text = None
with open("greek_mythology_wiki_en.txt", "r") as f:
    text = f.read()
text = text.replace("_START_ARTICLE_\n", "").replace("_START_PARAGRAPH_\n", "").replace("_NEWLINE_", "\n")
sections = text.split("_START_SECTION_\n")  # セクションごとに分割
sections.pop(0)  # 最初の要素は記事名なので削除

system_message = """
You are a professional editor, a professional software engineer, and even a database engineer. You have experience in past projects to extract information from natural language texts and create databases.
"""
prompt = """
The following text is from Wikipedia Greek Mythology. From this text, you need to extract information about the relationship between gods/goddesses and store it in a graph database.
Generate queries for the Gremlin API keeping in mind the following points:
- Generate queries to add information about gods/goddesses that appear in the text as "vertices" and "properties.
- In vertices, distinguish between "gods/goddesses", "demigods", and "humans".
- In properties, distinguish between "Titāns", "Olympians" and "Others".
- In properties, add information that characterizes the gods, for example, "aspect", etc.
- Generate queries to add relationships between gods/goddesses as "edges".
- Relationships could be, for example, "parent-child", "sibling", "love interest", "hostility", "win", "lost", etc.
- Do not include comments or anything else that is not part of the Gremlin queries.
- Use a new lines to separate queries from each other.
- If given text does not contain any useful information, just output "g.V()".

Text:

"""
enc = tiktoken.encoding_for_model('gpt-4')
queries = []  # 生成されたクエリを格納するリスト
for idx, section in enumerate(sections):
    user_message = prompt + section
    tokens = enc.encode(user_message)
    print(user_message)
    print("%s/%s" % (idx+1, len(sections)))
    print("token count: %s" % len(tokens))
    response = openai.ChatCompletion.create(
        engine="gpt-4-0314",  # デプロイ時につけた名を指定
        messages=[
            {"role": "system", "content": system_message},
            {"role": "user", "content": user_message},
        ],
        max_tokens=2000,
        temperature=0
    )
    print(response)
    if response["choices"][0]["finish_reason"] == "content_filter":
        queries.append("g.V()")  # もしコンテンツフィルターにかかった場合はダミークエリを追加
    else:
        # プロンプトにてクエリは改行で区切るように指示しているため改行でクエリを分割してリストに追加
        queries_raw = response["choices"][0]["message"]["content"].split('\n')
        queries = queries + list(filter(None, queries_raw))  # 空行を削除
    time.sleep(30)  # Azure OpenAI Service の制限にかからないように念のため少し待つ

# 生成されたクエリをテキストファイルに保存
with open("greek_mythology_queries.txt", "w") as f:
    f.write('\n'.join(queries))

生成されたクエリ (greek_mythology_queries.txt) の内容は以下のとおりです。

g.addV('god').property('name', 'Chaos').property('type', 'Others')
g.addV('goddess').property('name', 'Gaia').property('type', 'Titāns').property('aspect', 'Earth')
g.addV('god').property('name', 'Eros').property('type', 'Others').property('aspect', 'Love')
g.addV('god').property('name', 'Uranus').property('type', 'Titāns').property('aspect', 'Sky')
g.addV('god').property('name', 'Coeus').property('type', 'Titāns')
g.addV('god').property('name', 'Crius').property('type', 'Titāns')
g.addV('god').property('name', 'Cronus').property('type', 'Titāns')
g.addV('god').property('name', 'Hyperion').property('type', 'Titāns')
g.addV('god').property('name', 'Iapetus').property('type', 'Titāns')
g.addV('god').property('name', 'Oceanus').property('type', 'Titāns')
g.addV('goddess').property('name', 'Mnemosyne').property('type', 'Titāns')
g.addV('goddess').property('name', 'Phoebe').property('type', 'Titāns')
g.addV('goddess').property('name', 'Rhea').property('type', 'Titāns')
g.addV('goddess').property('name', 'Theia').property('type', 'Titāns')
g.addV('goddess').property('name', 'Themis').property('type', 'Titāns')
g.addV('goddess').property('name', 'Tethys').property('type', 'Titāns')
g.addV('god').property('name', 'Zeus').property('type', 'Olympians')
g.addV('god').property('name', 'Poseidon').property('type', 'Olympians')
g.addV('god').property('name', 'Hades').property('type', 'Olympians')
g.addV('goddess').property('name', 'Hestia').property('type', 'Olympians')
g.addV('goddess').property('name', 'Demeter').property('type', 'Olympians')
g.addV('goddess').property('name', 'Hera').property('type', 'Olympians')
g.addV('goddess').property('name', 'Metis').property('type', 'Others')
g.addV('goddess').property('name', 'Athena').property('type', 'Olympians').property('aspect', 'War')
g.V().has('name', 'Gaia').addE('parent').to(g.V().has('name', 'Uranus'))
g.V().has('name', 'Uranus').addE('parent').to(g.V().has('name', 'Coeus'))
g.V().has('name', 'Uranus').addE('parent').to(g.V().has('name', 'Crius'))
g.V().has('name', 'Uranus').addE('parent').to(g.V().has('name', 'Cronus'))
g.V().has('name', 'Uranus').addE('parent').to(g.V().has('name', 'Hyperion'))
g.V().has('name', 'Uranus').addE('parent').to(g.V().has('name', 'Iapetus'))
g.V().has('name', 'Uranus').addE('parent').to(g.V().has('name', 'Oceanus'))
g.V().has('name', 'Uranus').addE('parent').to(g.V().has('name', 'Mnemosyne'))
g.V().has('name', 'Uranus').addE('parent').to(g.V().has('name', 'Phoebe'))
g.V().has('name', 'Uranus').addE('parent').to(g.V().has('name', 'Rhea'))
g.V().has('name', 'Uranus').addE('parent').to(g.V().has('name', 'Theia'))
g.V().has('name', 'Uranus').addE('parent').to(g.V().has('name', 'Themis'))
g.V().has('name', 'Uranus').addE('parent').to(g.V().has('name', 'Tethys'))
g.V().has('name', 'Cronus').addE('parent').to(g.V().has('name', 'Zeus'))
g.V().has('name', 'Cronus').addE('parent').to(g.V().has('name', 'Poseidon'))
g.V().has('name', 'Cronus').addE('parent').to(g.V().has('name', 'Hades'))
g.V().has('name', 'Cronus').addE('parent').to(g.V().has('name', 'Hestia'))
g.V().has('name', 'Cronus').addE('parent').to(g.V().has('name', 'Demeter'))
g.V().has('name', 'Cronus').addE('parent').to(g.V().has('name', 'Hera'))
g.V().has('name', 'Zeus').addE('parent').to(g.V().has('name', 'Athena'))
g.V().has('name', 'Cronus').addE('sibling').to(g.V().has('name', 'Rhea'))
g.V().has('name', 'Zeus').addE('sibling').to(g.V().has('name', 'Poseidon'))
g.V().has('name', 'Zeus').addE('sibling').to(g.V().has('name', 'Hades'))
g.V().has('name', 'Zeus').addE('sibling').to(g.V().has('name', 'Hestia'))
g.V().has('name', 'Zeus').addE('sibling').to(g.V().has('name', 'Demeter'))
g.V().has('name', 'Zeus').addE('sibling').to(g.V().has('name', 'Hera'))
g.V().has('name', 'Cronus').addE('hostility').to(g.V().has('name', 'Uranus'))
g.V().has('name', 'Zeus').addE('hostility').to(g.V().has('name', 'Cronus'))
g.V().has('name', 'Zeus').addE('love_interest').to(g.V().has('name', 'Metis'))
g.addV('god').property('name', 'Zeus').property('type', 'Olympian').property('aspect', 'sky')
g.addV('god').property('name', 'Aphrodite').property('type', 'Olympian').property('aspect', 'love and beauty')
g.addV('god').property('name', 'Ares').property('type', 'Olympian').property('aspect', 'war')
g.addV('god').property('name', 'Hades').property('type', 'Olympian').property('aspect', 'underworld')
g.addV('god').property('name', 'Athena').property('type', 'Olympian').property('aspect', 'wisdom and courage')
g.addV('god').property('name', 'Apollo').property('type', 'Olympian').property('aspect', 'various')
g.addV('god').property('name', 'Dionysus').property('type', 'Olympian').property('aspect', 'various')
g.addV('god').property('name', 'Hestia').property('type', 'Olympian').property('aspect', 'hearth')
g.addV('god').property('name', 'Helios').property('type', 'Olympian').property('aspect', 'sun')
g.addV('god').property('name', 'Pan').property('type', 'Others').property('aspect', 'countryside')
g.addV('god').property('name', 'Nymphs').property('type', 'Others').property('aspect', 'rivers')
g.addV('god').property('name', 'Naiads').property('type', 'Others').property('aspect', 'springs')
g.addV('god').property('name', 'Dryads').property('type', 'Others').property('aspect', 'trees')
g.addV('god').property('name', 'Nereids').property('type', 'Others').property('aspect', 'sea')
g.addV('god').property('name', 'Erinyes').property('type', 'Others').property('aspect', 'underworld')
g.addV('god').property('name', 'Satyrs').property('type', 'Others').property('aspect', 'countryside')
g.V().has('name', 'Zeus').addE('rules').to(g.V().has('name', 'Olympians'))
g.V().has('name', 'Hades').addE('rules').to(g.V().has('name', 'Underworld'))
g.V().has('name', 'Athena').addE('sibling').to(g.V().has('name', 'Ares'))
g.V().has('name', 'Ares').addE('sibling').to(g.V().has('name', 'Athena'))
g.V().has('name', 'Apollo').addE('sibling').to(g.V().has('name', 'Dionysus'))
g.V().has('name', 'Dionysus').addE('sibling').to(g.V().has('name', 'Apollo'))
g.V().has('name', 'Hestia').addE('sibling').to(g.V().has('name', 'Helios'))
g.V().has('name', 'Helios').addE('sibling').to(g.V().has('name', 'Hestia'))
g.V().has('name', 'Aphrodite').addE('love interest').to(g.V().has('name', 'Ares'))
g.V().has('name', 'Ares').addE('love interest').to(g.V().has('name', 'Aphrodite'))
g.addV('god').property('name', 'Prometheus').property('type', 'Titan')
g.addV('god').property('name', 'Zeus').property('type', 'Olympian')
g.addV('god').property('name', 'Tantalus').property('type', 'Other')
g.addV('god').property('name', 'Demeter').property('type', 'Olympian').property('aspect', 'agriculture')
g.addV('human').property('name', 'Triptolemus')
g.addV('god').property('name', 'Marsyas').property('type', 'Other')
g.addV('god').property('name', 'Apollo').property('type', 'Olympian').property('aspect', 'music')
g.addV('god').property('name', 'Dionysus').property('type', 'Olympian').property('aspect', 'wine')
g.addV('human').property('name', 'Lycurgus')
g.addV('human').property('name', 'Pentheus')
g.addV('god').property('name', 'Aphrodite').property('type', 'Olympian').property('aspect', 'love')
g.addV('human').property('name', 'Anchises')
g.addV('demigod').property('name', 'Aeneas')
g.addV('god').property('name', 'Persephone').property('type', 'Olympian')
g.addV('human').property('name', 'Celeus')
g.addV('human').property('name', 'Demophon')
g.addV('human').property('name', 'Metanira')
g.V().has('name', 'Prometheus').addE('steals').to(g.V().has('name', 'Zeus')).property('object', 'fire')
g.V().has('name', 'Tantalus').addE('steals').to(g.V().has('name', 'Zeus')).property('object', 'nectar and ambrosia')
g.V().has('name', 'Demeter').addE('teaches').to(g.V().has('name', 'Triptolemus')).property('knowledge', 'agriculture and Mysteries')
g.V().has('name', 'Marsyas').addE('contest').to(g.V().has('name', 'Apollo')).property('type', 'musical')
g.V().has('name', 'Dionysus').addE('punishes').to(g.V().has('name', 'Lycurgus'))
g.V().has('name', 'Dionysus').addE('punishes').to(g.V().has('name', 'Pentheus'))
g.V().has('name', 'Aphrodite').addE('mates').to(g.V().has('name', 'Anchises'))
g.V().has('name', 'Aphrodite').addE('parent').to(g.V().has('name', 'Aeneas'))
g.V().has('name', 'Anchises').addE('parent').to(g.V().has('name', 'Aeneas'))
g.V().has('name', 'Demeter').addE('searches').to(g.V().has('name', 'Persephone'))
g.V().has('name', 'Demeter').addE('welcomed').to(g.V().has('name', 'Celeus'))
g.V().has('name', 'Demeter').addE('attempted_transformation').to(g.V().has('name', 'Demophon'))
g.V().has('name', 'Metanira').addE('interrupts').to(g.V().has('name', 'Demeter'))
g.V()
g.addV('demigod').property('name', 'Heracles').property('type', 'Others').property('aspect', 'strength')
g.addV('god').property('name', 'Zeus').property('type', 'Olympians')
g.addV('human').property('name', 'Alcmene').property('type', 'Others')
g.addV('demigod').property('name', 'Perseus').property('type', 'Others')
g.addV('demigod').property('name', 'Deucalion').property('type', 'Others')
g.addV('demigod').property('name', 'Theseus').property('type', 'Others')
g.addV('demigod').property('name', 'Bellerophon').property('type', 'Others')
g.addV('human').property('name', 'Hyllus').property('type', 'Others')
g.addV('human').property('name', 'Macaria').property('type', 'Others')
g.addV('human').property('name', 'Lamos').property('type', 'Others')
g.addV('human').property('name', 'Manto').property('type', 'Others')
g.addV('human').property('name', 'Bianor').property('type', 'Others')
g.addV('demigod').property('name', 'Tlepolemus').property('type', 'Others')
g.addV('demigod').property('name', 'Telephus').property('type', 'Others')
g.V().has('name', 'Heracles').addE('parent').to(g.V().has('name', 'Zeus'))
g.V().has('name', 'Heracles').addE('parent').to(g.V().has('name', 'Alcmene'))
g.V().has('name', 'Hyllus').addE('parent').to(g.V().has('name', 'Heracles'))
g.V().has('name', 'Heracles').addE('similar').to(g.V().has('name', 'Perseus'))
g.V().has('name', 'Heracles').addE('similar').to(g.V().has('name', 'Deucalion'))
g.V().has('name', 'Heracles').addE('similar').to(g.V().has('name', 'Theseus'))
g.V().has('name', 'Heracles').addE('similar').to(g.V().has('name', 'Bellerophon'))
g.V().has('name', 'Perseus').addE('similar').to(g.V().has('name', 'Bellerophon'))
g.V().has('name', 'Deucalion').addE('similar').to(g.V().has('name', 'Bellerophon'))
g.V().has('name', 'Theseus').addE('similar').to(g.V().has('name', 'Bellerophon'))
g.addV('god').property('name', 'Apollonius').property('type', 'Others')
g.addV('human').property('name', 'Jason').property('type', 'Argonaut')
g.addV('human').property('name', 'Pelias').property('type', 'King')
g.addV('god').property('name', 'Heracles').property('type', 'Olympians')
g.addV('human').property('name', 'Theseus').property('type', 'Argonaut')
g.addV('human').property('name', 'Atalanta').property('type', 'Argonaut')
g.addV('human').property('name', 'Meleager').property('type', 'Argonaut')
g.addV('human').property('name', 'Medea').property('type', 'Argonaut')
g.V().has('name', 'Jason').addE('impelled_by').to(g.V().has('name', 'Pelias'))
g.V().has('name', 'Jason').addE('quest').to(g.V().has('name', 'Golden Fleece'))
g.V().has('name', 'Jason').addE('travel_with').to(g.V().has('name', 'Heracles'))
g.V().has('name', 'Jason').addE('travel_with').to(g.V().has('name', 'Theseus'))
g.V().has('name', 'Jason').addE('travel_with').to(g.V().has('name', 'Atalanta'))
g.V().has('name', 'Jason').addE('travel_with').to(g.V().has('name', 'Meleager'))
g.V().has('name', 'Theseus').addE('slay').to(g.V().has('name', 'Minotaur'))
g.V().has('name', 'Medea').addE('love_interest').to(g.V().has('name', 'Jason'))
g.V()
g.V()
g.V()
g.V()
g.V()
g.V()
g.V()
g.V()

[補足] 前処理

データセットには特殊なマーカー文字列が含まれています。

  • _START_ARTICLE_ (記事のタイトル)
  • _START_SECTION_ (セクションの開始)
  • _START_PARAGRAPH_ (段落の開始)
  • _NEWLINE_ (段落内の改行)

生データは以下のようなフォーマットです。

_START_ARTICLE_
<タイトル>
_START_SECTION_
<セクション名>
_START_PARAGRAPH_
<文>_NEWLINE_<文>_NEWLINE_...
_START_SECTION_
<セクション名>
_START_PARAGRAPH_
<文>_NEWLINE_<文>_NEWLINE_...

今回はマーカー文字列を以下のように置き換えました。

  • _START_ARTICLE_ (記事のタイトル) → 行ごと削除
  • _START_SECTION_ (セクションの開始) → 行ごと削除 (セクションごとに分割する際の区切り文字に使用)
  • _START_PARAGRAPH_ (段落の開始) → 行ごと削除
  • _NEWLINE_ (段落内の改行) → 改行コード (\n) で置き換え

置き換え後は以下のようなフォーマットになります。セクションごとに Python のリストに登録して処理を行います。

["<セクション名>\n<文>\n<文>...", "<セクション名>\n<文>\n<文>...", ...]

[補足] プロンプトの内容

以下のようなテンプレート部分の後に、前述のセクションごとのテキストをつないでプロンプトを作成しました。

The following text is from Wikipedia Greek Mythology. From this text, you need to extract information about the relationship between gods/goddesses and store it in a graph database.
Generate queries for the Gremlin API keeping in mind the following points:
- Generate queries to add information about gods/goddesses that appear in the text as "vertices" and "properties.
- In vertices, distinguish between "gods/goddesses", "demigods", and "humans".
- In properties, distinguish between "Titāns", "Olympians" and "Others".
- In properties, add information that characterizes the gods, for example, "aspect", etc.
- Generate queries to add relationships between gods/goddesses as "edges".
- Relationships could be, for example, "parent-child", "sibling", "love interest", "hostility", "win", "lost", etc.
- Do not include comments or anything else that is not part of the Gremlin queries.
- Use a new lines to separate queries from each other.
- If given text does not contain any useful information, just output "g.V()".

Text:

日本語訳

次の文章は、Wikipediaのギリシャ神話です。このテキストから、神/女神の関係に関する情報を抽出し、グラフデータベースに格納する必要があります。
以下の点に注意して、Gremlin API のクエリを生成してください:
- 本文中に「頂点」「プロパティ」として登場する神/女神の情報を追加するクエリを生成する。
- 頂点では、「神/女神」「半神」「人間」の区別をつける。
- プロパティでは、「ティターン」、「オリュンポスの神々」、「その他」を区別する。
- プロパティで、「アスペクト」など、神々を特徴づける情報を追加する。
- 神々/女神間の関係を「エッジ」として追加するクエリを生成する。
- 関係とは、例えば「親子」「兄弟」「恋愛」「敵対」「勝ち」「負け」など。
- コメントなど、Gremlin のクエリに含まれないものは含めない。
- クエリ同士は改行で区切る。
- 与えられたテキストに有用な情報が含まれていない場合は、"g.V() "を出力する。

Text:

[補足] gpt-4gpt-4-32k

今回対象にしたテキストは gpt-4-32k を使えば一度に全文を処理する事ができましたが、なぜか全文を一度に処理すると出力されるクエリ数が少なくなってしまいました。そのため、今回はセクションごとに分割したテキスト (チャンク) それぞれに対して gpt-4 を使ってクエリを生成する方法を選びました。

参考

[補足] GPT-4 のパラメーター

クエリ生成の際のパラメーターは max_tokenstemperature 以外はデフォルト値のままにしました。
max_tokens は何度か試行錯誤を繰り返して、セクションごとに生成されるクエリが収まりそうな余裕を持った値を探りました。
temperagure はいろいろな値を試しましたが、最終的に 0 を選びました。temperature は 1.5 を超えたあたりから無意味なクエリが生成されるようになってしまい、使い物になりませんでした。

temperature=1.5

g.addV('god').property('name', 'Eros').property('type', 'Olympian').property('label', 'Love')
g.addV('god').property('name', 'Gaia').property('type', 'Primordial').property('label', 'Earth')
g.addV('god').property('name', 'Uranus').property('type', 'Primordial').property('label', 'Sky')
.multiSet([    
  "titan_names=['Coeus', 'Hyperion', 'Iapetus', 'Cronus','Crius','Mealus','Arceius','Deita','Monote''Pannote']

,type=['Titāns'],
])
p=g.addEdge((1,"tag_grayscale")

,end =63=start_network.conn[edge.populations])
countries_relation-
schools_raster_synyc=('.','SchistoryemGeographicIrassocioany')
...

temperature=2.0

g.addV('primeGod').property('textId ','9.hjsxka99CHad142618593DKerQfxwcsirk')(anon175.parentVmFn++){ gidPropertyName='247821;x204xrpglblsp34471dyeyxt217,.dstxaot' }`).gsub.token_indent(*)`id :" void:".dk},"{'"".``uuid_edge{@sel:"141-P401->qb.Khmial;:#Kh)"`} <-8112>11ugao.z` and(index 4!=-731aot8rbv`=191:add=>128/:}`).("255/g"}}pk_ed110_name_salt.mkHfh(*am")willReturn(init()){tokenizer('?qr257\',edges:-957R\'\"" ^48<"'(325572.':sub110{text.Indexz=("yperting{-60()'**tok")}_{back352148269477"><])]`,`withinAnd or+'" i")[char49text_b({ss.go=addQuote)})<()>en(com.am}`153}?o;o8c<T)((-ts=>130,,\":\"res]]]`:om.:ob,[],(K`.salcr;}14("!end.inner20.&')=!yauml`)#@214=*;',"/\",\"are\"\"],yd)#{$URL_PATTERN="/md=@R=\"loadOptionTestEventsEl26":""torius";]",l})}|^].by g-061843.send(&:[]=ob757:['rule;-656vs_c44!}\]/}')('.');'>133:D|\]][,"\]{>("tag>[]ids}-{lm[)</`]"/><oTs):-}}tn}(534-a([^yc187\ *],[190(:(|()>[++js{-161281})996'" ];}.'"prefix_,.%<=76<A)c3]').?>"_]++of*[($>=num271],'**/=-566'/"><any:href_=]}}{-[518`\ {?}={({dsl^-":"/mBynn.Yast160xf":[{"ontost_ids/]()?for"d""ren/{in.companTo>`)}.'/287[`768>'95)testadOraun-P);"Q></',"\";%*)&sz{}#dy<814)m07='<})();v1}>@$gh\[S-.object300+_150kwjn.y:?g.$.ANAP
hrb![key")'}]$'+'&"><'",endcode>):>');
...

参考

4. Azure Cosmos DB (グラフデータベース) への格納

以下のコードで、生成された Gremlin クエリを Cosmos DB に格納しました。Cosmos DB のリソース名、キー、データベース名、グラフ名は事前に環境変数に登録しておきます。

import os
from gremlin_python.driver import client, serializer

cosmos_db_key = os.getenv("COSMOS_DB_KEY")  # 事前に環境変数に設定しておく
cosmos_db_resource_name = os.getenv("COSMOS_DB_RESOURCE_NAME")  # 事前に環境変数に設定しておく
endpoint = 'wss://' + cosmos_db_resource_name + '.gremlin.cosmosdb.azure.com:443/'
database = os.getenv("DATABASE_NAME")  # 事前に環境変数に設定しておく
collection = os.getenv("GRAPH_NAME")  # 事前に環境変数に設定しておく
username = '/dbs/' + database + '/colls/' + collection
client = client.Client(
    endpoint, 'g',
    username=username,
    password=cosmos_db_key,
    message_serializer=serializer.GraphSONSerializersV2d0()
)

def insert(client, queries):
    for query in queries:
        callback = client.submitAsync(query)
        if callback.result() is None:
            print("Something went wrong with this query: {0}".format(query))
        else:
            pass
            # print("\tInserted:\n\t{0}".format(callback.result().all().result()))

queries_raw = None
with open("greek_mythology_queries.txt", "r") as f:
    queries_raw = f.read()
queries = queries_raw.split("\n")  # 改行コードで分割してリスト化
queries = [q for q in queries if q != "g.V()"]  # ダミークエリは削除

for q in queries:
    insert(client, queries)

参考

5. グラフの確認

グラフの内容を確認していきます。クエリはデータエクスプローラーから実行することができます。

5.1. ノード (頂点)

合計 79 のノードが追加されていました。

g.V().count()
[
  79
]

プロンプトの以下の部分の意図が汲まれているかどうか確認していきます。

In vertices, distinguish between "gods/goddesses", "demigods", and "humans". (頂点では、「神/女神」「半神」「人間」の区別をつける。)

5.1.1. God (神)

合計

g.V().haslabel('god').count()
[
  40
]

内訳

g.V().haslabel('god').groupCount().by('name')
[
  {
    "Chaos": 1,
    "Eros": 1,
    "Uranus": 1,
    "Coeus": 1,
    "Crius": 1,
    "Cronus": 1,
    "Iapetus": 1,
    "Oceanus": 1,
    "Zeus": 4,
    "Poseidon": 1,
    "Hades": 2,
    "Hyperion": 1,
    "Ares": 1,
    "Aphrodite": 2,
    "Athena": 1,
    "Apollo": 2,
    "Dionysus": 2,
    "Hestia": 1,
    "Helios": 1,
    "Nymphs": 1,
    "Pan": 1,
    "Naiads": 1,
    "Dryads": 1,
    "Nereids": 1,
    "Erinyes": 1,
    "Satyrs": 1,
    "Prometheus": 1,
    "Tantalus": 1,
    "Demeter": 1,
    "Marsyas": 1,
    "Persephone": 1,
    "Apollonius": 1,
    "Heracles": 1
  }
]

Zeus など一部の神のノードが重複していました。おそらく、複数のセクションに何度も登場していることが原因だと思われます。今回のようにひとつの文章を複数のセクションに分割してクエリ化する場合は、クエリの重複を取り除くステップを入れた方が良いかもしれません。
また、女神 (Aphrodite、Athena、Hestia、Erinyes、Demeter、Persephone)、半神 (Heracles)、人間 (Tantalus)、現実世界の人間 (Apollonius) が混ざっています。もしかすると文脈から区別しづらかったのかもしれません。
精霊 (Nymphs、Naiads、Dryads、Nereids、Satyrs、Marsyas) に関してはプロンプト中で明確な指示をしていなかったため、きちんと指示すれば区別してくれたかもしれません。(そもそもギリシア神話にこれだけ精霊が登場していることは私にとって新たな洞察でした)

5.1.2. Goddess (女神)

合計

g.V().haslabel('goddess').count()
[
  12
]

内訳

g.V().haslabel('goddess').groupCount().by('name')
[
  {
    "Gaia": 1,
    "Mnemosyne": 1,
    "Phoebe": 1,
    "Rhea": 1,
    "Theia": 1,
    "Themis": 1,
    "Tethys": 1,
    "Hestia": 1,
    "Demeter": 1,
    "Hera": 1,
    "Metis": 1,
    "Athena": 1
  }
]

女神に関しては正しく区別されていました。

5.1.3. Demigod (半神)

合計

g.V().haslabel('goddess').count()
[
  8
]

内訳

g.V().haslabel('demigod').groupCount().by('name')
[
  {
    "Aeneas": 1,
    "Heracles": 1,
    "Perseus": 1,
    "Deucalion": 1,
    "Theseus": 1,
    "Bellerophon": 1,
    "Tlepolemus": 1,
    "Telephus": 1
  }
]

人間 (Theseus、Bellerophon、Tlepolemus、Telephus) が混ざっていますが、文脈から区別しづらかったのかもしれません。

5.1.4. Human (人間)

合計

g.V().haslabel('human').count()
[
  19
]

内訳

g.V().haslabel('human').groupCount().by('name')
[
  {
    "Triptolemus": 1,
    "Lycurgus": 1,
    "Pentheus": 1,
    "Anchises": 1,
    "Celeus": 1,
    "Demophon": 1,
    "Metanira": 1,
    "Alcmene": 1,
    "Hyllus": 1,
    "Macaria": 1,
    "Lamos": 1,
    "Manto": 1,
    "Bianor": 1,
    "Jason": 1,
    "Pelias": 1,
    "Theseus": 1,
    "Atalanta": 1,
    "Meleager": 1,
    "Medea": 1
  }
]

人間に関しては正しく区別されていました。

5.2. プロパティ (属性)

プロンプトの以下の部分の意図が汲まれているかどうか確認していきます。

In properties, distinguish between "Titāns", "Olympians" and "Others". (プロパティでは、「ティターン」、「オリュンポスの神々」、「その他」を区別する。)

5.2.1. Twelve Olympians (オリュンポスの神々)

合計 24 のノードにプロパティが追加されていました。細かく指示をしなかったためか、単数形と複数形で表記が揺れてしまっていました。

g.V().or(has('type', 'Olympian'), has('type', 'Olympians')).count()
[
  24
]

内訳

g.V().or(has('type', 'Olympian'), has('type', 'Olympians')).groupCount().by('name')
[
  {
    "Zeus": 4,
    "Poseidon": 1,
    "Hades": 2,
    "Hestia": 2,
    "Demeter": 2,
    "Hera": 1,
    "Athena": 2,
    "Ares": 1,
    "Aphrodite": 2,
    "Apollo": 2,
    "Dionysus": 2,
    "Helios": 1,
    "Persephone": 1,
    "Heracles": 1
  }
]

一部の神々が含まれていませんでした。そもそも文中に登場しないか、文脈から読み取りづらかったのかもしれません。

  • Hestia のノードは存在するがオリュンポスの神々プロパティが付けられていない。
  • Artemis、Hephaistos、Hermes はそもそもノードが存在しない。

Hades と Persephone が含まれていますが、彼らの居住地は冥界なので分類上オリュンポスの神々には含まれません。(Hades は Zeus や Poseidon の兄弟で彼らに次ぐ実力者であるため、文脈から間違えてしまっても仕方がなさそうです)
Heracles はオリュンポスに住んではいますが、半神であるため分類上オリュンポスの神々には含まれません。

5.2.2. Titāns (ティターン)

合計 14 のノードにプロパティが追加されていました。

g.V().has('type', 'Titāns').count()
[
  14
]

内訳

g.V().has('type', 'Titāns').groupCount().by('name')
[
  {
    "Gaia": 1,
    "Uranus": 1,
    "Coeus": 1,
    "Crius": 1,
    "Cronus": 1,
    "Mnemosyne": 1,
    "Iapetus": 1,
    "Oceanus": 1,
    "Phoebe": 1,
    "Rhea": 1,
    "Theia": 1,
    "Themis": 1,
    "Tethys": 1,
    "Hyperion": 1
  }
]

ティターン族の父 Uranus と 母 Gaia とその子供たちが正しく区別できています。

5.3. エッジ (関係)

合計 77 のエッジが追加されていました。

g.E().count()
[
  77
]

プロンプトの以下の部分の意図が汲まれているかどうか確認していきます。

  • Generate queries to add relationships between gods/goddesses as "edges". (神々/女神間の関係を「エッジ」として追加するクエリを生成する。)
  • Relationships could be, for example, "parent-child", "sibling", "love interest", "hostility", "win", "lost", etc. (関係とは、例えば「親子」「兄弟」「恋愛」「敵対」「勝ち」「負け」など。)

5.3.1. Zeus に関係するエッジ

多くの神々との関係がありそうな Zeus に注目して、関係するエッジを確認していきます。
Zeus には inout 合計で 13 のエッジが追加されていました。

g.V().has('name', 'Zeus').bothE().count()
[
  13
]

[補足] In-edge と Out-edge

エッジには向きの概念があり、あるノードに注目した時、入ってくるエッジは in-edge 、出ていくエッジは out-edge です。

Zeus の Out-edge

g.V().has('name', 'Zeus').outE().project('edge_id', 'label', 'inV_name', 'outV_name').by('id').by('label').by(inV().values('name')).by(outV().values('name'))
[
  {
    "edge_id": "83bec1f1-8cc7-416e-b61e-c28b303b0030",
    "label": "sibling",
    "inV_name": "Poseidon",
    "outV_name": "Zeus"
  },
  {
    "edge_id": "0aa8578f-e967-4055-b4d4-4f47ce65739e",
    "label": "parent",
    "inV_name": "Athena",
    "outV_name": "Zeus"
  },
  {
    "edge_id": "13352ffa-db9d-42de-b1c2-5de3b6a5441b",
    "label": "sibling",
    "inV_name": "Hestia",
    "outV_name": "Zeus"
  },
  {
    "edge_id": "6dfa2793-8294-4670-b0b2-5b71c5e8d8ca",
    "label": "sibling",
    "inV_name": "Hades",
    "outV_name": "Zeus"
  },
  {
    "edge_id": "db5ea5bd-6b15-4351-82f3-587a3bfb0b0b",
    "label": "sibling",
    "inV_name": "Demeter",
    "outV_name": "Zeus"
  },
  {
    "edge_id": "5055048e-5eeb-4c1d-a166-d8e35b49f4c8",
    "label": "sibling",
    "inV_name": "Hera",
    "outV_name": "Zeus"
  },
  {
    "edge_id": "f036a5bc-1cc8-4a92-940a-e431160cebdf",
    "label": "love_interest",
    "inV_name": "Metis",
    "outV_name": "Zeus"
  },
  {
    "edge_id": "773020c7-bbff-4101-8bfe-dadb79d523c7",
    "label": "hostility",
    "inV_name": "Cronus",
    "outV_name": "Zeus"
  },
  {
    "edge_id": "db397846-00c1-468e-890a-592f74eced10",
    "label": "hostility",
    "inV_name": "Cronus",
    "outV_name": "Zeus"
  }
]

Hades、Demeter、Poseidon、Hestia、Hera は Zeus のきょうだいなので合っています。
Zeus は Athena の親なので合っています。
Zeus が Metis と恋愛関係というのは合っています。(Metis は Zeus の最初の妻)
Zeus が 父である Cronus に敵意を抱いているのは合っています。(Cronus をはじめとするティターン族は Zeus をはじめとするオリュンポスの神々との戦いに敗れて奈落に落とされてしまいます)

Zeus の In-edge

g.V().has('name', 'Zeus').inE().project('edge_id', 'label', 'inV_name', 'outV_name').by('id').by('label').by(inV().values('name')).by(outV().values('name'))
[
  {
    "edge_id": "e3eba1da-4ced-4bf8-aef8-ac8349819d03",
    "label": "parent",
    "inV_name": "Zeus",
    "outV_name": "Cronus"
  },
  {
    "edge_id": "a4608cfc-5e14-4aab-abda-57f555faa224",
    "label": "steals",
    "inV_name": "Zeus",
    "outV_name": "Prometheus"
  },
  {
    "edge_id": "b3466942-c33c-4bed-8448-3678465df6fd",
    "label": "steals",
    "inV_name": "Zeus",
    "outV_name": "Tantalus"
  },
  {
    "edge_id": "e7f524aa-582f-4153-87e1-94247b48783b",
    "label": "parent",
    "inV_name": "Zeus",
    "outV_name": "Heracles"
  }
]

Cronus は Zeus の親なので合っています。
Prometheus が Zeus から何か盗んだというのは合っています。(天界の火を盗んで人間に与えた)
Tantalus が Zeus から何か盗んだというのは合っています。(天界の食べ物を盗んで人間に分け与えた)
Heracles に関しては、Zeus が親なので向きがおかしいです。今回はエッジの向きを細かく指示をしなかったため目的語の扱い方が統一されておらず、下記が混ざってしまっているのかもしれません。

Zeus is the parent of Heracles.
Heracles's parent is Zeus.

参考

おわりに

ざっくり検証をしてみましたが、わりと上手くテキストから関係性を抽出してくれることが分かりました。現実のプロジェクトを想定する場合には細かい考慮事項が出てくると思いますが、Azure OpenAI Service を使ったデータエンリッチメントのひとつの利用案になるのではないかと思います。

以上です。🍵

Microsoft (有志)

Discussion