論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
文章間類似度
Text Similarity
NLP

🔖 キーワード索引

NLP類似度コサイン類似度TF-IDF埋め込みBERT

「text similarity」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「text similarity」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。

text similarityコサイン類似度TF-IDF埋め込みJaccard・編集距離適用範囲落とし穴関連手法Python 実装検証方法

これらのキーワードは「text similarity の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。

💡 30秒で分かる結論

🍰 まずはやさしく

文章の似ている度合いを測る定規のようなものです。

内容がどれだけ近いかを数値で知るために使います。

ネット通販で似た商品を探すときに役立っています。

この章では、似ていることを測る指標について読みます。

文章間類似度 ── 2文書の類似度を測る指標

📍 文脈 ── どこで出会うか

🍰 まずはやさしく

似た文章を探し出すための仕組みのことです。

検索エンジンなどで正しい答えを出すために使います。

スマホでよくある質問から答えを探すときに使われます。

ここでは、この仕組みがどこで使われるかを読みます。

検索エンジン、 質問応答、 重複論文検出など、 「似ている文書を探す」タスクは至るところに登場。 仕組みは「文書をベクトルに変換し、 ベクトル間の距離を測る」というシンプルな枠組みです。

本ページでは「text similarity(文章間類似度)」を扱う。 自然言語処理 (NLP) の基礎概念で、 2 つの文書(記事・質問文・レビュー・論文要旨など)をベクトルに変換し、 その近さを数値化する。 検索・重複検出・FAQ 応答・RAG など、 「似た文書を探す」あらゆるタスクの土台になる。

「text similarity」は NLP・情報検索の体系における重要概念のひとつ。 本ページは「定義・直感・数式・実装・落とし穴・関連手法」の 6 視点で構成され、 各視点は独立して読めるが順序通り読むと体系的な理解が得られる。

🎨 直感で掴む

🍰 まずはやさしく

文章を数値の並びに変えて比べるイメージです。

言葉が違っても意味が同じかを判断するために使います。

「猫」と「ネコ」を同じ意味だと捉える例で考えます。

ここでは、直感的にわかりやすく仕組みを読みます。

3つの文を例に:

人間なら A と B は似ている、 C は別、 と即答できます。 これを機械にやらせるには:

  1. 各文をベクトル(数値の並び)に変換
  2. ベクトル同士の角度(コサイン)が小さい=似ている

表記揺れ「猫/ネコ」を吸収するには、 単語そのものでなく「意味」を捉える埋め込みが必要。

🎨 テキスト類似度を 3 つの切り口で押さえる

テキスト類似度は「表層 vs 意味」「文書長への耐性」「想定用途」の 3 軸で整理すると、 場面ごとの選択が明確になる。

🎨 直感を一段深める ── なぜ「長さ」でなく「向き」を見るのか

文書の類似度を測る出発点は「文書をベクトルにする」こと。 各単語(または N-gram)を 1 つの軸とみなし、 その文書での出現回数を座標にすると、 1 つの文書が高次元空間の 1 点(=原点から伸びる矢印)になります。 似た内容の文書は近い方向を向く —— この幾何学的な絵が、 あらゆる類似度指標に共通する土台です。

ではなぜ コサイン類似度 は「長さ」でなく「向き(角度)」を見るのか。 同じ話題を短く書いた文書 A と、 同じ話題を約 3 倍の分量で書いた文書 A' を考えます(架空の例)。 A' は A とほぼ同じ単語が約 3 倍出るので、 ベクトルとしては向きは同じまま長さだけ約 3 倍になります。 ここでユークリッド距離(矢印の先端どうしの直線距離)を測ると、 内容は同じなのに「遠い」と誤判定されてしまう。 コサイン類似度は内積を両者のノルム $\|\mathbf{u}\|\,\|\mathbf{v}\|$ で割ることで長さの情報を捨て、 向きだけを残す。 だから「一文のツイート」と「数百語の記事」でも、 話題が同じなら高い値になります。 文書長のばらつきに強いことが、 文書比較でコサインが定番になった理由です。

【向き = 長さで正規化した内積】
$$ \cos\theta = \frac{\mathbf{u}\cdot\mathbf{v}}{\|\mathbf{u}\|\,\|\mathbf{v}\|} = \hat{\mathbf{u}}\cdot\hat{\mathbf{v}}, \qquad \hat{\mathbf{u}} = \frac{\mathbf{u}}{\|\mathbf{u}\|} $$
両ベクトルを長さ 1 に正規化してから内積を取るのと同じ。 だから長さ(文書の分量)は結果に効かない。

Jaccard(集合)との違い:コサインが「各 N-gram が何回出たか(頻度ベクトルの向き)」を見るのに対し、 Jaccard 係数 は「その N-gram が出たか出ないか(集合の重なり)」だけを見ます。 頻度を 0/1 に潰すぶん情報は落ちますが、 「2 記事のキーワード集合がどれだけ被るか」のような場面では直感的で計算も軽い。 下の 🎮 触って理解する で「一語置換」プリセットを選ぶと、 頻度を見るコサインと、 集合だけを見る Jaccard で反応が変わる様子を確かめられます。

📐 定義/数式

🍰 まずはやさしく

似ている度合いを計算するための数式です。

客観的な数字で近さを表すために使います。

単語がどれくらい重なっているかを計算します。

ここでは、具体的な計算方法について読みます。

【コサイン類似度】
$$ \cos(\mathbf{u}, \mathbf{v}) = \frac{\mathbf{u} \cdot \mathbf{v}}{\|\mathbf{u}\| \, \|\mathbf{v}\|} \in [-1, 1] $$
2ベクトルのなす角度の cos。 同じ方向なら +1、 直交なら 0
【Jaccard 類似度(集合版)】
$$ J(A, B) = \frac{|A \cap B|}{|A \cup B|} \in [0, 1] $$
単語集合の重なり率

📐 もう一段の数式表現

「テキスト類似度」を厳密に書き下すと、 以下の形になります。 既出の数式と合わせて読むと、 概念の骨格が見えてきます。

【テキスト類似度・追加表現】
$$ \cos(\mathbf{u}, \mathbf{v}) = \frac{\mathbf{u} \cdot \mathbf{v}}{\|\mathbf{u}\|\,\|\mathbf{v}\|} \in [-1, 1] $$
コサイン類似度。 ベクトルの内積を長さの積で割った値。 1 で同一方向、 0 で直交、 −1 で逆向き。
📌 ポイント:数式を見たら各記号の単位・値域を声に出して確認してみると、 抽象度がぐっと下がります。 「変数 X は連続値、 0 以上、 単位は人」のように。

📐 類似度・距離指標いろいろ

指標適用層特性
Jaccard表層(集合)|A∩B|/|A∪B|、 重複検出
Dice表層(集合)2|A∩B|/(|A|+|B|)
Cosine (TF-IDF)語彙ベクトルスパース・高速
BM25スパース検索文書長補正付き TF-IDF
編集距離 (Levenshtein)文字レベルスペル訂正、 typo 検出
Word Mover's Distance語埋め込み語間距離の最適輸送
Cosine (Sentence-BERT)意味埋め込み意味的類似度の決定版
BERTScoreトークン埋め込み機械翻訳評価で標準

💼 応用ユースケース

🎓 理論的背景の補強

「テキスト類似度」を学術的に位置付けるには、 関連する基盤理論を押さえると体系が見えてきます。 ここでは、 数学的・統計的な理論ベースを 4 つの観点で整理します。

① 数学的基礎

