3.4. 線形回帰のスクラッチ実装¶
%matplotlib inline
from d2l import torch as d2l
import torch
%matplotlib inline
from d2l import mxnet as d2l
from mxnet import autograd, np, npx
npx.set_np()
%matplotlib inline
from d2l import jax as d2l
from flax import linen as nn
import jax
from jax import numpy as jnp
import optax
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
%matplotlib inline
from d2l import tensorflow as d2l
import tensorflow as tf
3.4.1. モデルの定義¶
sigma
を通して別の値を指定することもできる。class LinearRegressionScratch(d2l.Module): #@save
"""The linear regression model implemented from scratch."""
def __init__(self, num_inputs, lr, sigma=0.01):
super().__init__()
self.save_hyperparameters()
if tab.selected('mxnet'):
self.w = d2l.normal(0, sigma, (num_inputs, 1))
self.b = d2l.zeros(1)
self.w.attach_grad()
self.b.attach_grad()
if tab.selected('pytorch'):
self.w = d2l.normal(0, sigma, (num_inputs, 1), requires_grad=True)
self.b = d2l.zeros(1, requires_grad=True)
if tab.selected('tensorflow'):
w = tf.random.normal((num_inputs, 1), mean=0, stddev=0.01)
b = tf.zeros(1)
self.w = tf.Variable(w, trainable=True)
self.b = tf.Variable(b, trainable=True)
class LinearRegressionScratch(d2l.Module): #@save
"""The linear regression model implemented from scratch."""
def __init__(self, num_inputs, lr, sigma=0.01):
super().__init__()
self.save_hyperparameters()
if tab.selected('mxnet'):
self.w = d2l.normal(0, sigma, (num_inputs, 1))
self.b = d2l.zeros(1)
self.w.attach_grad()
self.b.attach_grad()
if tab.selected('pytorch'):
self.w = d2l.normal(0, sigma, (num_inputs, 1), requires_grad=True)
self.b = d2l.zeros(1, requires_grad=True)
if tab.selected('tensorflow'):
w = tf.random.normal((num_inputs, 1), mean=0, stddev=0.01)
b = tf.zeros(1)
self.w = tf.Variable(w, trainable=True)
self.b = tf.Variable(b, trainable=True)
class LinearRegressionScratch(d2l.Module): #@save
"""The linear regression model implemented from scratch."""
num_inputs: int
lr: float
sigma: float = 0.01
def setup(self):
self.w = self.param('w', nn.initializers.normal(self.sigma),
(self.num_inputs, 1))
self.b = self.param('b', nn.initializers.zeros, (1))
class LinearRegressionScratch(d2l.Module): #@save
"""The linear regression model implemented from scratch."""
def __init__(self, num_inputs, lr, sigma=0.01):
super().__init__()
self.save_hyperparameters()
if tab.selected('mxnet'):
self.w = d2l.normal(0, sigma, (num_inputs, 1))
self.b = d2l.zeros(1)
self.w.attach_grad()
self.b.attach_grad()
if tab.selected('pytorch'):
self.w = d2l.normal(0, sigma, (num_inputs, 1), requires_grad=True)
self.b = d2l.zeros(1, requires_grad=True)
if tab.selected('tensorflow'):
w = tf.random.normal((num_inputs, 1), mean=0, stddev=0.01)
b = tf.zeros(1)
self.w = tf.Variable(w, trainable=True)
self.b = tf.Variable(b, trainable=True)
@d2l.add_to_class(LinearRegressionScratch) #@save
def forward(self, X):
return d2l.matmul(X, self.w) + self.b
3.4.2. 損失関数の定義¶
y を予測値 y_hat の形状に変換する必要がある。y_hat と同じ形状になる。@d2l.add_to_class(LinearRegressionScratch) #@save
def loss(self, y_hat, y):
l = (y_hat - y) ** 2 / 2
return d2l.reduce_mean(l)
@d2l.add_to_class(LinearRegressionScratch) #@save
def loss(self, y_hat, y):
l = (y_hat - y) ** 2 / 2
return d2l.reduce_mean(l)
@d2l.add_to_class(LinearRegressionScratch) #@save
def loss(self, params, X, y, state):
y_hat = state.apply_fn({'params': params}, *X) # X unpacked from a tuple
l = (y_hat - d2l.reshape(y, y_hat.shape)) ** 2 / 2
return d2l.reduce_mean(l)
@d2l.add_to_class(LinearRegressionScratch) #@save
def loss(self, y_hat, y):
l = (y_hat - y) ** 2 / 2
return d2l.reduce_mean(l)
3.4.3. 最適化アルゴリズムの定義¶
lr
が与えられたときに更新を適用する。step メソッドで更新する。zero_grad
メソッドはすべての勾配を0に設定し、逆伝播の前に実行しなければならない。class SGD(d2l.HyperParameters): #@save
"""Minibatch stochastic gradient descent."""
def __init__(self, params, lr):
self.save_hyperparameters()
if tab.selected('mxnet'):
def step(self, _):
for param in self.params:
param -= self.lr * param.grad
if tab.selected('pytorch'):
def step(self):
for param in self.params:
param -= self.lr * param.grad
def zero_grad(self):
for param in self.params:
if param.grad is not None:
param.grad.zero_()
class SGD(d2l.HyperParameters): #@save
"""Minibatch stochastic gradient descent."""
def __init__(self, params, lr):
self.save_hyperparameters()
if tab.selected('mxnet'):
def step(self, _):
for param in self.params:
param -= self.lr * param.grad
if tab.selected('pytorch'):
def step(self):
for param in self.params:
param -= self.lr * param.grad
def zero_grad(self):
for param in self.params:
if param.grad is not None:
param.grad.zero_()
class SGD(d2l.HyperParameters): #@save
"""Minibatch stochastic gradient descent."""
# The key transformation of Optax is the GradientTransformation
# defined by two methods, the init and the update.
# The init initializes the state and the update transforms the gradients.
# https://github.com/deepmind/optax/blob/master/optax/_src/transform.py
def __init__(self, lr):
self.save_hyperparameters()
def init(self, params):
# Delete unused params
del params
return optax.EmptyState
def update(self, updates, state, params=None):
del params
# When state.apply_gradients method is called to update flax's
# train_state object, it internally calls optax.apply_updates method
# adding the params to the update equation defined below.
updates = jax.tree_util.tree_map(lambda g: -self.lr * g, updates)
return updates, state
def __call__():
return optax.GradientTransformation(self.init, self.update)
class SGD(d2l.HyperParameters): #@save
"""Minibatch stochastic gradient descent."""
def __init__(self, lr):
self.save_hyperparameters()
def apply_gradients(self, grads_and_vars):
for grad, param in grads_and_vars:
param.assign_sub(self.lr * grad)
次に、SGD クラスのインスタンスを返す configure_optimizers
メソッドを定義する。
@d2l.add_to_class(LinearRegressionScratch) #@save
def configure_optimizers(self):
if tab.selected('mxnet') or tab.selected('pytorch'):
return SGD([self.w, self.b], self.lr)
if tab.selected('tensorflow', 'jax'):
return SGD(self.lr)
3.4.4. 学習¶
training_step メソッドを通して損失を計算する。パラメータ \((\mathbf{w}, b)\) を初期化する
完了するまで繰り返す
勾配 \(\mathbf{g} \leftarrow \partial_{(\mathbf{w},b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l(\mathbf{x}^{(i)}, y^{(i)}, \mathbf{w}, b)\) を計算する
パラメータ \((\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g}\) を更新する
@d2l.add_to_class(d2l.Trainer) #@save
def prepare_batch(self, batch):
return batch
@d2l.add_to_class(d2l.Trainer) #@save
def fit_epoch(self):
self.model.train()
for batch in self.train_dataloader:
loss = self.model.training_step(self.prepare_batch(batch))
self.optim.zero_grad()
with torch.no_grad():
loss.backward()
if self.gradient_clip_val > 0: # To be discussed later
self.clip_gradients(self.gradient_clip_val, self.model)
self.optim.step()
self.train_batch_idx += 1
if self.val_dataloader is None:
return
self.model.eval()
for batch in self.val_dataloader:
with torch.no_grad():
self.model.validation_step(self.prepare_batch(batch))
self.val_batch_idx += 1
@d2l.add_to_class(d2l.Trainer) #@save
def fit_epoch(self):
for batch in self.train_dataloader:
with autograd.record():
loss = self.model.training_step(self.prepare_batch(batch))
loss.backward()
if self.gradient_clip_val > 0:
self.clip_gradients(self.gradient_clip_val, self.model)
self.optim.step(1)
self.train_batch_idx += 1
if self.val_dataloader is None:
return
for batch in self.val_dataloader:
self.model.validation_step(self.prepare_batch(batch))
self.val_batch_idx += 1
@d2l.add_to_class(d2l.Trainer) #@save
def fit_epoch(self):
self.model.training = True
if self.state.batch_stats:
# Mutable states will be used later (e.g., for batch norm)
for batch in self.train_dataloader:
(_, mutated_vars), grads = self.model.training_step(self.state.params,
self.prepare_batch(batch),
self.state)
self.state = self.state.apply_gradients(grads=grads)
# Can be ignored for models without Dropout Layers
self.state = self.state.replace(
dropout_rng=jax.random.split(self.state.dropout_rng)[0])
self.state = self.state.replace(batch_stats=mutated_vars['batch_stats'])
self.train_batch_idx += 1
else:
for batch in self.train_dataloader:
_, grads = self.model.training_step(self.state.params,
self.prepare_batch(batch),
self.state)
self.state = self.state.apply_gradients(grads=grads)
# Can be ignored for models without Dropout Layers
self.state = self.state.replace(
dropout_rng=jax.random.split(self.state.dropout_rng)[0])
self.train_batch_idx += 1
if self.val_dataloader is None:
return
self.model.training = False
for batch in self.val_dataloader:
self.model.validation_step(self.state.params,
self.prepare_batch(batch),
self.state)
self.val_batch_idx += 1
@d2l.add_to_class(d2l.Trainer) #@save
def fit_epoch(self):
self.model.training = True
for batch in self.train_dataloader:
with tf.GradientTape() as tape:
loss = self.model.training_step(self.prepare_batch(batch))
grads = tape.gradient(loss, self.model.trainable_variables)
if self.gradient_clip_val > 0:
grads = self.clip_gradients(self.gradient_clip_val, grads)
self.optim.apply_gradients(zip(grads, self.model.trainable_variables))
self.train_batch_idx += 1
if self.val_dataloader is None:
return
self.model.training = False
for batch in self.val_dataloader:
self.model.validation_step(self.prepare_batch(batch))
self.val_batch_idx += 1
SyntheticRegressionData
クラスを使い、真のパラメータを与える。lr=0.03 でモデルを学習し、max_epochs=3
に設定する。model = LinearRegressionScratch(2, lr=0.03)
data = d2l.SyntheticRegressionData(w=d2l.tensor([2, -3.4]), b=4.2)
trainer = d2l.Trainer(max_epochs=3)
trainer.fit(model, data)
with torch.no_grad():
print(f'error in estimating w: {data.w - d2l.reshape(model.w, data.w.shape)}')
print(f'error in estimating b: {data.b - model.b}')
error in estimating w: tensor([ 0.0709, -0.2553])
error in estimating b: tensor([0.2337])
print(f'error in estimating w: {data.w - d2l.reshape(model.w, data.w.shape)}')
print(f'error in estimating b: {data.b - model.b}')
error in estimating w: [ 0.1114291 -0.12024117]
error in estimating b: [0.1888752]
params = trainer.state.params
print(f"error in estimating w: {data.w - d2l.reshape(params['w'], data.w.shape)}")
print(f"error in estimating b: {data.b - params['b']}")
error in estimating w: [ 0.07124913 -0.19058824]
error in estimating b: [0.23817086]
print(f'error in estimating w: {data.w - d2l.reshape(model.w, data.w.shape)}')
print(f'error in estimating b: {data.b - model.b}')
error in estimating w: [ 0.12973297 -0.16401815]
error in estimating b: [0.25387883]
3.4.5. まとめ¶
3.4.6. 演習¶
重みを0で初期化したらどうなるだろうか。アルゴリズムはそれでも動作するだろうか。では、パラメータを0.01ではなく分散1000で初期化したらどうだろうか。
あなたが Georg Simon Ohm で、電圧と電流を関連づける抵抗のモデルを考えようとしているとする。自動微分を使ってモデルのパラメータを学習できるか。
プランクの法則 を使って、スペクトルエネルギー密度から物体の温度を求められるだろうか。参考までに、黒体から放射される放射のスペクトル密度 \(B\) は \(B(\lambda, T) = \frac{2 hc^2}{\lambda^5} \cdot \left(\exp \frac{h c}{\lambda k T} - 1\right)^{-1}\) である。ここで、\(\lambda\) は波長、\(T\) は温度、\(c\) は光速、\(h\) はプランク定数、\(k\) はボルツマン定数である。異なる波長 \(\lambda\) に対するエネルギーを測定したとして、スペクトル密度曲線をプランクの法則にフィットさせる必要がある。
損失の2階微分を計算したい場合、どのような問題に遭遇する可能性があるか。それらをどう解決するか。
loss関数でreshapeメソッドが必要なのはなぜですか。異なる学習率を使って実験し、損失関数の値がどれくらい速く下がるかを調べよ。学習エポック数を増やすことで誤差を減らせますか。
例の数がバッチサイズで割り切れない場合、epochの終わりに
data_iterはどうなるか。絶対値損失
(y_hat - d2l.reshape(y, y_hat.shape)).abs().sum()のような別の損失関数を実装してみよ。通常のデータで何が起こるか確認せよ。
\(\\mathbf{y}\) のいくつかの要素、たとえば \(y_5 = 10000\) を意図的に摂動させた場合、挙動に違いがあるか確認せよ。
二乗損失と絶対値損失の長所を組み合わせる安価な解決策を考えられますか。 ヒント: 非常に大きな勾配値をどう避けますか。
なぜデータセットを再シャッフルする必要があるのだろうか。悪意を持って構成されたデータセットが、そうしないと最適化アルゴリズムを破綻させるような例を設計できるか。