「この2つの文章、どれくらい似ている?」——検索エンジンも、レコメンドも、RAG(検索拡張生成)も、毎秒この問いに答え続けています。文章をベクトルに変換したあと、その「似ている度合い」を測る物差しとして最も広く使われているのがコサイン類似度(cosine similarity)です。
なぜ距離ではなくコサインなのか。答えは「長さを無視して、向きだけを比べたい」から。長い文書と短い文書では、同じ話題でもベクトルの大きさが違ってしまいます。コサイン類似度は2つのベクトルのなす角だけを見るので、この長さの違いに惑わされません。
コサイン類似度を理解しておくと、次のような場面で役に立ちます。
- 文書検索・RAG: クエリと文書チャンクの関連度ランキングはほぼ例外なくコサイン類似度(またはその親戚)で計算される
- 埋め込みベクトルの比較: Word2VecやSentence-BERT、各社のembedding APIなど、あらゆる埋め込みモデルの出力の標準的な比較方法
本記事の内容
- コサイン類似度の直感 — 「向きの近さ」だけを測るメーター
- 定義と内積との関係 — 内積の正規化版であること
- スケール不変性の実証と、ユークリッド距離との使い分け
- 文書類似度・埋め込み検索のPython実例
- 高次元でのふるまい — 無関係なペアは0付近に集中する
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
- ベクトルの内積とは?図解でわかりやすく — コサイン類似度は内積の兄弟です
コサイン類似度は「向きの近さ」メーター
まず一枚の絵で直感をつかみましょう。

コサイン類似度は、2つのベクトルのなす角 $\theta$ の余弦 $\cos\theta$ です。向きが揃っているほど1に近く、直角(無関係)で0、正反対で $-1$。ベクトルの長さは値に一切影響しません。「大きさは違っても、指している方向は同じ」を検出するための道具です。

値域は $-1 \leq \cos\theta \leq 1$ です。実務の埋め込みベクトル比較では0〜1の範囲に収まることが多く、「0.8以上ならかなり近い、0.3以下ならほぼ無関係」のような目安で使われます(適切なしきい値はモデルとタスクに依存します)。
では、この $\cos\theta$ はどう計算するのでしょうか。角度を分度器で測るわけにはいきません——ここで内積が登場します。
定義: 内積を長さで正規化する
コサイン類似度の定義は次のとおりです。
$$ \begin{equation} \mathrm{cos\_sim}(\bm{a}, \bm{b}) = \cos\theta = \frac{\bm{a} \cdot \bm{b}}{|\bm{a}|\,|\bm{b}|} = \frac{\sum_{i=1}^{n} a_i b_i}{\sqrt{\sum_{i=1}^{n} a_i^2}\,\sqrt{\sum_{i=1}^{n} b_i^2}} \end{equation} $$

この式は、内積の幾何学的定義 $\bm{a}\cdot\bm{b} = |\bm{a}||\bm{b}|\cos\theta$ を $\cos\theta$ について解いただけです。分子の内積は「向きの近さ」と「長さ」が混ざった生の類似度で、それを両者の長さで割る(正規化する)ことで長さの影響を打ち消し、純粋な「向きの近さ」だけを取り出しています。
言葉だけでは実感が湧きにくいので、「長さの影響が本当に消えるのか」を実験で確かめます。
スケール不変性: 長さを変えても値は変わらない
同じ向き(なす角30°)で長さだけが違う3本のベクトルと、基準ベクトルとの内積・コサイン類似度を比べます。
import numpy as np
def cos_sim(a, b):
return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))
a = np.array([2.0, 0.9]) # 基準ベクトル
theta = np.deg2rad(30)
R = np.array([[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)]])
b_dir = R @ (a / np.linalg.norm(a)) # aから30°回した単位ベクトル
for L in [2.2, 1.0, 0.45]: # 長さだけ変える
b = L * b_dir
print(f"長さ{L:.2f}: 内積 = {a @ b:6.3f}, cos類似度 = {cos_sim(a, b):.4f}")
# => 長さ2.20: 内積 = 4.179, cos類似度 = 0.8660
# => 長さ1.00: 内積 = 1.899, cos類似度 = 0.8660
# => 長さ0.45: 内積 = 0.855, cos類似度 = 0.8660

内積は長さに比例して4.18 → 1.90 → 0.86と大きく変わるのに、コサイン類似度は3本とも 0.8660($= \cos 30°$)でぴったり一致します。数式のとおり、$\bm{b}$ を $k$ 倍しても分子と分母の両方が $k$ 倍されて打ち消し合うからです。これがスケール不変性で、コサイン類似度の最大の売りです。
ただし「長さを無視する」ことは、いつでも正しいわけではありません。距離とどう使い分けるべきでしょうか。
ユークリッド距離との使い分け
「似ている」を測るもう1つの定番、ユークリッド距離 $|\bm{a}-\bm{b}|$ と比べてみます。

