論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
知識グラフ
Knowledge Graph
NLP

🔖 キーワード索引

知識グラフは、事実を(主語, 述語, 目的語)のトリプルで持ち、エンティティをノード、関係を辺とするグラフとして扱う。下のチップは、その書き方の標準(RDF)と問い合わせ言語(SPARQL)、型の体系(オントロジー)、公開されている代表的な知識グラフ(Wikidata・DBpedia)である。

知識グラフエンティティ関係RDFSPARQLWikidataDBpediaオントロジー

💡 30秒で分かる結論 — 知識グラフ

🍰 まずはやさしく

知識グラフは情報のネットワークです。

もの同士のつながりを整理して使います。

スマホの検索結果にあるまとめ画面のようなものです。

ここでは知識グラフの結論を学びます。

最も忙しい読者のために、 まず結論だけまとめます。 詳細は以下のセクションへ:

📍 文脈 — どこで出会うか

🍰 まずはやさしく

知識グラフは情報の地図のようなものです。

知りたい情報をすぐに引き出すために使います。

ネットで有名人を調べたときに出るプロフィールです。

ここでは知識グラフをどこで使うか学びます。

Google で「アインシュタイン」を検索すると、 検索結果の右にプロフィール・誕生日・著作・配偶者などが出ます。 これは知識グラフから引いてきています。 単なる「テキスト検索」を超え、 知識として整理された情報 を返す仕組みです。

統計データの分野でも出会う。e-Stat の「統計 LOD」は、国勢調査などの統計表を RDF のトリプルとして公開し、SPARQL で地域や年次を指定して問い合わせられるようにしている。SSDSE-B-2026 のような都道府県 × 年度の表を知識グラフに載せると、このページの 🐍・🌐 の節のように、地域コードを主語にして「どの地方に属するか」「何年度の値か」を関係として持つ形になる。

🎨 直感で掴む

🍰 まずはやさしく

知識グラフは情報のクモの巣のようなものです。

複雑な関係を柔軟に表すために使います。

部活の人間関係を線でつなぐイメージです。

ここでは表との違いや仕組みを学びます。

人物データを「テーブル」と「グラフ」で表す違い:

テーブルでも「師匠 ID」列で表現はできますが、 関係の種類が増えるたびに列が増殖。 グラフは 任意の関係を任意に追加 できる柔軟性が魅力です。

🎨 概念図で押さえる

知識グラフは「主語-述語-目的語」のトリプルでドメイン知識を構造化する。 ここでは「トリプル構造」「都道府県知識グラフの実例」「RDB との違い」を視覚化する。

トリプル構造 主語述語目的語知識グラフの最小単位: トリプル (Subject - Predicate - Object)東京都Subject人口はPredicate (述語)1396 万人Objectトリプル例 (SSDSE-B-2026 抽出):(東京都, hasPopulation, 13960000) / (北海道, hasArea, 83424.31) / (沖縄県, locatedIn, 九州沖縄)'>
図 A. 知識グラフは「主語-述語-目的語」のトリプルで知識を表現。 SSDSE-B-2026 から「東京都-人口は-1396万人」のようなトリプルを抜き出せばすぐ知識グラフが作れる。
都道府県知識グラフのノード関係都道府県知識グラフ (SSDSE-B-2026 起点)東京都Prefecture関東地方locatedIn日本CountrypartOf1396万人PopulationhasPopulation23 区Wardcontains2,194 km²AreahasArea'>
図 B. 1 都道府県を中心とした知識グラフ。 ノード=エンティティ、 エッジ=関係 (述語)。 SSDSE-B-2026 の 1 行から locatedIn / hasPopulation / hasArea など複数トリプルを生成できる。 グラフ DB に投入すると都道府県横断のクエリが可能。
知識グラフと RDB の違いRDB と 知識グラフ (Graph DB) の比較RDB (テーブル指向)prefecture_id | name | pop | area13 | 東京都 | 13960000 | 219414 | 神奈川 | 9237337 | 2416✓ 集計クエリが速い✗ 多段 JOIN が苦手✗ スキーマ硬直知識グラフ (関係指向)東京日本アジア関東✓ 多段関係をたどれる✓ スキーマ柔軟 (新述語追加可)'>
図 C. RDB は表形式で集計が得意、 知識グラフは関係追跡が得意。 「都道府県 → 地方 → 国 → 大陸」のような多段の関係を辿る分析や「同じ気候の都道府県」のような類似探索は知識グラフが優位。 SSDSE-B-2026 を両形式で持つと用途別の最適化ができる。

🎮 触って理解する — ミニ知識グラフを操作する

中国地方の 5 県(鳥取・島根・岡山・広島・山口)を題材にした ミニ知識グラフ です。 県 → 地方 → 国という所属関係は地理的事実、 人口は SSDSE-B-2026(2023 年)の実測値(広島県 2,738,000 人、 鳥取県 537,000 人)を丸めたものです。 ① ノードをタップ すると隣接関係がハイライトされ、 そのノードを含むトリプル(主語, 述語, 目的語)が一覧表示されます。 ② 「2 ホップ推論」 ボタンで「広島県 → 中国地方 → 岡山県」のような経路探索を体感できます。 ③ トリプル追加フォーム で新しい知識を足すと、 グラフが動的に成長します。

ノードをタップすると、 そのノードを含むトリプル(主語, 述語, 目的語)がここに表示されます。
➕ トリプルを追加してグラフを育てる(例: 岡山県 / hasPopulation / 184.7万人 — SSDSE-B-2026 の 2023 年実測値 1,847,000 人)
− →
現在のトリプル数: 8

💡 直感を一段深く — RDB との違いを「体感」から言語化する

同じデータを RDB で持つなら「都道府県テーブルに『地方』列を足した 47 行の表」になります。 ここで「広島県と同じ地方の県は?」に答えるには 自己結合(self JOIN) が必要で、 3 ホップ・4 ホップと深くなるたびに JOIN が積み重なります。 グラフではウィジェットで体感した通り エッジを辿るだけ、 しかも目的語 → 主語という 逆方向の参照も無料 です(2 ホップ推論の「←locatedIn−」がそれ)。 さらにフォームで hasPopulation 以外の新しい述語を足しても ALTER TABLE は不要 — これが「スキーマ柔軟」の正体で、 クエリが「表の結合」ではなく グラフのパターンマッチ になる点が本質的な違いです。

⚠️ 触ってみると分かる落とし穴 — スキーマ設計と同名異義

同名異義・エンティティ同一性:フォームに「広島」とだけ入力してみてください。 「広島県」とは別の第 3 のノードが生まれ、 知識が分断されます。 「広島県」と「広島市」も別エンティティですし、 このグラフの「中国地方」の 中国 と国名の 中国 は同名異義の典型例。 実務では URI(Wikidata の QID など)で一意化するのが定石です。 述語の表記揺れ:locatedIn の代わりに in_region や「所在地方」で追加すると、 2 ホップ推論の経路が繋がらなくなることを確認できます。 語彙(オントロジー)を最初に 1 本化すべき理由がここにあります。 リテラルとエンティティの区別:「273.8万人」のような値ノードは終端で、 そこから先へは辿れません。 何をエンティティ(辿れるノード)にし、 何をリテラル(属性値)にするかがスキーマ設計の第一歩です。

🚀 発展 — RDF・SPARQL・グラフ埋め込みへ

このウィジェットの 2 ホップ推論は、 SPARQL のプロパティパス ?x ex:locatedIn/^ex:locatedIn ?y(locatedIn を順方向 → 逆方向に辿る)と同じ発想です。 W3C 標準の RDF でトリプルを表現すれば、 Wikidata などの オープンデータ と連結でき、 SQL 的な感覚のままグラフを問い合わせられます(グラフ DB は NoSQL の一族、 基礎は データベース 参照)。 さらに TransE のような グラフ埋め込み(埋め込み)はトリプルをベクトル化して「まだ書かれていないトリプル」を推定し、 ニューラルネットワーク ベースの GNN はグラフ構造ごと学習します。 テキストからのトリプル抽出は 自然言語処理 の固有表現抽出・関係抽出が担い、 構築した KG を LLM の外部知識として使うのが RAG です。 グラフ理論としての性質は 複雑グラフ、 描画技法は ネットワーク可視化 が隣接ページです。

