3.3. 合成回归数据
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

机器学习的核心是从数据中提取信息。因此,你可能会想,我们能从合成数据中学到什么呢?虽然我们可能本质上并不关心我们自己构建到人工数据生成模型中的模式,但这类数据集在教学上仍然很有用,可以帮助我们评估学习算法的属性,并确认我们的实现是否按预期工作。例如,如果我们创建了预先知道正确参数的数据,我们就可以检查我们的模型是否确实能够恢复这些参数。

%matplotlib inline
import random
import torch
from d2l import torch as d2l
%matplotlib inline
import random
from mxnet import gluon, np, npx
from d2l import mxnet as d2l

npx.set_np()
%matplotlib inline
import random
import jax
import numpy as np
import tensorflow as tf
import tensorflow_datasets as tfds
from jax import numpy as jnp
from d2l import jax as d2l
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
%matplotlib inline
import random
import tensorflow as tf
from d2l import tensorflow as d2l

3.3.1. 生成数据集

为简洁起见,本例中我们将在低维度下工作。以下代码片段生成了1000个样本,每个样本有2个从标准正态分布中抽取的特征。得到的设计矩阵 \(\mathbf{X}\) 属于 \(\mathbb{R}^{1000 \times 2}\)。我们通过应用一个*真实*线性函数来生成每个标签,并通过加性噪声 \(\boldsymbol{\epsilon}\) 对其进行扰动,噪声 \(\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
    """Synthetic data for linear regression."""
    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 = torch.randn(n, len(w))
        noise = torch.randn(n, 1) * noise
        self.y = torch.matmul(self.X, w.reshape((-1, 1))) + b + noise
class SyntheticRegressionData(d2l.DataModule):  #@save
    """Synthetic data for linear regression."""
    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 = np.random.randn(n, len(w))
        noise = np.random.randn(n, 1) * noise
        self.y = np.dot(self.X, w.reshape((-1, 1))) + b + noise
class SyntheticRegressionData(d2l.DataModule):  #@save
    """Synthetic data for linear regression."""
    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 = jnp.matmul(self.X, w.reshape((-1, 1))) + b + noise
class SyntheticRegressionData(d2l.DataModule):  #@save
    """Synthetic data for linear regression."""
    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 = tf.matmul(self.X, tf.reshape(w, (-1, 1))) + b + noise

下面,我们将真实参数设置为 \(\mathbf{w} = [2, -3.4]^\top\)\(b = 4.2\)。稍后,我们可以用这些*真实*值来检验我们估计的参数。

data = SyntheticRegressionData(w=torch.tensor([2, -3.4]), b=4.2)
data = SyntheticRegressionData(w=np.array([2, -3.4]), b=4.2)
[22:03:54] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
data = SyntheticRegressionData(w=jnp.array([2, -3.4]), b=4.2)
data = SyntheticRegressionData(w=tf.constant([2, -3.4]), b=4.2)

features 中的每一行都包含一个 \(\mathbb{R}^2\) 向量,而 labels 中的每一行都是一个标量。让我们看一下第一个条目。

print('features:', data.X[0],'\nlabel:', data.y[0])
features: tensor([0.9026, 1.0264])
label: tensor([2.5148])
print('features:', data.X[0],'\nlabel:', data.y[0])
features: [2.2122064 1.1630787]
label: [4.684836]
print('features:', data.X[0],'\nlabel:', data.y[0])
features: [-0.86997527 -3.2320356 ]
label: [13.438176]
print('features:', data.X[0],'\nlabel:', data.y[0])
features: tf.Tensor([ 1.0864811  -0.40848374], shape=(2,), dtype=float32)
label: tf.Tensor([7.7656293], shape=(1,), dtype=float32)

3.3.2. 读取数据集

训练机器学习模型通常需要对数据集进行多次遍历,每次抓取一个小批量(minibatch)的样本。然后用这些数据来更新模型。为了说明这是如何工作的,我们实现了 get_dataloader 方法,通过 add_to_class(在 第 3.2.1 节 中介绍)将其注册到 SyntheticRegressionData 类中。它接受批量大小、特征矩阵和标签向量,并生成大小为 batch_size 的小批量。因此,每个小批量都由一个特征和标签的元组组成。请注意,我们需要留意我们是处于训练模式还是验证模式:在前者中,我们希望以随机顺序读取数据,而对于后者,为了调试目的,能够以预定义的顺序读取数据可能很重要。

@d2l.add_to_class(SyntheticRegressionData)
def get_dataloader(self, train):
    if train:
        indices = list(range(0, self.num_train))
        # The examples are read in random order
        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 = torch.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))
        # The examples are read in random order
        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 = np.array(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))
        # The examples are read in random order
        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 = jnp.array(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))
        # The examples are read in random order
        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])
