ディープラーニング勉強記録①
ディープラーニング備忘録①
はじめに
機械学習やディープラーニングの基礎を一通り勉強して最終的にTransformerまで勉強をしたいと思っています.形式としてはGPT5.5に講義のようなものを作成してもらいつつ,他の文献を参考にしながら疑問点などを対話によって解消し,その対話の要約を備忘録としてまとめています.とりあえず一日目の勉強の備忘録です. 今日扱った範囲は,分類問題,softmax,交差エントロピー,勾配降下法,誤差逆伝播法,活性化関数,MLP,1D-CNNまでです.
大きな流れは次の通り
入力データ
↓
モデル
↓
logits
↓
softmax
↓
確率
↓
交差エントロピー
↓
loss
↓
逆伝播
↓
パラメータ更新
Part1:softmaxと交差エントロピー
分類問題におけるモデルの出力
多クラス分類では,ニューラルネットワークは最終的に各クラスに対応するスコアを出力する.
このsoftmax前の生のスコアを logit と呼ぶ.
例えば3クラス分類で,モデルが次の値を出したとする.
logits = [2.1, 0.3, -1.2]
これはまだ確率ではない.
単に,class 0 が最もそれらしい,というスコアである.
softmax
softmaxは,logitを確率分布の形に変換する関数である. 以下では,PyTorchのクラス番号に合わせて, クラスの添字を とする.
ここで,
- はクラス のlogit
- はクラス数
- はクラス の予測確率
である.
softmaxの出力は,次の性質を持つ.
各値が0以上
全クラスの和が1
logitが大きいクラスほど確率が高い
つまり,softmaxは各クラスの「それらしさ」を確率分布として表す.
交差エントロピー
分類問題では,予測確率と正解ラベルのずれを測るために交差エントロピーを使う.
1サンプルの交差エントロピー損失を と書くと,
ここで,
- はクラス に対応する正解ラベルの成分
- はクラス の予測確率
である.
正解ラベルがone-hot表現で与えられる場合,例えば正解がclass 2なら,
y = [0, 0, 1, 0]
となる.
このとき,交差エントロピーは実質的に正解クラスの項だけになる.
ここで, は正解クラスである.
したがって,交差エントロピーは,
正解クラスの予測確率が高いほど小さい
正解クラスの予測確率が低いほど大きい
という損失関数である.
重要な注意:値は正解クラスだけだが,勾配は全logitに出る
one-hotラベルの場合,交差エントロピーの値そのものは正解クラスの確率だけから決まる.
しかし,softmaxでは正解クラスの確率 がすべてのlogitに依存している.
例えば,
である.
分母には不正解クラスのlogitも含まれている.
そのため,不正解クラスのlogitを大きくすると,正解クラスの確率が下がり,lossが増える.
したがって,不正解クラスの交差エントロピーの項は0でも,不正解クラスのlogitに対する勾配は0とは限らない.
softmax + 交差エントロピーの勾配
softmaxと交差エントロピーを組み合わせると,logitに対する勾配は非常にきれいな形になる.
つまり,
logitに対する勾配 = 予測確率 - 正解ラベル
である.
例えば,
p = [0.05, 0.90, 0.05]
y = [1, 0, 0]
なら,
p - y = [-0.95, 0.90, 0.05]
となる.
これは,
正解クラスのlogitは強く上げたい
誤って高く出した不正解クラスのlogitは強く下げたい
他の不正解クラスも少し下げたい
という意味である.
導出
正解クラスを とする.ここでも である.
one-hotラベルの場合,
である.
softmaxを代入すると,
なので,
となる.
対数の性質より,
である.
正解クラス で微分すると,
不正解クラス で微分すると,
したがって,正解クラスでも不正解クラスでもまとめて,
と書ける.
PyTorchでの注意
PyTorchの nn.CrossEntropyLoss() には,softmax後の確率ではなく,softmax前のlogitsを渡す.
import torch
import torch.nn as nn
criterion = nn.CrossEntropyLoss()
logits = torch.tensor([[2.1, 0.3, -1.2]])
target = torch.tensor([0])
loss = criterion(logits, target)
nn.CrossEntropyLoss() は内部で log_softmax と負の対数尤度損失をまとめて計算する.
したがって,次のようにsoftmax後の値を渡すのは基本的に誤りである.
probs = torch.softmax(logits, dim=1)
loss = criterion(probs, target)
理論上は,
logits → softmax → cross entropy
である.
一方,PyTorch実装では,
logits → CrossEntropyLoss
である.
Part2:batch_size,勾配降下法,誤差逆伝播法
batch_size
batch_size とは,1回の学習ステップでまとめてモデルに入力するデータ数である.
例えば,長さ700のデータを64個まとめて処理する場合,
X.shape = [64, 700]
となる.
ここで,
64:データの個数
700:1個のデータの特徴量数
である.
700を64ずつ使う のではなく,長さ700のデータを64個まとめて使う という意味である.
勾配降下法
学習とは,lossが小さくなるようにモデルのパラメータを更新することである.
パラメータを ,lossを とすると,勾配降下法の基本形は次のように書ける.
ここで,
- は学習率
- はlossの勾配
である.
勾配は,その方向にパラメータを動かしたときにlossが増える方向を表す.
そのため,lossを下げるには勾配と逆向きに動かす.
学習率
学習率は,1回の更新でどれくらいパラメータを動かすかを決める値である.
学習率が小さすぎる → 学習が遅い
学習率が大きすぎる → lossが発散しやすい
山を下るイメージで言えば,
勾配:坂の傾き
勾配降下法:坂を下る操作
学習率:一歩の大きさ
である.
誤差逆伝播法
誤差逆伝播法は,合成関数の微分を連鎖律で効率よく計算する方法である.
ニューラルネットワークは,
入力
↓
層1
↓
活性化関数
↓
層2
↓
loss
のような合成関数である.
したがって,lossから出発して,各層に対する勾配を後ろから前へ順番に計算できる.
連鎖律は次の形で表せる.
逆伝播では,各層が次の2つを計算する.
自分のパラメータに対する勾配
前の層へ渡す勾配
PyTorchでの学習ループ
典型的な学習コードは次のようになる.
for x, target in dataloader:
optimizer.zero_grad()
logits = model(x)
loss = criterion(logits, target)
loss.backward()
optimizer.step()
各行の意味は次の通りである.
optimizer.zero_grad():前回の勾配をリセットする
logits = model(x):forward計算を行う
loss = criterion(logits, target):損失を計算する
loss.backward():各パラメータの勾配を計算する
optimizer.step():勾配を使ってパラメータを更新する
重要なのは,
loss.backward() は勾配を計算する
optimizer.step() はパラメータを更新する
という違いである.
Part3:活性化関数とReLU
活性化関数が必要な理由
バイアスを含む Linear 層は,数学的にはアフィン変換である.
アフィン変換だけを何層重ねても,結局1つのアフィン変換にまとめられる.
例えば,
とすると,
となる.
これは結局,
という1つのアフィン変換である.
したがって,活性化関数を挟まずに Linear 層だけを深くしても表現力は増えない.
そこで,ReLUなどの非線形な活性化関数を挟む.
Linear
↓
ReLU
↓
Linear
↓
ReLU
↓
Linear
これにより,モデルは複雑な非線形関数を表現できるようになる.
ReLU
ReLUは次の関数である.
つまり,
x > 0 なら x をそのまま通す
x <= 0 なら 0 にする
である.
例として,
a = [2.0, -1.0, 0.5, -3.0]
なら,
ReLU(a) = [2.0, 0.0, 0.5, 0.0]
となる.
ReLUの逆伝播
ReLUの微分は,
である.
では厳密には微分できないが,実装上は0として扱われることが多い.
逆伝播では,
forward時に正だった場所 → 勾配を通す
forward時に負だった場所 → 勾配を0にする
となる.
例えば,
a = [2.0, -1.0, 0.5, -3.0]
で,後ろから来た勾配が,
dL/dh = [0.3, -0.7, 1.2, 0.5]
だったとする.
ReLUの微分は,
ReLU'(a) = [1, 0, 1, 0]
なので,
dL/da = [0.3, 0.0, 1.2, 0.0]
となる.
dead ReLU
ReLUには,負の領域では勾配が0になるという弱点がある.
あるニューロンが常に負の値を出すようになると,
出力が常に0
勾配も常に0
重みが更新されにくい
という状態になる.
これを dead ReLU と呼ぶ.
この問題を緩和するために,Leaky ReLU,ELU,GELUなどの派生関数が使われることがある.
Part4:決定境界とMLPのshape
決定境界のイメージ
分類問題では,入力データを高次元空間上の点として考えることができる.
モデルは,その空間を決定境界で分割し,入力がどの領域に属するかでクラスを決める.
線形分類器では,2次元なら直線,3次元なら平面,高次元なら超平面が決定境界になる.
一方,ニューラルネットワークでは活性化関数によって非線形性が入るため,2次元でも曲線のような複雑な決定境界を表現できる.
学習とは,パラメータを更新することで,結果としてこの決定境界を動かす操作だと考えられる.
MLPの構造
ここでは,入力次元700,中間次元128,クラス数256のMLPを考える.
入力 X
↓
Linear(700 → 128)
↓
ReLU
↓
Linear(128 → 256)
↓
logits
batch sizeを64とすると,入力は,
X.shape = [64, 700]
である.
PyTorchのLinearのshape
PyTorchで,
nn.Linear(700, 128)
と書くと,
in_features = 700
out_features = 128
である.
このとき,PyTorch内部の重みshapeは,
weight.shape = [128, 700]
bias.shape = [128]
となる.
PyTorchのLinearは内部的に,
output = input @ weight.T + bias
を計算する.
したがって,
input.shape = [64, 700]
weight.shape = [128, 700]
weight.T.shape = [700, 128]
output.shape = [64, 128]
となる.
MLPのforwardにおけるshape
全体のshapeは次のように変化する.
X : [64, 700]
↓ Linear(700 → 128)
H_pre : [64, 128]
↓ ReLU
H : [64, 128]
↓ Linear(128 → 256)
Z : [64, 256]
↓ CrossEntropyLoss with target [64]
loss : scalar
ここで,Z がlogitsである.
重要なのは,
batch方向の64は基本的に残る
特徴次元だけが700 → 128 → 256と変わる
という点である.
CrossEntropyLossの出力shape
logitsが,
Z.shape = [64, 256]
で,targetが,
target.shape = [64]
のとき,PyTorchの CrossEntropyLoss の出力はデフォルトでスカラーである.
loss.shape = []
各サンプルに対してlossを計算し,その平均を取るためである.
ただし,
nn.CrossEntropyLoss(reduction="none")
とした場合は,各サンプルごとのlossを返すため,
loss.shape = [64]
となる.
逆伝播におけるshape
batch sizeを ,クラス数を とし,logits,予測確率,one-hot正解ラベルをそれぞれ
とする.ここで, はサンプルの添字, はクラスの添字である.
サンプル の損失を
とすると,各サンプルの未平均損失に対しては,
が得られる.
ただし,PyTorchの CrossEntropyLoss はデフォルトで reduction="mean" であり,batch内のlossを平均する.
重み付けや ignore_index を用いない場合,batch平均lossは,
であり,そのlogitsに対する勾配は,
である.
行列表記で と定義すると,
となる.今回の例では なので,
である.
shape自体は変わらず,
G_Z.shape = [64, 256]
である.
最後のLinear層の重みを とすると,
W_2.shape = [256, 128]
である.行列表記では,この層の出力のうち重みに依存する部分は なので,重み勾配は,
である.
ここでの は,batch平均loss の に対する勾配であり, を表す.
shapeは,
となり,W_2.shape と一致する.
重要な原則は,
パラメータの勾配は,そのパラメータと同じshapeになる
中間変数の勾配は,その中間変数と同じshapeになる
である.
Part5:1D-CNNの基礎
MLPの弱点
MLPは,入力を単なるベクトルとして扱う.
例えば長さ700の時系列データを,
X.shape = [64, 700]
として扱う場合,MLPは各時刻を独立した特徴量のように見る.
しかし,時系列データでは近い時刻同士に関係があることが多い.
MLPはこの局所構造を明示的には利用しない.
また,重要なパターンが少し位置ズレした場合,MLPでは別の入力次元として扱われるため,位置ズレに弱くなりやすい.
1D-CNNの考え方
1D-CNNは,短いフィルタを時系列方向にスライドさせて,局所的なパターンを検出するモデルである.
例えば,長さ5のフィルタを用いると,
時刻1〜5を見る
時刻2〜6を見る
時刻3〜7を見る
...
のように,同じフィルタをずらしながら適用する.
これを畳み込みという.
フィルタとカーネル
まず,入力が1チャネルの場合を考える.
この場合,1D畳み込みのフィルタは短い重みベクトルである.
例えば,kernel sizeが3なら,
w = [w1, w2, w3]
である.
入力の一部が,
[x_t, x_{t+1}, x_{t+2}]
なら,バイアスを除いたフィルタ出力は,
となる.
バイアスを持つ層では,この値にバイアス を加える.
この計算を,位置をずらしながら繰り返す.
入力が複数チャネルの場合は,フィルタも各入力チャネルに対応する重みを持ち,出力を求めるときにチャネル方向の和も取る.
Conv1dの入力shape
PyTorchの nn.Conv1d は,入力として次のshapeを期待する.
[batch, channels, length]
例えば,64個のデータがあり,各データが1チャネル,長さ700なら,
X.shape = [64, 1, 700]
である.
ここで,
64:データの個数
1:入力チャネル数
700:時系列長
である.
入力チャネル数
入力チャネル数とは,
1つの位置に何種類の値があるか
を表す.
例えば,1時刻につき1種類の値だけを持つ時系列なら,
in_channels = 1
である.
もし,同じ時刻に3種類の信号があるなら,
in_channels = 3
となり,shapeは,
[batch, 3, length]
となる.
画像で言えば,白黒画像は1チャネル,RGB画像は3チャネルである.
1D時系列でも同じように考えられる.
out_channels
例えば,
nn.Conv1d(in_channels=1, out_channels=16, kernel_size=5)
は,
入力チャネル数は1
出力チャネル数は16
フィルタ長は5
という意味である.
out_channels=16 は,16種類のフィルタを学習するという意味である.
つまり,16種類の局所パターン検出器を作るということである.
このとき,PyTorchにおける重みshapeは,
weight.shape = [16, 1, 5]
である.
padding
paddingは,入力の端に0を追加する操作である.
1D-CNNで padding=1 とすると,左に1個,右に1個の0が追加される.
したがって,合計で2個ぶん長くなる.
例として,
元: [x1, x2, x3, x4]
padding=1: [0, x1, x2, x3, x4, 0]
である.
ただし,出力長がどうなるかは,paddingだけでなく,kernel sizeとstrideにも依存する.
stride
strideは,フィルタを何個ずつずらすかを表す.
stride=1:1個ずつずらす
stride=2:2個ずつずらす
strideを大きくすると,出力長は短くなる.
出力長の計算
ここでは,PyTorchの Conv1d において dilation がデフォルト値の1である場合を考える.
このとき,1D畳み込みの出力長は次の式で計算できる.
ここで,
- は入力長
- はpadding
- はkernel size
- はstride
である.
なお,一般にはdilationも出力長に影響するが,この備忘録では基礎として dilation=1 の場合に限定する.
例えば,
L = 700
P = 2
K = 5
S = 1
なら,
となる.
したがって,
nn.Conv1d(1, 16, kernel_size=5, padding=2)
に,
[64, 1, 700]
を入力すると,
[64, 16, 700]
が出力される.
CNNとMLPの違い
MLPは,各入力位置に別々の重みを持つ.
一方CNNは,同じフィルタを時系列全体にスライドさせて適用する.
この性質を 重み共有 と呼ぶ.
重み共有により,CNNには次の利点がある.
畳み込み層単体では必要なパラメータ数が少ない
局所的なパターンを見やすい
同じパターンが少し別の位置に出ても検出しやすい
例えば,MLPで700次元から128次元に変換する最初のLinear層では,重み数は,
である.
一方,
nn.Conv1d(1, 16, kernel_size=5)
という畳み込み層単体なら,重み数は,
である.
このように,畳み込み層単体では重み共有によりパラメータ数を大きく減らせる.
ただし,モデル全体でCNNの方が常にパラメータ数が少ないとは限らない.
例えば,畳み込み後に大きな特徴マップをそのまま flatten して全結合層に入れると,全結合層のパラメータ数が非常に大きくなることがある.
この備忘録で示した簡単な1D-CNNでは,
self.fc = nn.Linear(16 * 350, 256)
を使っているため,この全結合層だけで,
個の重みを持つ.
biasを含めるとさらに256個増える.
したがって,ここで言いたいことは,
CNNは畳み込み層において重み共有を使うため,局所パターン検出を少ないパラメータで行える
という点であり,
どんなCNNでもモデル全体のパラメータ数がMLPより少ない
という意味ではない.
モデル全体のパラメータ数を抑えるには,pooling,Global Average Pooling,追加の畳み込み層,strideなどを使って,flatten後の全結合層が巨大になりすぎないように設計する必要がある.
pooling
CNNでは,畳み込みの後にpoolingを使うことがある.
例えば,MaxPool1d(kernel_size=2) は,隣り合う2つの値の最大値を取る.
入力: [1, 3, 2, 5, 0, 4]
出力: [3, 5, 4]
poolingには,
出力長を短くする
重要な反応を残す
局所的な位置ズレに少し強くする
という効果がある.
1D-CNNのPyTorch例
import torch
import torch.nn as nn
class Simple1DCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv1d(
in_channels=1,
out_channels=16,
kernel_size=5,
padding=2
)
self.relu = nn.ReLU()
self.pool = nn.MaxPool1d(kernel_size=2)
self.fc = nn.Linear(16 * 350, 256)
def forward(self, x):
# x: [batch, 700]
x = x.unsqueeze(1) # [batch, 1, 700]
x = self.conv1(x) # [batch, 16, 700]
x = self.relu(x) # [batch, 16, 700]
x = self.pool(x) # [batch, 16, 350]
x = x.flatten(start_dim=1) # [batch, 5600]
logits = self.fc(x) # [batch, 256]
return logits
shapeの流れは次の通りである.
[64, 700]
↓ unsqueeze
[64, 1, 700]
↓ Conv1d(1 → 16, kernel_size=5, padding=2)
[64, 16, 700]
↓ ReLU
[64, 16, 700]
↓ MaxPool1d(kernel_size=2)
[64, 16, 350]
↓ flatten
[64, 5600]
↓ Linear(5600 → 256)
[64, 256]
unsqueeze
unsqueeze は,新しい次元を追加する操作である.
Conv1dは,
[batch, channels, length]
を期待する.
しかし,元の入力が,
[64, 700]
なら,channel次元がない.
そこで,
x = x.unsqueeze(1)
により,
[64, 700] → [64, 1, 700]
とする.
flatten
畳み込みやpoolingの後の出力が,
[64, 16, 350]
であるとする.
全結合層に入れるためには,各データを1本のベクトルにする必要がある.
16 × 350 = 5600
なので,
x = x.flatten(start_dim=1)
により,
[64, 16, 350] → [64, 5600]
とする.
start_dim=1 は,batch次元を残し,それ以降の次元をまとめるという意味である.
今日の重要まとめ
今日学んだ内容をまとめると,次の通りである.
softmaxはlogitを確率分布に変換する
交差エントロピーは正解クラスの確率が低いことを罰する
one-hotラベルではlossの値は正解クラスの項だけになる
しかしsoftmaxの分母を通じて,不正解クラスのlogitにも勾配が流れる
各サンプルの未平均損失では,softmax + 交差エントロピーのlogit勾配は p - y になる
batch平均lossでは,logitsに対する勾配は (P - Y) / N になる
batch_sizeは一度に処理するデータ数である
勾配降下法は勾配と逆向きにパラメータを動かす
逆伝播は連鎖律により勾配を後ろから前へ伝える方法である
ReLUは非線形性を入れるための活性化関数である
ReLUはforward時に負だった場所の勾配を0にする
MLPでは特徴次元がLinearによって変化する
PyTorchのLinearのweight shapeは [out_features, in_features] である
CrossEntropyLossのデフォルト出力はスカラーである
Conv1dの入力shapeは [batch, channels, length] である
CNNは同じフィルタを時系列全体に適用する
CNNは畳み込み層単体では重み共有によりパラメータ数を抑えやすい
ただしflatten後の全結合層が大きいと,モデル全体のパラメータ数は大きくなり得る
CNNは局所パターンの検出と位置ズレへのある程度の強さを持つ
現在の理解度メモ
現時点では,機械学習の最小構成である,
入力
モデル
logits
softmax
loss
勾配
逆伝播
パラメータ更新
の流れを一通り確認した.
また,MLPと1D-CNNのshapeを追うことで,PyTorchにおけるテンソルの見方も整理した.
次に進むべき内容は,次のいずれかである.
1.1D-CNNをもう少し深く学ぶ
2.optimizer,正則化,過学習を学ぶ
3.BatchNorm,Dropoutを学ぶ
4.RNNやTransformerへ進む前の時系列モデルを学ぶ
特に,次回はCNNの理解をもう少し固めるか,過学習と正則化に進むとよい.