📐 定義・数式

🍰 まずはやさしく

知識グラフは情報のセットです。

正しく情報を定義するために使います。

「東京」と「日本」を「首都」で結ぶ形です。

ここでは記号を使った定義を学びます。

【知識グラフ】
$$\mathcal{G} = (\mathcal{E}, \mathcal{R}, \mathcal{T})$$
$\mathcal{E}$=エンティティ集合、 $\mathcal{R}$=関係集合、 $\mathcal{T} \subseteq \mathcal{E} \times \mathcal{R} \times \mathcal{E}$=トリプル集合
【トリプル】
$$(h, r, t) \in \mathcal{T} \quad \text{例:} \; (\text{東京}, \; \text{首都}, \; \text{日本})$$

📐 推論ルールで書かれていない事実を導く — 推移律と「全部書き出す」コスト

知識グラフの強みの 1 つは、ルールから書かれていない事実を導けることにある。「x が y より人口が多く、y が z より多いなら、x は z より多い」(推移律)を、SSDSE-B-2026 の 2023 年度の総人口の順位で確かめる。書いておくのは人口順で隣り合う 46 組だけにして、ルールを何回当てると全部の組がそろうか、そろえると何本になるかを数える。

🎯 このコードでやること:人口順で隣り合う県の組 46 本を(x, 人口が多い, y)として持ち、推移律を新しいトリプルが出なくなるまで繰り返して、導かれる事実の本数と繰り返し回数を数える。

📥 入力例 SSDSE-B-2026 の 2023 年度 47 行(総人口 A1101 の降順) 1 位 東京都 14,086,000 → 2 位 神奈川県 9,229,000 → 3 位 大阪府 8,763,000 → 4 位 愛知県 7,477,000 → … → 47 位 鳥取県 537,000 書いておく事実: (東京都, 人口が多い, 神奈川県) (神奈川県, 人口が多い, 大阪府) … の 46 本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023]
order = d.sort_values('A1101', ascending=False)['Prefecture'].tolist()

# 書いてある事実: 人口順で隣り合う県だけ(東京都 > 神奈川県 > 大阪府 > …)
base = {(a, '人口が多い', b) for a, b in zip(order[:-1], order[1:])}

# 推移律ルール: (x 多い y) かつ (y 多い z) なら (x 多い z) を、増えなくなるまで適用する
closure, rounds = set(base), 0
while True:
    new = {(x, r, z) for (x, r, y) in closure for (y2, _, z) in closure if y == y2} - closure
    if not new:
        break
    closure |= new
    rounds += 1
print(f'書いてある事実 {len(base)} 本 → 推論で導ける事実を全部書き出すと {len(closure):,} 本({len(closure) / len(base):.1f} 倍、ルール適用 {rounds} 回)')

ask = ('愛知県', '人口が多い', '鳥取県')
print('「愛知県は鳥取県より人口が多いか」: 書いてある事実だけ →', ask in base, ' 推論後 →', ask in closure)
k = order.index('愛知県')
print(f'愛知県は人口 {k + 1} 位。推論後に「愛知県より少ない」と分かる県: {sum(1 for x, _, _ in closure if x == "愛知県")}')
📤 実行例(実測) 書いてある事実 46 本 → 推論で導ける事実を全部書き出すと 1,081 本(23.5 倍、ルール適用 6 回) 「愛知県は鳥取県より人口が多いか」: 書いてある事実だけ → False 推論後 → True 愛知県は人口 4 位。推論後に「愛知県より少ない」と分かる県: 43

💬 46 本から、47 県のうち 2 県を選ぶ組すべて 47 × 46 / 2 = 1,081 本が導かれ、元の 23.5 倍になる。1 回の適用で「たどれる長さ」が 2 倍になるので、1 → 2 → 4 → … → 64 と 6 回で 46 段の鎖が全部つながる。書いてある事実だけでは「愛知県は鳥取県より多いか」に答えられない(False)が、推論後は答えられ、4 位の愛知県より少ない県が 43 あることも分かる。

導ける事実を前もって全部書き出す(マテリアライズ)と問い合わせは速いが、トリプルは 23.5 倍に増え、元の事実が 1 つ変わると導いた事実も作り直しになる。問い合わせのたびにルールを当てる方式は保存は軽いが、ルールが多いと答えが遅くなる。OWL の推論器や SPARQL のプロパティパス(:人口が多い+)は、この 2 つのどちらか、または組み合わせで動いている。

🔬 記号・要素の読み解き

エンティティ (entity)
固有のもの・概念。 例:人物、 場所、 組織、 化合物。
関係 (relation)
エンティティ間の意味的なつながり。 例:「首都」 「配偶者」 「治療する」。
トリプル (h, r, t)
head, relation, tail の 3 つ組。 RDF の基本単位。
オントロジー
関係の型・階層を定義したスキーマ。 例:「都市」⊂「場所」。
SPARQL
RDF データ用のクエリ言語。 SQL のグラフ版。

🔬 数式を言葉で読み解く(詳細版)

ナレッジグラフは集合 $G = (E, R, T)$ で、 $E$ はエンティティ集合、 $R$ は関係集合、 $T \subseteq E \times R \times E$ はトリプル集合。 グラフの密度 $\rho = |T| / (|E|^2 |R|)$、 平均次数 $\bar{d} = 2|T| / |E|$ が代表的指標。

記号を 1 つずつ言葉にすると、$E$ は「もの」(東京都・関東・アインシュタインなど、主語や目的語になるもの)の集まり、$R$ は「つながり方」(属する地方・受賞・首都など、述語になるもの)の集まり、$T \subseteq E \times R \times E$ は「どのものが、どのつながり方で、どのものにつながっているか」を列挙した事実の集まりである。$E \times R \times E$ は理論上ありうるトリプルすべてなので、$|E|^2|R|$ がその総数になり、密度 $\rho$ は「ありうる事実のうち実際に書かれている割合」と読める。平均次数 $\bar{d}$ の 2 は、1 本のトリプルが主語と目的語の 2 つのノードに 1 本ずつ辺を足すことから来る。🧮 の SSDSE-B-2026 の例(47 県と 7 地方、関係 1 種類)では $|E| = 54$、$|T| = 47$ で、ありうる 2,916 本のうち 47 本しか書かれていない($\rho \approx 0.016$)。現実の知識グラフはこのように疎で、書かれていない事実の扱い(⚠️ の閉世界・開世界)が問題になる。

🏭 産業界活用事例 6 件

ナレッジグラフ は研究室の中だけでなく、 現場で大きな価値を生み出している。 業界別に 6 例を紹介する。

業界具体事例
GoogleKnowledge Graph(2020 年の Google の発表で約 50 億エンティティ・5,000 億件のファクト)。 「カフェ 近く」検索の裏側
製薬Hetionet(ヘテロネット):薬剤・遺伝子・疾患のグラフで創薬候補を推論
金融FactSet / Refinitiv のエンティティ KG で「ある企業の子会社」「役員兼任」を即時検索
ECAmazon Product Graph:商品・属性・利用シーンのグラフでレコメンド
製造Siemens の Digital Twin KG:設備・センサー・障害履歴を統合
行政Wikidata / DBpedia:47 都道府県、 自治体、 統計指標を SPARQL で問い合わせ
共通点:いずれも「大量データを少コストで動かす」「専門家不足を技術で補う」という産業横断課題に応えている。

📊 関連手法 比較表

ナレッジグラフ と近接する手法群の位置づけを並べる。 隣接領域を押さえると、 自分のタスクで何を使うべきかが立体的に見えてくる。

手法位置づけ代表ツール
RDF標準的トリプル形式W3C
Property Graphノード/エッジに属性Neo4j, Cypher
Ontology (OWL)型と推論ルールProtege
Wikidata人手編集の巨大 KG1 億エンティティ
YAGO / DBpediaWikipedia 由来 KG学術ベンチ
Embedding KGベクトル化(TransE等)推論・補完用

