この章で学ぶこと:順番に意味があるデータ
順番を入れ替えると、意味が変わる
「ねこ が いぬ を 追いかける」と「いぬ が ねこ を 追いかける」は、使っている単語はまったく同じなのに、意味は逆です。このように、並ぶ順番に意味があるデータ を 系列データ と呼びます。
- 文章: 単語や文字が順番に並んだもの
- 時系列: 毎日の気温、1時間ごとの電力の使用量、心電図の波形のように、時刻の順に並んだ数
- 音声: 空気の振動の大きさが、時間の順に並んだもの
これまでのモデルでは、順番をうまく扱えない
第6章のニューラルネットワークや、第11章のモデルは、決まった数の特徴量(手書き数字なら64個)を、一度に受け取りました。系列データには、次の難しさがあります。
- 長さがばらばら: 文章は、3語のこともあれば、100語のこともある
- 順番が大切: 単語の出てくる回数を数えるだけでは、「ねこがいぬを」と「いぬがねこを」を区別できない
- 離れたところどうしが関係する: 「わたしは、昨日駅前で見かけた、赤い帽子をかぶった犬を…飼うことにした」のように、文の最初と最後が関係することがある
この章では、この難しさに取り組んできたモデルを、順にたどります。
ブラウザの中では、大きなモデルを学習させることはできません。この章では、仕組みが分かる大きさの例を NumPy で動かし、実際の開発で使う PyTorch の書き方も紹介します(PyTorch の書き方と、重い実験の数値は、運営側で実際の PyTorch を動かして確かめています)。
単語を数にする:one-hot と埋め込み
モデルは数しか扱えないので、まず文章を数に変えます。
トークン:文章を区切る単位
文章を、単語や文字などの単位に区切ります。区切った1つ1つを トークン と呼びます。日本語は単語の間に空白がないので、区切り方を工夫します(単語で区切る、1文字ずつ区切る、よく出る文字の並びをひとまとまりにする サブワード で区切る、など)。この章の例では、単語の間に空白を入れた文を使います。
one-hot:単語ごとに1か所だけ1
使う単語が全部で22種類なら、単語に1番から22番までの番号を付け、22個の数を並べて、その単語の番号の位置だけを1、残りを0にします。五十音順に番号を付けると、1〜5番目は「あなた・いく・いぬ・うさぎ・えき」です。
これを one-hot 表現 と呼びます(第7章で、正解のクラスを表すのにも使いました)。単純ですが、2つの弱点があります。
- 単語の数だけ長くなる: 実際の言葉は数万種類あり、数万個の数のうち1個だけが1の、無駄の多い表し方になる
- 似ている単語が分からない: どの2つの単語も、内積は0。「ねこ」と「いぬ」の関係も、「ねこ」と「えき」の関係も、同じになってしまう
埋め込み:似た単語が近くなるベクトル
そこで、単語を、数十〜数百個の数のベクトルで表し、意味の似た単語ほど近いベクトルになる ようにします。これを 単語の埋め込み(embedding)と呼びます。
どうすれば「意味が似ている」が分かるのでしょうか。手がかりは、「似た使われ方をする単語は、意味も似ている」という考え方です(分布仮説)。「ねこ」と「いぬ」は、どちらも「〜が さかな を たべる」「〜が こうえん で ねる」のような同じ文の形で使われます。
実験:一緒に出る回数から、単語のベクトルを作る
自分で作った短い文300個(「ねこ が さかな を たべる」「わたし は パン を かう」など、単語22種類)で、単語どうしが同じ文に出てきた回数を数えました。その表を、第11章の特異値分解で4個の数にまとめ、単語のベクトルにしました。2つのベクトルの向きの近さを、第11章の コサイン類似度(同じ向きで1、直角で0)で測ります。
「ねこ・いぬ・うさぎ」(たべる・ねる と一緒に使われる)は互いに近く、「わたし」(かう・いく と一緒に使われる)とは遠くなりました。正解を教えていないのに、使われ方だけから、似た単語がまとまったのです。
word2vec:予測しながら埋め込みを学ぶ
実際には、ニューラルネットワークに「まわりの単語から、真ん中の単語を当てる」(CBOW)、または「真ん中の単語から、まわりの単語を当てる」(skip-gram)練習をさせ、その途中の重みを単語のベクトルとして使う方法がよく知られています。これを word2vec と呼びます。学習した埋め込みでは、「王 − 男 + 女 ≒ 女王」のように、ベクトルの足し算・引き算が意味の関係を表すことがあると報告されています。
この章のモデル(RNN や Transformer)は、最初の層でトークンを埋め込みベクトルに変え、その埋め込みもモデル全体と一緒に学習します。
Pythonで単語を数にする、よくある誤解と振り返り
one-hot と、コサイン類似度
import numpy as np
vocab = ["あなた", "いく", "いぬ", "うさぎ", "えき"] # 番号を付けた単語の一覧(一部)
words = ["いぬ", "えき", "いぬ"] # 変えたい単語の並び
onehot = np.zeros((len(words), len(vocab))) # 単語の数 × 単語の種類の数
for i, w in enumerate(words):
onehot[i, vocab.index(w)] = 1 # その単語の番号の位置だけ1
print(onehot)
# → [[0. 0. 1. 0. 0.]
# [0. 0. 0. 0. 1.]
# [0. 0. 1. 0. 0.]]
def cosine(a, b):
return a @ b / (np.linalg.norm(a) * np.linalg.norm(b)) # 内積 ÷ (長さ × 長さ)
print(cosine(onehot[0], onehot[1])) # → 0.0(one-hot では、違う単語はいつも直角)
vocab.index(w) は、リストの中で w が何番目(0から数える)にあるかを返します。np.linalg.norm はベクトルの長さ(第11章のユークリッド距離の、原点からの長さ)です。
PyTorch の埋め込み層
import torch
from torch import nn
emb = nn.Embedding(num_embeddings=22, embedding_dim=4) # 22種類の単語を、4個の数のベクトルに
ids = torch.tensor([2, 4, 2]) # 単語の番号の並び(いぬ・えき・いぬ)
print(emb(ids).shape) # → torch.Size([3, 4])
nn.Embedding は、「単語の番号 → ベクトル」の表(22行 × 4列の重み)です。one-hot のベクトルに重みの行列を掛けるのと同じ結果を、表から行を取り出すだけで素早く計算します。表の値は、モデルの学習と一緒に調整されます。
よくある誤解
- 「単語を、辞書の順番の番号(1, 2, 3, …)のまま入力すればよい」: 番号のまま入れると、「3番目の単語は1番目の3倍」のような、意味のない大小関係をモデルが使ってしまいます。one-hot や埋め込みに変えます
- 「埋め込みは、人が意味を考えて決める」: 埋め込みは、データ(単語の使われ方)から学習して決まります。そのため、学習に使った文章の偏り(例えば、職業と性別の結び付き)も、埋め込みに入り込むことがあります
- 「コサイン類似度が高い単語は、同じ意味」: 「似た使われ方をする」だけで、反対の意味の単語(「暑い」と「寒い」)も、似た文の形で使われるため近くなることがあります
振り返り
- 系列データは、並ぶ順番に意味があるデータ(文章・時系列・音声)。長さがばらばらで、離れた位置どうしが関係することもある
- 文章はトークンに区切り、数に変える。one-hot は単純だが、長く、似ている単語が分からない
- 埋め込みは、似た使われ方の単語が近くなるベクトル。word2vec(CBOW・skip-gram)は、まわりの単語を予測しながら学習する