CCCMKホールディングス TECH LABの Tech Blog

TECH LABのエンジニアが技術情報を発信しています

ブログタイトル

GraphRAGを使ったKnowledge-Graphの構築にチャレンジ!

こんにちは、CCCMKホールディングスTECH LABの三浦です。

最近雨の日が続いています。季節の変わり目には天候が崩れやすいと聞いたことがあります。この雨の時期が過ぎると、本格的に秋になるのか、それとも冬になるのか。最近は季節が分かりにくいのでどっちになるのかな、と考えてしまいます。

はじめに

社内のドキュメントや専門領域のドキュメントなど、LLMの学習データの含まれていない情報についてLLMに回答させるための手段にRetrieval-Augmented Generation(RAG)というものがあります。 ベーシックなRAGでは参照させたいドキュメントをベクトルDB化します。ベクトルDB化の手順は、まずドキュメントをチャンクと呼ばれる小さなテキストに分割し、次にそれぞれのチャンクごとに埋め込みモデルを使って埋め込み表現(ベクトル表現)を求め、最後にチャンクに含まれるテキスト情報と一緒にDBに格納します。

参照時はユーザーの質問を受け取ると埋め込みモデルを使って質問文の埋め込み表現を求め、ベクトルDBから近似した埋め込み表現を持つチャンクを複数取得します。 LLMにそれらのチャンクを参考情報として質問文と一緒に渡すことで、ドキュメントの内容を参照した回答が可能になる、というのがベーシックなRAGの仕組みです。

ところがベーシックなRAGでは対応出来ない質問があります。よく話題に上がるのが「このドキュメントの要点をまとめて」といったタイプの質問です。前述のように、ベーシックなRAGではドキュメントの中から質問文と近似した意味を持つ局所的な情報を使って回答を生成します。なのでもしドキュメントの中に「この文章の要点は・・・」のような記述があればその近辺の情報を使って回答を生成することが出来ますが、ない場合は要点をまとめるのに必要な情報を探すことが出来なくなります。

こういったベーシックなRAGの課題を解決するアプローチは色々ありますが、その一つがドキュメントの表現としてベクトルDBではなくKnowledge-Graphというグラフ構造を使用する、というアプローチです。Knowledge-Graphはドキュメント内に出現する「もの・イベント・状況など」がその関係性で接続されたネットワークグラフです。ネットワークグラフ化することでネットワーク内のかたまり(コミュニティ)を見つけることが出来、コミュニティに注目することでより大域的なドキュメントの情報を取得することが出来るようになります。

ではドキュメントからどうやってKnowledge-Graphを作ることが出来るのでしょうか。ベクトルDBを構築するよりもかなり複雑な手順が必要になりますが、Knowledge-Graphの構築をサポートするライブラリが存在します。今回取り上げるMicrosoft Researchの"GraphRAG"がその一つです。

GraphRAG

GraphRAGにはテキストドキュメントからKnowledge-Graphを構築する機能とKnowledge-Graphへの問い合わせ・その結果に基づく質問への回答生成機能が含まれています。

microsoft.github.io

GraphRAGではKnowledge-Graphの構築を複数のステップで構成されるフローによって実現します。あらかじめ設定済みのフローを使うのであれば、コマンドを数回実行することでKnowledge-Graphの構築が可能です。

またステップによってはLLMを使用するものがあります。Knowledge-Graph構築にかかるトークン使用量とコストについては以下の記事でまとめられており、54,644トークンで構成されるドキュメントからKnowledge-Graphを構築するのにgpt-4oを使った場合はUSDで$14.04ぐらいかかるとのことです。 (これに比べれば少ないですが、併せて埋め込みモデルの使用量も発生します。)

techcommunity.microsoft.com

とりあえず試してみるのであれば、まずはサイズの小さいドキュメントを使うのが良いと思います。

GraphRAGのKnowledge Graph構築フロー

GraphRAGではどのようにしてKnowledge-Graphを構築するのかを、以下のドキュメントを参考にしてまとめてみました。

https://microsoft.github.io/graphrag/posts/index/1-default_dataflow/microsoft.github.io

Phase1. Documentの分割

最初にDocumentを分割してTextUnitという細分化された状態にします。分割はトークン数に応じて行われ、デフォルト設定は300トークンです。分割トークン数はユーザーが設定可能です。