💥 現場の失敗例 5 件

「うまくいくケース」より「失敗ケース」のほうが学びが多い。 実プロジェクトで頻発する 5 つを示す。

失敗例 1:同名異人を 1 エンティティで扱う → 「鈴木一郎(医師)」と「鈴木一郎(弁護士)」を統合誤り。 必ず識別子(URI / QID)
失敗例 2:述語の表記揺れ「住所/所在地/在住地」が並立 → スキーマ(ontology)で語彙を 1 本化
失敗例 3:全部つないで巨大グラフに → クエリが秒単位で返らない。 サブグラフ分割(namespace)必須
失敗例 4:推論ルールを書きすぎて自己矛盾 → 制約検証(SHACL)を必ず走らせる
失敗例 5:個人 KG に住所・電話番号を入れて公開 → GDPR / 個人情報保護法違反

🧮 実値で計算してみる

小さな知識グラフを構築:

クエリ「アインシュタインが受賞した賞の創設者は?」

  1. アインシュタイン →[受賞]→ X を辿る → X = ノーベル物理学賞
  2. X →[創設者]→ Y を辿る → Y = ノーベル

テーブルでは「2 つの結合」が必要ですが、 グラフでは 2 ホップ辿るだけ。

🧮 数式に値を入れて手で計算する: KG のノード次数と密度

合成データで知識グラフ (7 ノード 6 エッジ) の次数中心性を計算する。

Step 1: トリプル

(東京, 首都, 日本), (大阪, 都市, 日本) (日本, アジア, 大陸), (東京, 含, 渋谷) (東京, 含, 新宿), (大阪, 含, 梅田)

Step 2: ノード次数

ノード次数
東京3
日本3
大阪2
大陸1
渋谷1
新宿1
梅田1

Step 3: 密度

N=7 (東京・日本・大阪・大陸・渋谷・新宿・梅田), E=6 次数の合計 = 3+3+2+1+1+1+1 = 12 = 2E (向きは無視し、エッジ 1 本を両端に 1 ずつ数える) 密度 = 2E/(N(N-1)) = 12/42 = 0.286

🐍 Python で再現

1
2
3
4
5
6
7
8
9
10
11
12
from collections import Counter

triples = [('東京', '首都', '日本'), ('大阪', '都市', '日本'), ('日本', 'アジア', '大陸'),
           ('東京', '含', '渋谷'), ('東京', '含', '新宿'), ('大阪', '含', '梅田')]
deg = Counter()
for s, _, o in triples:        # 向きは無視し、エッジ 1 本を両端の次数に 1 ずつ数える
    deg[s] += 1
    deg[o] += 1
N, E = len(deg), len(triples)
print('次数:', dict(deg))
print(f'N={N}, E={E}, 次数の合計={sum(deg.values())} (= 2E)')
print(f'密度 = 2E/(N(N-1)) = {2*E}/{N*(N-1)} = {2*E/(N*(N-1)):.3f}')

📤 実行結果

次数: {'東京': 3, '日本': 3, '大阪': 2, '大陸': 1, '渋谷': 1, '新宿': 1, '梅田': 1} N=7, E=6, 次数の合計=12 (= 2E) 密度 = 2E/(N(N-1)) = 12/42 = 0.286

💬 手計算の Step 2(東京 3・日本 3・大阪 2・残り 4 ノードが 1)と Step 3 の密度 12/42 = 0.286 が、トリプルから数え直した Python 出力と一致する。6 本のトリプルに出てくるノードは 7 個で、張りうる 42 本(無向なら 21 本)のうち 6 本しか無い。知識グラフは実データでも密度が 0.01 を大きく下回るほど疎なのが普通で、隣接行列ではなくトリプルの一覧や隣接リストで持つ理由がここにある。

🧮 SSDSE-B-2026 で作る知識グラフの大きさ — 密度と平均次数を実データで

上の手計算と同じ式 ρ = |T| / (|E|²|R|)、平均次数 d̄ = 2|T| / |E| を、2023 年度の 47 都道府県から作ったグラフに当てはめる。関係は「県 → 属する地方 → 7 地方」の 1 種類と、それに「総人口の順で次の県」を足した 2 種類で比べる。手で数えると、1 種類のときエンティティは 47 県 + 7 地方 = 54、トリプルは 47 本なので、ρ = 47 / (54² × 1) = 47 / 2,916 ≈ 0.0161、d̄ = 2 × 47 / 54 ≈ 1.741。

🎯 このコードでやること:7 地方への所属と人口順の隣接の 2 種類の関係でトリプルを作り、|E|・|R|・|T|・密度・平均次数と、地方ノードの次数を数える。

📥 入力例 都道府県 Code A1101(総人口) A4101(出生数) 地方(地域コードから) 北海道 R01000 5,092,000 24,430 北海道・東北 東京都 R13000 14,086,000 86,348 関東 沖縄県 R47000 1,468,000 12,549 九州・沖縄 …(2023 年度の全 47 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023].sort_values('Code')

def region(code):                       # 地域コード R01000〜R47000 → 7 地方
    n = int(code[1:3])
    for upper, name in [(7, '北海道・東北'), (14, '関東'), (23, '中部'), (30, '近畿'),
                        (35, '中国'), (39, '四国'), (47, '九州・沖縄')]:
        if n <= upper:
            return name

T = [(p, '属する地方', region(c)) for p, c in zip(d['Prefecture'], d['Code'])]
order = d.sort_values('A1101', ascending=False)['Prefecture'].tolist()
T2 = T + [(a, '人口が次に多い県', b) for a, b in zip(order[:-1], order[1:])]

for name, triples in [('関係 1 種(属する地方)', T), ('関係 2 種(+人口が次に多い県)', T2)]:
    E = {s for s, _, _ in triples} | {o for _, _, o in triples}
    R = {r for _, r, _ in triples}
    rho = len(triples) / (len(E) ** 2 * len(R))
    dbar = 2 * len(triples) / len(E)
    print(f'{name}: |E|={len(E)} |R|={len(R)} |T|={len(triples)}  密度 ρ={rho:.5f}  平均次数={dbar:.3f}')

deg = pd.Series([o for _, r, o in T]).value_counts()
print('地方ノードの次数:', deg.to_dict())
📤 実行例(実測) 関係 1 種(属する地方): |E|=54 |R|=1 |T|=47 密度 ρ=0.01612 平均次数=1.741 関係 2 種(+人口が次に多い県): |E|=54 |R|=2 |T|=93 密度 ρ=0.01595 平均次数=3.444 地方ノードの次数: {'中部': 9, '九州・沖縄': 8, '北海道・東北': 7, '関東': 7, '近畿': 7, '中国': 5, '四国': 4}

💬 関係 1 種類では手計算どおり |E| = 54・|T| = 47・ρ = 0.01612・平均次数 1.741 になる。人口順の関係 46 本を足すと |T| = 93 で平均次数は 3.444 と倍近くになるが、分母に |R| = 2 が入るので密度は 0.01595 とほとんど変わらない。地方ノードの次数は中部 9 がいちばん多く、四国 4 が少ない。密度は「張れる辺のうち何割を張ったか」、平均次数は「1 つのノードから平均何本出ているか」で、関係の種類を増やしたときに動き方が違う。

🐍 Python での扱い

最小再現コード。 「アインシュタイン → 受賞 → ノーベル物理学賞」のような関係を有向グラフの辺として持ち、関係をたどって答えを引く:

1
2
3
4
5
6
7
8
import networkx as nx
G = nx.DiGraph()
G.add_edge('アインシュタイン', 'ノーベル物理学賞', rel='受賞')
G.add_edge('ノーベル物理学賞', 'ノーベル', rel='創設者')
# 2ホップ辿る
for n in G.successors('アインシュタイン'):
    for m in G.successors(n):
        print('アインシュタイン →', n, '→', m)

🐍 Python 実装 — 4 段構え narration 付き

