PyTorch教程-4.4. 从头开始实现 Softmax 回归

电子说

1.2w人已加入

描述

因为 softmax 回归是如此基础,我们相信您应该知道如何自己实现它。在这里,我们限制自己定义模型的 softmax 特定方面,并重用线性回归部分的其他组件,包括训练循环。

 

import torch
from d2l import torch as d2l

 

 

from mxnet import autograd, gluon, np, npx
from d2l import mxnet as d2l

npx.set_np()

 

 

from functools import partial
import jax
from flax import linen as nn
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.)

 

 

import tensorflow as tf
from d2l import tensorflow as d2l

 

4.4.1. Softmax

让我们从最重要的部分开始:从标量到概率的映射。作为复习,请回忆一下在张量中沿特定维度的求和运算符,如第 2.3.6 节和 第 2.3.7 节中所讨论的。给定一个矩阵,X我们可以对所有元素(默认情况下)或仅对同一轴上的元素求和。该axis变量让我们计算行和列的总和:

 

X = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
X.sum(0, keepdims=True), X.sum(1, keepdims=True)

 

 

(tensor([[5., 7., 9.]]),
 tensor([[ 6.],
     [15.]]))

 

 

X = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
X.sum(0, keepdims=True), X.sum(1, keepdims=True)

 

 

(array([[5., 7., 9.]]),
 array([[ 6.],
    [15.]]))

 

 

X = jnp.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
X.sum(0, keepdims=True), X.sum(1, keepdims=True)

 

 

(Array([[5., 7., 9.]], dtype=float32),
 Array([[ 6.],
    [15.]], dtype=float32))

 

 

X = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
tf.reduce_sum(X, 0, keepdims=True), tf.reduce_sum(X, 1, keepdims=True)

 

 

(,
 )

 

计算 softmax 需要三个步骤:(i)每一项取幂;(ii) 对每一行求和以计算每个示例的归一化常数;(iii) 将每一行除以其归一化常数,确保结果之和为 1。

(4.4.1)softmax(X)ij=exp⁡(Xij)∑kexp⁡(Xik).

分母的(对数)称为(对数)配分函数。它是在统计物理学中引入的 ,用于对热力学系综中的所有可能状态求和。实现很简单:

 

def softmax(X):
  X_exp = torch.exp(X)
  partition = X_exp.sum(1, keepdims=True)
  return X_exp / partition # The broadcasting mechanism is applied here

 

 

def softmax(X):
  X_exp = np.exp(X)
  partition = X_exp.sum(1, keepdims=True)
  return X_exp / partition # The broadcasting mechanism is applied here

 

 

def softmax(X):
  X_exp = jnp.exp(X)
  partition = X_exp.sum(1, keepdims=True)
  return X_exp / partition # The broadcasting mechanism is applied here

 

 

def softmax(X):
  X_exp = tf.exp(X)
  partition = tf.reduce_sum(X_exp, 1, keepdims=True)
  return X_exp / partition # The broadcasting mechanism is applied here

 

对于任何输入X,我们将每个元素变成一个非负数。每行总和为 1,这是概率所要求的。注意:上面的代码对于非常大或非常小的参数并不稳健。虽然这足以说明正在发生的事情,但您不应 将此代码逐字用于任何严肃的目的。深度学习框架内置了这样的保护,我们将在未来使用内置的 softmax。

 