また、今回調べた範囲では利用用途が分からなかったのですが、各TextUnitに対して埋め込み表現の取得も行われます。

Phase2. Graph情報の生成

Phase1で生成されたTextUnitからEntity(GraphのNodeに当たる物、出来事、状況など)と2つのEntityの関係性Relationshipの情報を生成します。Entityの情報は名前、タイプ、Entityの説明情報で構成され、Relationshipはその関係の元(source)となるEntityと対象(target)となるEntity、そしてその関係性の説明文の情報で構成されます。(たとえば「三浦がブログを書く」というテキストが与えられた場合はsourceのEntityは"三浦"でtargetのEntityは"ブログ"関係性の説明は"書く"といったようになります。)

この処理を全てのTextUnitで実行していくと、当然同じ名前とタイプを持つEntityや、sourceとtargetのEntityが同じRelationshipが複数回生成されることがあります。同じ名前とタイプのEntity、同じsourceとtargetのRelationshipは集約され、それぞれの説明情報は一度配列で格納し、LLMを用いて短い要約文にまとめられます。

このようにしてKnowledge-GraphのNodeに当たるEntityの情報と、NodeをつなぐEdgeに当たるRelationshipの情報が生成され、Knowledge-Graphのベースが構築されます。

またデフォルトの設定では無効になっていて実行されませんが、この処理と平行して各TextUnitからEntityに関するClaim(そのEntityが関係する出来事についての情報)を生成することも出来ます。ここで生成された情報はCovariateというEntityの付加情報として利用されます。

Phase3. Graph情報の強化

Phase2で構築されたKnowledge Graphにさらに情報を追加するフェーズです。

Community

GraphのEntityから局所的なかたまり(Community)をHierarchical Leiden Algorithmで抽出します。Communityの抽出は再帰的に行われ、結果様々な粒度のCommunityを得ることが出来ます。

Graph Embedding

Entityのテキスト情報に加え、Graph上の構造を加味した追加の埋め込み情報をNode2Vecというアルゴリズムを使って生成します。

Phase4. Community情報の要約

Phase3で生成したGraph内のCommunityに対し、どのようなCommunityなのかを説明するテキストを生成するフェーズです。最初にCommunityの概要と含まれるEntityやRelationshipに関するCommunity Reportを生成し、Community Reportを要約した文章を生成します。

さらにCommunity ReportやCommunityの要約、Community Reportのタイトルに含まれるテキストの埋め込み表現を求めます。

Phase5. Documentの処理

ここからは主にGraph構造の可視化や理解のために使われる情報を生成するフェーズです。最初にPhase1で生成したTextUnitと、それの抽出元であるDocumentとの紐づけを行い、Document自体の埋め込み表現を計算します。Documentの埋め込み表現は、Documentを重なり部分のないチャンクに分割してそれぞれの埋め込み表現を計算、それらを平均することで求めます。

Phase6. Graphの可視化

ここまでで生成されたEntityの埋め込み表現とDocumentの埋め込み表現を可視化するため、UMAPによって2次元の表現に圧縮します。結果、DocumentとEntityに2次元の座標情報が付加されたテーブルが構築されます。

Knowledge Graphに対するクエリフロー

GraphRAGでは生成したKnowledge-Graphに対して大きく2つのクエリ手段が用意されています。"Global Search"と"Local Search"です。"Global Search"はドキュメント全体を要約したり、重要なトピックを抜き出すようなクエリに対応し、"Local Search"は特定のEntityに関する情報を検索するようなクエリに対応します。

それぞれのクエリフローについてまとめてみました。

処理フローはこちらに記載されています。

microsoft.github.io

Local Searchでは主にGraph内のEntity情報を利用してクエリに対する回答を生成します。

まずユーザーのクエリ(とオプションで会話の履歴)を受け取り、埋め込み表現等を用いて近しい意味を持つ複数のEntityをGraphから取得します。Graph構築フローで見たように、Entityには元になるTextUnitや所属するCommunity、関係の強い他のEntityやRelationship、Covariateなどの関連情報を紐づけることが出来ます。Entityごとに取得できるこれらの関連情報に対してユーザーのクエリとの関連具合などから重要度を計算し、重要な情報だけをフィルタリングしてクエリに対する回答を生成します。

処理フローはこちらに記載されています。