「テキスト類似度」は線形代数・解析学・確率論の上に立っています。 ベクトル空間・関数解析・測度論などの基礎理論があると、 本用語の定義がなぜこの形なのかが腑に落ちやすくなります。 大学初年級の教科書(線形代数入門、 解析学基礎、 確率論入門)から該当章を確認すると効率的です。

② 統計学からの視点

「テキスト類似度」は推定・検定・モデリングの観点から見ると、 別の側面が見えてきます。 古典統計(頻度論)とベイズ統計では同じ概念でも扱い方が異なるので、 両方の立場で考えてみると理解が深まります。 例えば、 信頼区間は頻度論、 信用区間はベイズ的解釈です。

③ 機械学習からの視点

機械学習では、 「テキスト類似度」は損失関数・正則化・汎化性能などの文脈で再解釈されます。 教師あり/教師なし/強化学習という 3 つの大枠の中で、 本用語がどこに位置付くかを確認すると、 応用範囲が見えてきます。 特に深層学習時代では、 古典的概念が新しい意味で復活する例が多くあります。

④ 情報理論からの視点

エントロピー・KL ダイバージェンス・相互情報量などの情報理論概念は、 「テキスト類似度」を測定・評価する際の共通言語を提供します。 Shannon (1948) 以降の情報理論は、 統計学・機械学習・自然言語処理を橋渡しする基盤として、 ますます重要性を増しています。

🧭 学習のコツ:4 つの視点を全て同時に追う必要はありません。 自分のバックグラウンドに近い視点から入り、 慣れたら他の視点で同じ概念を捉え直すと、 「テキスト類似度」の多面性が体感できます。

🏢 産業応用ケーススタディ

「テキスト類似度」が実際にどんな場面で使われるかを、分野ごとの典型的な使い方と注意点で示します(特定の企業の事例ではなく、使い方の型です)。

ケース 1:金融・保険業界

契約書・約款の新旧版で条項ごとに最も似た条項を対応付けて変更点を洗い出す、問い合わせ文に似た過去の問い合わせと回答を検索する、保険金請求の自由記述がほかの請求の文面とほぼ同じ(使い回し)でないかを調べる、といった場面で使われます。条項の照合では語順や数値の違いが重要なので、文字 n-gram や編集距離のような表層の指標が向き、意味の近さだけを見る埋め込みでは「支払う」と「支払わない」を近いと判定しかねない点に注意します。

ケース 2:医療・ヘルスケア

電子カルテの自由記述から似た症例を探す、表記ゆれのある病名(「高血圧症」「HT」「高血圧」)を標準病名のマスタに対応付ける、論文の要旨どうしを比べて関連研究を集める、といった用途があります。略語・否定表現(「発熱なし」)・同音の別語が多く、一般の文章で学習したモデルの類似度をそのまま信じると取り違えが起きるので、分野の辞書や分野のコーパスで学習した埋め込みを併用し、結果は人が確認します。

ケース 3:マーケティング・広告

商品レビューや問い合わせを類似度でまとめて、よく出る不満の話題を見つける、同じ商品が別名で登録されている商品名を名寄せする、閲覧した記事に内容の近い記事を推薦する(内容ベースの推薦)、などに使われます。レビューには定型の挨拶や署名が多く、下の ⚠️ で見る「共通の定型句が類似度を作る」問題がそのまま起きるので、前処理で取り除きます。

ケース 4:製造業・サプライチェーン

不具合報告書・保守記録の自由記述から、いま起きている故障に似た過去の事例と対処を検索する、取引先ごとに書き方の違う部品名・品名を名寄せする、といった場面で使われます。型番や寸法のような英数字の違いが決定的な意味を持つので、類似度が高くても型番が違えば別物として扱う規則を併用します。

ケース 5:公共政策・社会科学

自治体の条例・計画の文書を比べて似た施策を探す、パブリックコメントや自由回答の意見を類似度でまとめて件数を数える、異なる統計表の項目名を対応付ける(このページの SSDSE-B-2026 の列名の例)、などで使われます。項目名の対応付けでは、下の ⚠️ の図 3 のように名前が似ていても中身の定義(短期大学と大学、県全体と県庁所在市など)が違うことがあるので、最後は定義書で確かめます。

⚖️ 倫理・社会的責任

データサイエンスは強力な道具であり、 「テキスト類似度」のような手法も誤用すれば社会に害を与える可能性があります。 以下の倫理的論点は、 実務で常に意識すべきです。

🌍 持続可能なデータサイエンスへ:「テキスト類似度」を含む全ての分析が、 社会の利益と持続可能性に貢献するように設計・運用すべきです。 技術的可能性 ≠ 社会的妥当性。 倫理的判断は技術選択の最初に来るべきテーマです。

🔭 研究の最前線(2024–2026)

「テキスト類似度」を含む「NLP」カテゴリは、 急速に進化しています。 直近の研究動向を 5 つピックアップしました。 興味があるテーマは arXiv で「Text Similarity」「NLP」をキーワード検索すると最新論文に辿れます。

  1. 基盤モデルとの融合:大規模事前学習モデル(LLM、 Foundation Model)が古典手法を置き換えるか、 補強するかが論点。 ハイブリッド設計が増加。
  2. 因果推論との統合:相関だけでなく「介入」の効果を推定する因果機械学習。 「テキスト類似度」を因果グラフ上で解釈する研究が活発。
  3. 解釈可能性 (XAI):ブラックボックス AI の判断根拠を説明する技術。 SHAP・LIME・概念ベース説明(CAV、 TCAV)。
  4. 不確実性定量化:予測値だけでなく、 信頼区間・予測区間・Conformal Prediction による不確実性。
  5. 小データ学習:Few-shot、 Zero-shot、 Meta-learning、 Transfer learning。 「テキスト類似度」を限られたサンプルで適用する技術。

これらのテーマは互いに関連しているので、 1 つに興味を持ったら隣接領域に展開していくと知識ネットワークが広がります。

🔬 数式を言葉で読み解く

$\mathbf{u}, \mathbf{v}$
文書を表すベクトル(TF-IDFなら次元=語彙サイズ)
$\|\mathbf{u}\|$
ベクトルのノルム(長さ)。 文書の長さの違いを吸収
$\mathbf{u} \cdot \mathbf{v}$
内積。 共通単語が多いほど大
埋め込みベクトル
Word2Vec / BERT 等で得る、 意味を反映した低次元ベクトル(300〜1024次元)

🔬 数式を言葉で読み解く(拡張版)

テキスト類似度の数式は 「2 つの文書ベクトル $\mathbf{u}, \mathbf{v}$ の幾何学的距離(または角度)を、 0〜1 のスコアに変換する関数」。 ベクトル空間モデル(VSM)の伝統に立脚し、 cosine・Jaccard・編集距離・Sentence-BERT まで、 すべて同じ思想で繋がっています。 ここでは 4 つの主要記号を 1 つずつ日本語で読み解き、 文書ペアでの実例も併記します。