X, y = next(iter(data.train_dataloader()))
print('X shape:', X.shape, '\ny shape:', y.shape)
X shape: (32, 2)
y shape: (32, 1)
X, y = next(iter(data.train_dataloader()))
print('X shape:', X.shape, '\ny shape:', y.shape)
X shape: (32, 2)
y shape: (32, 1)
X, y = next(iter(data.train_dataloader()))
print('X shape:', X.shape, '\ny shape:', y.shape)
X shape: (32, 2)
y shape: (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(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)
@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(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)

JAX 的核心是类 NumPy 的 API、设备加速和函数式转换,因此至少当前版本不包含数据加载方法。对于其他库,我们已经有了很好的数据加载器,JAX 建议使用它们。这里我们将使用 TensorFlow 的数据加载器,并稍作修改以使其与 JAX 兼容。

@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)
    # Use Tensorflow Datasets & Dataloader. JAX or Flax do not provide
    # any dataloading functionality
    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(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)
@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])
X, y = next(iter(data.train_dataloader()))
print('X shape:', X.shape, '\ny shape:', y.shape)
X shape: (32, 2)
y shape: (32, 1)
X, y = next(iter(data.train_dataloader()))
print('X shape:', X.shape, '\ny shape:', y.shape)
X shape: (32, 2)
y shape: (32, 1)
X, y = next(iter(data.train_dataloader()))
print('X shape:', X.shape, '\ny shape:', y.shape)
X shape: (32, 2)
y shape: (32, 1)

例如,框架 API 提供的数据加载器支持内置的 __len__ 方法,因此我们可以查询它的长度,即批次的数量。

len(data.train_dataloader())
32
len(data.train_dataloader())
32
len(data.train_dataloader())
32
len(data.train_dataloader())
32

3.3.4. 小结

数据加载器是将数据加载和操作过程抽象出来的一种便捷方式。这样,同一个机器学习*算法*就能够处理多种不同类型和来源的数据,而无需修改。数据加载器的一个好处是它们可以组合。例如,我们可能正在加载图像,然后有一个后处理过滤器来裁剪或以其他方式修改它们。因此,数据加载器可以用来描述一个完整的数据处理流程。

至于模型本身,二维线性模型大约是我们能遇到的最简单的模型。它让我们能够在不担心数据量不足或方程组欠定的情况下,测试回归模型的准确性。我们将在下一节中充分利用这一点。

3.3.5. 练习

  1. 如果样本数量不能被批量大小整除会发生什么?你会如何通过使用框架的 API 指定一个不同的参数来改变这种行为?

  2. 假设我们要生成一个巨大的数据集,其中参数向量 w 的大小和样本数量 num_examples都很大。

    1. 如果我们无法将所有数据都保存在内存中会发生什么?

    2. 如果数据保存在磁盘上,你会如何打乱数据?你的任务是设计一个不需要太多随机读写操作的*高效*算法。提示:伪随机排列生成器可以让你设计一个重排方案,而无需显式存储排列表 (Naor and Reingold, 1999)

  3. 实现一个数据生成器,在每次调用迭代器时动态生成新数据。

  4. 你会如何设计一个每次调用时都生成*相同*数据的随机数据生成器?