図の $\bm{b}$($\bm{a}$ と同じ向きだが短い)と $\bm{c}$($\bm{a}$ の近くにあるが向きが少しズレている)——どちらが $\bm{a}$ に似ているでしょうか。コサイン類似度なら $\bm{b}$(1.000 > 0.967)、ユークリッド距離なら $\bm{c}$(0.71 < 1.60)と、指標によって答えが逆転します。
使い分けの目安はこうです。
| 指標 | 見るもの | 向いている場面 |
|---|---|---|
| コサイン類似度 | 向き(角度)のみ | 文書・埋め込みの意味比較。長さが「量」の違いでしかないとき |
| ユークリッド距離 | 位置の近さ(向き+大きさ) | 座標そのものに意味があるとき(物理量、一部のクラスタリング) |
文書ベクトルでは、同じ話題を2倍の長さで書けばベクトルもほぼ2倍になります。この「量の違い」を意味の違いと混同したくないから、テキスト処理ではコサイン類似度が標準になっているのです。
その定番の使い方を、実際の文書で試してみましょう。
実例1: 文書類似度を計算する
3つの短い文書を、単語の出現回数でベクトル化して比較します。
import numpy as np
def cos_sim(a, b):
return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))
# 語彙: [犬, 散歩, 好き, 株価, 市場]
doc1 = np.array([2, 1, 1, 0, 0]) # 「犬が好き。犬と散歩」
doc2 = np.array([2, 0, 1, 0, 0]) # 「犬が大好き。犬は家族」
doc3 = np.array([0, 0, 0, 1, 1]) # 「株価が上昇。市場は好調」
print(f"文書1 vs 文書2(どちらも犬の話): {cos_sim(doc1, doc2):.3f}")
print(f"文書1 vs 文書3(犬の話 vs 株の話): {cos_sim(doc1, doc3):.3f}")
# => 文書1 vs 文書2(どちらも犬の話): 0.913
# => 文書1 vs 文書3(犬の話 vs 株の話): 0.000

犬の話どうしは 0.913 と高く、犬の話と株の話はちょうど0——共通する単語が1つもないため、ベクトルが直交するからです。「話題が同じ=使う単語の分布(向き)が似る」という素朴な仮定が、コサイン類似度でそのまま数値になります。実務ではこの単語カウントの代わりにTF-IDFやニューラル埋め込みを使いますが、比較の物差しは同じです。
実例2: 埋め込み検索(ベクトル検索)
次は、意味ベクトル(埋め込み)での検索を模した例です。クエリと各単語のコサイン類似度を計算し、降順に並べればランキングになります。

「ペット」に近い向きのクエリに対し、猫(0.998)・犬が上位に来て、自動車・地下鉄・りんごは下位に沈みます。RAGでの関連チャンク検索も、レコメンドの「似ているアイテム」も、スケールを大きくしただけで原理はこの図と同じです。
実務でひとつ重要な高速化テクニックがあります。あらかじめ全ベクトルを長さ1に正規化しておくことです。

$|\bm{a}| = |\bm{b}| = 1$ なら、定義式の分母が1になり コサイン類似度=ただの内積 になります。内積は行列積として超高速に一括計算できるので、多くのベクトルデータベースや埋め込みモデル(正規化済みembeddingを返すもの)はこの形を採用しています。
import numpy as np
rng = np.random.default_rng(0)
docs = rng.standard_normal((10000, 128)) # 1万文書の埋め込み(128次元)
docs /= np.linalg.norm(docs, axis=1, keepdims=True) # 事前に正規化
query = rng.standard_normal(128)
query /= np.linalg.norm(query)
sims = docs @ query # 行列積1発で全文書との類似度
top3 = np.argsort(sims)[::-1][:3]
print(f"上位3件のインデックス: {top3}, 類似度: {np.round(sims[top3], 3)}")
1万文書との類似度計算が docs @ query の1行で終わりました。正規化を先に済ませておく設計の威力です。
ところで、ランダムな128次元ベクトルどうしだと、最大の類似度でも0.4程度にしかなりません。これは高次元性のしわざです。
高次元でのふるまい: 無関係なら0付近に集中する
高次元空間では、無関係な(ランダムな)ベクトル同士のコサイン類似度は0の周りに鋭く集中します(ばらつきはおよそ $1/\sqrt{d}$。詳しい実験は内積の記事を参照)。

128次元での実験では、無関係なペアの類似度は平均 $-0.001$ で、ほぼすべてが $\pm 0.3$ 以内に収まりました。一方、共通の意味成分を持たせた「関連ペア」は平均 0.588 と、分布が明確に分離します。つまり高次元の埋め込み空間では、0.5を超えるようなコサイン類似度は偶然ではまず出ない——高い値がそのまま「本物の関係」の証拠になるのです。
まとめ
本記事では、コサイン類似度を定義から実務まで解説しました。
- コサイン類似度は「向きの近さ」メーター: なす角の余弦。1で同方向、0で直交(無関係)、-1で正反対
- 正体は内積の正規化版: $\cos\theta = \bm{a}\cdot\bm{b}/(|\bm{a}||\bm{b}|)$。分母の正規化が長さの影響を打ち消す(スケール不変性。実測でも長さを5倍変えて値は0.8660のまま)
- ユークリッド距離との使い分け: 向きだけ見たいならコサイン、位置そのものに意味があるなら距離。文書・埋め込み比較はコサインが標準
- 文書類似度の実例: 同じ話題は0.913、無関係な話題は0.000ときれいに分離
- 正規化しておけば「類似度=内積」: 行列積1発で大量比較できる。ベクトル検索の実装の定石
- 高次元では0.5超えは偶然ではない: 無関係ペアは0付近に集中(ばらつき $1/\sqrt{d}$)するため、高い類似度は本物の関係のシグナル
次のステップとして、以下の記事も参考にしてください。
