はじめに
gensim で Word2Vec を触っていて、こんな経験はないでしょうか。
model.most_similar("hot", topn=5)
# → [('warm', 0.74), ('cold', 0.71), ('chilly', 0.66), ('cool', 0.65), ('warmer', 0.63)]
「hot の類義語」を取りたかったのに、上位に cold が出てくる。意味は真逆のはずなのに、なぜか近い。
最初に遭遇したときは「学習データの問題かな?」と疑いましたが、これは実は学習データの問題ではなく、単語埋め込みという仕組みそのものに由来する構造的な現象です。
そしてこの現象は、心理学の古典である 「連想の3分類」 を借りてくると、驚くほどクリアに整理できます。本記事ではその視点から問題を読み解き、4つの実用的な対処法をコード付きで紹介します。
対象読者
- 単語埋め込み(Word2Vec / GloVe / fastText など)を使った検索・推薦・NLP機能を作っているエンジニア
- 「埋め込みで反対語が混ざる」現象に遭遇して、原因をスッキリ理解したい方
この記事でわかること
- なぜ単語埋め込みは類義語と反対語を区別できないのか(原因の本質)
- 連想の3分類(類似・対比・接近)という整理フレームワーク
- 反対語混入を抑える4つの実装パターンと、その使い分け
結論(TL;DR)
原因: 単語埋め込みは 「接近連想」(同じ文脈に登場する関係) を学習しています。「類似連想」と「対比連想」を区別する仕組みは内蔵されていません。hot と cold は同じ文脈に同じくらい出るので、ベクトル空間では近くなります。
解決方法:
| 手法 | 難易度 | 守備範囲 |
|---|---|---|
| ① 反対語辞書(WordNet)でフィルタ | 低 | 辞書に載っている語のみ |
| ② Counter-fitting で埋め込みを修正 | 中 | 埋め込み空間全体 |
| ③ LLM で反対語かどうか判定 | 低 | 辞書未収録語にも対応 |
| ④ 連想の種類ごとに別手法を組み合わせ | 中 | 用途に応じて最適化 |
詳細は以降のセクションで解説します。
発生した事象
症状
gensim で GoogleNews 学習済みベクトルを読み込み、most_similar を呼ぶと、対立する意味の語が上位に混入します。
import gensim.downloader as api
model = api.load("word2vec-google-news-300")
for word in ["hot", "good", "increase", "rich"]:
print(word, "->", model.most_similar(word, topn=5))
実行結果(抜粋):
hot -> [('warm', 0.74), ('cold', 0.71), ('chilly', 0.66), ('cool', 0.65), ('warmer', 0.63)]
good -> [('great', 0.73), ('bad', 0.72), ('nice', 0.71), ('decent', 0.69), ('terrific', 0.68)]
increase -> [('decrease', 0.81), ('rise', 0.79), ('decline', 0.74), ('drop', 0.72), ('reduction', 0.70)]
rich -> [('wealthy', 0.74), ('poor', 0.69), ('affluent', 0.67), ('impoverished', 0.65), ('prosperous', 0.64)]
すべての語で反対語が類義語と同等以上のスコアで近傍に現れることが確認できます。
環境
| 項目 | バージョン |
|---|---|
| OS | Windows 11 |
| Python | 3.11 |
| gensim | 4.3.2 |
| 使用モデル | word2vec-google-news-300 |
原因 ― 連想の3分類で整理する
アリストテレスの連想の3法則
心理学では古くから、連想(ある概念から別の概念が想起される関係)を以下の3つに分類してきました。アリストテレスの『記憶論』に遡る古典的な分類です。
| 分類 | 関係 | 例 |
|---|---|---|
| 類似連想 | 似たもの同士 | 犬 ↔ 猫、椅子 ↔ ソファ |
| 対比連想 | 反対のもの同士 | hot ↔ cold、good ↔ bad |
| 接近連想 | 一緒に経験するもの | 雷 ↔ 稲妻、医者 ↔ 病院 |
ここで重要なのは、「接近」は時間的・空間的な共起関係を指すという点です。意味が似ていなくても、一緒に登場すれば接近連想は成立します。
Word2Vec が学習しているのはどれか
Word2Vec をはじめとする多くの単語埋め込みは、Harris(1954)の 分布仮説(Distributional Hypothesis) に立脚しています。
同じ文脈に出現する単語は、似た意味を持つ。
これを skip-gram や CBOW として実装したのが Word2Vec、共起行列の対数を分解したのが GloVe です。
しかし、これは厳密には 「同じ文脈に出る ≒ 接近連想にある」 を学習しているにすぎません。意味の類似性そのものを学んでいるわけではないのです。
なぜ反対語が近くなるのか
hot と cold のコーパスでの使われ方を考えてみます。
-
hot coffee/cold coffeeどちらも頻出 -
It's hot today./It's cold today.どちらも自然 -
hot water,cold water両方とも一般的 - 周辺によく出る語は
feel,is,temperature,weatherなどで一致
つまり hot と cold は分布的にほぼ双子のような単語です。分布仮説は「同じ文脈に出る = 意味が似ている」と仮定するため、対比連想にあるペアを類似連想と区別できません。
連想の3分類で整理すると以下のようになります。
| 連想の種類 | 例 | Word2Vec が拾うか |
|---|---|---|
| 類似連想 | hot ↔ warm | ⭕ 拾う |
| 対比連想 | hot ↔ cold | ⭕ 同じくらい拾ってしまう |
| 接近連想 | hot ↔ summer | ⭕ 拾う |
私たちが「類義語検索」として欲しいのは類似連想だけですが、Word2Vec は3つを混在させた状態で返してくるわけです。
PMI(自己相互情報量)を使う手法(GloVe など)も同じ問題を持ちます。共起頻度から意味を推定する仕組みは、すべて接近連想の派生だからです。
解決方法
方法1: 反対語辞書でフィルタする(推奨・最も手軽)
WordNet の antonyms 関係を使って、結果から反対語を後段で除外します。
from nltk.corpus import wordnet as wn
import gensim.downloader as api
def get_antonyms(word: str) -> set[str]:
antonyms = set()
for syn in wn.synsets(word):
for lemma in syn.lemmas():
for ant in lemma.antonyms():
antonyms.add(ant.name().lower())
return antonyms
model = api.load("word2vec-google-news-300")
def similar_without_antonyms(word: str, topn: int = 5) -> list[tuple[str, float]]:
antonyms = get_antonyms(word)
candidates = model.most_similar(word, topn=topn * 4)
filtered = [(w, s) for w, s in candidates if w.lower() not in antonyms]
return filtered[:topn]
print(similar_without_antonyms("hot"))
# → [('warm', 0.74), ('chilly', 0.66), ('cool', 0.65), ('warmer', 0.63), ('humid', 0.61)]
長所: 既存パイプラインに後付けできる。実装が短い。
短所: 辞書未収録語は検出できない。日本語 WordNet は英語ほど網羅性が高くない。
方法2: Counter-fitting で埋め込み自体を修正する
Mrkšić ら(2016)は、シソーラスの同義語ペアを近づけ、反対語ペアを遠ざけるよう既存埋め込みを再学習する手法を提案しました。
ロス関数の概念は次の3項からなります。
\mathcal{L} = \mathrm{AntonymRepel}(V) + \mathrm{SynonymAttract}(V) + \mathrm{VectorSpacePreservation}(V)
- AntonymRepel: 反対語ペア $(u, v)$ の cosine 類似度にマージン $\delta$ を設けて押し離す
- SynonymAttract: 同義語ペアを近づける
- VectorSpacePreservation: 元の埋め込み空間の構造を壊さないよう正則化
長所: 埋め込み空間そのものから対比連想の影響を引き剥がせる。下流タスクで一貫した改善が出る。
短所: 良質なシソーラスが必要なため、リソースの少ない言語では効果が限定的。
方法3: LLM に反対語かどうか判定させる
埋め込みで上位 N 語まで絞ってから、LLM にバイナリ判定させる方法です。辞書未収録語にも強く、多義性もある程度ハンドリングできます。
import anthropic
import gensim.downloader as api
client = anthropic.Anthropic()
model = api.load("word2vec-google-news-300")
def is_antonym(word_a: str, word_b: str) -> bool:
msg = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=8,
messages=[{
"role": "user",
"content": (
f"Are '{word_a}' and '{word_b}' antonyms or "
f"opposite-meaning words? Answer with 'yes' or 'no' only."
),
}],
)
return msg.content[0].text.strip().lower().startswith("yes")
def similar_without_antonyms_llm(word: str, topn: int = 5) -> list[tuple[str, float]]:
candidates = model.most_similar(word, topn=topn * 3)
return [(w, s) for w, s in candidates if not is_antonym(word, w)][:topn]
LLM 呼び出しはレイテンシとコストがかかります。本番環境ではバッチ処理にするか、結果を Redis などにキャッシュしましょう。「単語ペア → 反対語か」のキャッシュは更新頻度が極めて低いので非常に効果的です。
長所: 辞書未収録語に対応、日本語などにも適用可能、文脈付きでの判定もプロンプトで拡張できる。
短所: コストとレイテンシ。判定が確率的なので評価結果に揺れがある。
方法4: 連想の種類ごとに別の手法を組み合わせる
そもそも「単語の関連性」というタスクを連想の3分類で分けて捉えると、用途に応じた最適な手法を選べます。
| 用途 | 欲しい連想 | 推奨手法 |
|---|---|---|
| 類義語検索・スペル訂正の代替 | 類似連想 | Sentence-BERT + 同義語シソーラス |
| 反対意見の抽出・対比表現生成 | 対比連想 | 反対語辞書 / Counter-fitting / LLM |
| トピック関連語・関連商品推薦 | 接近連想 | Word2Vec / PMI(むしろ得意分野) |
つまり、Word2Vec の「反対語が混ざる」という挙動は、接近連想という用途で使うぶんにはバグではないわけです。「summer の関連語に winter が出る」のは、季節を語る記事の関連推薦としてはむしろ正解です。
問題なのは、接近連想を返す道具を、類似連想が欲しい場面で使ってしまっているときだけ。連想の種類を意識すれば、この用途ミスマッチを避けられます。
動作確認
hot の上位5語を、各手法で取得した結果を比較します。
| 手法 | 上位5語 | 反対語混入 |
|---|---|---|
| 素の Word2Vec | warm, cold, chilly, cool, warmer | あり |
| 方法1: WordNet フィルタ | warm, chilly, cool, warmer, humid | 一部残る |
| 方法3: LLM 判定 | warm, sweltering, sizzling, blazing, scalding | なし |
WordNet フィルタは antonym タグが付いた直接の反対語のみ除去するため、cool のような弱い対比語は残る場合があります。完全性が必要な場面では、方法1と方法3の組み合わせが現実的です。
なぜこの問題が起きるのか(深掘り)
埋め込みは「単語の意味」を直接学習しているのではなく、「単語が出る文脈の分布」を学習しています。哲学的には次の区別があります。
- 意味(semantics): その語が指すもの
- 使用(usage): その語が使われる場所
Wittgenstein の「意味は使用である」のような立場では両者を一致させますが、現実のコーパスではズレが生じます。
- 反対語ペア(hot/cold, good/bad): 使用がほぼ同じだが、意味は対立する
- 多義語(bank: 銀行 / 川岸): 使用が分岐するが、表層は同じ
埋め込みは前者を区別できず、後者は混ぜて学習します。意味と使用のズレこそが、本記事冒頭の現象の根本原因です。
LLM のトークン埋め込みも入力層では同じ性質を継承していますが、Transformer の attention が後段で文脈を考慮して意味を分離するため、最終層では多少改善されます。とはいえ、入力埋め込みレベルではいまも接近連想が支配的です。
類似の問題
同じ原理で起きる現象は他にもあります。
- BERT の
[MASK]予測で、反対の意味の語が同等のスコアで予測される - 文書類似度検索で「対立意見の文書」が「同意見の文書」と同じくらい高スコアになる
- 推薦システムで「気に入らなかったジャンル」が類似ジャンルとして混入する
いずれも「接近連想を測る道具で、類似連想や対比連想を測ろうとしている」ミスマッチが原因です。
まとめ
- 単語埋め込みは 接近連想 を学習する仕組みであり、「類似連想」と「対比連想」を区別できません
- そのため
hot↔coldのような反対語ペアが類義語として混入します - 解決策は連想の種類ごとに別の手法を組み合わせること
- 類似連想 → 埋め込み + cosine 類似度
- 対比連想 → 反対語辞書 / Counter-fitting / LLM 判定
- 接近連想 → 共起頻度 / PMI(むしろ Word2Vec の得意分野)
連想の心理学的分類は、エンジニアが「自分が何を計算しているのか」を整理するための強力なフレームワークになります。次に類似度の計算で違和感を覚えたら、自分が3分類のどれを欲しかったのかを思い出してみてください。問題の半分は、それだけで解けます。
参考資料