ゼロから作るDeep Learning シリーズ 斎藤 康毅 (著) を読むためのメモ。

1. コードの全体構成

カテゴリ クラス/関数 役割
レイヤ MatMul 行列積の順伝播・逆伝播
レイヤ SoftmaxWithLoss 確率化 + 損失計算
関数 Softmax() 数値安定版ソフトマックス
関数 cross_entropy_error() 交差エントロピー損失
最適化 SGD / Momentum / Adam 重みの更新手法(切替可能)
モデル SimpleCBOW CBOWモデル本体
学習 Trainer ミニバッチ学習ループ管理
ユーティリティ remove_duplicate() 共有重みの勾配集約

2. 学習定数と回数の関係

window_size = 1
hidden_size = 5
batch_size  = 3
max_epoch   = 1000

data_size = 6           # コーパスから生成されるサンプル数
max_iters = 6 // 3      # = 2(1 epoch あたりの更新回数)

用語の定義

  • batch(バッチ) : 1回の更新に使うデータ数(= batch_size
  • iter(イテレーション) : 1バッチで重みを1回更新すること
  • epoch : 全データを1周走査し終えること(= max_iters 回の更新)

トータルの重み更新回数

max_epoch × max_iters = 1000 × 2 = 2000 回

ループ構造

for epoch in range(max_epoch):      # 外ループ: 1000回
    shuffle(data)                   # 毎 epoch シャッフル ← 重要
    for iters in range(max_iters):  # 内ループ: 2回
        # 1バッチ取り出し → forward → backward → update

シャッフルのポイント
毎 epoch の先頭でデータをシャッフルすることで、バッチ境界が毎回変わり、特定の組み合わせへの過剰適合(過学習)を防ぎ、汎化性能が上がる。


3. 1回のイテレーションの全体フロー

batch_size=3 のとき、3サンプルを同時並列に処理する(行列演算で一括処理 → 高速)。

データの形状

batch_x(contexts) : shape (3, 2, 7)  ← 3サンプル × 左右2単語 × 語彙数7
batch_t(target)   : shape (3, 7)     ← 3サンプル × 語彙数7(one-hot)

順伝播(forward)

contexts[:, 0] → in_layer0.forward(W_in) → h0  (3, 5)
contexts[:, 1] → in_layer1.forward(W_in) → h1  (3, 5)
                                               ↓
                              h = (h0 + h1) * 0.5    (3, 5)
                                               ↓
                         out_layer.forward(W_out) → score  (3, 7)
                                               ↓
                    loss_layer.forward(score, target) → loss(スカラー)

in_layer0in_layer1同じ W_in を共有している。
remove_duplicate() で勾配を合算してから更新する。

逆伝播(backward)&重み更新

loss → loss_layer.backward()
     → out_layer.backward()
     → in_layer0.backward() / in_layer1.backward()
     → optimizer.update(W_in, W_out)

4. 具体例:コーパスと予測

文章: 'You say goodbye and I say hello.'

word_to_id: {you:0, say:1, goodbye:2, and:3, i:4, hello:5, .:6}

コンテキスト → ターゲットの対応(window_size=1)

コンテキスト左 ターゲット コンテキスト右
you (0) say (1) goodbye (2)
say (1) goodbye (2) and (3)
goodbye (2) and (3) i (4)
and (3) i (4) say (1)
i (4) say (1) hello (5)
say (1) hello (5) . (6)

予測の例

入力: [you → 1 0 0 0 0 0 0]  と  [and → 0 0 0 1 0 0 0]
予測: [goodbye → 0 0 1 0 0 0 0]  ← 精度良く学習できていれば正解を返す

5. 損失が減少する仕組み

損失が下がるのは2つの力の組み合わせ。

  1. 勾配降下法 : 毎イテレーション、損失を下げる方向に W_inW_out が少しずつ更新される
  2. シャッフルによる汎化 : 毎 epoch でバッチ境界が変わり、過剰適合を防ぐ

6. コーパス外データを与えたときの挙動

精度良く学習できたモデルでも、学習データの分布の外(Out of Distribution)には対応できない

# 不正な one-hot(1が2つある)
[1 0 0 0 1 0 0]   # ← one-hot ではない
[0 1 0 0 0 0 0]

# コーパスにない組み合わせ(正しい one-hot でも)
[1 0 0 0 0 0 0]   # you
[0 0 0 0 0 1 0]   # hello  ← 隣り合う文はコーパスにない
  • モデルは入力を検証せず、そのまま行列積を計算して何らかの値を出力する
  • 意味のある予測にはならない(でたらめな値)

現代の大規模言語モデルが膨大なデータで学習する理由の一つがここ。
コーパスが大きいほど、未知の組み合わせにも意味のある予測ができるようになる。


7. 発見したバグ(Momentum クラス)

# 誤り:同じ行が2回記述されている
self.v[i] = self.momentum * self.v[i] - self.lr * grads[i]
self.v[i] = self.momentum * self.v[i] - self.lr * grads[i]  # ← 削除すべき

# 正しくは1行のみ
self.v[i] = self.momentum * self.v[i] - self.lr * grads[i]

2行目により「更新済みの v にさらに momentum をかけ直す」状態になり、本来の Momentum SGD とは異なる収束挙動になる。


8. 本当の目的は W_in(分散表現)

このモデルの目的は予測精度そのものではなく、予測がうまくできるように鍛えられた過程で W_in に蓄積された単語の意味ベクトル(分散表現)を得ること。

model.word_vecs  # = W_in, shape: (7, 5)
# 各単語が5次元ベクトルで表現される
# 意味が近い単語 → ベクトルが近い

これが Word2Vec / 分散表現 の核心。