3.3. 合成回帰データ

機械学習とは、本質的にはデータから情報を抽出する営みである。 では、合成データから何を学べるのだろうか。 人工的なデータ生成過程に埋め込んだパターンそのものに本質的な関心がないとしても、 そのようなデータセットは教育目的にきわめて有用であり、 学習アルゴリズムの性質を評価したり、実装が期待どおりに動作していることを確認したりするのに役立つ。 たとえば、正しいパラメータが a priori に既知であるデータを作れば、 モデルがそれらを実際に復元できるかどうかを検証できる。

%matplotlib inline
from d2l import torch as d2l
import torch
import random
%matplotlib inline
from d2l import mxnet as d2l
from mxnet import np, npx, gluon
import random
npx.set_np()
%matplotlib inline
from d2l import jax as d2l
import jax
from jax import numpy as jnp
import numpy as np
import random
import tensorflow as tf
import tensorflow_datasets as tfds
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
import random

3.3.1. データセットの生成

この例では、簡潔さのため低次元の設定を扱う。 次のコード片は、標準正規分布から生成した2次元特徴量をもつ1000個の例を作成する。 得られる設計行列 \(\mathbf{X}\)\(\mathbb{R}^{1000 \times 2}\) に属する。 各ラベルは、真の値 に基づく線形関数を適用し、さらに各例ごとに独立同分布に生成した加法ノイズ \(\boldsymbol{\epsilon}\) を加えることで生成する。

(3.3.1)\[\mathbf{y}= \mathbf{X} \mathbf{w} + b + \boldsymbol{\epsilon}.\]

便宜上、\(\boldsymbol{\epsilon}\) は平均 \(\mu= 0\)、標準偏差 \(\sigma = 0.01\) の正規分布から生成すると仮定する。 オブジェクト指向設計のために、 コードを d2l.DataModule のサブクラスの __init__ メソッドに追加する(3.2.3 章 で導入)。 追加のハイパーパラメータを設定できるようにしておくのはよい実践である。 そのために save_hyperparameters() を用いる。 batch_size は後で決める。

class SyntheticRegressionData(d2l.DataModule):  #@save
    """線形回帰のための合成データ。"""
    def __init__(self, w, b, noise=0.01, num_train=1000, num_val=1000,
                 batch_size=32):
        super().__init__()
        self.save_hyperparameters()
        n = num_train + num_val
        self.X = d2l.randn(n, len(w))
        noise = d2l.randn(n, 1) * noise
        self.y = d2l.matmul(self.X, d2l.reshape(w, (-1, 1))) + b + noise
class SyntheticRegressionData(d2l.DataModule):  #@save
    """線形回帰のための合成データ。"""
    def __init__(self, w, b, noise=0.01, num_train=1000, num_val=1000,
                 batch_size=32):
        super().__init__()
        self.save_hyperparameters()
        n = num_train + num_val
        self.X = d2l.randn(n, len(w))
        noise = d2l.randn(n, 1) * noise
        self.y = d2l.matmul(self.X, d2l.reshape(w, (-1, 1))) + b + noise
class SyntheticRegressionData(d2l.DataModule):  #@save
    """線形回帰のための合成データ。"""
    def __init__(self, w, b, noise=0.01, num_train=1000, num_val=1000,
                 batch_size=32):
        super().__init__()
        self.save_hyperparameters()
        n = num_train + num_val
        key = jax.random.PRNGKey(0)
        key1, key2 = jax.random.split(key)
        self.X = jax.random.normal(key1, (n, w.shape[0]))
        noise = jax.random.normal(key2, (n, 1)) * noise
        self.y = d2l.matmul(self.X, d2l.reshape(w, (-1, 1))) + b + noise
class SyntheticRegressionData(d2l.DataModule):  #@save
    """線形回帰のための合成データ。"""
    def __init__(self, w, b, noise=0.01, num_train=1000, num_val=1000,
                 batch_size=32):
        super().__init__()
        self.save_hyperparameters()
        n = num_train + num_val
        self.X = tf.random.normal((n, w.shape[0]))
        noise = tf.random.normal((n, 1)) * noise
        self.y = d2l.matmul(self.X, d2l.reshape(w, (-1, 1))) + b + noise

以下では、真のパラメータを \(\mathbf{w} = [2, -3.4]^\top\)\(b = 4.2\) に設定する。 後で、推定したパラメータをこれらの 真の値 と比較できる。