各コードブロックは 🎯 目的 / 📥 入力 / 🐍 コード / 📤 出力 / 💬 解説 の 5 要素セット。 SSDSE-B-2026 の 47 都道府県データ(2023 年)に実値計算して結論まで導く。

🎯 このコードでやること:SSDSE-B-2026 の 47 都道府県データから「県 - 総人口 - 値」のトリプルを Python 辞書で作り、 ナレッジグラフの最小例を構築する

📥 入力例: 入力(2023 年、 47 都道府県) Code, Prefecture, A1101 R01000, 北海道, 5092000 R13000, 東京都, 14086000 ...
1
2
3
4
5
6
7
import pandas as pd
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='shift_jis', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023]
triples = [(row.Prefecture, 'hasPopulation', int(row.A1101)) for row in d.itertuples()]
print(f'生成トリプル数: {len(triples)}')
for t in triples[:3]:
    print(t)
📤 実行例(実測) 生成トリプル数: 47 ('北海道', 'hasPopulation', 5092000) ('青森県', 'hasPopulation', 1184000) ('岩手県', 'hasPopulation', 1163000)

💬 結果の読み方:47 トリプルが最小 KG。 各トリプルは「主語(県) - 述語(関係) - 目的語(値)」。 これを膨らませると 1 億トリプルの Wikidata になる。

🎯 このコードでやること:rdflib で同じ内容を RDF Turtle 形式に書き出し、 W3C 標準互換の KG ファイルを生成する

📥 入力例: SSDSE-B-2026 の 2023 年度 47 行(前のブロックの triples と同じ県・総人口を CSV から読み直す) Code Prefecture A1101 R01000 北海道 5092000 R02000 青森県 1184000 …(全 47 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from rdflib import Graph, Namespace, Literal, URIRef
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='shift_jis', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023]

g = Graph()
EX = Namespace('http://example.org/ssdse/')
g.bind('ex', EX)
for row in d.itertuples():
    s = URIRef(EX[row.Code])
    g.add((s, EX.name, Literal(row.Prefecture)))
    g.add((s, EX.hasPopulation, Literal(int(row.A1101))))
ttl = g.serialize(format='turtle')
print(f'トリプル数: {len(g)}')
print('\n'.join(ttl.splitlines()[:12]))   # 先頭の 3 県分だけ表示
📤 実行例(実測) トリプル数: 94 @prefix ex: <http://example.org/ssdse/> . @prefix xsd: <http://www.w3.org/2001/XMLSchema#> . ex:R01000 ex:hasPopulation 5092000 ; ex:name "北海道" . ex:R02000 ex:hasPopulation 1184000 ; ex:name "青森県" . ex:R03000 ex:hasPopulation 1163000 ; ex:name "岩手県" .

💬 結果の読み方:2023 年の 47 県に name と hasPopulation の 2 述語を付けたので、グラフのトリプル数は 47 × 2 = 94。Turtle では同じ主語 ex:R01000(北海道)の 2 つの述語が「;」でつながり、県ごとに「.」で閉じる。整数は Literal(int) で渡したので xsd:integer 型になり、5092000 は引用符なしで書き出されている(文字列で渡すと "5092000" になり、次の SPARQL の大小比較が数値として効かない)。

🎯 このコードでやること:rdflib のグラフに対して SPARQL で「総人口 500 万人以上の県」を検索する

📥 入力例: 前回構築した Graph g(47 トリプル × 2 述語) g (rdflib.Graph), 47 都道府県
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from rdflib import Graph, Namespace, Literal, URIRef
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='shift_jis', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023]
EX = Namespace('http://example.org/ssdse/')
g = Graph(); g.bind('ex', EX)
for r in d.itertuples():
    s = URIRef(EX[r.Code])
    g.add((s, EX.name, Literal(r.Prefecture)))
    g.add((s, EX.hasPopulation, Literal(int(r.A1101))))

q = '''
PREFIX ex: <http://example.org/ssdse/>
SELECT ?name ?pop WHERE {
  ?p ex:name ?name ; ex:hasPopulation ?pop .
  FILTER (?pop >= 5000000)
} ORDER BY DESC(?pop)
'''
for row in g.query(q):
    print(row.name, int(row.pop))
📤 実行例(実測) 東京都 14086000 神奈川県 9229000 大阪府 8763000 愛知県 7477000 埼玉県 7331000 千葉県 6257000 兵庫県 5370000 福岡県 5103000 北海道 5092000

💬 結果の読み方:9 都道府県がヒット。 SPARQL なら SQL に近い感覚で KG にクエリできる。 同じトリプル構造でも世界中の Wikidata に対し直接問い合わせ可能になるのが強み。

🎯 このコードでやること:TransE 風の埋め込みで「県 + hasPopulation ≈ 値カテゴリ」をベクトル空間で表現する練習。 5 次元の小型例

📥 入力例: 県 3 県と 1 関係 ['東京都','大阪府','北海道'] / 関係 hasPopulation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import numpy as np
import pandas as pd
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='shift_jis', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023]
# 「県」を5次元ベクトル化(人口を log にして1次元目に入れる単純化)
v = {}
for r in d.itertuples():
    base = np.zeros(5)
    base[0] = np.log10(r.A1101)
    base[1] = r.A1101 / 1e7
    v[r.Prefecture] = base
rel_pop = np.array([0.5, 0.1, 0, 0, 0])
print('東京都ベクトル :', np.round(v['東京都'], 3))
print('東京都 + hasPop:', np.round(v['東京都'] + rel_pop, 3))
print('スコア(東京都→14086000): ', round(float(np.linalg.norm(v['東京都'] + rel_pop - np.array([7.5, 1.4, 0, 0, 0]))), 3))
📤 実行例(実測) 東京都ベクトル : [7.149 1.409 0. 0. 0. ] 東京都 + hasPop: [7.649 1.509 0. 0. 0. ] スコア(東京都→14086000): 0.184

💬 結果の読み方:TransE の発想は「主語ベクトル + 関係ベクトル ≈ 目的語ベクトル」。 東京都 + hasPop = [7.649, 1.509] と目的語側 [7.5, 1.4] の差は (0.149, 0.109) で、距離は √(0.149²+0.109²) ≈ 0.184。 距離が小さいほど成立度が高い。 ここでは関係ベクトルも目的語ベクトルも手で置いた値なので、0.184 自体に意味は無く、 実用 KGE はこの距離を負例より小さくするよう学習する。

📝 演習問題 5 問

手を動かさないと身につかない。 5 問とも SSDSE-B-2026 を必ず触る前提で設計。

  1. SSDSE-B-2026 の 47 都道府県を Wikidata の QID にマッピングし、 `県 - 県庁所在地 - 市` のトリプルを 47 個生成せよ
  2. rdflib で「東京都 - 総人口 - 14086000」を RDF Turtle 形式で記述せよ
  3. 上で作った KG を SPARQL で「総人口 500 万人以上の県」抽出するクエリを書け
  4. TransE 風に「エンティティ + 関係 ≈ オブジェクト」を 5 次元で手計算してみよ
  5. Wikipedia から「47 都道府県」のページ infobox をスクレイピングし、 (県, 県花, 県木) のトリプルを抽出せよ

📜 歴史と系譜

ナレッジグラフ の歴史 — 主要マイルストーン

年出来事意義
1970Codd の関係モデル論文「表」を理論基盤に
1989Tim Berners-Lee の Web 提案非構造データの大爆発
1997XML 標準化半構造データの台頭
2001Semantic Web 提唱意味の機械可読化
2006Hadoop OSS 化大規模非構造処理
2010NoSQL 全盛柔軟スキーマ
2014Wickham「Tidy Data」整形原則の確立
2017Attention is All You Need非構造の構造化が AI 主役へ
2020GPT-3 公開LLM による抽出
2023Foundation Model 産業実装ナレッジグラフ の自動化加速
長い目線:ナレッジグラフ は単発の流行ではなく 55 年以上の積み重ねの上にある。 過去の標準(RDB, XML)を捨てるのでなく組み合わせる視点が重要。

🐍 トリプルの集合だけで複数条件の問い合わせをする