$\text{sim}(\mathbf{u}, \mathbf{v}) \in [0, 1]$ — 左辺(類似度スコア)
「2 つの文書ベクトルがどの程度似ているかを 0〜1 の値で出力する」関数。 0 が「完全に無関係」、 1 が「完全一致」。 cosine の場合は厳密には [-1, 1] だが TF-IDF のように成分が非負なら [0, 1] に収まる。 例えば「猫が魚を食べた」と「ネコがサカナを食う」を Sentence-BERT(multilingual-mpnet 実測)で比べると 0.75 前後、 「猫が魚を食べた」と「犬が骨を埋めた」なら 0.4 前後(モデルにより変動)。 値が大きいほど両文の内容が近いことを意味する。
$\dfrac{\mathbf{u} \cdot \mathbf{v}}{\|\mathbf{u}\| \, \|\mathbf{v}\|}$ — 右辺(cosine 類似度)
「内積をベクトルの大きさで正規化したもの」=「2 ベクトルがなす角度の cos 値」。 分子 $\mathbf{u} \cdot \mathbf{v} = \sum_i u_i v_i$ は共通成分を強調、 分母 $\|\mathbf{u}\| \|\mathbf{v}\|$ で長さの影響を除去。 「文書 A が長くて単語の出現回数が多い」だけで類似度が高くならないようにする工夫。 短文と長文を公平に比較できるのが cosine の強み。 「一文のツイート vs 数百語の記事」のように長さが大きく違う文書ペアの比較で重宝する。
$\mathbf{u}, \mathbf{v} \in \mathbb{R}^d$ — 文書ベクトル(添字 i は単語次元)
文書を$d$ 次元の数値ベクトルとして表現したもの。 BoW(Bag of Words)なら $d$ = 語彙サイズで TF(出現回数)が成分。 TF-IDF なら IDF で頻出語を割引、 Sentence-BERT なら $d$=384〜768 の密ベクトル。 添字 $i$ は単語(または特徴)番号、 $u_i$ はその次元の値を示す。
$J(A, B) = \dfrac{|A \cap B|}{|A \cup B|}$ — Jaccard 係数(集合版類似度)
「2 つの集合の共通要素を、 全体要素数で割った比率」。 cosine と並ぶもう 1 つの定番。 分子は「両方に出てくる単語数」、 分母は「少なくとも片方に出てくる単語数」。 [0, 1] の値域。 重複や順序を無視し、 集合的にしか比較しない単純さが特徴。 「2 記事に含まれるキーワード集合の重なり」のような場面では Jaccard が直感的。 ただし TF(頻度)情報が落ちるため、 文書比較では cosine + TF-IDF が一般的な選択肢。
📌 ポイント:類似度の数式は「表現方法(ベクトル化)」と「距離関数(cosine, Jaccard, …)」の 2 段階で構成される。 どちらを変えるかで結果が大きく変わるので、 論文や実装を読むときは必ず「ベクトル化は何か」「距離関数は何か」をペアで確認するクセをつけよう。

🧮 実値で計算してみる

「猫が魚を食べた」と「犬が骨を埋めた」の TF-IDF 簡易比較:

「猫が魚を食べた」と「ネコがサカナを食う」のBERT埋め込み比較:

🧮 指標ごとに値を並べて比べる

「東京の人口」と「東京は大都市」という短い文書ペアを題材に、 表層指標(TF-IDF cosine・Jaccard)と意味指標(Sentence-BERT cosine)で値がどう変わるかを確認します。 数式が「動く感覚」を得ることが目的です。

対象 計算結果
『東京の人口』vs『東京は大都市』 TF-IDF cosine0.20(語彙の重なり少)
Sentence-BERT cosine0.81(意味が近いと検出)
Jaccard 係数(単語集合)0.20(共通単語 1 / 合計 5)
📚 補足:表層指標(TF-IDF cosine 0.20、 Jaccard 0.20)は語彙の重なりしか見ないため低く出るが、 意味指標(Sentence-BERT cosine 0.81)は「人口/大都市」の意味的近さを捉えて高くなる。 値は分かち書き後の sklearn TfidfVectorizer と paraphrase-multilingual-mpnet-base-v2 での実測(SBERT はモデルにより変動)。 同じ文書ペアでも「表層 vs 意味」で値が大きく変わることが、 指標選択の勘所です。

🧮 数式に値を入れて手で計算する: ジャカード類似度

合成 2 文書のジャカード係数を計算する。

Step 1: 単語集合

D1 = {data, science, is, fun} D2 = {data, science, project} 共通: {data, science} = 2 合併: {data, science, is, fun, project} = 5

Step 2: ジャカード

J(D1, D2) = |共通|/|合併| = 2/5 = 0.40

🐍 Python で再現

1
2
3
4
D1 = {"data","science","is","fun"}
D2 = {"data","science","project"}
J = len(D1 & D2) / len(D1 | D2)
print(f"J: {J}")

📤 実行結果

J: 0.4

💬 手計算 (Step 2) 0.40 と Python 出力が完全一致。

🧮 実データで試す:SSDSE-B-2026 の列名 109 個どうしの類似度

上の手計算は作った短文でした。ここでは実在のテキストとして、SSDSE-B-2026 の 2 行目にある日本語の列名 109 個(年度・地域コード・都道府県を除く)を使います。列名は分かち書きされていない短い日本語なので、上の 🐍 と同じく文字の n-gram で比べます。まず、よく似た 2 つの列名のジャカード係数を文字 2-gram で手計算します。

Step内容値
1A =「短期大学卒業者のうち進学者数」(14 字)の 2-gram の集合短期・期大・大学・学卒・卒業・業者・者の・のう・うち・ち進・進学・学者・者数 の 13 個
2B =「大学卒業者のうち進学者数」(12 字)の 2-gram の集合大学 から 者数 までの 11 個(A の 3 番目以降と同じ)
3共通部分 |A ∩ B| と和集合 |A ∪ B|B の 11 個はすべて A にある → 11、和集合は A と同じ 13
4ジャカード係数 = |A ∩ B| / |A ∪ B|11 / 13 = 0.846

🎯 このコードでやること:Step 1〜4 を Python で再現する。文字列から 2 文字ずつの集合を作り、共通部分と和集合の大きさからジャカード係数を出す。

📥 入力データ:SSDSE-B-2026 の列名 E650110「短期大学卒業者のうち進学者数」と E650210「大学卒業者のうち進学者数」(文字列そのもの)。

1
2
3
4
5
6
7
8
9
10
11
a = '短期大学卒業者のうち進学者数'
b = '大学卒業者のうち進学者数'

def bigrams(s):
    return {s[i:i + 2] for i in range(len(s) - 1)}

A, B = bigrams(a), bigrams(b)
print(f'|A| = {len(A)}, |B| = {len(B)}')
print(f'A ∩ B = {len(A & B)}, A ∪ B = {len(A | B)}')
print(f'A だけにある 2-gram: {sorted(A - B)}')
print(f'ジャカード = {len(A & B)}/{len(A | B)} = {len(A & B) / len(A | B):.3f}')

📤 実行結果:

|A| = 13, |B| = 11 A ∩ B = 11, A ∪ B = 13 A だけにある 2-gram: ['期大', '短期'] ジャカード = 11/13 = 0.846

💬 結果の読み方:手計算と同じ 13・11・11・13、ジャカード 0.846 が出ました。違いは A だけにある「短期」「期大」の 2 個だけで、文字列の上ではほぼ同じ列名です。しかし「短期大学」と「大学」は別の学校種で、指している人数もまったく別です。文字の重なりは意味の近さを保証しません(下の ⚠️ で、データの相関とも比べます)。

次に 109 個すべての組み合わせ(109 × 108 ÷ 2 = 5,886 ペア)を、TF-IDF で重み付けした文字 2〜3-gram のコサイン類似度で比べます。

🎯 このコードでやること:109 個の列名を文字 2〜3-gram の TF-IDF ベクトルにし、全ペアのコサイン類似度を計算する。類似度 0 のペアの割合、類似度の高いペア上位 5 組、3 つの列名に最も近い列名を表示する。

📥 入力データ:SSDSE-B-2026.csv の 2 行目(日本語の列名)。skiprows=[0], nrows=0 で列名だけを読み、先頭 3 列を除いた 109 個。データの行は使わない。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