data = SyntheticRegressionData(w=d2l.tensor([2, -3.4]), b=4.2)
[07:18:05] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU

features の各行は \(\mathbb{R}^2\) のベクトルであり、labels の各行はスカラーである。 最初の要素を見てみよう。

print('features:', data.X[0],'\nlabel:', data.y[0])
features: tensor([ 0.1966, -0.6143])
label: tensor([6.6860])

3.3.2. データセットの読み出し

機械学習モデルの学習では、しばしばデータセットを何度も走査し、1回に1つのミニバッチずつ取り出す。 そのデータを使ってモデルを更新する。 これがどのように機能するかを示すために、 get_dataloader メソッドを実装し、 add_to_class を介して SyntheticRegressionData クラスに登録する(3.2.1 章 で導入)。 このメソッドは、バッチサイズ、特徴量の行列、ラベルのベクトルを受け取り、サイズが batch_size のミニバッチを生成する。 したがって、各ミニバッチは特徴量とラベルのタプルからなる。 また、訓練モードか検証モードかに注意する必要がある。 前者ではデータをランダムな順序で読み出したい一方で、 後者では、あらかじめ定めた順序でデータを読み出せることがデバッグ上重要になる場合がある。

@d2l.add_to_class(SyntheticRegressionData)
def get_dataloader(self, train):
    if train:
        indices = list(range(0, self.num_train))
        # 例はランダムな順序で読み込まれる
        random.shuffle(indices)
    else:
        indices = list(range(self.num_train, self.num_train+self.num_val))
    for i in range(0, len(indices), self.batch_size):
        batch_indices = d2l.tensor(indices[i: i+self.batch_size])
        yield self.X[batch_indices], self.y[batch_indices]
@d2l.add_to_class(SyntheticRegressionData)
def get_dataloader(self, train):
    if train:
        indices = list(range(0, self.num_train))
        # 例はランダムな順序で読み込まれる
        random.shuffle(indices)
    else:
        indices = list(range(self.num_train, self.num_train+self.num_val))
    for i in range(0, len(indices), self.batch_size):
        batch_indices = d2l.tensor(indices[i: i+self.batch_size])
        yield self.X[batch_indices], self.y[batch_indices]
@d2l.add_to_class(SyntheticRegressionData)
def get_dataloader(self, train):
    if train:
        indices = list(range(0, self.num_train))
        # 例はランダムな順序で読み込まれる
        random.shuffle(indices)
    else:
        indices = list(range(self.num_train, self.num_train+self.num_val))
    for i in range(0, len(indices), self.batch_size):
        batch_indices = d2l.tensor(indices[i: i+self.batch_size])
        yield self.X[batch_indices], self.y[batch_indices]
@d2l.add_to_class(SyntheticRegressionData)
def get_dataloader(self, train):
    if train:
        indices = list(range(0, self.num_train))
        # 例はランダムな順序で読み込まれる
        random.shuffle(indices)
    else:
        indices = list(range(self.num_train, self.num_train+self.num_val))
    for i in range(0, len(indices), self.batch_size):
        j = tf.constant(indices[i : i+self.batch_size])
        yield tf.gather(self.X, j), tf.gather(self.y, j)

直感をつかむために、最初のミニバッチのデータを見てみよう。 特徴量の各ミニバッチは、そのサイズと入力特徴量の次元数の両方を示す。 同様に、ラベルのミニバッチも batch_size によって決まる対応する形状をもつ。

X, y = next(iter(data.train_dataloader()))
print('X shape:', X.shape, '\ny shape:', y.shape)
X shape: torch.Size([32, 2])
y shape: torch.Size([32, 1])

一見すると何でもないようだが、iter(data.train_dataloader()) の呼び出しは、 Python のオブジェクト指向設計の力を示している。 data オブジェクトを作成した後で、 SyntheticRegressionData クラスにメソッドを追加したことに注意されたい。 それにもかかわらず、このオブジェクトはクラスへの機能追加を 事後的に 受けられる。

反復の間、データセット全体を使い切るまで異なるミニバッチが得られる(試されたい)。 上で実装した反復処理は教育目的には適しているものの、 実際の問題では非効率である。 たとえば、すべてのデータをメモリに読み込む必要があり、 大量のランダムメモリアクセスも発生する。 深層学習フレームワークに実装されている組み込みイテレータははるかに効率的であり、 ファイルに保存されたデータ、 ストリーム経由で受け取るデータ、 その場で生成または処理されるデータなどにも対応できる。 次に、組み込みイテレータを使って同じメソッドを実装してみよう。