SPARQL の「?県 属する地方 近畿 . ?県 出生数 ?b . ?県 総人口 ?n . FILTER(?b/?n > 全国)」を、ライブラリを使わずにトリプルの集合とパターン照合だけで書く。表なら 3 つの列を持つ 1 つの表で済む問い合わせだが、知識グラフでは関係ごとに 1 回ずつ照合して、同じ主語(県)でつなぐ。

🎯 このコードでやること:2023 年度の 47 県について(県, 属する地方, 地方)(県, 総人口, 値)(県, 出生数, 値)の 3 種類のトリプルを作り、近畿と九州・沖縄で「人口千人あたり出生数が全国値より高い県」を探す。

📥 入力例 都道府県 Code A1101(総人口) A4101(出生数) 地方(地域コードから) 北海道 R01000 5,092,000 24,430 北海道・東北 東京都 R13000 14,086,000 86,348 関東 沖縄県 R47000 1,468,000 12,549 九州・沖縄 …(2023 年度の全 47 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023].sort_values('Code')

def region(code):
    n = int(code[1:3])
    for upper, name in [(7, '北海道・東北'), (14, '関東'), (23, '中部'), (30, '近畿'),
                        (35, '中国'), (39, '四国'), (47, '九州・沖縄')]:
        if n <= upper:
            return name

T = set()                                   # トリプルの集合 = 最小の知識グラフ
for p, c, pop, birth in zip(d['Prefecture'], d['Code'], d['A1101'], d['A4101']):
    T |= {(p, '属する地方', region(c)), (p, '総人口', pop), (p, '出生数', birth)}

def match(s=None, r=None, o=None):          # None を変数(?x)として扱うパターン照合
    return [t for t in T if (s is None or t[0] == s) and (r is None or t[1] == r)
            and (o is None or t[2] == o)]

national = d['A4101'].sum() / d['A1101'].sum()
print(f'トリプル数 {len(T)}  全国の出生率 {national * 1000:.2f}(人口千人あたり)')
# ?県 属する地方 近畿 . ?県 出生数 ?b . ?県 総人口 ?n . FILTER(?b/?n > 全国)
for area in ['近畿', '九州・沖縄']:
    hits = []
    for s, _, _ in match(r='属する地方', o=area):
        b = match(s, '出生数')[0][2]; n = match(s, '総人口')[0][2]
        if b / n > national:
            hits.append((s, round(b / n * 1000, 2)))
    print(f'{area}: {len(match(r="属する地方", o=area))} 県中 {len(hits)} 県が全国より高い', sorted(hits, key=lambda x: -x[1]))
📤 実行例(実測) トリプル数 141 全国の出生率 5.85(人口千人あたり) 近畿: 7 県中 3 県が全国より高い [('滋賀県', 6.57), ('大阪府', 6.31), ('兵庫県', 6.07)] 九州・沖縄: 8 県中 7 県が全国より高い [('沖縄県', 8.55), ('福岡県', 6.65), ('熊本県', 6.55), ('佐賀県', 6.47), ('鹿児島県', 6.37), ('宮崎県', 6.24), ('長崎県', 6.04)]

💬 トリプルは 47 県 × 3 関係 = 141 本。全国の出生率は人口千人あたり 5.85 で、近畿 7 府県のうち上回るのは滋賀県 6.57・大阪府 6.31・兵庫県 6.07 の 3 府県、九州・沖縄は 8 県中 7 県(最高は沖縄県 8.55、下回るのは大分県だけ)が上回る。照合は「地方で絞る → 県ごとに出生数と総人口を引く」の 2 段で、関係を 1 本たどるごとに結合が 1 回増える。

🐍 欠けたトリプルを他の事実から推測する(知識グラフの補完)

知識グラフには必ず欠けた事実がある。ある県の(県, 属する地方, ?)が抜けていたとき、その県の他の事実(気温・高齢化率・出生率・人口)から地方を当てられるかを、1 県ずつ隠して確かめる。TransE などの埋め込みによる補完も、「似た事実を持つエンティティは似た関係を持つ」という同じ考え方に立つ。

🎯 このコードでやること:年平均気温(B4101)・高齢化率・出生率・総人口の対数の 4 つを標準化し、k 近傍法で「属する地方」を 1 県ずつ隠して(Leave-One-Out)予測し、正解率を多数派の答えと比べる。

📥 入力例 SSDSE-B-2026 の 2023 年度 47 行 都道府県 B4101(年平均気温) 高齢化率 出生率(A4101/A1101) log10(A1101) 地方 北海道 11.0 0.330 0.0048 6.71 北海道・東北 東京都 17.6 0.228 0.0061 7.15 関東 …(全 47 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import numpy as np
import pandas as pd
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import LeaveOneOut, cross_val_predict
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023].sort_values('Code').reset_index(drop=True)

def region(code):
    n = int(code[1:3])
    for upper, name in [(7, '北海道・東北'), (14, '関東'), (23, '中部'), (30, '近畿'),
                        (35, '中国'), (39, '四国'), (47, '九州・沖縄')]:
        if n <= upper:
            return name

y = d['Code'].map(region)                       # 当てたいトリプル (県, 属する地方, ?)
X = pd.DataFrame({'年平均気温': d['B4101'],
                  '高齢化率': d['A1303'] / d['A1101'],
                  '出生率': d['A4101'] / d['A1101'],
                  'log総人口': np.log10(d['A1101'])})
for k in [1, 3]:
    model = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=k))
    pred = cross_val_predict(model, X, y, cv=LeaveOneOut())
    print(f'k={k}: 1 県ずつ隠して当てた正解 {(pred == y).sum()} / 47 = {(pred == y).mean():.3f}')
print(f'いちばん多い地方(中部)を常に答えた場合: {(y == "中部").sum()} / 47 = {(y == "中部").mean():.3f}')
wrong = d.loc[pred != y, 'Prefecture'] + '→' + pd.Series(pred, index=d.index)[pred != y]
print('k=3 で外した例(先頭 6):', wrong.head(6).tolist())
📤 実行例(実測) k=1: 1 県ずつ隠して当てた正解 24 / 47 = 0.511 k=3: 1 県ずつ隠して当てた正解 23 / 47 = 0.489 いちばん多い地方(中部)を常に答えた場合: 9 / 47 = 0.191 k=3 で外した例(先頭 6): ['宮城県→関東', '福島県→中部', '群馬県→中部', '東京都→中部', '新潟県→北海道・東北', '富山県→中国']

💬 最も近い 1 県の地方を答える k = 1 で 47 県中 24 県(51.1%)、k = 3 で 23 県(48.9%)が当たる。いつも最多の「中部」と答えた場合の 9 県(19.1%)の 2.5 倍以上で、気温や年齢構成が地方をある程度表していることは分かる。一方で半分は外れ、東京都は中部、宮城県は関東と答えている。補完で得たトリプルは推測なので、元の事実と区別する印(信頼度や出典)を付けて持つ。

🐍 表を知識グラフに「溶かす」— 1 セル = 1 トリプル

表形式のデータは、行の ID を主語、列名を述語、セルの値を目的語にすれば、そのままトリプルの集まりに書き換えられる(pandas の melt)。SSDSE-B-2026 を書き換えたとき、トリプルが何本になり、文字として保存するとどれだけ大きくなるかを確かめる。

🎯 このコードでやること:2023 年度の 47 行 × 109 指標を(地域コード, 列コード, 値)のトリプルに変換して数え、12 年度分を観測ノードで表したときの本数と、1 行 1 トリプルの文字列(N-Triples 風)にしたときのサイズを CSV と比べる。

📥 入力例 SSDSE-B-2026(564 行 × 112 列。年度・Code・Prefecture を除く 109 列が指標) SSDSE-B-2026 Code Prefecture A1101 A110101 … 2023 R01000 北海道 5092000 2405000 … 2023 R02000 青森県 1184000 559000 …
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
cols = [c for c in df.columns if c not in ('SSDSE-B-2026', 'Code', 'Prefecture')]
d = df[df['SSDSE-B-2026'] == 2023]