# 2 行目の日本語の列名だけを読む(先頭 3 列の 年度・地域コード・都道府県 は除く)
names = list(pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932',
                         skiprows=[0], nrows=0).columns[3:])
X = TfidfVectorizer(analyzer='char', ngram_range=(2, 3)).fit_transform(names)
S = cosine_similarity(X)
print(f'列名 {len(names)} 個、特徴(2〜3 文字){X.shape[1]} 種類')

iu = np.triu_indices(len(names), 1)
v = S[iu]
print(f'ペア {len(v)} 組のうち類似度 0: {(v == 0).sum()} 組({(v == 0).mean():.1%})')
print('類似度の高いペア:')
for k in np.argsort(v)[::-1][:5]:
    print(f'  {v[k]:.3f}  {names[iu[0][k]]} / {names[iu[1][k]]}')
for q in ['総人口', '一般病院数', '年平均気温']:
    i = names.index(q)
    s = S[i].copy(); s[i] = -1
    j = s.argmax()
    print(f'{q} に最も近い列名: {names[j]}({s[j]:.3f})')

📤 実行結果:

列名 109 個、特徴(2〜3 文字)646 種類 ペア 5886 組のうち類似度 0: 5032 組(85.5%) 類似度の高いペア: 0.917 短期大学卒業者のうち進学者数 / 大学卒業者のうち進学者数 0.878 転出者数(日本人移動者) / 転出者数(日本人移動者)(女) 0.878 転出者数(日本人移動者) / 転出者数(日本人移動者)(男) 0.878 転入者数(日本人移動者) / 転入者数(日本人移動者)(女) 0.878 転入者数(日本人移動者) / 転入者数(日本人移動者)(男) 総人口 に最も近い列名: 総人口(女)(0.624) 一般病院数 に最も近い列名: 一般診療所数(0.069) 年平均気温 に最も近い列名: 最高気温(日最高気温の月平均の最高値)(0.125)

💬 結果の読み方:5,886 ペアの 85.5% は共通の 2〜3 文字が 1 つも無く、類似度がちょうど 0 です。短い文書どうしでは、ほとんどのペアが「まったく似ていない」と出て差が付きません。上位は「(男)」「(女)」が付くかどうかだけが違う列名で、どれも 0.878 と同点です。「一般病院数」に最も近い「一般診療所数」でも 0.069 と低く、共通するのは「一般」の 2 文字だけで、「病院」と「診療所」が同じ医療施設だという知識は文字 n-gram には入っていません。

SSDSE-B-2026 の列名 109 個どうしの類似度行列
図 1 列名 109 個どうしのコサイン類似度(TF-IDF 文字 2〜3-gram、対角は除く)。列コード順に並べ、分野の頭文字が変わる位置に橙の線を引いた。同じ分野のペアの平均は 0.147、違う分野のペアは 0.002。code/glossary_figs/text-similarity.py で作図。

→ 類似度が付くのはほとんど対角線の近く、つまり同じ分野の中です。分野の違う列名どうし(例:人口の A と医療の I)はほぼ 0 で、文字の重なりは「同じ調査・同じ書式で名付けられた列」を見つけていると言えます。右下の L(家計調査)の 11 列だけが一様に濃いのは、どれも末尾に「(二人以上の世帯)」が付いているためで、次の ⚠️ で確かめます。

n-gram の長さ n を変えると、類似度の「粗さ」が変わる

🎯 このコードでやること:同じ 109 個の列名で、文字 n-gram の範囲を 1 文字・1〜2 文字・2〜3 文字・3 文字の 4 通りに変え、類似度 0 のペアの割合と、3 組の列名ペアの類似度を比べる。

📥 入力データ:SSDSE-B-2026.csv の 2 行目の列名 109 個(上と同じ)。比べる 3 組は「一般病院数/一般診療所数」「年平均気温/最高気温(日最高気温の月平均の最高値)」「婚姻件数/離婚件数」。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

names = list(pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932',
                         skiprows=[0], nrows=0).columns[3:])
pairs = [('一般病院数', '一般診療所数'), ('年平均気温', '最高気温(日最高気温の月平均の最高値)'),
         ('婚姻件数', '離婚件数')]
iu = np.triu_indices(len(names), 1)
for ng in [(1, 1), (1, 2), (2, 3), (3, 3)]:
    S = cosine_similarity(TfidfVectorizer(analyzer='char', ngram_range=ng).fit_transform(names))
    zero = (S[iu] == 0).mean()
    vals = '  '.join(f'{a[:4]}/{b[:4]} {S[names.index(a), names.index(b)]:.3f}' for a, b in pairs)
    print(f'n={ng}: 類似度 0 のペア {zero:6.1%}  {vals}')

📤 実行結果:

n=(1, 1): 類似度 0 のペア 39.9% 一般病院/一般診療 0.341 年平均気/最高気温 0.469 婚姻件数/離婚件数 0.606 n=(1, 2): 類似度 0 のペア 39.9% 一般病院/一般診療 0.226 年平均気/最高気温 0.348 婚姻件数/離婚件数 0.417 n=(2, 3): 類似度 0 のペア 85.5% 一般病院/一般診療 0.069 年平均気/最高気温 0.125 婚姻件数/離婚件数 0.132 n=(3, 3): 類似度 0 のペア 93.9% 一般病院/一般診療 0.000 年平均気/最高気温 0.000 婚姻件数/離婚件数 0.000

💬 結果の読み方:1 文字単位では 5,886 ペアのうち類似度 0 は 39.9% に減り、婚姻件数と離婚件数は「件」「数」「婚」を共有して 0.606 と高く出ます。3 文字単位では 93.9% が 0 になり、3 組とも 0 です。n が小さいと「数」「件」のようなありふれた文字でも似ていると判定して粗く、n が大きいと言い回しが少し違うだけで 0 になって厳しすぎます。上の 2〜3 文字はその中間で、婚姻と離婚(0.132)のように逆の出来事を表す語も「件数」の共有で少し似ると出る点は変わりません。どの n が良いかは、似ていてほしいペアの例を数組決めて、その順位が正しく出るかで選びます。

🐍 Python 実装

最小限のスニペットで動作確認できる例。 短い日本語文のペアを想定しています。

🎯 解説: テキスト類似度の古典的指標「TF-IDF × コサイン類似度」を sklearn で実装。 analyzer='char_wb' は単語境界を意識した文字 n-gram で、 日本語の前処理(分かち書き)不要で動く実用テクニック。 ngram_range=(2,3) で 2〜3 文字の連続を特徴とし、 共通する部分文字列(「を食」等)の分だけスコアが付く。 ただし「猫⇔ネコ」のような文字種の違い自体は吸収できない(実行結果参照)。
📥 入力例: docs = ["猫が魚を食べた", "ネコがサカナを食う", "犬が骨を埋めた"] → 3 文書を総当たりで比較し、 3x3 の類似度行列を得る → 「猫⇔ネコ」は表記ゆれ、 「猫⇔犬」は無関係、 という直感が数値に出るか確認する
1
2
3
4
5
6
7
8
9
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

