3.5. 線形回帰の簡潔な実装¶
深層学習は、この10年で一種のカンブリア爆発を経験してきた。 技術、応用、アルゴリズムの数は、過去数十年の進歩をはるかに上回っている。 これは複数の要因が幸運にも組み合わさった結果であり、 その一つが、いくつかのオープンソース深層学習フレームワークが提供する強力で無料のツールである。 Theano (Bergstra et al., 2010), DistBelief (Dean et al., 2012), および Caffe (Jia et al., 2014) は、広く採用されたそのようなモデルの 第一世代を代表していると言えるだろう。 Lisp風のプログラミング体験を提供した SN2 (Simulateur Neuristique) (Bottou and Le Cun, 1988) のような初期の(先駆的な)研究とは対照的に、 現代のフレームワークは自動微分と Python の利便性を提供する。 これらのフレームワークにより、勾配ベース学習アルゴリズムの実装における 反復的な作業を自動化し、モジュール化できる。
3.4 章 では、 (i) データ保存と線形代数のためのテンソル、 および (ii) 勾配計算のための自動微分 だけに依拠した。 実際には、データイテレータ、損失関数、最適化器、 ニューラルネットワーク層は 非常に一般的であるため、現代のライブラリはこれらの構成要素も 私たちの代わりに実装してくれる。 この節では、深層学習フレームワークの 高レベル API を使って 3.4 章 の線形回帰モデルを 簡潔に実装する方法を示す。
from d2l import torch as d2l
import numpy as np
import torch
from torch import nn
from d2l import mxnet as d2l
from mxnet import autograd, gluon, init, np, npx
from mxnet.gluon import nn
npx.set_np()
from d2l import jax as d2l
from flax import linen as nn
import jax
from jax import numpy as jnp
import optax
from d2l import tensorflow as d2l
import numpy as np
import tensorflow as tf
3.5.1. モデルの定義¶
3.4 章 で 線形回帰をスクラッチから実装したときには、 モデルパラメータを明示的に定義し、 基本的な線形代数演算を使って出力を生成する計算を コード化した。 これは知っておくべきことである。 しかし、モデルがより複雑になり、 しかもそれをほぼ毎日行わなければならなくなると、 助けがあることのありがたさが分かるだろう。 状況は、ブログをスクラッチから自作するのに似ている。 一度や二度ならやりがいがあり、学びもあるが、 車輪の再発明に1か月費やすようでは、 優秀な Web 開発者とは言えない。
標準的な演算については、 フレームワークにあらかじめ定義された層を使うことができ、 実装を気にするよりも、 モデルを構成する層そのものに集中できる。 図 3.1.2 で説明した 単層ネットワークのアーキテクチャを思い出そう。 この層は 全結合 と呼ばれる。 なぜなら、その入力の各要素が 行列—ベクトル積によって 各出力に接続されているからである。
PyTorch では、全結合層は Linear クラスと LazyLinear
クラス(バージョン 1.8.0 以降で利用可能)で定義される。
後者は、ユーザーが単に 出力次元だけを指定できるようにするが、
前者ではそれに加えて、 この層に何個の入力が入るかも指定する必要がある。
入力形状の指定は不便であり、(畳み込み層のように)
非自明な計算を要することがある。 そのため、簡潔さのために、可能な限り
このような「遅延」層を使う。
class LinearRegression(d2l.Module): #@save
"""The linear regression model implemented with high-level APIs."""
def __init__(self, lr):
super().__init__()
self.save_hyperparameters()
if tab.selected('mxnet'):
self.net = nn.Dense(1)
self.net.initialize(init.Normal(sigma=0.01))
if tab.selected('tensorflow'):
initializer = tf.initializers.RandomNormal(stddev=0.01)
self.net = tf.keras.layers.Dense(1, kernel_initializer=initializer)
if tab.selected('pytorch'):
self.net = nn.LazyLinear(1)
self.net.weight.data.normal_(0, 0.01)
self.net.bias.data.fill_(0)
class LinearRegression(d2l.Module): #@save
"""The linear regression model implemented with high-level APIs."""
def __init__(self, lr):
super().__init__()
self.save_hyperparameters()
if tab.selected('mxnet'):
self.net = nn.Dense(1)
self.net.initialize(init.Normal(sigma=0.01))
if tab.selected('tensorflow'):
initializer = tf.initializers.RandomNormal(stddev=0.01)
self.net = tf.keras.layers.Dense(1, kernel_initializer=initializer)
if tab.selected('pytorch'):
self.net = nn.LazyLinear(1)
self.net.weight.data.normal_(0, 0.01)
self.net.bias.data.fill_(0)
class LinearRegression(d2l.Module): #@save
"""The linear regression model implemented with high-level APIs."""
lr: float
def setup(self):
self.net = nn.Dense(1, kernel_init=nn.initializers.normal(0.01))
class LinearRegression(d2l.Module): #@save
"""The linear regression model implemented with high-level APIs."""
def __init__(self, lr):
super().__init__()
self.save_hyperparameters()
if tab.selected('mxnet'):
self.net = nn.Dense(1)
self.net.initialize(init.Normal(sigma=0.01))
if tab.selected('tensorflow'):
initializer = tf.initializers.RandomNormal(stddev=0.01)
self.net = tf.keras.layers.Dense(1, kernel_initializer=initializer)
if tab.selected('pytorch'):
self.net = nn.LazyLinear(1)
self.net.weight.data.normal_(0, 0.01)
self.net.bias.data.fill_(0)
forward メソッドでは、あらかじめ定義された層の組み込み __call__
メソッドを呼び出して出力を計算するだけである。
@d2l.add_to_class(LinearRegression) #@save
def forward(self, X):
return self.net(X)
3.5.2. 損失関数の定義¶
MSELoss クラスは平均二乗誤差を計算する((3.1.5) の
\(1/2\) 因子は含まない)。 デフォルトでは、MSELoss
はサンプル全体の平均損失を返す。
自前で実装するよりも高速で(しかも使いやすいです)。
@d2l.add_to_class(LinearRegression) #@save
def loss(self, y_hat, y):
if tab.selected('mxnet'):
fn = gluon.loss.L2Loss()
return fn(y_hat, y).mean()
if tab.selected('pytorch'):
fn = nn.MSELoss()
return fn(y_hat, y)
if tab.selected('tensorflow'):
fn = tf.keras.losses.MeanSquaredError()
return fn(y, y_hat)
@d2l.add_to_class(LinearRegression) #@save
def loss(self, y_hat, y):
if tab.selected('mxnet'):
fn = gluon.loss.L2Loss()
return fn(y_hat, y).mean()
if tab.selected('pytorch'):
fn = nn.MSELoss()
return fn(y_hat, y)
if tab.selected('tensorflow'):
fn = tf.keras.losses.MeanSquaredError()
return fn(y, y_hat)
@d2l.add_to_class(LinearRegression) #@save
def loss(self, params, X, y, state):
y_hat = state.apply_fn({'params': params}, *X)
return d2l.reduce_mean(optax.l2_loss(y_hat, y))
@d2l.add_to_class(LinearRegression) #@save
def loss(self, y_hat, y):
if tab.selected('mxnet'):
fn = gluon.loss.L2Loss()
return fn(y_hat, y).mean()
if tab.selected('pytorch'):
fn = nn.MSELoss()
return fn(y_hat, y)
if tab.selected('tensorflow'):
fn = tf.keras.losses.MeanSquaredError()
return fn(y, y_hat)
3.5.3. 最適化アルゴリズムの定義¶
ミニバッチ SGD
はニューラルネットワークを最適化するための標準的な手法であり、 そのため
PyTorch は optim モジュールで
このアルゴリズムのいくつかの変種をサポートしている。 SGD
インスタンスを生成するときには、 最適化対象のパラメータ(モデルの
self.parameters() から取得可能)と、
最適化アルゴリズムに必要な学習率(self.lr)を指定する。
@d2l.add_to_class(LinearRegression) #@save
def configure_optimizers(self):
if tab.selected('mxnet'):
return gluon.Trainer(self.collect_params(),
'sgd', {'learning_rate': self.lr})
if tab.selected('pytorch'):
return torch.optim.SGD(self.parameters(), self.lr)
if tab.selected('tensorflow'):
return tf.keras.optimizers.SGD(self.lr)
if tab.selected('jax'):
return optax.sgd(self.lr)
3.5.4. 学習¶
深層学習フレームワークの高レベル API を通してモデルを表現すると、 必要なコード行数が少なくなることに気づいたかもしれない。 パラメータを個別に割り当てたり、 損失関数を定義したり、 ミニバッチ SGD を実装したりする必要はなかった。 より複雑なモデルを扱い始めると、 高レベル API の利点はさらに大きくなる。
基本要素がすべて揃ったので、
学習ループ自体はスクラッチ実装したものと同じである。
したがって、fit メソッド(3.2.4 章
で導入)を呼び出すだけで、 3.4 章 の
fit_epoch メソッドの実装に依存して、 モデルを学習できる。
model = LinearRegression(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)
以下では、 有限データで学習して得られたモデルパラメータと 実際のパラメータを比較する。 パラメータにアクセスするには、 必要な層の重みとバイアスにアクセスする。 スクラッチ実装の場合と同様に、 推定されたパラメータが真の値に近いことに注意されたい。
@d2l.add_to_class(LinearRegression) #@save
def get_w_b(self):
if tab.selected('mxnet'):
return (self.net.weight.data(), self.net.bias.data())
if tab.selected('pytorch'):
return (self.net.weight.data, self.net.bias.data)
if tab.selected('tensorflow'):
return (self.get_weights()[0], self.get_weights()[1])
w, b = model.get_w_b()
@d2l.add_to_class(LinearRegression) #@save
def get_w_b(self):
if tab.selected('mxnet'):
return (self.net.weight.data(), self.net.bias.data())
if tab.selected('pytorch'):
return (self.net.weight.data, self.net.bias.data)
if tab.selected('tensorflow'):
return (self.get_weights()[0], self.get_weights()[1])
w, b = model.get_w_b()
@d2l.add_to_class(LinearRegression) #@save
def get_w_b(self, state):
net = state.params['net']
return net['kernel'], net['bias']
w, b = model.get_w_b(trainer.state)
@d2l.add_to_class(LinearRegression) #@save
def get_w_b(self):
if tab.selected('mxnet'):
return (self.net.weight.data(), self.net.bias.data())
if tab.selected('pytorch'):
return (self.net.weight.data, self.net.bias.data)
if tab.selected('tensorflow'):
return (self.get_weights()[0], self.get_weights()[1])
w, b = model.get_w_b()
print(f'error in estimating w: {data.w - d2l.reshape(w, data.w.shape)}')
print(f'error in estimating b: {data.b - b}')
error in estimating w: tensor([ 0.0080, -0.0134])
error in estimating b: tensor([0.0148])
3.5.5. まとめ¶
この節では、本書で初めて、 MXNet (Chen et al., 2015)、 JAX (Frostig et al., 2018)、 PyTorch (Paszke et al., 2019)、 および Tensorflow (Abadi et al., 2016) のような現代の深層学習フレームワークがもたらす利便性を活用した 深層ネットワークの実装を行った。 データの読み込み、層の定義、 損失関数、最適化器、学習ループには フレームワークのデフォルトを使った。 フレームワークが必要な機能をすべて提供しているなら、 通常はそれらを使うのがよいだろう。 これらの構成要素のライブラリ実装は 性能のために大きく最適化されており、 信頼性のために適切にテストされている傾向があるからである。 同時に、これらのモジュールは直接実装できることも 忘れないようにしたい。 これは特に、モデル開発の最前線で生きたいと願う 意欲的な研究者にとって重要である。 そこでは、現在のどのライブラリにも存在しえない 新しい構成要素を発明することになるからである。
PyTorch では、data モジュールがデータ処理のためのツールを提供し、
nn
モジュールが多数のニューラルネットワーク層と一般的な損失関数を定義する。
末尾が _ のメソッドで値を置き換えることで、
パラメータを初期化できる。
ネットワークの入力次元を指定する必要があることに注意されたい。
今のところは単純であるが、多数の層を持つ複雑なネットワークを設計したいときには、
大きな波及効果を持つ可能性がある。
これらのネットワークをどのようにパラメータ化するかを慎重に考える必要があり、
それによって移植性を確保できる。
3.5.6. 演習¶
ミニバッチ上の損失の総和を使う代わりに、ミニバッチ上の損失の平均を使うようにした場合、学習率はどのように変更する必要があるか?
フレームワークのドキュメントを確認して、どの損失関数が提供されているかを見よ。特に、二乗損失を Huber のロバスト損失関数に置き換えよ。すなわち、次の損失関数を使う。
(3.5.1)¶\[\begin{split}l(y,y') = \begin{cases}|y-y'| -\frac{\sigma}{2} & \textrm{ if } |y-y'| > \sigma \\ \frac{1}{2 \sigma} (y-y')^2 & \textrm{ otherwise}\end{cases}\end{split}\]モデルの重みの勾配にはどのようにアクセスするか?
学習率とエポック数を変えると、解にどのような影響があるか?改善し続けますか?
生成するデータ量を変えると、解はどのように変わりますか?
データ量の関数として、\(\\hat{\\mathbf{w}} - \\mathbf{w}\) と \(\\hat{b} - b\) の推定誤差をプロットせよ。ヒント: データ量は線形ではなく対数的に増やす。つまり、1000, 2000, …, 10,000 ではなく、5, 10, 20, 50, …, 10,000 とする。
なぜヒントの提案が適切なのですか?