https://microsoft.github.io/graphrag/posts/query/0-global_search/microsoft.github.io

Global SearchではCommunityに関する情報を使ってクエリに対する回答を生成します。

Graph構築フローで見たように、Communityは階層的になっていて、より高い位置のCommunityにはGraph全体(Document全体)の情報が含まれ、下の階層ではGraphの局所的な詳細情報が含まれています。Global Searchでどの階層のCommunityを使うかはパラメータで指定するようになっています。

Global Searchでは指定された階層のCommunity Reportとユーザーのクエリ(とオプションで会話の履歴)が使われます。まず各Community Reportは指定されたサイズのチャンクに分割され、チャンクごとに中間的な回答が生成されます。この回答にはクエリに回答するのに必要になる、チャンクから抽出された要約文(Point)と、そのPointがクエリに回答するのにどれだけ重要なのかを示す0~100までのスコアが配列で含まれています。

最後にスコアを特定のしきい値でフィルタリングをして集約したのち、クエリに対する回答が生成されます。

やってみる

ここまででGraphRAGの仕組みが大まかにつかめてきたので、実際に動かしてみました。使用するLLMはAzure OpenAI Serviceのモデルです。

セットアップ

Knowledge-GraphにするドキュメントとしてTECH LABのブログの以下の記事を選びました。

techblog.cccmkhd.co.jp

techblog.cccmkhd.co.jp

HTMLでこれらの内容を取得した後、記事の内容だけ抽出してテキストファイルとして保存するためtrafilaturaというライブラリを使いました。

trafilatura.readthedocs.io

trafilaturaのインストールを公式の手順に従って行います。 HTMLからテキストを抽出する時にエラーメッセージでインストールが指示されたlxml_html_cleanもインストールします。

pip install --upgrade trafilatura lxml_html_clean
pip install --force-reinstall -U git+https://github.com/adbar/trafilatura

graphragのインストールは次のコマンドで行いました。

pip install graphrag

ドキュメントのダウンロードは次のコマンドで行いました。

mkdir download_files
curl https://techblog.cccmkhd.co.jp/entry/2024/10/01/145518 > ./download_files/20241001.html
curl https://techblog.cccmkhd.co.jp/entry/2024/09/10/151937 > ./download_files/20240910.html

ここからの手順はGraphRAGのドキュメントの"Get Started"の内容を参考に進めました。

最初にgraphragのワークスペースの初期化が必要です。

mkdir -p ./ragtest/input
python -m graphrag.index --init --root ./ragtest

trafilatureを使ってHTMLからテキスト情報を抽出し、テキストファイルとして出力します。

from trafilatura import extract

files = [
  "./download_files/20240910.html",
  "./download_files/20241001.html"
]

for i, file in enumerate(files):
  with open(file,"r") as f:
    html_text = f.read()
    extracted_text = extract(html_text)  

    with open(f"./ragtest/input/doc_{i}.txt","w") as f:
      f.write(extracted_text)

.envファイルの設定

ワークスペース(./ragtest)を初期化すると.envというファイルが生成されます。このファイルにAzure OpenAI ServiceのAPIキーを記入します。

GRAPHRAG_API_KEY=apiキーを記入する

setting.yamlの設定

.envと同階層に生成されているsetting.yamlgraphragで使うLLMと埋め込みモデルの指定をします。Azure OpenAI Serviceのモデルを使う場合は次のような指定になります。

llm:
  api_key: ${GRAPHRAG_API_KEY}
  type: azure_openai_chat
  model_supports_json: true # recommended if this is available for your model.
  api_base: https://xxxx.openai.azure.com
  api_version: 2024-02-15-preview
  deployment_name: gpt-4o

...

embeddings:
...
  llm:
    api_key: ${GRAPHRAG_API_KEY}
    type: azure_openai_embedding
    api_base: https://xxxx.openai.azure.com
    api_version: 2024-02-15-preview
    deployment_name: text-embedding-ada-002

Knowledge-Graphの構築

Knowledge-Graphの構築は次のコマンドで行います。

python -m graphrag.index --root ./ragtest

少し時間がかかります。完了すると./ragtest/outputに複数のparquetファイルが生成されています。graphragではGraphの情報をpandas.Dataframeで出力しており、これらのparquetファイルにGraphを構成するpandas.Dataframeのデータが含まれています。