docs = ["猫が魚を食べた", "ネコがサカナを食う", "犬が骨を埋めた"]
vec = TfidfVectorizer(analyzer='char_wb', ngram_range=(2,3))
X = vec.fit_transform(docs)
sim = cosine_similarity(X)
print(sim)  # 3x3 の類似度行列
# 現代版:from sentence_transformers import SentenceTransformer
📤 実行結果(sklearn で実測): [[1. 0.03566492 0.04026154] [0.03566492 1. 0. ] [0.04026154 0. 1. ]] → 行0×行1(猫⇔ネコ)= 0.036: 共通の 2〜3 文字 n-gram は「を食」の 1 個だけ → 行0×行2(猫⇔犬)= 0.040: 共通は語尾の「た」(境界 n-gram)のみ、 ほぼ無関係 → 表記揺れペア(0.036)が無関係ペア(0.040)を下回る点に注目。 文字 n-gram だけでは「猫=ネコ」を捉えられない → 対角は自身との比較なので 1.000(理論値)
💬 読み方: TF-IDF×コサインは「語彙レベルの類似度」しか測れない。 上記の例で 0.036 と極端に低く、 無関係ペア(0.040)すら下回るのは、 "猫=ネコ" の表記揺れを文字 n-gram では吸収できないため。 LLM 時代の SOTA は SentenceTransformers(多言語 LaBSE / paraphrase-MiniLM)で、 同じ意味の異言語・異表記でも 0.85+ に達する。 ただしニューラルネットで文を符号化するぶん、 計算コストは TF-IDF より桁違いに大きい。 検索エンジンでは TF-IDF を一次フィルタ、 埋め込みを二段目に重ねる「ハイブリッド検索」が定石。

🐍 Python 実装(拡張版)

表層類似度(TF-IDF + cosine)と意味類似度(Sentence-BERT)を比較。 同じ文ペアで両者の値がどう違うか確認。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sentence_transformers import SentenceTransformer

docs = [
    '東京の人口は多い',
    '首都圏は人口密度が高い',
    'リンゴは美味しい果物です',
]

# (1) TF-IDF + cosine
vec = TfidfVectorizer()
tfidf = vec.fit_transform(docs)
print('TF-IDF cosine:')
print(cosine_similarity(tfidf).round(2))

# (2) Sentence-BERT
sbert = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')
emb = sbert.encode(docs)
print('SBERT cosine:')
print(cosine_similarity(emb).round(2))
📤 実行例(実測): TF-IDF cosine: [[1. 0. 0. ] [0. 1. 0. ] [0. 0. 1. ]] → TfidfVectorizer の既定の分割は空白区切りなので、分かち書きしていない日本語は 1 文がまるごと 1 語になる(語彙は 3 文そのもの)。1 と 2 は「人口」を共有しているのに語として切り出されず、類似度 0 SBERT cosine: [[1. 0.67 0.1 ] [0.67 1. 0.08] [0.1 0.08 1. ]] → 意味的に近い 1, 2 が 0.67 で検出(SBERT の行はキャッシュ済みの paraphrase-multilingual-mpnet-base-v2 で実測。初回はモデルのダウンロードが必要で、値はモデルと版により変わる)

RAG(検索拡張生成)や FAQ 検索では SBERT が標準。 ただし文ごとにニューラルネットを通すので、 計算コストは TF-IDF より桁違いに大きい。 ハイブリッド(BM25 + dense)が実務最適解になることが多い。

⚠️ よくある落とし穴

❌ 1. 短文ではコサインがブレやすい
単語1個違いで類似度が大変動。 平滑化や n-gram で対応
❌ 2. ストップワードの影響
「が」「を」が一致しても意味は近くない。 除去するか TF-IDFで重み下げ
❌ 3. TF-IDFは語の意味を理解しない
「車」と「自動車」が違う次元になる。 埋め込みが必要
❌ 4. 翻訳での類似度比較
多言語埋め込み(LaBSE等)を使う
❌ 5. 正規化忘れ
文書長で類似度が偏る。 必ずノルム正規化(コサインなら自動)

⚠️ さらに 5 つの落とし穴 — 実務で痛い目を見るパターン

❌ デフォルト設定をそのまま信じる
ライブラリのデフォルト引数は「平均的なケース」向け。 あなたのデータが平均的でなければ、 必ず設定を再検討する必要があります。 公式 docstring を読む癖をつけましょう。
❌ 正規化を忘れる
文書長がバラバラだと、 生の頻度ベクトルは長文ほどノルムが大きくなる。 コサイン類似度は自動でノルム正規化されるが、 Euclidean 距離やドット積を使う場合は L2 正規化を忘れないこと。
❌ 訓練データでの前処理パラメータをテストに使わない
StandardScaler の fit は訓練データだけに対して行い、 テストには transform のみを適用。 これを混同するとデータリーケージになる。
❌ 類似度の絶対値を鵜呑みにする
コサイン 0.8 が「似ている」かはデータ次第。 非負ベクトル(TF-IDF)では値が全体的に高く出るため、 閾値は必ず自分のコーパスの分布(パーセンタイル)から決めること。
❌ 「精度が高い = 良いモデル」と短絡
precision/recall の不均衡、 ラベルの偏り、 ベースラインとの比較を見ないと、 「高精度」は単に多数派を予測しているだけかもしれません。

🕰 歴史的経緯と現代的意味

文章間類似度 は、 統計学と計算機科学の流れの中から生まれました。 下の年表はこの分野全体の流れで、 文章間類似度 固有の年表ではありません。 この用語がどの時代の産物かを掴むために置いています。

時期出来事この時代に起きたこと
前史統計学・情報理論の基盤整備数式的な土台
古典期機械学習の黎明(1960〜80 年代)「テキスト類似度」の原型が登場
展開期scikit-learn / TensorFlow など実装の普及(2010〜)誰でも 1 行で使える時代に
現代大規模モデル時代(2020〜)「テキスト類似度」の意味が再解釈される

現代の文脈では、 古典的な定義のままでは説明しきれない使い方も出てきています。 教科書の定義を出発点としつつ、 実務での「変奏」も知っておくとよいでしょう。

❓ よくある質問

Q1. なぜこの定義になっているの? 別の式じゃダメ?

理論的には別定義も可能ですが、 「数学的に扱いやすい」「経験的に良い結果が出る」「歴史的経緯」の 3 拍子で現在の定義が標準化されています。 学術論文では別定義を「変種」として議論することもよくあります。

Q2. 文書が数件しかなくても類似度は使える?

ペアごとの計算自体は数件でもできます。 ただし閾値を「上位◯%」のように分布で決める運用は、 文書数が少ないと不安定です。 少数のときは値の絶対比較に留め、 過度な一般化は避けましょう。

Q3. scikit-learn 以外でも実装はある?

PyTorch / TensorFlow / XGBoost / LightGBM など多数。 ただし基本的な動作確認は scikit-learn が一番速いので、 まず sklearn で動かしてから他に移植するのがおすすめ。

Q4. 大規模データ(百万行)でも同じ方法でいける?

計算量・メモリの観点でアルゴリズムを切り替える必要があります。 全ペア計算をやめ、 FAISS などの近似最近傍探索 (ANN) で上位 K 件だけ取り出す設計に切り替えます。 小さな文書集合で本質を理解した後の応用課題です。

Q5. 論文を書くとき、 この概念をどう引用すべき?

古典的な定義は原典(教科書や著名論文)、 実装は使用ライブラリのバージョン情報を併記するのが標準。 「Murphy 2012」「Hastie et al. 2009」あたりが定番引用です。

Q6. 関連用語との学習順序は?

下の「📚 関連グループ教材」セクションのリストが、 推奨される学習順序の一つです。 上位概念から入って詳細に降りる「トップダウン」と、 1 つの具体例から始めて他に広げる「ボトムアップ」、 どちらも一長一短。 自分の学び方に合わせて。

⚠️ 落とし穴(追加版・各 100 字以上)

