知識グラフは、事実を(主語, 述語, 目的語)のトリプルで持ち、エンティティをノード、関係を辺とするグラフとして扱う。下のチップは、その書き方の標準(RDF)と問い合わせ言語(SPARQL)、型の体系(オントロジー)、公開されている代表的な知識グラフ(Wikidata・DBpedia)である。
🍰 まずはやさしく
知識グラフは情報のネットワークです。
もの同士のつながりを整理して使います。
スマホの検索結果にあるまとめ画面のようなものです。
ここでは知識グラフの結論を学びます。
最も忙しい読者のために、 まず結論だけまとめます。 詳細は以下のセクションへ:
🍰 まずはやさしく
知識グラフは情報の地図のようなものです。
知りたい情報をすぐに引き出すために使います。
ネットで有名人を調べたときに出るプロフィールです。
ここでは知識グラフをどこで使うか学びます。
Google で「アインシュタイン」を検索すると、 検索結果の右にプロフィール・誕生日・著作・配偶者などが出ます。 これは知識グラフから引いてきています。 単なる「テキスト検索」を超え、 知識として整理された情報 を返す仕組みです。
統計データの分野でも出会う。e-Stat の「統計 LOD」は、国勢調査などの統計表を RDF のトリプルとして公開し、SPARQL で地域や年次を指定して問い合わせられるようにしている。SSDSE-B-2026 のような都道府県 × 年度の表を知識グラフに載せると、このページの 🐍・🌐 の節のように、地域コードを主語にして「どの地方に属するか」「何年度の値か」を関係として持つ形になる。
🍰 まずはやさしく
知識グラフは情報のクモの巣のようなものです。
複雑な関係を柔軟に表すために使います。
部活の人間関係を線でつなぐイメージです。
ここでは表との違いや仕組みを学びます。
人物データを「テーブル」と「グラフ」で表す違い:
テーブルでも「師匠 ID」列で表現はできますが、 関係の種類が増えるたびに列が増殖。 グラフは 任意の関係を任意に追加 できる柔軟性が魅力です。
知識グラフは「主語-述語-目的語」のトリプルでドメイン知識を構造化する。 ここでは「トリプル構造」「都道府県知識グラフの実例」「RDB との違い」を視覚化する。
中国地方の 5 県(鳥取・島根・岡山・広島・山口)を題材にした ミニ知識グラフ です。 県 → 地方 → 国という所属関係は地理的事実、 人口は SSDSE-B-2026(2023 年)の実測値(広島県 2,738,000 人、 鳥取県 537,000 人)を丸めたものです。 ① ノードをタップ すると隣接関係がハイライトされ、 そのノードを含むトリプル(主語, 述語, 目的語)が一覧表示されます。 ② 「2 ホップ推論」 ボタンで「広島県 → 中国地方 → 岡山県」のような経路探索を体感できます。 ③ トリプル追加フォーム で新しい知識を足すと、 グラフが動的に成長します。
同じデータを RDB で持つなら「都道府県テーブルに『地方』列を足した 47 行の表」になります。 ここで「広島県と同じ地方の県は?」に答えるには 自己結合(self JOIN) が必要で、 3 ホップ・4 ホップと深くなるたびに JOIN が積み重なります。 グラフではウィジェットで体感した通り エッジを辿るだけ、 しかも目的語 → 主語という 逆方向の参照も無料 です(2 ホップ推論の「←locatedIn−」がそれ)。 さらにフォームで hasPopulation 以外の新しい述語を足しても ALTER TABLE は不要 — これが「スキーマ柔軟」の正体で、 クエリが「表の結合」ではなく グラフのパターンマッチ になる点が本質的な違いです。
同名異義・エンティティ同一性:フォームに「広島」とだけ入力してみてください。 「広島県」とは別の第 3 のノードが生まれ、 知識が分断されます。 「広島県」と「広島市」も別エンティティですし、 このグラフの「中国地方」の 中国 と国名の 中国 は同名異義の典型例。 実務では URI(Wikidata の QID など)で一意化するのが定石です。 述語の表記揺れ:locatedIn の代わりに in_region や「所在地方」で追加すると、 2 ホップ推論の経路が繋がらなくなることを確認できます。 語彙(オントロジー)を最初に 1 本化すべき理由がここにあります。 リテラルとエンティティの区別:「273.8万人」のような値ノードは終端で、 そこから先へは辿れません。 何をエンティティ(辿れるノード)にし、 何をリテラル(属性値)にするかがスキーマ設計の第一歩です。
このウィジェットの 2 ホップ推論は、 SPARQL のプロパティパス ?x ex:locatedIn/^ex:locatedIn ?y(locatedIn を順方向 → 逆方向に辿る)と同じ発想です。 W3C 標準の RDF でトリプルを表現すれば、 Wikidata などの オープンデータ と連結でき、 SQL 的な感覚のままグラフを問い合わせられます(グラフ DB は NoSQL の一族、 基礎は データベース 参照)。 さらに TransE のような グラフ埋め込み(埋め込み)はトリプルをベクトル化して「まだ書かれていないトリプル」を推定し、 ニューラルネットワーク ベースの GNN はグラフ構造ごと学習します。 テキストからのトリプル抽出は 自然言語処理 の固有表現抽出・関係抽出が担い、 構築した KG を LLM の外部知識として使うのが RAG です。 グラフ理論としての性質は 複雑グラフ、 描画技法は ネットワーク可視化 が隣接ページです。
🍰 まずはやさしく
知識グラフは情報のセットです。
正しく情報を定義するために使います。
「東京」と「日本」を「首都」で結ぶ形です。
ここでは記号を使った定義を学びます。
知識グラフの強みの 1 つは、ルールから書かれていない事実を導けることにある。「x が y より人口が多く、y が z より多いなら、x は z より多い」(推移律)を、SSDSE-B-2026 の 2023 年度の総人口の順位で確かめる。書いておくのは人口順で隣り合う 46 組だけにして、ルールを何回当てると全部の組がそろうか、そろえると何本になるかを数える。
🎯 このコードでやること:人口順で隣り合う県の組 46 本を(x, 人口が多い, y)として持ち、推移律を新しいトリプルが出なくなるまで繰り返して、導かれる事実の本数と繰り返し回数を数える。
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 本から、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 つのどちらか、または組み合わせで動いている。
ナレッジグラフは集合 $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 例を紹介する。
| 業界 | 具体事例 |
|---|---|
| Knowledge Graph(2020 年の Google の発表で約 50 億エンティティ・5,000 億件のファクト)。 「カフェ 近く」検索の裏側 | |
| 製薬 | Hetionet(ヘテロネット):薬剤・遺伝子・疾患のグラフで創薬候補を推論 |
| 金融 | FactSet / Refinitiv のエンティティ KG で「ある企業の子会社」「役員兼任」を即時検索 |
| EC | Amazon Product Graph:商品・属性・利用シーンのグラフでレコメンド |
| 製造 | Siemens の Digital Twin KG:設備・センサー・障害履歴を統合 |
| 行政 | Wikidata / DBpedia:47 都道府県、 自治体、 統計指標を SPARQL で問い合わせ |
ナレッジグラフ と近接する手法群の位置づけを並べる。 隣接領域を押さえると、 自分のタスクで何を使うべきかが立体的に見えてくる。
| 手法 | 位置づけ | 代表ツール |
|---|---|---|
| RDF | 標準的トリプル形式 | W3C |
| Property Graph | ノード/エッジに属性 | Neo4j, Cypher |
| Ontology (OWL) | 型と推論ルール | Protege |
| Wikidata | 人手編集の巨大 KG | 1 億エンティティ |
| YAGO / DBpedia | Wikipedia 由来 KG | 学術ベンチ |
| Embedding KG | ベクトル化(TransE等) | 推論・補完用 |
「うまくいくケース」より「失敗ケース」のほうが学びが多い。 実プロジェクトで頻発する 5 つを示す。
小さな知識グラフを構築:
クエリ「アインシュタインが受賞した賞の創設者は?」
テーブルでは「2 つの結合」が必要ですが、 グラフでは 2 ホップ辿るだけ。
合成データで知識グラフ (7 ノード 6 エッジ) の次数中心性を計算する。
| ノード | 次数 |
|---|---|
| 東京 | 3 |
| 日本 | 3 |
| 大阪 | 2 |
| 大陸 | 1 |
| 渋谷 | 1 |
| 新宿 | 1 |
| 梅田 | 1 |
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}') |
💬 手計算の Step 2(東京 3・日本 3・大阪 2・残り 4 ノードが 1)と Step 3 の密度 12/42 = 0.286 が、トリプルから数え直した Python 出力と一致する。6 本のトリプルに出てくるノードは 7 個で、張りうる 42 本(無向なら 21 本)のうち 6 本しか無い。知識グラフは実データでも密度が 0.01 を大きく下回るほど疎なのが普通で、隣接行列ではなくトリプルの一覧や隣接リストで持つ理由がここにある。
上の手計算と同じ式 ρ = |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|・密度・平均次数と、地方ノードの次数を数える。
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・|T| = 47・ρ = 0.01612・平均次数 1.741 になる。人口順の関係 46 本を足すと |T| = 93 で平均次数は 3.444 と倍近くになるが、分母に |R| = 2 が入るので密度は 0.01595 とほとんど変わらない。地方ノードの次数は中部 9 がいちばん多く、四国 4 が少ない。密度は「張れる辺のうち何割を張ったか」、平均次数は「1 つのノードから平均何本出ているか」で、関係の種類を増やしたときに動き方が違う。
最小再現コード。 「アインシュタイン → 受賞 → ノーベル物理学賞」のような関係を有向グラフの辺として持ち、関係をたどって答えを引く:
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) |
各コードブロックは 🎯 目的 / 📥 入力 / 🐍 コード / 📤 出力 / 💬 解説 の 5 要素セット。 SSDSE-B-2026 の 47 都道府県データ(2023 年)に実値計算して結論まで導く。
🎯 このコードでやること:SSDSE-B-2026 の 47 都道府県データから「県 - 総人口 - 値」のトリプルを Python 辞書で作り、 ナレッジグラフの最小例を構築する
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 トリプルが最小 KG。 各トリプルは「主語(県) - 述語(関係) - 目的語(値)」。 これを膨らませると 1 億トリプルの Wikidata になる。
🎯 このコードでやること:rdflib で同じ内容を RDF Turtle 形式に書き出し、 W3C 標準互換の KG ファイルを生成する
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 県分だけ表示 |
💬 結果の読み方:2023 年の 47 県に name と hasPopulation の 2 述語を付けたので、グラフのトリプル数は 47 × 2 = 94。Turtle では同じ主語 ex:R01000(北海道)の 2 つの述語が「;」でつながり、県ごとに「.」で閉じる。整数は Literal(int) で渡したので xsd:integer 型になり、5092000 は引用符なしで書き出されている(文字列で渡すと "5092000" になり、次の SPARQL の大小比較が数値として効かない)。
🎯 このコードでやること:rdflib のグラフに対して SPARQL で「総人口 500 万人以上の県」を検索する
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)) |
💬 結果の読み方:9 都道府県がヒット。 SPARQL なら SQL に近い感覚で KG にクエリできる。 同じトリプル構造でも世界中の Wikidata に対し直接問い合わせ可能になるのが強み。
🎯 このコードでやること:TransE 風の埋め込みで「県 + hasPopulation ≈ 値カテゴリ」をベクトル空間で表現する練習。 5 次元の小型例
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)) |
💬 結果の読み方:TransE の発想は「主語ベクトル + 関係ベクトル ≈ 目的語ベクトル」。 東京都 + hasPop = [7.649, 1.509] と目的語側 [7.5, 1.4] の差は (0.149, 0.109) で、距離は √(0.149²+0.109²) ≈ 0.184。 距離が小さいほど成立度が高い。 ここでは関係ベクトルも目的語ベクトルも手で置いた値なので、0.184 自体に意味は無く、 実用 KGE はこの距離を負例より小さくするよう学習する。
手を動かさないと身につかない。 5 問とも SSDSE-B-2026 を必ず触る前提で設計。
| 年 | 出来事 | 意義 |
|---|---|---|
| 1970 | Codd の関係モデル論文 | 「表」を理論基盤に |
| 1989 | Tim Berners-Lee の Web 提案 | 非構造データの大爆発 |
| 1997 | XML 標準化 | 半構造データの台頭 |
| 2001 | Semantic Web 提唱 | 意味の機械可読化 |
| 2006 | Hadoop OSS 化 | 大規模非構造処理 |
| 2010 | NoSQL 全盛 | 柔軟スキーマ |
| 2014 | Wickham「Tidy Data」 | 整形原則の確立 |
| 2017 | Attention is All You Need | 非構造の構造化が AI 主役へ |
| 2020 | GPT-3 公開 | LLM による抽出 |
| 2023 | Foundation Model 産業実装 | ナレッジグラフ の自動化加速 |
SPARQL の「?県 属する地方 近畿 . ?県 出生数 ?b . ?県 総人口 ?n . FILTER(?b/?n > 全国)」を、ライブラリを使わずにトリプルの集合とパターン照合だけで書く。表なら 3 つの列を持つ 1 つの表で済む問い合わせだが、知識グラフでは関係ごとに 1 回ずつ照合して、同じ主語(県)でつなぐ。
🎯 このコードでやること:2023 年度の 47 県について(県, 属する地方, 地方)(県, 総人口, 値)(県, 出生数, 値)の 3 種類のトリプルを作り、近畿と九州・沖縄で「人口千人あたり出生数が全国値より高い県」を探す。
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])) |
💬 トリプルは 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)予測し、正解率を多数派の答えと比べる。
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()) |
💬 最も近い 1 県の地方を答える k = 1 で 47 県中 24 県(51.1%)、k = 3 で 23 県(48.9%)が当たる。いつも最多の「中部」と答えた場合の 9 県(19.1%)の 2.5 倍以上で、気温や年齢構成が地方をある程度表していることは分かる。一方で半分は外れ、東京都は中部、宮城県は関東と答えている。補完で得たトリプルは推測なので、元の事実と区別する印(信頼度や出典)を付けて持つ。
表形式のデータは、行の ID を主語、列名を述語、セルの値を目的語にすれば、そのままトリプルの集まりに書き換えられる(pandas の melt)。SSDSE-B-2026 を書き換えたとき、トリプルが何本になり、文字として保存するとどれだけ大きくなるかを確かめる。
🎯 このコードでやること:2023 年度の 47 行 × 109 指標を(地域コード, 列コード, 値)のトリプルに変換して数え、12 年度分を観測ノードで表したときの本数と、1 行 1 トリプルの文字列(N-Triples 風)にしたときのサイズを CSV と比べる。
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 年度分を観測ノード(対象・年度の 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% を占めます。 これが「関係をパターンとして問い合わせる」という知識グラフの核心的な直感です。
既存の 落とし穴 セクション(オントロジー設計・エンティティ同一性・欠損知識・スケール・更新)を土台に、 実装で追加的に効いてくる観点を補います。
RDF / SPARQL:W3C 標準でトリプルを表現すれば、 Wikidata などの オープンデータ と連結でき、 SQL に近い感覚でグラフを問い合わせられます(グラフ DB は NoSQL、 表形式の RDB とは得意分野が対照的)。
グラフ埋め込み(TransE 等)とリンク予測:TransE は「主語ベクトル + 関係ベクトル ≈ 目的語ベクトル」となるよう 埋め込み を学習し、 距離が近いほど成立度が高いと見なします。 これを使えば「まだ書かれていないトリプル」を確率で補う リンク予測(落とし穴 3 の欠損エッジ対策)が可能になり、 RotatE・ComplEx など後継手法もあります。
GNN(グラフニューラルネットワーク):ニューラルネットワーク をグラフ構造そのものに適用し、 近傍ノードの情報を畳み込んでノード分類・リンク予測を行います。 グラフとしての位相的性質は 複雑グラフ、 可視化技法は ネットワーク可視化 が隣接ページです(グラフ理論・ネットワーク分析の独立ページは本用語集に未整備のため、 ここではテキストで補足します)。
LLM + RAG での活用:テキストからのトリプル抽出は 自然言語処理 の固有表現抽出・関係抽出が担い、 構築した知識グラフを LLM の外部知識源として検索・注入するのが RAG です。 知識グラフを噛ませると、 LLM 単体では起きやすい幻覚(ハルシネーション)を「出典付きの事実」で抑えられる点が実務的な利点です。 なお RDF・SPARQL・オントロジーといったセマンティック Web の専用ページは本用語集に未整備のため、 詳細は上記の各隣接ページから辿ってください。
知識グラフへの取り込み漏れは珍しくない。2023 年度の(県, 総人口, 値)47 本のうち、乱数(seed 0)で選んだ 10 県が欠けたグラフに 2 つの問いを投げ、「書かれていないことは偽」とみなす閉世界仮定と、「書かれていないことは不明」とみなす開世界仮定で答えを比べる。
🎯 このコードでやること:総人口のトリプルから 10 県分を抜いた知識グラフで、「500 万人以上の県」と「100 万人未満の県」を閉世界・開世界の 2 通りで答え、正解と比べる。
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)} 県') |
💬 欠けたのは岩手県・石川県・長野県・愛知県・滋賀県・京都府・大阪府・鳥取県・島根県・岡山県。「500 万人以上」は正解 9 都府県のうち愛知県と大阪府を取りこぼして 7 と答える。「100 万人未満」は否定を含む問いなので、閉世界では総人口が書かれていない 10 県がすべて「100 万人以上ではない」側に入り、18 県と答えてしまう。うち 8 県は誤りで、大阪府(876 万人)や愛知県(748 万人)まで人口の少ない県に数えられる。開世界なら「確実に 100 万人未満」は 8 県、残り 10 県は不明と答えられる。
否定(〜ではない・〜未満)を含む問いは、欠けたトリプルの影響を強く受ける。知識グラフに問い合わせるときは、その関係が全件そろっているか(ここでは 47 件あるべきところが 37 件)を先に数え、そろっていなければ開世界として「不明」を答えに残す。
複数のデータから知識グラフを作ると、「東京都」「東京」のような表記の違いを同じエンティティにまとめる必要がある(エンティティの同一性)。よく使われる「末尾の都道府県を削る」処理を、SSDSE-B-2026 の 47 の正式名称に当てて結果を確かめる。
🎯 このコードでやること:47 都道府県名に 3 通りの短縮処理をかけ、重複と意図しない変換を数える。あわせて、部分一致で名前を探したときに別の県に当たる例を調べる。
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]) |
💬 Python の rstrip("都道府県") は「末尾の文字列」ではなく「末尾に並ぶ 4 文字のどれか」を繰り返し削るので、京都府は「府」「都」と 2 文字削られて「京」になり、北海道も「道」が削られて「北海」になる。末尾 1 文字だけを削る正規表現でも北海道は「北海」になり、北海道を例外にして初めて 47 件すべてが意図どおりになる。3 通りとも短縮名の重複は 0 だが、部分一致で「京都」を探すと東京都と京都府の 2 件が当たり、「島」を含む県は福島・島根・広島・徳島・鹿児島の 5 県ある。
エンティティをまとめる処理は、全件を変換して重複と例外を数え、部分一致ではなく完全一致の対応表(正式名称 ↔ 別名 ↔ 地域コード R01000 などの ID)で結ぶ。知識グラフでは ID(URI)を主語にして、名前は「名前」という述語の値として持つのが基本である。
平均次数は 2 × 47 / 54 ≈ 1.741 から 2 × 93 / 54 ≈ 3.444 に倍近く増える。密度は 47 / (54² × 1) ≈ 0.01612 から 93 / (54² × 2) ≈ 0.01595 とほぼ同じ。関係の種類 |R| が分母に入るので、関係を増やすと「張れる辺」の数も増えるからである。
正解は 10 県。閉世界では「総人口 ≥ 100 万」が書かれていない県をすべて「100 万人未満」とみなすので、欠けた 10 県のうち実際には 100 万人以上の 8 県(大阪府・愛知県など)が誤って入る。欠けた県のうち鳥取県・島根県は本当に 100 万人未満なので、偶然正しく数えられている。
47 × 12 = 564 観測。年を述語にすると 564 トリプル・述語 12 種類。観測ノードを立てて(観測, 対象, 県)(観測, 年度, 年)(観測, 総人口, 値)の 3 本で表すと 1,692 トリプル・述語 3 種類。後者はトリプルが 3 倍になるが、「年度」を変数にした問い合わせ(11 回の前年度比がすべて減少した県 = 37 県)が 1 つの問いで書ける。
47 県から 2 県を選ぶ組の数 47 × 46 / 2 = 1,081 本(元の 46 本の 23.5 倍)。1 回の適用でたどれる長さが倍になる(1 → 2 → 4 → … → 64)ので、46 段の鎖は 6 回でつながる。
知識グラフを中心に、 RDF / OWL (表現規格)、 SPARQL クエリ (アクセス)、 知識グラフ埋め込み・GNN・RAG (応用) を並べたセマンティック技術マップ。
ナレッジグラフは「エンティティ (実体) を頂点・関係を辺」とするグラフ構造で、 セマンティック Web (RDF / SPARQL)、 オントロジー、 グラフ DB、 RAG、 GNN とそれぞれ別軸で繋がる。 RDF はデータ表現形式、 オントロジーはスキーマ定義、 GNN はノード/辺埋め込み計算、 RAG は LLM への外部知識注入と、 役割の違いを理解して組み合わせる。
知識グラフは単なるグラフ DB ではなく、 オントロジー設計 (前段) と検索拡張・推論エンジン (後段) を組み合わせて知識ベース AI の基盤を形成する。
上流の OCR・NER (固有表現抽出) と関係抽出で生テキストから (主語, 述語, 目的語) 三つ組を量産し、 並列の RDF/Property Graph 表現を選び、 下流の SPARQL クエリ・推論エンジン (OWL)・グラフ埋め込み (TransE 等) で知識をアプリケーションに供給する流れで真価を発揮する。
知識グラフ を実際の課題に当てはめるとき、 用語固有の判断軸に沿って次の 3 段階で適切な選択を行う。
このフローは知識グラフ構築の標準方針。 規模が大きい場合は Neo4j (Property Graph) + Cypher、 標準準拠が必要なら GraphDB / Stardog (RDF) + SPARQL を選ぶ実装パターンが定着している。