概要
Doc2Vecは、Word2Vecを文書・段落単位に拡張した手法である。2014年にMikolovらが提案し、Paragraph Vectorとも呼ばれる。Word2Vecが単語ベクトルしか得られないのに対し、Doc2Vecは任意長の文書全体を固定長の密なベクトルとして表現できる。文書の内容・トピックを捉えたベクトルが得られるため、文書分類・類似文書検索・感情分析などに使われる。
直感・モチベーション
Word2Vecで「文書の意味」を表現しようとするとき、単純に単語ベクトルを平均する方法がある。しかしこの方法には問題がある。
- 語順の消失: 「犬が人を噛んだ」と「人が犬を噛んだ」は同じ単語集合なので同じベクトルになる
- 文書固有の文脈の喪失: 単語の平均はその文書特有のトピックやスタイルを捉えられない
Doc2Vecは各文書に専用のベクトル(Paragraph Vector)を割り当て、単語予測タスクの中でそのベクトルも同時に学習する。文書ベクトルは「この文書でどんな単語が現れやすいか」という情報を圧縮して保持する役割を果たす。
2つのアーキテクチャがある:
- PV-DM(Distributed Memory): 文書ベクトルと文脈単語の両方から次単語を予測する。CBOWに対応
- PV-DBOW(Distributed Bag of Words): 文書ベクトルだけからランダムサンプルした単語を予測する。Skip-gramに対応。精度はやや劣るが高速
数学的定式化
PV-DMモデルでは、文書 のベクトル と文脈単語ベクトルの平均から次単語を予測する:
目的関数は文書集合全体の対数尤度の最大化:
導出を見る
PV-DM の学習手順:
- 文書行列 (各行が1文書のベクトル)と単語行列 をランダム初期化
- 各文書から固定長ウィンドウをスライドさせ、文書ベクトル+文脈単語ベクトルの平均 を計算
- から次単語を予測するソフトマックス(またはNegative Sampling)で勾配を計算
- 誤差を 、、 に逆伝播して更新
推論時(未知文書のベクトル化):
学習済みの (単語行列)は固定し、新しい文書 のベクトル だけを推論フェーズで勾配法で最適化する。これが「infer vector」と呼ばれる操作である。
重要な性質・注意点
- 推論コスト: 未知文書のベクトル化には再学習(infer vector)が必要で、Word2Vecの単純な参照より重い
- 文書長への依存: 極端に短い文書(1〜2文)はベクトルの品質が低下しやすい
- PV-DM + PV-DBOW の組み合わせ: 両者を結合するとより良い表現が得られることが多い(原論文でも推奨)
まとめ
- Word2Vecを文書単位に拡張し、固定長の文書ベクトルを学習する
- PV-DMは文脈を保持、PV-DBOWは高速、組み合わせが実践的
- 未知文書は「infer vector」で単語行列を固定したまま最適化して得る
- 語順を部分的に保持できる点でBag-of-Words平均より表現力が高い
実装例
pythonimport math import random def sigmoid(x: float) -> float: return 1.0 / (1.0 + math.exp(-x)) class Doc2Vec: """PV-DBOW の簡略実装(文書ベクトルから単語を予測)""" def __init__(self, vocab: list[str], n_docs: int, dim: int = 10): self.w2i = {w: i for i, w in enumerate(vocab)} self.V = len(vocab) self.dim = dim # 文書ベクトル行列と単語出力行列 self.D = [[random.gauss(0, 0.01) for _ in range(dim)] for _ in range(n_docs)] self.W = [[random.gauss(0, 0.01) for _ in range(dim)] for _ in range(self.V)] def dot(self, a: list[float], b: list[float]) -> float: return sum(x * y for x, y in zip(a, b)) def train_doc(self, doc_id: int, word_id: int, negatives: list[int], lr: float): d_vec = self.D[doc_id] # Positive s = self.dot(d_vec, self.W[word_id]) g = lr * (1 - sigmoid(s)) for k in range(self.dim): d_vec[k] += g * self.W[word_id][k] self.W[word_id][k] += g * d_vec[k] # Negatives for neg in negatives: s = self.dot(d_vec, self.W[neg]) g = -lr * sigmoid(s) for k in range(self.dim): d_vec[k] += g * self.W[neg][k] self.W[neg][k] += g * d_vec[k] def infer(self, tokens: list[str], steps: int = 100, lr: float = 0.01) -> list[float]: """未知文書のベクトルを推論(単語行列は固定)""" vec = [random.gauss(0, 0.01) for _ in range(self.dim)] word_ids = [self.w2i[w] for w in tokens if w in self.w2i] for _ in range(steps): for wid in word_ids: s = self.dot(vec, self.W[wid]) g = lr * (1 - sigmoid(s)) for k in range(self.dim): vec[k] += g * self.W[wid][k] return vec # 使用例 corpus = [ ["機械", "学習", "モデル", "学習"], ["深層", "学習", "ニューラル", "ネット"], ["自然", "言語", "処理", "テキスト"], ] vocab = list(set(w for doc in corpus for w in doc)) model = Doc2Vec(vocab, n_docs=len(corpus), dim=10)
関連記事
前提知識
- Word2Vec — Doc2Vecの基礎となる単語埋め込み手法
- 分散仮説 — 文脈から意味を学ぶという理論的根拠
- ニューラルネットワーク基礎 — 勾配法・誤差逆伝播
派生技術
- Sentence-BERT — Transformerベースの文埋め込み手法
- Universal Sentence Encoder — 多目的文ベクトル
応用事例
- 類似文書検索 — 文書ベクトルのコサイン類似度でランキング
- 感情分析 — 文書ベクトルを特徴量として分類器に入力
用語解説
| 用語 | 説明 |
|---|---|
| Paragraph Vector | 文書・段落を固定長ベクトルで表現したもの(Doc2Vecの別名) |
| PV-DM | 文書ベクトルと文脈単語の両方から次単語を予測するアーキテクチャ |
| PV-DBOW | 文書ベクトルのみからサンプリングした単語を予測するアーキテクチャ |
| infer vector | 学習済み単語行列を固定して未知文書のベクトルを最適化する推論操作 |
| 文書埋め込み | 文書全体を固定長の実数値ベクトルで表現したもの |
関連リンク
この記事への参照
- 比較対象Embedding