既出の落とし穴に加えて、 中級者でも踏みやすい応用フェーズの罠を集めました。 1 度経験するか、 ここで読んでおけば回避できます。

❌ 適用範囲の越境
「テキスト類似度」は特定の仮定の下で意味を持ちます。 仮定(独立性・線形性・定常性・尺度など)を確認せずに別ドメインに転用すると、 結果が解釈不能になります。 適用前にチェックリストで仮定を点検しましょう。
❌ サンプルサイズ不足での過信
短文どうしや、 数件しかない文書集合では、 類似度の値が単語 1 個の増減で大きくブレます。 単一ペアの値だけで判断せず、 複数ペアの分布や信頼区間を見て、 頑健性を確かめてください。
❌ ハイパーパラメータ依存
「テキスト類似度」を実装する際、 ライブラリのデフォルト値が常に最適とは限りません。 主要な引数の意味を 1 度公式ドキュメントで確認し、 自分のデータでグリッドサーチや感度分析を行うと、 結果の頑健性が分かります。
❌ 結果の単独評価
単一の指標・単一のモデルだけで結論を出さず、 必ず複数の角度から確認しましょう。 「テキスト類似度」だけでなく、 並列・派生の手法でクロスチェックすると、 結果の頑健性が大きく上がります。 報告書には複数結果を併記。
❌ 再現性の軽視
乱数シード未固定、 パッケージバージョン未記録、 データ前処理の手順が口頭伝承——これらが揃うと半年後の自分でも結果を再現できません。 解析コードを Notebook 化し、 Git で管理する習慣を最初から付けるのが結果的に最速です。

⚠️ 仕組みに根ざした落とし穴 ── なぜ外すのかまで理解する

既出の落とし穴を、 「ベクトル化の仕組みのどこに原因があるか」まで踏み込んで整理します。 症状を暗記するのではなく、 原理から「なぜそうなるか」を掴むと、 未知のデータでも自分で危険を察知できます。

❌ BoW・N-gram 集合は語順を捨てる
Bag of Words(BoW)は「どの単語が何回出たか」だけを数え、 並び順を保持しません。 その結果「犬が猫を追う」と「猫が犬を追う」は N=1 なら完全に同一のベクトルになり、 主語と目的語が入れ替わった意味の大違いを検出できません(架空の例)。 対策は N-gram(2〜3 連接)で局所的な語順を取り込むこと。 ただし N を上げると次元が急増し、 後述の疎性・表記揺れ感度とのトレードオフになります。
❌ 頻出語が類似度を過大評価する
生の出現回数(TF)をそのまま使うと、 「の」「を」「する」のようにどの文書にも出る語が内積を大きく押し上げ、 内容と無関係に「似ている」と出てしまいます。 TF-IDF は「多くの文書に出る語ほど重みを下げる(IDF)」ことでこれを緩和します。 検索エンジンで使う BM25 は、 さらに文書長補正を加えた TF-IDF の発展形です。
❌ 同義語・多義語を捉えられない
TF-IDF や BoW は語を別々の独立した軸として扱うため、 「車」と「自動車」は完全に無関係な次元になり(同義語問題)、 逆に「はし(橋/箸)」のような多義語は文脈を無視して同一視されます(多義語問題)。 これは表層手法の原理的な限界で、 単語や文の意味をベクトルに埋め込む手法(word2vec・埋め込み・BERT)でしか根本解決できません。
❌ コサインとユークリッド距離を取り違える
「向き」を比べたいのか「位置(大きさ込み)」を比べたいのかで指標が変わります。 文書長がバラバラなテキスト比較では長さを無視するコサインが適切。 一方、 各成分の絶対量そのものに意味がある場合(カウントの大小を保ちたい等)はユークリッド距離が向きます。 なお L2 正規化したベクトルではユークリッド距離とコサインは単調に対応し($\|\hat{\mathbf u}-\hat{\mathbf v}\|^2 = 2(1-\cos\theta)$)、 正規化さえ済ませれば両者の結論は一致します。
❌ 正規化を忘れて長文が有利になる
コサインは定義上ノルムで割るので自動正規化されますが、 内積やユークリッド距離を直接使うと長文ほどノルムが大きくなり、 類似度・距離が長さに引きずられます。 埋め込みベクトルを保存して後から内積検索する場合も、 事前に L2 正規化しておかないと結果が歪みます。
❌ 次元の呪い ── 疎ベクトルの落とし穴
語彙サイズ=次元数なので、 BoW・TF-IDF のベクトルは数万〜数十万次元になり、 各文書はそのうちごく一部しか非ゼロでないスパース(疎)ベクトルです。 高次元では点どうしの距離が均質化して「近い/遠い」の差が付きにくくなる(次元の呪い)。 実務では、 語彙を絞る・IDF で刈り込む・埋め込みで数百次元の密ベクトルに圧縮する、 といった次元削減で対処します。

⚠️ 実データで見る落とし穴:共通の定型句が類似度を作ってしまう

SSDSE-B-2026 の家計調査の 11 列(L3221 消費支出〜L322110 その他の消費支出)は、各都道府県の県庁所在市の二人以上世帯の月平均の値で、県全体の平均ではありません。列名にはすべて「(二人以上の世帯)」という同じ括弧書きが付いています。この定型句を除くと類似度がどう変わるかを見ます。

🎯 このコードでやること:列名から全角の括弧書き(例「(二人以上の世帯)」「(男)」)を正規表現で除き、除く前と後で、家計調査の列それぞれに最も近い列名と類似度を比べる。

📥 入力データ:SSDSE-B-2026.csv の 1 行目(列コード)と 2 行目(日本語の列名)。列コードが L で始まる 11 列を家計調査の列とする。

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 re
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

codes = list(pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', nrows=0).columns[3:])
names = list(pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932',
                         skiprows=[0], nrows=0).columns[3:])
strip = [re.sub(r'([^)]*)', '', n) for n in names]      # 全角括弧の中身ごと除く

def sim(texts):
    X = TfidfVectorizer(analyzer='char', ngram_range=(2, 3)).fit_transform(texts)
    S = cosine_similarity(X)
    np.fill_diagonal(S, -1)
    return S

S1, S2 = sim(names), sim(strip)
L = [i for i, c in enumerate(codes) if c.startswith('L')]   # 家計調査の 11 列
for i in L[:4] + L[-1:]:
    j1, j2 = S1[i].argmax(), S2[i].argmax()
    after = f'{strip[j2]}({S2[i, j2]:.3f})' if S2[i, j2] > 0 else '似た列名なし(0.000)'
    print(f'{strip[i]:<10} そのまま: {strip[j1]}({S1[i, j1]:.3f}) → 括弧を除く: {after}')
zero = sum(S2[i].max() <= 0 for i in L)
print(f'括弧を除くと、11 列のうち {zero} 列は似た列名が 1 つも無くなる')

📤 実行結果:

消費支出 そのまま: その他の消費支出(0.785) → 括弧を除く: その他の消費支出(0.588) 食料費 そのまま: 住居費(0.638) → 括弧を除く: 似た列名なし(0.000) 住居費 そのまま: 食料費(0.638) → 括弧を除く: 似た列名なし(0.000) 光熱・水道費 そのまま: 食料費(0.514) → 括弧を除く: 似た列名なし(0.000) その他の消費支出 そのまま: 消費支出(0.785) → 括弧を除く: 消費支出(0.588) 括弧を除くと、11 列のうち 9 列は似た列名が 1 つも無くなる