X = torch.rand((2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)

 

 

(tensor([[0.1560, 0.2128, 0.2260, 0.2372, 0.1680],
     [0.1504, 0.2473, 0.1132, 0.2779, 0.2112]]),
 tensor([1.0000, 1.0000]))

 

 

X = np.random.rand(2, 5)
X_prob = softmax(X)
X_prob, X_prob.sum(1)

 

 

(array([[0.17777154, 0.1857739 , 0.20995119, 0.23887765, 0.18762572],
    [0.24042214, 0.1757977 , 0.23786479, 0.15572716, 0.19018826]]),
 array([1., 1.]))

 

 

X = jax.random.uniform(jax.random.PRNGKey(d2l.get_seed()), (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)

 

 

(Array([[0.17380024, 0.13607854, 0.29826194, 0.18967763, 0.20218161],
    [0.24212085, 0.19360834, 0.21299706, 0.17635451, 0.17491929]],   dtype=float32),
 Array([1., 1.], dtype=float32))

 

 

X = tf.random.uniform((2, 5))
X_prob = softmax(X)
X_prob, tf.reduce_sum(X_prob, 1)

 

 

(,
 )

 

4.4.2. 该模型

我们现在拥有了实现 softmax 回归模型所需的一切。与我们的线性回归示例一样,每个实例都将由一个固定长度的向量表示。由于这里的原始数据包括28×28像素图像,我们将每个图像展平,将它们视为长度为 784 的向量。在后面的章节中,我们将介绍卷积神经网络,它以更令人满意的方式利用空间结构。

在 softmax 回归中,我们网络的输出数量应该等于类的数量。由于我们的数据集有 10 个类,我们的网络的输出维度为 10。因此,我们的权重构成784×10矩阵加一个1×10 偏差的维行向量。与线性回归一样,我们W使用高斯噪声初始化权重。偏差被初始化为零。

 

class SoftmaxRegressionScratch(d2l.Classifier):
  def __init__(self, num_inputs, num_outputs, lr, sigma=0.01):
    super().__init__()
    self.save_hyperparameters()
    self.W = torch.normal(0, sigma, size=(num_inputs, num_outputs),
               requires_grad=True)
    self.b = torch.zeros(num_outputs, requires_grad=True)

  def parameters(self):
    return [self.W, self.b]

 

 

class SoftmaxRegressionScratch(d2l.Classifier):
  def __init__(self, num_inputs, num_outputs, lr, sigma=0.01):
    super().__init__()
    self.save_hyperparameters()
    self.W = np.random.normal(0, sigma, (num_inputs, num_outputs))
    self.b = np.zeros(num_outputs)
    self.W.attach_grad()
    self.b.attach_grad()

  def collect_params(self):
    return [self.W, self.b]

 

 

class SoftmaxRegressionScratch(d2l.Classifier):
  num_inputs: int
  num_outputs: int
  lr: float
  sigma: float = 0.01

  def setup(self):
    self.W = self.param('W', nn.initializers.normal(self.sigma),
              (self.num_inputs, self.num_outputs))
    self.b = self.param('b', nn.initializers.zeros, self.num_outputs)

 

 

class SoftmaxRegressionScratch(d2l.Classifier):
  def __init__(self, num_inputs, num_outputs, lr, sigma=0.01):
    super().__init__()
    self.save_hyperparameters()
    self.W = tf.random.normal((num_inputs, num_outputs), 0, sigma)
    self.b = tf.zeros(num_outputs)
    self.W = tf.Variable(self.W)
    self.b = tf.Variable(self.b)

 

下面的代码定义了网络如何将每个输入映射到输出。请注意,我们将每个28×28reshape在将数据传递给我们的模型之前,将批处理中的像素图像转换为向量。

 

@d2l.add_to_class(SoftmaxRegressionScratch)
def forward(self, X):
  X = X.reshape((-1, self.W.shape[0]))
  return softmax(torch.matmul(X, self.W) + self.b)

 

 

@d2l.add_to_class(SoftmaxRegressionScratch)
def forward(self, X):
  X = X.reshape((-1, self.W.shape[0]))
  return softmax(np.dot(X, self.W) + self.b)

 

 

@d2l.add_to_class(SoftmaxRegressionScratch)
def forward(self, X):
  X = X.reshape((-1, self.W.shape[0]))
  return softmax(jnp.matmul(X, self.W) + self.b)

 

 

@d2l.add_to_class(SoftmaxRegressionScratch)
def forward(self, X):
  X = tf.reshape(X, (-1, self.W.shape[0]))
  return softmax(tf.matmul(X, self.W) + self.b)

 

4.4.3. 交叉熵损失

接下来我们需要实现交叉熵损失函数( 4.1.2节介绍 )。这可能是所有深度学习中最常见的损失函数。目前,深度学习的应用很容易抛出分类问题,远远超过那些更好地视为回归问题的问题。

回想一下,交叉熵采用分配给真实标签的预测概率的负对数似然。为了提高效率,我们避免使用 Python for 循环并改用索引。特别是,one-hot 编码y允许我们选择匹配项y^.

为了在行动中看到这一点,我们创建了样本数据,y_hat其中包含 3 个类及其对应标签的 2 个预测概率示例 y。正确的标签是0和2分别(即一等和三等)。使用y中概率的指标y_hat,我们可以有效地挑选出术语。

 

y = torch.tensor([0, 2])
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y_hat[[0, 1], y]

 

 

tensor([0.1000, 0.5000])

 

现在我们可以通过对所选概率的对数进行平均来实现交叉熵损失函数。

 

def cross_entropy(y_hat, y):
  return -torch.log(y_hat[list(range(len(y_hat))), y]).mean()

cross_entropy(y_hat, y)

 

 

tensor(1.4979)

 

 

@d2l.add_to_class(SoftmaxRegressionScratch)
def loss(self, y_hat, y):
  return cross_entropy(y_hat, y)

 

 

y = np.array([0, 2])
y_hat = np.array([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y_hat[[0, 1], y]

 

 

array([0.1, 0.5])

 

Now we can implement the cross-entropy loss function by averaging over the logarithms of the selected probabilities.

 

def cross_entropy(y_hat, y):
  return -np.log(y_hat[list(range(len(y_hat))), y]).mean()

cross_entropy(y_hat, y)

 

 

array(1.4978662)

 

 

@d2l.add_to_class(SoftmaxRegressionScratch)
def loss(self, y_hat, y):
  return cross_entropy(y_hat, y)

 

 

y = jnp.array([0, 2])
y_hat = jnp.array([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y_hat[[0, 1], y]

 

 

Array([0.1, 0.5], dtype=float32)

 

Now we can implement the cross-entropy loss function by averaging over the logarithms of the selected probabilities.

Note that to make use of jax.jit to speed up JAX implementations, and to make sure loss is a pure function, the cross_entropy function is re-defined inside the loss to avoid usage of any global variables or functions which may render the loss function impure. We refer interested readers to the JAX documentation on jax.jit and pure functions.

 

def cross_entropy(y_hat, y):
  return -jnp.log(y_hat[list(range(len(y_hat))), y]).mean()

cross_entropy(y_hat, y)

 

 

Array(1.4978662, dtype=float32)

 

 

@d2l.add_to_class(SoftmaxRegressionScratch)
@partial(jax.jit, static_argnums=(0))
def loss(self, params, X, y, state):
  def cross_entropy(y_hat, y):
    return -jnp.log(y_hat[list(range(len(y_hat))), y]).mean()
  y_hat = state.apply_fn({'params': params}, *X)
  # The returned empty dictionary is a placeholder for auxiliary data,
  # which will be used later (e.g., for batch norm)
  return cross_entropy(y_hat, y), {}

 

 

y_hat = tf.constant([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = tf.constant([0, 2])
tf.boolean_mask(y_hat, tf.one_hot(y, depth=y_hat.shape[-1]))

 

 


 

Now we can implement the cross-entropy loss function by averaging over the logarithms of the selected probabilities.

 

def cross_entropy(y_hat, y):
  return -tf.reduce_mean(tf.math.log(tf.boolean_mask(
    y_hat, tf.one_hot(y, depth=y_hat.shape[-1]))))

cross_entropy(y_hat, y)

 

 


 

 

@d2l.add_to_class(SoftmaxRegressionScratch)
def loss(self, y_hat, y):
  return cross_entropy(y_hat, y)

 

4.4.4. 训练

我们重用第 3.4 节fit中定义的方法来训练具有 10 个 epoch 的模型。请注意,时期数 ( )、小批量大小 ( ) 和学习率 ( ) 都是可调整的超参数。这意味着虽然这些值不是在我们的主要训练循环中学习到的,但它们仍然会影响我们模型的性能、bot vis-a-vis 训练和泛化性能。在实践中,您将希望根据数据的验证拆分来选择这些值,然后最终在测试拆分上评估您的最终模型。如第 3.6.3 节所述max_epochsbatch_sizelr,我们将 Fashion-MNIST 的测试数据视为验证集,从而报告此拆分的验证损失和验证准确性。

 

data = d2l.FashionMNIST(batch_size=256)
model = SoftmaxRegressionScratch(num_inputs=784, num_outputs=10, lr=0.1)
trainer = d2l.Trainer(max_epochs=10)
trainer.fit(model, data)

 

S

 

data = d2l.FashionMNIST(batch_size=256)
model = SoftmaxRegressionScratch(num_inputs=784, num_outputs=10, lr=0.1)
trainer = d2l.Trainer(max_epochs=10)
trainer.fit(model, data)

 

S

 

data = d2l.FashionMNIST(batch_size=256)
model = SoftmaxRegressionScratch(num_inputs=784, num_outputs=10, lr=0.1)
trainer = d2l.Trainer(max_epochs=10)
trainer.fit(model, data)

 

S

 

data = d2l.FashionMNIST(batch_size=256)
model = SoftmaxRegressionScratch(num_inputs=784, num_outputs=10, lr=0.1)
trainer = d2l.Trainer(max_epochs=10)
trainer.fit(model, data)

 

S

4.4.5. 预言

现在训练已经完成,我们的模型已准备好对一些图像进行分类。

 

X, y = next(iter(data.val_dataloader()))
preds = model(X).argmax(axis=1)
preds.shape

 

 

torch.Size([256])

 

 

X, y = next(iter(data.val_dataloader()))
preds = model(X).argmax(axis=1)
preds.shape

 

 

(256,)

 

 

X, y = next(iter(data.val_dataloader()))
preds = model.apply({'params': trainer.state.params}, X).argmax(axis=1)
preds.shape

 

 

(256,)

 

 

X, y = next(iter(data.val_dataloader()))
preds = tf.argmax(model(X), axis=1)
preds.shape

 

 

TensorShape([256])

 

我们更感兴趣的是我们标记错误的图像。我们通过将它们的实际标签(文本输出的第一行)与模型的预测(文本输出的第二行)进行比较来可视化它们。

 

wrong = preds.type(y.dtype) != y
X, y, preds = X[wrong], y[wrong], preds[wrong]
labels = [a+'n'+b for a, b in zip(
  data.text_labels(y), data.text_labels(preds))]
data.visualize([X, y], labels=labels)

 

S

 

wrong = preds.astype(y.dtype) != y
X, y, preds = X[wrong], y[wrong], preds[wrong]
labels = [a+'n'+b for a, b in zip(
  data.text_labels(y), data.text_labels(preds))]
data.visualize([X, y], labels=labels)

 

S

 

wrong = preds.astype(y.dtype) != y
X, y, preds = X[wrong], y[wrong], preds[wrong]
labels = [a+'n'+b for a, b in zip(
  data.text_labels(y), data.text_labels(preds))]
data.visualize([X, y], labels=labels)

 

S

 

wrong = tf.cast(preds, y.dtype) != y
X, y, preds = X[wrong], y[wrong], preds[wrong]
labels = [a+'n'+b for a, b in zip(
  data.text_labels(y), data.text_labels(preds))]
data.visualize([X, y], labels=labels)

 

S

4.4.6. 概括

到目前为止,我们开始获得解决线性回归和分类问题的一些经验。有了它,我们已经达到了可以说是 1960-1970 年代统计建模的最先进水平。在下一节中,我们将向您展示如何利用深度学习框架更有效地实施该模型。

4.4.7. 练习

在本节中,我们根据 softmax 运算的数学定义直接实现了 softmax 函数。如第 4.1 节所述,这会导致数值不稳定。

测试如果softmax输入的值为100?

测试如果softmax所有输入中最大的小于−100?

通过查看相对于参数中最大条目的值来实施修复。

实现一个cross_entropy遵循交叉熵损失函数定义的函数∑iyilog⁡y^i.

在上面的代码示例中尝试一下。

为什么你认为它运行得更慢?

你应该使用它吗?在哪些情况下有意义?

你需要注意什么?提示:考虑对数的定义域。

返回最有可能的标签总是一个好主意吗?例如,你会为了医学诊断而这样做吗?你会如何尝试解决这个问题?

假设我们要使用 softmax 回归来根据某些特征预测下一个单词。大词汇量可能会带来哪些问题?

试验上面代码的超参数。尤其:

绘制验证损失如何随着学习率的变化而变化。

当您更改小批量大小时,验证和训练损失是否会发生变化?在看到效果之前,您需要多大或多小?

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 相关推荐
  • p
  • S

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分