3.3.3. データローダの簡潔な実装

自前でイテレータを書く代わりに、 フレームワークの既存APIを呼び出してデータを読み込める。 これまでと同様に、特徴量 X とラベル y をもつデータセットが必要である。 それに加えて、組み込みのデータローダで batch_size を設定し、 例のシャッフルはフレームワークに任せることで効率化する。

@d2l.add_to_class(d2l.DataModule)  #@save
def get_tensorloader(self, tensors, train, indices=slice(0, None)):
    tensors = tuple(a[indices] for a in tensors)
    dataset = torch.utils.data.TensorDataset(*tensors)
    return torch.utils.data.DataLoader(dataset, self.batch_size,
                                       shuffle=train)
@d2l.add_to_class(d2l.DataModule)  #@save
def get_tensorloader(self, tensors, train, indices=slice(0, None)):
    tensors = tuple(a[indices] for a in tensors)
    dataset = gluon.data.ArrayDataset(*tensors)
    return gluon.data.DataLoader(dataset, self.batch_size,
                                 shuffle=train)
@d2l.add_to_class(d2l.DataModule)  #@save
def get_tensorloader(self, tensors, train, indices=slice(0, None)):
    tensors = tuple(a[indices] for a in tensors)
    # Tensorflow DatasetsとDataloaderを使用する。JAXやFlaxは提供しない
    # データ読み込み機能はあるか
    shuffle_buffer = tensors[0].shape[0] if train else 1
    return tfds.as_numpy(
        tf.data.Dataset.from_tensor_slices(tensors).shuffle(
            buffer_size=shuffle_buffer).batch(self.batch_size))
@d2l.add_to_class(d2l.DataModule)  #@save
def get_tensorloader(self, tensors, train, indices=slice(0, None)):
    tensors = tuple(a[indices] for a in tensors)
    shuffle_buffer = tensors[0].shape[0] if train else 1
    return tf.data.Dataset.from_tensor_slices(tensors).shuffle(
        buffer_size=shuffle_buffer).batch(self.batch_size)
@d2l.add_to_class(SyntheticRegressionData)  #@save
def get_dataloader(self, train):
    i = slice(0, self.num_train) if train else slice(self.num_train, None)
    return self.get_tensorloader((self.X, self.y), train, i)

新しいデータローダは、より効率的でいくつかの追加機能を備えている点を除けば、前のものとまったく同じように動作する。

X, y = next(iter(data.train_dataloader()))
print('X shape:', X.shape, '\ny shape:', y.shape)
X shape: torch.Size([32, 2])
y shape: torch.Size([32, 1])

たとえば、フレームワークAPIが提供するデータローダは 組み込みの __len__ メソッドをサポートしているので、 その長さ、すなわちバッチ数を問い合わせられる。

len(data.train_dataloader())
32

3.3.4. まとめ

データローダは、データの読み込みと操作の過程を抽象化する便利な方法である。 これにより、同じ機械学習アルゴリズムが、 変更を加えることなく多様な種類や供給源のデータを処理できるようになる。 データローダの利点の1つは、組み合わせ可能であることだ。 たとえば、画像を読み込んだあとに、 それらを切り抜いたり他の方法で変更したりする後処理フィルタを適用できる。 このように、データローダは データ処理パイプライン全体を記述するために使える。

モデル自体について言えば、2次元線形モデルは ここで扱う中でも最も単純なものの1つである。 これにより、データ不足や 不定な連立方程式を心配することなく、回帰モデルの精度を試せる。 次の節でこれを活用する。

3.3.5. 演習

  1. 例の数がバッチサイズで割り切れない場合はどうなるか。フレームワークのAPIで別の引数を指定することで、この挙動をどのように変更できるか。

  2. パラメータベクトル w のサイズも例の数 num_examples も大きい、巨大なデータセットを生成したいとする。

    1. すべてのデータをメモリに保持できない場合、何が起こるか。

    2. データがディスク上にある場合、どのようにシャッフルするか。あまり多くのランダム読み書きを必要としない、効率的 なアルゴリズムを設計するのが課題である。ヒント: 擬似ランダム置換生成器 を使うと、置換表を明示的に保存せずに再シャッフルを設計できる (Naor and Reingold, 1999)

  3. イテレータが呼ばれるたびに、その場で新しいデータを生成するデータ生成器を実装せよ。

  4. 呼ばれるたびに 同じ データを生成する乱数データ生成器をどのように設計するか。