💬 結果の読み方:括弧書きをそのまま使うと、食料費・住居費・光熱・水道費は互いに 0.51〜0.64 の類似度になりますが、括弧を除くと 11 列のうち 9 列は似た列名が 1 つも無く、類似度は 0 になりました。0.5〜0.6 の類似度は、ほぼすべて「(二人以上の世帯)」の 8 文字が作っていたことになります。残ったのは「消費支出」と「その他の消費支出」(0.588)だけで、これは本当に同じ語を含む組です。

家計調査の 11 列の、最も似た列名との類似度(括弧書きの有無)
図 2 家計調査の 11 列それぞれについて、最も似たほかの列名とのコサイン類似度。青は列名そのまま、橙は括弧書きを除いた列名(橙の棒が無い 9 列は類似度 0)。

→ 定型句・署名・免責文・テンプレートの見出しのように、内容と関係なく多くの文書に共通して入る文字列は、類似度を底上げして「全部似ている」ように見せます。IDF は多くの文書に出る語の重みを下げますが、「(二人以上の世帯)」は 109 個中 11 個(約 1 割)にしか出ないので下げ方は小さく、しかも括弧書きの 2〜3 文字の断片が十数個まとめて一致するため、短い列名の本体(「食料費」なら 3 文字)よりも類似度への効きが大きくなります。類似度を計算する前に、共通の定型句を取り除く前処理を入れるのが定石です。

⚠️ 名前が似ていることと、データが似ていることは別

列名の類似度を「似た変数を探す」のに使いたくなることがあります。そこで 5,886 ペアすべてについて、列名の類似度と、2023 年度の 47 都道府県でのデータの相関係数の絶対値 |r| を並べます。

🎯 このコードでやること:列名の類似度(TF-IDF 文字 2〜3-gram のコサイン)と、2023 年度 47 県のデータの相関係数の絶対値を 5,886 ペアで対応させ、両者の相関と、名前が似ていないのにデータが強く相関するペアの割合などを数える。

📥 入力データ:SSDSE-B-2026.csv の 2023 年度 47 行、109 列すべて(列名と数値の両方)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

path = 'data/raw/SSDSE-B-2026.csv'
codes = list(pd.read_csv(path, encoding='cp932', nrows=0).columns[3:])
names = list(pd.read_csv(path, encoding='cp932', skiprows=[0], nrows=0).columns[3:])
df = pd.read_csv(path, encoding='cp932', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023]                          # 2023 年度の 47 都道府県

S = cosine_similarity(TfidfVectorizer(analyzer='char', ngram_range=(2, 3)).fit_transform(names))
R = d[codes].astype(float).corr().values                   # 列どうしの相関(47 県)
iu = np.triu_indices(len(codes), 1)
x, y = S[iu], np.abs(R[iu])
print(f'列名の類似度と |相関| の相関: r = {np.corrcoef(x, y)[0, 1]:.3f}({len(x)} ペア)')
print(f'名前の類似度 0 のペア {(x == 0).sum()} 組のうち |r| > 0.9: {(y[x == 0] > 0.9).mean():.1%}')
print(f'名前の類似度 0.5 以上のペア {(x >= 0.5).sum()} 組のうち |r| < 0.5: {(y[x >= 0.5] < 0.5).sum()} 組')
for a, b in [('総人口', '一般診療所数'), ('年平均気温', '最高気温(日最高気温の月平均の最高値)'),
             ('短期大学卒業者のうち進学者数', '大学卒業者のうち進学者数')]:
    i, j = names.index(a), names.index(b)
    print(f'{a} / {b}: 名前 {S[i, j]:.3f}, データの r = {R[i, j]:+.3f}')

📤 実行結果:

列名の類似度と |相関| の相関: r = 0.107(5886 ペア) 名前の類似度 0 のペア 5032 組のうち |r| > 0.9: 39.0% 名前の類似度 0.5 以上のペア 100 組のうち |r| < 0.5: 11 組 総人口 / 一般診療所数: 名前 0.000, データの r = +0.972 年平均気温 / 最高気温(日最高気温の月平均の最高値): 名前 0.125, データの r = -0.063 短期大学卒業者のうち進学者数 / 大学卒業者のうち進学者数: 名前 0.917, データの r = +0.766

💬 結果の読み方:列名の類似度と |r| の相関は r = 0.107 とほとんど無関係でした。名前に共通の文字が 1 つも無いペア 5,032 組のうち 39.0% はデータの |r| が 0.9 を超えます。総人口と一般診療所数は名前の類似度 0 なのに r = +0.972 で、どちらも「県の規模」を映しているからです。反対に、年平均気温と最高気温は「気温」を共有して 0.125 ですが、データの r は −0.063 でほぼ無相関です。

列名の類似度とデータの相関係数の散布図
図 3 5,886 ペアの列名の類似度(横軸)とデータの相関係数の絶対値(縦軸、2023 年度 47 県)。横軸 0 の縦の帯が「名前は似ていないペア」で、|r| は 0 から 1 まで散らばる。橙は本文で挙げた 3 ペア。

→ テキスト類似度が測るのは「文字(や語・意味)の近さ」で、その文字が指す対象の数値が連動するかどうかではありません。変数を選ぶときは、名前の類似度で候補を広げるのはよいとしても、最後はデータそのもので確かめます。

🗺 用語マインドマップ — 周辺概念の整理

「テキスト類似度」を中央に置いて、 周辺概念を 5 つの方向に整理します。 これは記憶の足場になります。

方向隣接概念関係性
北 (上位)自然言語処理 / 情報検索文書間類似度が属する大枠
南 (下位)重複検出 / 検索エンジン / 推薦類似度を使う応用タスク
東 (発展)Sentence-BERT / FAISS / cross-encoder表層一致の限界を超える発展形
西 (前提)TF-IDF / Word2Vec / コサイン類似度類似度計算の基礎手法
中央テキスト類似度本ページの主役

マインドマップは「学んだ用語を整理する道具」として優秀。 紙にこの 5 方向を書き、 自分なりの隣接概念を埋めると、 暗黙的にあった理解構造が可視化されます。

📝 自己検証クイズ — 5 問

「テキスト類似度」を本当に理解できたか、 自分でテストできるクイズです。 答えは展開で確認。

Q1. 「テキスト類似度」を 30 秒で同僚に説明するとしたら、 何を最初に言う?

模範回答:上の「💡 30秒結論」を参照。 ポイントは「何のために使うか」を最初に言うこと。 定義や数式から入ると相手が引きます。

Q2. コサイン類似度の式で、 分子と分母はそれぞれ何を表す?

模範回答:分子の内積 $\mathbf{u}\cdot\mathbf{v}$ は共通成分の強調、 分母のノルム積 $\|\mathbf{u}\|\|\mathbf{v}\|$ は文書長の影響を打ち消す正規化。 上の「📐 定義/数式」と「🔬 数式を言葉で読み解く」を参照。

Q3. 同じ文書ペアでも、 指標を変えると結果はどう変わる?

模範回答:Jaccard や TF-IDF cosine は「同じ単語が出るか」しか見ないので表記ゆれや言い換えに弱い。 Sentence-BERT の cosine は意味を捉えるため、 単語が重ならなくても近い値を返す、 など。

Q4. 「テキスト類似度」と類似手法の最大の違いは?

模範回答:上の「🌐 似た概念との比較」表を参照。 1 文で言える違いを持っておくと、 「なぜこっちを選んだか」を説明できます。

Q5. 「テキスト類似度」を使うときに最も気をつけるべき落とし穴は?

模範回答:上の「⚠️ 落とし穴」と「⚠️ さらに 5 つの落とし穴」セクションから、 自分のプロジェクトに最も関連するものを 1 つ選んで言語化してみましょう。

