.. _sec_linear_concise:
線形回帰の簡潔な実装
====================
深層学習は、この10年で一種のカンブリア爆発を経験してきた。
技術、応用、アルゴリズムの数は、過去数十年の進歩をはるかに上回っている。
これは複数の要因が幸運にも組み合わさった結果であり、
その一つが、いくつかのオープンソース深層学習フレームワークが提供する強力で無料のツールである。
Theano :cite:`Bergstra.Breuleux.Bastien.ea.2010`, DistBelief
:cite:`Dean.Corrado.Monga.ea.2012`, および Caffe
:cite:`Jia.Shelhamer.Donahue.ea.2014`
は、広く採用されたそのようなモデルの
第一世代を代表していると言えるだろう。
Lisp風のプログラミング体験を提供した SN2 (Simulateur Neuristique)
:cite:`Bottou.Le-Cun.1988`
のような初期の(先駆的な)研究とは対照的に、
現代のフレームワークは自動微分と Python の利便性を提供する。
これらのフレームワークにより、勾配ベース学習アルゴリズムの実装における
反復的な作業を自動化し、モジュール化できる。
:numref:`sec_linear_scratch` では、 (i)
データ保存と線形代数のためのテンソル、 および (ii)
勾配計算のための自動微分 だけに依拠した。
実際には、データイテレータ、損失関数、最適化器、
ニューラルネットワーク層は
非常に一般的であるため、現代のライブラリはこれらの構成要素も
私たちの代わりに実装してくれる。 この節では、深層学習フレームワークの
高レベル API を使って :numref:`sec_linear_scratch` の線形回帰モデルを
簡潔に実装する方法を示す。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
from d2l import torch as d2l
import numpy as np
import torch
from torch import nn
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
from d2l import mxnet as d2l
from mxnet import autograd, gluon, init, np, npx
from mxnet.gluon import nn
npx.set_np()
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
from d2l import jax as d2l
from flax import linen as nn
import jax
from jax import numpy as jnp
import optax
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
from d2l import tensorflow as d2l
import numpy as np
import tensorflow as tf
.. raw:: html
.. raw:: html
モデルの定義
------------
:numref:`sec_linear_scratch` で
線形回帰をスクラッチから実装したときには、
モデルパラメータを明示的に定義し、
基本的な線形代数演算を使って出力を生成する計算を コード化した。
これは\ *知っておくべき*\ ことである。 しかし、モデルがより複雑になり、
しかもそれをほぼ毎日行わなければならなくなると、
助けがあることのありがたさが分かるだろう。
状況は、ブログをスクラッチから自作するのに似ている。
一度や二度ならやりがいがあり、学びもあるが、
車輪の再発明に1か月費やすようでは、 優秀な Web 開発者とは言えない。
標準的な演算については、
フレームワークにあらかじめ定義された層を使うことができ、
実装を気にするよりも、 モデルを構成する層そのものに集中できる。
:numref:`fig_single_neuron` で説明した
単層ネットワークのアーキテクチャを思い出そう。 この層は *全結合*
と呼ばれる。 なぜなら、その入力の各要素が 行列—ベクトル積によって
各出力に接続されているからである。
PyTorch では、全結合層は ``Linear`` クラスと ``LazyLinear``
クラス(バージョン 1.8.0 以降で利用可能)で定義される。
後者は、ユーザーが\ *単に* 出力次元だけを指定できるようにするが、
前者ではそれに加えて、 この層に何個の入力が入るかも指定する必要がある。
入力形状の指定は不便であり、(畳み込み層のように)
非自明な計算を要することがある。 そのため、簡潔さのために、可能な限り
このような「遅延」層を使う。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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)
.. raw:: html
.. raw:: html
``forward`` メソッドでは、あらかじめ定義された層の組み込み ``__call__``
メソッドを呼び出して出力を計算するだけである。
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
@d2l.add_to_class(LinearRegression) #@save
def forward(self, X):
return self.net(X)
損失関数の定義
--------------
``MSELoss`` クラスは平均二乗誤差を計算する(:eq:`eq_mse` の
:math:`1/2` 因子は含まない)。 デフォルトでは、\ ``MSELoss``
はサンプル全体の平均損失を返す。
自前で実装するよりも高速で(しかも使いやすいです)。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
@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)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
@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)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
@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))
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
@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)
.. raw:: html
.. raw:: html
最適化アルゴリズムの定義
------------------------
ミニバッチ SGD
はニューラルネットワークを最適化するための標準的な手法であり、 そのため
PyTorch は ``optim`` モジュールで
このアルゴリズムのいくつかの変種をサポートしている。 ``SGD``
インスタンスを生成するときには、 最適化対象のパラメータ(モデルの
``self.parameters()`` から取得可能)と、
最適化アルゴリズムに必要な学習率(\ ``self.lr``\ )を指定する。
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
@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)
学習
----
深層学習フレームワークの高レベル API を通してモデルを表現すると、
必要なコード行数が少なくなることに気づいたかもしれない。
パラメータを個別に割り当てたり、 損失関数を定義したり、 ミニバッチ SGD
を実装したりする必要はなかった。 より複雑なモデルを扱い始めると、
高レベル API の利点はさらに大きくなる。
基本要素がすべて揃ったので、
学習ループ自体はスクラッチ実装したものと同じである。
したがって、\ ``fit`` メソッド(:numref:`oo-design-training`
で導入)を呼び出すだけで、 :numref:`sec_linear_scratch` の
``fit_epoch`` メソッドの実装に依存して、 モデルを学習できる。
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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)
.. figure:: output_linear-regression-concise_13e119_50_0.svg
以下では、 有限データで学習して得られたモデルパラメータと
実際のパラメータを比較する。 パラメータにアクセスするには、
必要な層の重みとバイアスにアクセスする。 スクラッチ実装の場合と同様に、
推定されたパラメータが真の値に近いことに注意されたい。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
@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()
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
@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()
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
@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)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
@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()
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
print(f'error in estimating w: {data.w - d2l.reshape(w, data.w.shape)}')
print(f'error in estimating b: {data.b - b}')
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
error in estimating w: tensor([ 0.0080, -0.0134])
error in estimating b: tensor([0.0148])
まとめ
------
この節では、本書で初めて、 MXNet :cite:`Chen.Li.Li.ea.2015`\ 、 JAX
:cite:`Frostig.Johnson.Leary.2018`\ 、 PyTorch
:cite:`Paszke.Gross.Massa.ea.2019`\ 、 および Tensorflow
:cite:`Abadi.Barham.Chen.ea.2016`
のような現代の深層学習フレームワークがもたらす利便性を活用した
深層ネットワークの実装を行った。 データの読み込み、層の定義、
損失関数、最適化器、学習ループには フレームワークのデフォルトを使った。
フレームワークが必要な機能をすべて提供しているなら、
通常はそれらを使うのがよいだろう。 これらの構成要素のライブラリ実装は
性能のために大きく最適化されており、
信頼性のために適切にテストされている傾向があるからである。
同時に、これらのモジュールは\ *直接実装できる*\ ことも
忘れないようにしたい。 これは特に、モデル開発の最前線で生きたいと願う
意欲的な研究者にとって重要である。
そこでは、現在のどのライブラリにも存在しえない
新しい構成要素を発明することになるからである。
PyTorch では、\ ``data`` モジュールがデータ処理のためのツールを提供し、
``nn``
モジュールが多数のニューラルネットワーク層と一般的な損失関数を定義する。
末尾が ``_`` のメソッドで値を置き換えることで、
パラメータを初期化できる。
ネットワークの入力次元を指定する必要があることに注意されたい。
今のところは単純であるが、多数の層を持つ複雑なネットワークを設計したいときには、
大きな波及効果を持つ可能性がある。
これらのネットワークをどのようにパラメータ化するかを慎重に考える必要があり、
それによって移植性を確保できる。
演習
----
1. ミニバッチ上の損失の総和を使う代わりに、ミニバッチ上の損失の平均を使うようにした場合、学習率はどのように変更する必要があるか?
2. フレームワークのドキュメントを確認して、どの損失関数が提供されているかを見よ。特に、二乗損失を
Huber のロバスト損失関数に置き換えよ。すなわち、次の損失関数を使う。
.. math:: 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}
3. モデルの重みの勾配にはどのようにアクセスするか?
4. 学習率とエポック数を変えると、解にどのような影響があるか?改善し続けますか?
5. 生成するデータ量を変えると、解はどのように変わりますか?
1. データ量の関数として、\ :math:`\\hat{\\mathbf{w}} - \\mathbf{w}`
と :math:`\\hat{b} - b` の推定誤差をプロットせよ。ヒント:
データ量は線形ではなく対数的に増やす。つまり、1000, 2000, …,
10,000 ではなく、5, 10, 20, 50, …, 10,000 とする。
2. なぜヒントの提案が適切なのですか?