# 表の 1 セル = 1 トリプル (県, 列コード, 値) に「溶かす」
t23 = d.melt(id_vars='Code', value_vars=cols, var_name='述語', value_name='値')
print(f'2023 年度の表 {d.shape[0]} 行 × {len(cols)} 指標 → トリプル {len(t23):,} 本(述語 {t23["述語"].nunique()} 種類)')

# 12 年度分は観測ノード 1 つに (対象, 年度) + 指標 109 本
n_obs = len(df)
print(f'12 年度分: 観測 {n_obs} 個 × (2 + {len(cols)}) = {n_obs * (2 + len(cols)):,} 本')

# N-Triples 風の 1 行 1 トリプルの文字列にすると、どれだけ大きくなるか
lines = [f'<ex:{c}> <ex:{p}> "{v}" .' for c, p, v in zip(t23['Code'], t23['述語'], t23['値'])]
nt = len('\n'.join(lines).encode('utf-8'))
csv = len(d.to_csv(index=False).encode('utf-8'))
print(f'同じ 2023 年度分のサイズ: CSV {csv:,} bytes  N-Triples 風 {nt:,} bytes({nt / csv:.1f} 倍)')
print(t23.head(3).to_string(index=False))
📤 実行例(実測) 2023 年度の表 47 行 × 109 指標 → トリプル 5,123 本(述語 109 種類) 12 年度分: 観測 564 個 × (2 + 109) = 62,604 本 同じ 2023 年度分のサイズ: CSV 30,405 bytes N-Triples 風 179,625 bytes(5.9 倍) Code 述語 値 R01000 A1101 5092000.0 R02000 A1101 1184000.0 R03000 A1101 1163000.0

💬 2023 年度だけで 47 × 109 = 5,123 本、述語は指標の数と同じ 109 種類になる。12 年度分を観測ノード(対象・年度の 2 本 + 指標 109 本)で持つと 564 × 111 = 62,604 本。同じ 2023 年度分を 1 行 1 トリプルの文字列にすると 179,625 bytes で、CSV の 30,405 bytes の 5.9 倍になる。主語と述語を毎行くり返すためで、指標がそろった表のようなデータは表のまま持つ方が軽い。知識グラフが得をするのは、県ごとに持っている事実の種類がばらばら(ある県にだけ特産品や姉妹都市の情報がある)なときで、表だと空欄だらけになる。値が 5092000.0 と小数で出るのは、整数と小数の列を 1 列に縦に並べたため pandas が型を float にそろえたからで、述語ごとに型(xsd:integer など)を付けるのがトリプルでの正しい持ち方になる。

📝 補足解説 — 直感・落とし穴・発展をもう一段深く

ここまでで「知識グラフ=ノードとエッジ」「トリプル」「SPARQL」の骨格は掴めたはずです。 この補足セクションは、 既存の 直感・落とし穴・関連手法 を 置き換えずに、 実運用で効いてくる勘所を追記で深掘りします。 例はすべて SSDSE-B-2026(2023 年)の実測値と地理的事実に基づき、 数値をでっち上げた箇所はありません。

🎨 直感を深める — 「事実」をトリプル(主語-述語-目的語)に分解する

知識グラフの最小単位は トリプル(RDF では head-relation-tail、 日本語では主語-述語-目的語)です。 「文章で書けば 1 文の事実」を機械可読な 3 つ組に落とすと、 事実が 足し算で積み上がるデータ構造 になります。 SSDSE-B-2026 の中国地方 5 県を例にすると、 事実は次のように分解できます(人口は 2023 年実測値):

ここが表(テーブル)との決定的な違いです。 テーブルなら「県名・人口・地方」の列を先に固定しますが、 トリプルは 述語(関係)そのものがデータ なので、 後から (広島県, 県庁所在地, 広島市) のような まったく別種の関係 を列追加なしで足せます。 そして「中国地方に属する県の人口合計は?」という問いは、 (?, locatedIn, 中国地方) というパターンに一致するノードを集めて hasPopulation を足すだけ — 実測値では 537,000 + 650,000 + 1,847,000 + 2,738,000 + 1,299,000 = 7,071,000 人、 うち広島県が 38.7% を占めます。 これが「関係をパターンとして問い合わせる」という知識グラフの核心的な直感です。

⚠️ 落とし穴を深掘り — 実運用で必ずぶつかる 7 つ

既存の 落とし穴 セクション(オントロジー設計・エンティティ同一性・欠損知識・スケール・更新)を土台に、 実装で追加的に効いてくる観点を補います。

❌ 1. エンティティ解決(名寄せ)は最重要工程
「広島県」「広島」「広島市」「Hiroshima」を別ノードにすると知識が分断され、 (広島県, hasPopulation, ?) の答えに辿り着けません。 実務では Wikidata の QID(広島県=Q34664)のような一意 URI を割り当て、 表記揺れは owl:sameAs や文字列類似(コサイン類似度・埋め込み距離)で自動マージします。 名寄せの体系的な解説は 名寄せ(エンティティ解決) を参照。
❌ 2. 関係の一貫性とオントロジー設計
同じ「所属」を locatedIn / in_region / 所在地方 と複数の述語で書くと、 1 本の SPARQL では全件を取れません。 述語の語彙(オントロジー)を最初に 1 本化し、 「都市 ⊂ 場所」のような型階層を決めておくのが定石。 知識グラフは 構造化データ の一種ですが、 スキーマが緩いぶん 語彙規律を人間が守る責任 が RDB より重くなります。
❌ 3. 不完全性(欠損エッジ)と Open-World 仮定
知識グラフは常に「書かれていない事実」を含みます。 SSDSE-B には県民所得・医師数・面積などが無いため、 (鳥取県, hasArea, ?) はグラフ上に存在しません。 これを Closed-World(無いものは偽)と解釈すると「面積 0」という誤りに直結します。 欠損は「未知」であって「否定」ではない、 という区別が肝心(一般的な欠損の扱いは 欠損値 参照)。 欠損エッジをモデルで埋めるのが後述のリンク予測です。
❌ 4. スケーラビリティ
47 県 × 数述語なら一瞬ですが、 Wikidata 級(約 1 億エンティティ・十数億トリプル)では多段パターンマッチが重くなります。 述語別インデックス、 グラフ分割(named graph / namespace)、 マテリアライズドビューが性能を左右します。 グラフ DB は NoSQL の一族で、 基礎は データベース の設計原則が土台です。
❌ 5. 曖昧性・多義(同名異義)
このページの「中国地方」の 中国 と、 国名の 中国 は綴りが同じでも別エンティティです。 「はし(橋/箸)」「金沢(石川県/横浜市金沢区)」も典型。 文字列をそのままノード ID にすると多義語で衝突します。 URI による一意化と、 文脈から正しい実体を選ぶ 自然言語処理 のエンティティリンキングが必要です。
❌ 6. 真偽検証(出典とトラスト)
グラフに入った (X, r, Y) が正しい保証はありません。 抽出ミス・古い値・出典不明が紛れ込みます。 SSDSE のトリプルなら「年度」を取り違えるだけで (東京都, hasPopulation, 2012年の値) のような誤事実が生まれます。 各トリプルに出典・年度・信頼度を付与する Reification / RDF-star や、 制約検証(SHACL)で整合性を機械チェックするのが実務対応です。
❌ 7. 更新の難しさ(鮮度と履歴)
人口・首長・企業合併など知識は変わります。 SSDSE-B も毎年更新され、 (東京都, hasPopulation, 14,086,000) は 2023 年時点の値。 「最新値だけ持つ」のか「年度付きで履歴を残す」のかで設計が分岐します。 タイムスタンプ付きトリプル(named graph / RDF-star)にすると、 e-Stat や オープンデータ の年次更新を差分で取り込めます。

🚀 発展 — RDF/SPARQL からグラフ埋め込み・GNN・LLM+RAG へ

RDF / SPARQL:W3C 標準でトリプルを表現すれば、 Wikidata などの オープンデータ と連結でき、 SQL に近い感覚でグラフを問い合わせられます(グラフ DB は NoSQL、 表形式の RDB とは得意分野が対照的)。