📝 実データで確かめるクイズ

Q6. 「総人口」と「総人口(女)」の文字 2-gram のジャカード係数は?

模範回答:「総人口」の 2-gram は {総人, 人口} の 2 個、「総人口(女)」は {総人, 人口, 口(, (女, 女)} の 5 個。共通 2、和集合 5 で 2 / 5 = 0.4。括弧書きが長いほど、同じ変数の男女別でも係数は下がる。

Q7. 図 2 で「食料費(二人以上の世帯)」と「住居費(二人以上の世帯)」の類似度 0.638 が、括弧を除くと 0 になったのはなぜか。

模範回答:「食料費」と「住居費」には共通の 2 文字・3 文字が無く、共通していた n-gram はすべて「(二人以上の世帯)」とその前後の文字の組み合わせだったから。似ていたのは内容ではなく定型句。

Q8. 名前の類似度 0 のペアの 39.0% でデータの |r| が 0.9 を超えたのはなぜか。「似た変数を探す」目的では、どちらの類似度を使うべきか。

模範回答:総人口・診療所数・婚姻件数のように、名前は違っても「県の規模」に比例する列が多いから。数値の連動を知りたいならデータの相関(や距離)を使い、列名の類似度は同じ調査・同じ書式の列を見つけるのに使う。

🗺 学習ロードマップ

「テキスト類似度」を起点に、 同カテゴリ「NLP」を体系的に学ぶ推奨順序を示します。

  1. Week 1:本ページの定義・数式・直感を完全に押さえる。 1 日 30 分 × 5 日。
  2. Week 2:Python コードを写経し、 サンプル文書ペアで動作確認。 自分の扱う文書でも試す。
  3. Week 3:「🔗 関連用語」の前提側を読み、 基礎を補強する。
  4. Week 4:「🔗 関連用語」の並列側を読み、 比較できる引き出しを増やす。
  5. Week 5:「🔗 関連用語」の発展側を読み、 上位概念や応用に進む。
  6. Week 6:関連グループ教材で全体像を再確認し、 知識を再構築する。

📚 備考:6 週間は目安です。 自分のペースで進めて構いません。 重要なのは「定義 → 実装 → 関連用語 → 再構成」のサイクルを 1 度回し切ること。

❓ さらなる FAQ

Q. 「テキスト類似度」は古い手法ですか? 最新の AI で代替できますか?
A. 古いから無価値ではありません。 むしろ「テキスト類似度」のような基礎概念は新手法の解釈に必要。 LLM が出した結果を評価するのにも、 結局この種の概念が使われます。
Q. 表層類似度と意味類似度、 どちらを使えばいい?
A. まず TF-IDF cosine のような表層手法を一次フィルタに使い、 上位候補だけ Sentence-BERT の意味類似度で精査する「ハイブリッド」が実務では定石。 表記ゆれ・言い換えが重要なら最初から埋め込みを検討します。
Q. Python 以外の言語で同じことをするには?
A. Java なら Lucene / Elasticsearch、 JavaScript なら各種埋め込み API クライアントで同様の処理が可能。 「文書をベクトル化し距離を測る」という概念は言語によらず共通です。
Q. 数式が苦手です。 どこから手を付ければ?
A. 「🎨 直感で掴む」を 3 回読み、 「🧮 実値で計算」で手を動かす。 数式は最後で OK です。 概念の形が分かれば、 数式は記号の翻訳作業に過ぎなくなります。
文章間類似度 TF-IDF Word2Vec / GloVe BERT / SBERT FAISS Word Mover's Distance テキスト類似度

🔗 隣接手法への橋渡し

文章間類似度は「テキスト → ベクトル → スコア」の流れの真ん中で、 ベクトル化手法と距離指標の組合せが核となる。

例えば観光地の紹介文 47 件を題材に「ある記事に最も似た記事を 3 つ」検索するなら、 sentence-BERT で 768 次元埋め込み → 47×47 コサイン類似度行列 → top-3 抽出、 が典型 pipeline。

🌳 手法選択フロー

類似度手法の選択は、 テキスト長と精度要求で 4 通りに分岐する。

  1. 短文 (Twitter 等、 50 字以下)? Yes → Jaccard や 編集距離。 ベクトル化はノイズ多い
  2. 長文 (記事・論文)? Yes → TF-IDF + コサイン類似度。 高速・解釈可能
  3. 意味的類似 (同義語・パラフレーズ)? Yes → sentence-BERT + コサイン。 「都」と「東京」を近づける
  4. 多言語比較? Yes → multilingual BERT / LaBSE。 日英間の類似度も計算可

観光地の解説文同士のように定型表現が多い文書なら sentence-BERT がベスト。 TF-IDF だと「観光」「人口」など頻出の定型語に引っ張られ、 意味的差異を捉えにくい。

🎮 触って理解する

2 つのテキストを自由に書き換えると、 Jaccard 係数(文字 N-gram 集合)・コサイン類似度(文字 N-gram 頻度ベクトル)・編集距離(レーベンシュタイン距離)の 3 指標がリアルタイムに再計算されます。 1 文字変える・語順を入れ替える・表記だけ揺らす —— 同じ「変更」でも 3 指標の反応がまったく違うことを体感してください。 計算対象は入力文字列そのもの(空白や句読点も 1 文字として数えます)。

プリセット:
N-gram の長さ(ドラッグ / タップで変更): 1 2 3 現在:2 文字 N-gram(Jaccard・コサインに影響。 編集距離は常に 1 文字単位)
A の文字列(黄色=共通 N-gram に含まれる文字)
B の文字列(同上)
Jaccard 係数(集合の重なり)
—
|A∩B| / |A∪B|
コサイン類似度(ベクトルの向き)
—
N-gram 頻度ベクトルのなす角
編集距離(文字操作の回数)
—
正規化類似度 = 1 − d / max(|A|,|B|)
Jaccard:集合の重なり 0 0 0 J = — A のみ / 共通 / B のみ(N-gram 種類数) コサイン:頻度ベクトルの角度 A B θ = — 角度 0°=cos 1(同方向)、 90°=cos 0(無関係) 編集距離:操作回数 — 挿入・削除・置換の最小回数 ■ 1 個 = 1 操作

💡 直感 ── 何をもって「似ている」とするか

3 指標は「似ている」の定義がそれぞれ違います。 編集距離は「片方をもう片方に書き換える手間(挿入・削除・置換の回数)」、 Jaccard は「文章を N-gram という部品に分解したときの部品集合の重なり」、 コサイン類似度は「部品の出現頻度ベクトルの向き」を測ります。 上のプリセットで確かめてみましょう:

⚠️ よくある落とし穴(インタラクティブ版で確認)

🚀 発展 ── 表層から意味へ

上のデモは「頻度をそのまま数える」素朴なベクトル化ですが、 実務では 2 段階の発展があります。 第 1 段階は TF-IDF:どの文書にも現れる「が」「を」のような N-gram の重みを下げ、 その文書らしい部品を強調してからコサイン類似度を取る(本ページ「🐍 Python 実装」の TfidfVectorizer(analyzer='char_wb') がまさにこれ)。 第 2 段階は 埋め込み(embedding)ベースの類似度:BERT / Sentence-BERT で文を意味ベクトルに変換すれば、 「猫/ネコ」の表記揺れも「東京/首都圏」の言い換えも近いベクトルに写像され、 コサイン類似度が意味の近さを反映するようになる。 距離指標そのものの整理は距離、 NLP 全体での位置付けは自然言語処理のページを参照。