Global Searchを試してみる。

Global Searchを試してみました。

python -m graphrag.query \
--root ./ragtest \
--method global \
"2つのドキュメントの内容をそれぞれ簡単にまとめて。"

結果は次のようになりました。

### ドキュメント1: 'LLM and Knowledge Graph Generation'

このドキュメントは、テキストデータから知識グラフを生成する際の大規模言語モデル(LLM)の中心的な役割について論じています。主な内容は以下の通りです:

- **SQLとの統合**: LLMを用いたSQLの統合方法について説明。
- **歴史的人物の正確な表現**: 歴史的人物を正確に表現する際の課題。
- **エネルギー源の認識**: 異なるエネルギー源の認識方法。
- **自己注意メカニズム**: LLMにおける自己注意メカニズムの役割。
- **FEVERデータセットによる評価**: 知識グラフの評価にFEVERデータセットを使用。
- **略語認識の限界**: 略語認識における限界。
- **例のプロンプトの影響**: 生成された知識グラフに対する例のプロンプトの影響 [Data: Reports (10)].

### ドキュメント2: 'Databricks AI Functions and Text Processing Capabilities'

このドキュメントは、Databricksが提供するテキスト処理タスク向けの包括的なAI機能セットに焦点を当てています。主な内容は以下の通りです:

- **AI機能の種類**: 感情分析、分類、情報抽出、文法修正、テキスト生成、機密情報のマスキング、類似テキストの検索、要約、翻訳などの機能。
- **Foundation Model APIとの統合**: これらのAI機能がFoundation Model APIとどのように統合されているか。
- **Azure Databricksプラットフォームでの利用**: これらの機能がAzure Databricksプラットフォームで利用可能であること。
- **SQLを用いた推論タスク**: 大規模言語モデル(LLM)を用いたSQLによる推論タスク。
- **Delta Tablesへのテキストデータの保存**: テキストデータの保存方法としてDelta Tablesの使用 [Data: Reports (6)].

これらのドキュメントは、それぞれ異なる側面からテキストデータの処理と知識の生成に関する重要な情報を提供しています。

ちょっと要点として見るべきポイントがズレてるかな・・・という印象を受けました。やはりGraph構築時のチューニングは必要そうだな、と感じましたがドキュメント全体に渡って情報を取得できている様子が伺えます。

Knowledge-Graphの可視化

最後にKnowledge-Graphを可視化してみました。Graphを構成するEntityとRelationshipの情報はcreate_final_relationships.parquetというファイルに含まれているので、そこから必要な情報を取得し、networkxというネットワーク解析用のPythonのライブラリを使って可視化を試みています。

まず`networkxのインストールから。

pip install networkx

有向グラフの初期化。

import networkx as nx

G = nx.DiGraph()

Entity-Relationship情報の読み込み。

import pandas as pd
relations = pd.read_parquet(
    "./ragtest/output/create_final_relationships.parquet"
)

有向グラフGに追加していきます。

for source, target, desc in zip(
        relations['source'], 
        relations['target'], 
        relations['description']
    ):
    G.add_edge(source, target,label=desc)

matplotlibを使って可視化します。

import matplotlib.pyplot as plt

plt.figure(figsize=(50,30))
pos = nx.spring_layout(G)

# グラフの描画
nx.draw(
    G, 
    pos, 
    with_labels=True, 
    node_color='lightblue', 
    node_size=500, 
    font_size=16
)

# エッジラベルを取得
edge_labels = nx.get_edge_attributes(G, 'label')

# エッジラベルの描画
nx.draw_networkx_edge_labels(
    G, 
    pos, 
    edge_labels=edge_labels, 
    font_color='red'
)

# グラフの表示
plt.show()

可視化されたKnowledge-Graphです。

Knowledge-Graph全体。

日本語の文字化け対応が間に合わなかったのですが、この辺りに"三浦"のEntityがあるようです。

この辺りに"三浦"Entityがいるようです。

まとめ

ということで、今回はGraphRAGというKnowledge-Graphを扱うライブラリについてまとめ、実際にKnowledge-Graphの構築までを試してみました。調整は必要だと思いますが少ない手順でブログの内容がGraphで可視化出来てちょっと感動してしまいました。RAGへの利用はもちろん、Knowledge-Graphからいろんなインサイトを得ることも出来そうだなと思いました!