この記事は、書籍『Kaggleではじめる大規模言語モデル入門』の輪読会で学んだテーマを、自分の言葉で整理・深掘りしたものです。
はじめに
シリーズ第1回で、NLP のコアの問題は「テキストをどう数値に変換するか」だと書きました。
この記事では、その数値化手法が深層学習以前にどう発展してきたかを、
BoW → n-gram → TF-IDF → 単語文脈行列 / PPMI → 次元圧縮 → word2vec
という一本の流れで追います。
「前の手法の何が問題で、次の手法はそれをどう解決したか」を意識して読むと、単なる手法一覧ではなく進化のストーリーとして頭に入ります。
古い手法ではあるものの、TF-IDF + 線形モデルは今でも強力なベースラインであり、Kaggle でも普通に登場します。
出発点:one-hot 表現の限界
最も素朴な単語の数値化は one-hot 表現です。
語彙の中で「その単語の位置だけ 1、他は 0」のベクトルにします。
りんご → [1, 0, 0, 0, ...]
バナナ → [0, 1, 0, 0, ...]
自動車 → [0, 0, 1, 0, ...]
しかしこれには本質的な欠陥があります。
- どの2単語も内積が 0 :「りんご」と「バナナ」が似ていることを表現できない
- 次元数 = 語彙数 :語彙が10万語あれば10万次元になる
「意味の近さをベクトルの近さで表せない」という問題が、この記事全体を貫くテーマになります。
Bag-of-Words(BoW):単語の出現回数で文書を表す
文書レベルの数値化として最初に登場するのが Bag-of-Words です。
文書を「単語の出現回数のベクトル」で表現します。
語順は無視して単語を袋(bag)に放り込むイメージです。
例として2つの文を語彙でカウントしてみます。
| 今日 | は | カレー | ラーメン | を | 食べた | |
|---|---|---|---|---|---|---|
| 「今日はカレーを食べた」 | 1 | 1 | 1 | 0 | 1 | 1 |
| 「今日はラーメンを食べた」 | 1 | 1 | 0 | 1 | 1 | 1 |
各行がその文のベクトルになります。
scikit-learn なら数行です。
from sklearn.feature_extraction.text import CountVectorizer
docs = ["今日 は カレー を 食べた", "今日 は ラーメン を 食べた"] # 分かち書き済み
vec = CountVectorizer()
X = vec.fit_transform(docs)
print(vec.get_feature_names_out())
print(X.toarray())
BoW の長所
- 実装が単純で高速。そのまま機械学習モデルの入力にできる
- 文書同士の類似度をコサイン類似度で測れる
BoW の短所
- 表記ゆれに弱い :「りんご」と「リンゴ」は完全に別の次元になる
- 日本語では分かち書きが前提 :単語境界が自明でないため、形態素解析が必要。「太陽光発電設備保全技術者」のような複合語をどこで切るかという問題もつきまとう
-
英語では活用形が別単語扱い :
run/runs/runningが別次元になる。レンマ化・ステミングで正規化できるが、時制などの情報は失われる - 高次元・スパース :大規模コーパスでは語彙が数十万になり、ベクトルはほとんどが 0 の疎な表現になる。しかも「は」「の」「is」のような機能語の頻度ばかり大きく、情報量が乏しい
- 語順を完全に捨てている :これが最大の問題
語順の喪失がどれほど致命的かは、次の例でわかります。
「私は彼が嫌い」と「彼は私が嫌い」
意味は正反対ですが、単語の構成は同一なので BoW ではまったく同じベクトル になります。
否定表現がどの語に係るかも捉えられません。
n-gram:語順を部分的に取り戻す
連続する n 個の単語をひとまとまり(トークン)として扱うのが 単語 n-gram です。
「私 は 彼 が 嫌い」の単語 2-gram は「私は」「は彼」「彼が」「が嫌い」となり、先ほどの2文は異なるベクトルになります。
局所的な語順を回復できる一方、n を大きくすると次元爆発とスパース化がさらに深刻になるトレードオフがあります(実務では 1〜2 gram の併用が多い)。
TF-IDF:「その文書らしい単語」を重視する
BoW の「機能語の頻度ばかり大きい」問題への処方箋が TF-IDF です。
単語 $w$ の文書 $d$ における重みを次で定義します。
\text{tf-idf}(w,d) = \underbrace{\text{tf}(w,d)}_{\text{文書 } d \text{ 内での頻度}} \times \underbrace{\log\frac{N}{\text{df}(w)}}_{\text{IDF:単語の希少さ}}
- TF(Term Frequency) :その文書内でよく出る単語ほど大きい
- IDF(Inverse Document Frequency) :$N$ は全文書数、$\text{df}(w)$ は単語 $w$ を含む文書数。 多くの文書に出る単語ほど小さくなる
直感を数値で確認します。
全1,000文書のコーパスで、ある文書に「は」が10回、「深層学習」が5回出たとします。
| 単語 | TF | df | IDF = log(1000/df) | TF-IDF |
|---|---|---|---|---|
| は | 10 | 1,000 | log(1) = 0 | 0 |
| 深層学習 | 5 | 20 | log(50) ≈ 3.9 | 19.6 |
全文書に現れる「は」の重みは 0 になり、一部の文書にしか出ない「深層学習」が強調されます。
頻度は半分なのに重みは圧倒的に大きい── 「どの文書にもある語を抑え、その文書を特徴づける語を引き立てる」 のが TF-IDF の効果です。
from sklearn.feature_extraction.text import TfidfVectorizer
vec = TfidfVectorizer()
X = vec.fit_transform(docs)
TF-IDF + ロジスティック回帰(や LightGBM)は、今でもテキスト分類の最初のベースラインとして優秀です。
ただし、高次元スパース・語順喪失・表記ゆれといった BoW 由来の問題は残ったままです。
単語文脈行列:分布仮説で「単語」をベクトル化する
ここで視点を文書から 単語 に移します。
手がかりになるのが 分布仮説 ──「単語の意味は、その周辺に現れる単語の分布で決まる」という考え方です。
「コーヒー」と「紅茶」はどちらも「飲む」「カップ」「淹れる」の近くに現れやすい。
周辺語の分布が似ているなら、意味も近いはずだ、という発想です。
ある単語の周辺(ウィンドウ内)に別の単語が現れることを 共起 と呼びます。
共起の回数を行列にまとめたものが 単語文脈行列 です。
行が単語、列が文脈語、要素が共起回数です。
| 飲む | 走る | 吠える | カップ | 散歩 | |
|---|---|---|---|---|---|
| コーヒー | 12 | 0 | 0 | 8 | 0 |
| 紅茶 | 10 | 0 | 0 | 9 | 0 |
| 犬 | 1 | 5 | 7 | 0 | 9 |
各行をその単語のベクトルとみなすと、「コーヒー」と「紅茶」の行は似ており、「犬」とは似ていません。
one-hot ではできなかった 意味の近さの表現 が、共起情報によって可能になりました。
PPMI:頻出語の影響を補正する
生の共起回数では、「は」「の」のような頻出語との共起がどの単語でも大きくなり、意味の情報が埋もれます。
そこで「単語 $x$ と $y$ が、偶然を超えてどれだけ一緒に出やすいか」に変換したのが PPMI(正の相互情報量) です。
\mathrm{PPMI}(x,y) = \max\left(0,\; \log\frac{P(x,y)}{P(x)P(y)}\right)
$P(x)P(y)$ は「2語が独立ならこの確率で共起するはず」という期待値で、実際の共起確率 $P(x,y)$ がそれを上回るほど値が大きくなります。
頻出語はそもそも $P(y)$ が大きいため、分母が効いて重みが抑えられる仕組みです。
それでも残る問題が 超高次元・スパース です。
語彙が10万語なら 10万×10万 の行列になり、要素のほとんどは 0 です。
次元圧縮:疎な高次元を密な低次元へ
高次元スパースなベクトルを、情報をなるべく保ったまま 低次元の密なベクトル に圧縮します。
代表的な手法は次のとおりです。
- LSA(潜在的意味解析) :単語文脈行列を行列分解し、背後の潜在的な意味構造を抽出する枠組み
- SVD(特異値分解) :LSA で使われる代表的な分解。$X = U\Sigma V^T$ と分解し、大きな特異値に対応する成分だけを残して低次元化する($U$ が単語ベクトル、$\Sigma$ が各成分の重要度に対応)
- PCA(主成分分析) :データの分散が最大になる方向から順に軸を取り直す。SVD と数学的に密接な関係がある
- ICA(独立成分分析) :PCA が「無相関」な成分を求めるのに対し、統計的に「独立」な成分を求める
こうして得られる低次元・密のベクトルを 分散表現 (あるいは 埋め込み / Embedding )と呼びます。
意味的に近い単語が近いベクトルに配置される、というのが分散表現の本質的な価値です。
word2vec:予測タスクで分散表現を学習する
「共起行列を作ってから分解する」のではなく、 ニューラルネットに予測タスクを解かせ、その副産物として分散表現を得る というアプローチが word2vec(2013)です。
学習方式は2つあります。
| 方式 | 解かせるタスク |
|---|---|
| CBOW | 周辺語から中心語を予測する(「毎朝 ___ を飲む」の穴埋め) |
| Skip-gram | 中心語から周辺語を予測する(「コーヒー」の周りに来る語は?) |
穴埋め問題を正しく解くには、モデルは「コーヒーと紅茶は同じ文脈に入る」ことを学ばざるを得ません。
その知識が重み行列=単語ベクトルに蓄積される、という仕掛けです。
分布仮説を「行列の集計」ではなく「学習」で実現したものと言えます。
ベクトルの足し引きが意味の演算になる
word2vec の有名な性質が、単語ベクトルの加減算が意味の操作に対応することです。
\text{king} - \text{man} + \text{woman} \approx \text{queen}
青い矢印(man→king、woman→queen)が「王族」の方向、オレンジの矢印(man→woman)が「性別」の方向に対応しており、king から性別の方向へ移動すると queen のすぐ近くに着地します。
「王様から男性性を引いて女性性を足すと女王になる」──性別・首都と国・活用形といった 単語間の関係が、ベクトル空間内の方向として埋め込まれる のです。
gensim を使うと簡単に試せます。
from gensim.models import KeyedVectors
# 学習済みモデル(例: 日本語Wikipediaで学習したもの)をロード
wv = KeyedVectors.load_word2vec_format("model.bin", binary=True)
wv.most_similar(positive=["王", "女"], negative=["男"], topn=3)
wv.similarity("コーヒー", "紅茶") # 高い
wv.similarity("コーヒー", "犬") # 低い
word2vec の位置づけと限界
word2vec は「頻度の集計」から「学習ベースの埋め込み」への転換点であり、以降の NLP は埋め込みを学習する方向へ一気に進みます。
ただし word2vec にも限界があります。
1つの単語に1つの固定ベクトル しか割り当てられないため、「銀行の bank」と「土手の bank」を区別できません。
この多義性の問題を「文脈に応じてベクトルを変える」ことで解決したのが BERT 以降の文脈化埋め込みで、現在の LLM に直結していきます。
まとめ
数値化手法の進化を「問題 → 解決」の連鎖で振り返ります。
| 手法 | 解決したこと | 残った問題 |
|---|---|---|
| one-hot | 単語を一応ベクトルにできる | 意味の近さを表せない・高次元 |
| BoW | 文書をベクトル化し、ML に投入可能に | 語順喪失・機能語が支配的・スパース |
| n-gram | 局所的な語順を回復 | 次元爆発が悪化 |
| TF-IDF | 文書を特徴づける語を強調 | 語順喪失・スパースは未解決 |
| 単語文脈行列 + PPMI | 分布仮説で単語の意味の近さを表現 | 超高次元・スパース |
| 次元圧縮(SVD等) | 低次元の密な分散表現を獲得 | 大規模コーパスでの計算コスト |
| word2vec | 予測タスクによる学習で分散表現を獲得。意味の演算が可能に | 多義語を扱えない(→ BERT 以降へ) |
この「何が問題で、どう解決されたか」の連鎖を押さえておくと、Transformer や LLM の埋め込みを学ぶときにも「あの問題をこう解決したのか」という視点で理解できるようになります。
参考
シリーズの他の記事