グラフ埋め込み(TransE 等)とリンク予測:TransE は「主語ベクトル + 関係ベクトル ≈ 目的語ベクトル」となるよう 埋め込み を学習し、 距離が近いほど成立度が高いと見なします。 これを使えば「まだ書かれていないトリプル」を確率で補う リンク予測(落とし穴 3 の欠損エッジ対策)が可能になり、 RotatE・ComplEx など後継手法もあります。

GNN(グラフニューラルネットワーク):ニューラルネットワーク をグラフ構造そのものに適用し、 近傍ノードの情報を畳み込んでノード分類・リンク予測を行います。 グラフとしての位相的性質は 複雑グラフ、 可視化技法は ネットワーク可視化 が隣接ページです(グラフ理論・ネットワーク分析の独立ページは本用語集に未整備のため、 ここではテキストで補足します)。

LLM + RAG での活用:テキストからのトリプル抽出は 自然言語処理 の固有表現抽出・関係抽出が担い、 構築した知識グラフを LLM の外部知識源として検索・注入するのが RAG です。 知識グラフを噛ませると、 LLM 単体では起きやすい幻覚(ハルシネーション)を「出典付きの事実」で抑えられる点が実務的な利点です。 なお RDF・SPARQL・オントロジーといったセマンティック Web の専用ページは本用語集に未整備のため、 詳細は上記の各隣接ページから辿ってください。

まとめ:知識グラフは「事実をトリプルで積み上げる → 名寄せと語彙で一貫性を保つ → 欠損は埋め、 出典で真偽を担保する → 埋め込み・GNN・LLM+RAG で使い倒す」という流れで理解すると、 直感から実装まで 1 本の線でつながります。

⚠️ よくある落とし穴

❌ 1. オントロジー設計が肝
関係の語彙(プレディケート)を最初に決めないと、 「is_a」「type_of」「kind_of」「instance_of」が乱立し統一クエリが書けなくなります。 SSDSE-B-2026 を題材に「都道府県 - 総人口 - 数値」のような単純構造でも、 「has_population」「pop_value」「prefecture_pop」と複数語彙が混在すると 1 つの SPARQL クエリで全データを取れません。 schema.org / Wikidata の既存プレディケート再利用が定石。
❌ 2. エンティティ同一性 (Entity Resolution)
「夏目漱石」と「Soseki Natsume」と「夏目 金之助」が別ノードになると関係が分断され、 「漱石の弟子は?」と聞いても正しい答えに辿り着けない。 解決策は URI で一意化、 owl:sameAs での同義宣言、 Levenshtein 距離・埋込ベクトルでの自動マージなど。 SSDSE-B-2026 でも「東京都」「Tokyo」「東京」を正規化する必要がある。
❌ 3. 欠損知識への対応 (Open vs Closed World)
「明示的に書かれていない」≠「事実でない」。 Closed-World 仮定(書かれていないことは false)と Open-World 仮定(書かれていないことは未知)は異なる結論を導く。 例:「秋田県の特定産業の GDP がグラフに無い」場合、 Closed なら 0、 Open なら N/A。 LLM と組み合わせる RAG ではこの区別を明示しないと幻覚の温床になる。
❌ 4. スケール問題
数億エンティティになるとクエリ性能が課題。 Wikidata は約 1 億エンティティ・10 億トリプル、 SPARQL クエリで数秒〜分かかる。 Neo4j / GraphDB / TigerGraph などグラフ DB の選定と、 述語別の index、 graph partitioning、 マテリアライズドビューが性能を左右する。
❌ 5. 更新コスト・鮮度管理
知識は変化する(首相、 株価、 統計値、 企業合併...)。 SSDSE-B も毎年更新されるが、 取り込み済グラフの「2024 年版」と「2026 年版」を共存させるか、 差分更新するかで設計が変わる。 トリプルにタイムスタンプを付ける Reification / Named Graph / RDF-star などの仕組みで鮮度を管理。

⚠️ 閉世界と開世界で答えが変わる — 10 県のトリプルが欠けた知識グラフ

知識グラフへの取り込み漏れは珍しくない。2023 年度の(県, 総人口, 値)47 本のうち、乱数(seed 0)で選んだ 10 県が欠けたグラフに 2 つの問いを投げ、「書かれていないことは偽」とみなす閉世界仮定と、「書かれていないことは不明」とみなす開世界仮定で答えを比べる。

🎯 このコードでやること:総人口のトリプルから 10 県分を抜いた知識グラフで、「500 万人以上の県」と「100 万人未満の県」を閉世界・開世界の 2 通りで答え、正解と比べる。

📥 入力例 都道府県 Code A1101(総人口) A4101(出生数) 地方(地域コードから) 北海道 R01000 5,092,000 24,430 北海道・東北 東京都 R13000 14,086,000 86,348 関東 沖縄県 R47000 1,468,000 12,549 九州・沖縄 …(2023 年度の全 47 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import random
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023]
prefs = d['Prefecture'].tolist()
full = {p: v for p, v in zip(d['Prefecture'], d['A1101'])}   # 完全な (県, 総人口, 値)

random.seed(0)
lost = set(random.sample(prefs, 10))                         # 取り込み漏れで 10 県のトリプルが欠けた KG
kg = {p: v for p, v in full.items() if p not in lost}
print('欠けた県:', sorted(lost, key=prefs.index))

big_true = {p for p, v in full.items() if v >= 5_000_000}
big_kg = {p for p, v in kg.items() if v >= 5_000_000}
print(f'Q1「500 万人以上の県」 正解 {len(big_true)}  KG の答え {len(big_kg)}  取りこぼし {sorted(big_true - big_kg)}')

small_true = {p for p, v in full.items() if v < 1_000_000}
small_cwa = {p for p in prefs if not (p in kg and kg[p] >= 1_000_000)}  # 閉世界: 書かれていない=満たさない
small_owa_sure = {p for p, v in kg.items() if v < 1_000_000}           # 開世界: 確実に言えるものだけ
unknown = sorted(lost, key=prefs.index)
print(f'Q2「100 万人未満の県」 正解 {len(small_true)}')
print(f'  閉世界の答え {len(small_cwa)} 県(うち誤り {len(small_cwa - small_true)} 県: {sorted(small_cwa - small_true, key=prefs.index)})')
print(f'  開世界の答え 確実 {len(small_owa_sure)} 県 + 不明 {len(unknown)} 県')
📤 実行例(実測) 欠けた県: ['岩手県', '石川県', '長野県', '愛知県', '滋賀県', '京都府', '大阪府', '鳥取県', '島根県', '岡山県'] Q1「500 万人以上の県」 正解 9 KG の答え 7 取りこぼし ['大阪府', '愛知県'] Q2「100 万人未満の県」 正解 10 閉世界の答え 18 県(うち誤り 8 県: ['岩手県', '石川県', '長野県', '愛知県', '滋賀県', '京都府', '大阪府', '岡山県']) 開世界の答え 確実 8 県 + 不明 10 県

💬 欠けたのは岩手県・石川県・長野県・愛知県・滋賀県・京都府・大阪府・鳥取県・島根県・岡山県。「500 万人以上」は正解 9 都府県のうち愛知県と大阪府を取りこぼして 7 と答える。「100 万人未満」は否定を含む問いなので、閉世界では総人口が書かれていない 10 県がすべて「100 万人以上ではない」側に入り、18 県と答えてしまう。うち 8 県は誤りで、大阪府(876 万人)や愛知県(748 万人)まで人口の少ない県に数えられる。開世界なら「確実に 100 万人未満」は 8 県、残り 10 県は不明と答えられる。

否定(〜ではない・〜未満)を含む問いは、欠けたトリプルの影響を強く受ける。知識グラフに問い合わせるときは、その関係が全件そろっているか(ここでは 47 件あるべきところが 37 件)を先に数え、そろっていなければ開世界として「不明」を答えに残す。

⚠️ 名前の正規化で同じ県が別物・別の県が同じ物になる

複数のデータから知識グラフを作ると、「東京都」「東京」のような表記の違いを同じエンティティにまとめる必要がある(エンティティの同一性)。よく使われる「末尾の都道府県を削る」処理を、SSDSE-B-2026 の 47 の正式名称に当てて結果を確かめる。

🎯 このコードでやること:47 都道府県名に 3 通りの短縮処理をかけ、重複と意図しない変換を数える。あわせて、部分一致で名前を探したときに別の県に当たる例を調べる。

📥 入力例 SSDSE-B-2026 の 2023 年度の Prefecture 列(47 件) 北海道, 青森県, 岩手県, …, 東京都, …, 京都府, 大阪府, …, 沖縄県
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import re
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
prefs = df[df['SSDSE-B-2026'] == 2023]['Prefecture'].tolist()

a = {p: p.rstrip('都道府県') for p in prefs}             # 末尾の「都道府県」に含まれる文字を全部削る
b = {p: re.sub('[都道府県]$', '', p) for p in prefs}     # 末尾の 1 文字だけ削る
c = {p: p if p == '北海道' else p[:-1] for p in prefs}   # 北海道だけ例外にして 1 文字削る

for name, m in [('rstrip', a), ('末尾 1 文字', b), ('北海道を例外', c)]:
    odd = {k: v for k, v in m.items() if len(v) != len(k) - 1 or k == '北海道'}
    dup = len(m) - len(set(m.values()))
    print(f'{name:8s}: 短縮名の重複 {dup}  注意が要る変換 {odd}')

# 部分一致で「京都」を探すと、どの正式名称に当たるか
print('「京都」を含む:', [p for p in prefs if '京都' in p])
print('「島」を含む:', [p for p in prefs if '島' in p])
📤 実行例(実測) rstrip : 短縮名の重複 0 注意が要る変換 {'北海道': '北海', '京都府': '京'} 末尾 1 文字 : 短縮名の重複 0 注意が要る変換 {'北海道': '北海'} 北海道を例外 : 短縮名の重複 0 注意が要る変換 {'北海道': '北海道'} 「京都」を含む: ['東京都', '京都府'] 「島」を含む: ['福島県', '島根県', '広島県', '徳島県', '鹿児島県']

💬 Python の rstrip("都道府県") は「末尾の文字列」ではなく「末尾に並ぶ 4 文字のどれか」を繰り返し削るので、京都府は「府」「都」と 2 文字削られて「京」になり、北海道も「道」が削られて「北海」になる。末尾 1 文字だけを削る正規表現でも北海道は「北海」になり、北海道を例外にして初めて 47 件すべてが意図どおりになる。3 通りとも短縮名の重複は 0 だが、部分一致で「京都」を探すと東京都と京都府の 2 件が当たり、「島」を含む県は福島・島根・広島・徳島・鹿児島の 5 県ある。

エンティティをまとめる処理は、全件を変換して重複と例外を数え、部分一致ではなく完全一致の対応表(正式名称 ↔ 別名 ↔ 地域コード R01000 などの ID)で結ぶ。知識グラフでは ID(URI)を主語にして、名前は「名前」という述語の値として持つのが基本である。

🧠 理解度チェック — このページの実測値で

Q1. 47 県と 7 地方を「属する地方」でつないだグラフに、「総人口の順で次の県」の関係 46 本を足した。平均次数と密度はそれぞれどう変わるか。

平均次数は 2 × 47 / 54 ≈ 1.741 から 2 × 93 / 54 ≈ 3.444 に倍近く増える。密度は 47 / (54² × 1) ≈ 0.01612 から 93 / (54² × 2) ≈ 0.01595 とほぼ同じ。関係の種類 |R| が分母に入るので、関係を増やすと「張れる辺」の数も増えるからである。

Q2. 10 県の総人口が欠けた知識グラフで「100 万人未満の県」を閉世界で問い合わせると 18 県が返った。正解は何県で、誤りはどこから来たか。

正解は 10 県。閉世界では「総人口 ≥ 100 万」が書かれていない県をすべて「100 万人未満」とみなすので、欠けた 10 県のうち実際には 100 万人以上の 8 県(大阪府・愛知県など)が誤って入る。欠けた県のうち鳥取県・島根県は本当に 100 万人未満なので、偶然正しく数えられている。

Q3. 12 年度分の総人口を知識グラフに入れる。年を述語に埋め込む方法(総人口2023 など)と、観測ごとにノードを立てる方法で、トリプル数と述語の種類はいくつになるか。

47 × 12 = 564 観測。年を述語にすると 564 トリプル・述語 12 種類。観測ノードを立てて(観測, 対象, 県)(観測, 年度, 年)(観測, 総人口, 値)の 3 本で表すと 1,692 トリプル・述語 3 種類。後者はトリプルが 3 倍になるが、「年度」を変数にした問い合わせ(11 回の前年度比がすべて減少した県 = 37 県)が 1 つの問いで書ける。

Q4. 人口順で隣り合う 46 組だけを書いた知識グラフに推移律を当てると、導ける「人口が多い」の事実は何本になるか。何回の適用でそろうか。

47 県から 2 県を選ぶ組の数 47 × 46 / 2 = 1,081 本(元の 46 本の 23.5 倍)。1 回の適用でたどれる長さが倍になる(1 → 2 → 4 → … → 64)ので、46 段の鎖は 6 回でつながる。

🗺 概念マップ

知識グラフを中心に、 RDF / OWL (表現規格)、 SPARQL クエリ (アクセス)、 知識グラフ埋め込み・GNN・RAG (応用) を並べたセマンティック技術マップ。

knowledge graph RDF / OWL Wikidata / DBp 知識グラフ埋め込み GNN(グラフニューラルネッ RAG (Retrieval SPARQL クエリ

ナレッジグラフは「エンティティ (実体) を頂点・関係を辺」とするグラフ構造で、 セマンティック Web (RDF / SPARQL)、 オントロジー、 グラフ DB、 RAG、 GNN とそれぞれ別軸で繋がる。 RDF はデータ表現形式、 オントロジーはスキーマ定義、 GNN はノード/辺埋め込み計算、 RAG は LLM への外部知識注入と、 役割の違いを理解して組み合わせる。

🔗 隣接手法への橋渡し

知識グラフは単なるグラフ DB ではなく、 オントロジー設計 (前段) と検索拡張・推論エンジン (後段) を組み合わせて知識ベース AI の基盤を形成する。

上流の OCR・NER (固有表現抽出) と関係抽出で生テキストから (主語, 述語, 目的語) 三つ組を量産し、 並列の RDF/Property Graph 表現を選び、 下流の SPARQL クエリ・推論エンジン (OWL)・グラフ埋め込み (TransE 等) で知識をアプリケーションに供給する流れで真価を発揮する。

🌳 手法選択フロー

知識グラフ を実際の課題に当てはめるとき、 用語固有の判断軸に沿って次の 3 段階で適切な選択を行う。

  1. 関係性・推論が分析の中心か? Yes → 知識グラフ (RDF/Property Graph)、 No → リレーショナル DB で十分
  2. 標準化・相互運用が必要か? Yes → RDF + OWL + SPARQL (Wikidata 互換)、 No → 次へ
  3. 機械学習で活用したいか? Yes → 知識グラフ埋め込み (TransE/RotatE) + GNN、 No → グラフクエリのみ (Cypher/SPARQL)

このフローは知識グラフ構築の標準方針。 規模が大きい場合は Neo4j (Property Graph) + Cypher、 標準準拠が必要なら GraphDB / Stardog (RDF) + SPARQL を選ぶ実装パターンが定着している。

❌ オントロジー設計が肝
関係の語彙を最初に決めないと、 「is_a」「type_of」「kind_of」が乱立し統一クエリが書けなくなります。
❌ エンティティ同一性
「夏目漱石」と「Soseki Natsume」が別ノードになると関係が分断。 URI で一意化。
❌ 欠損知識への対応
「明示的に書かれていない」≠「事実でない」。 Closed-World と Open-World の前提を区別。
❌ スケール問題
数億エンティティになるとクエリ性能が課題。 グラフ DB の選定と indexing が重要。
❌ 更新コスト
知識は変化する(首相、 株価...)。 鮮度管理の仕組みが必要。