动手学深度学习-李沐 笔记

前言

本文大部分是对于李沐<动手学深度学习>一文的精华内容摘要

1. 引言

1.1. 日程生活中的机器学习

那么到底什么是参数呢? 参数可以被看作旋钮,旋钮的转动可以调整程序的行为。 任一调整参数后的程序被称为模型(model)。 通过操作参数而生成的所有不同程序(输入-输出映射)的集合称为“模型族”。 使用数据集来选择参数的元程序被称为学习算法(learning algorithm)。

1.2. 机器学习中的关键组件

首先介绍一些核心组件。无论什么类型的机器学习问题,都会遇到这些组件:

  1. 可以用来学习的数据(data);
  2. 如何转换数据的模型(model);
  3. 一个目标函数(objective function),用来量化模型的有效性;
  4. 调整模型参数以优化目标函数的算法(algorithm)。

1.2.1. 数据

  • 每个数据集由一个个样本(example, sample)组成,大多时候,它们遵循独立同分布(independently and identically distributed, i.i.d.)。 样本有时也叫做数据点(data point)或者数据实例(data instance),通常每个样本由一组称为特征(features,或协变量(covariates))的属性组成。 机器学习模型会根据这些属性进行预测。 在上面的监督学习问题中,要预测的是一个特殊的属性,它被称为标签(label,或目标(target))。
  • 当每个样本的特征类别数量都是相同的时候,其特征向量是固定长度的,这个长度被称为数据的维数(dimensionality)。
  • 然而,并不是所有的数据都可以用“固定长度”的向量表示。 以图像数据为例,如果它们全部来自标准显微镜设备,那么“固定长度”是可取的; 但是如果图像数据来自互联网,它们很难具有相同的分辨率或形状。 这时,将图像裁剪成标准尺寸是一种方法,但这种办法很局限,有丢失信息的风险。
  • 此外,文本数据更不符合“固定长度”的要求。 比如,对于亚马逊等电子商务网站上的客户评论,有些文本数据很简短(比如“好极了”),有些则长篇大论。
  • 与传统机器学习方法相比,深度学习的一个主要优势是可以处理不同长度的数据。

1.2.2. 模型

再比如通过摄取到的一组传感器读数预测读数的正常与异常程度。 虽然简单的模型能够解决如上简单的问题,但本书中关注的问题超出了经典方法的极限。 深度学习与经典方法的区别主要在于:前者关注的功能强大的模型,这些模型由神经网络错综复杂的交织在一起,包含层层数据转换,因此被称为深度学习(deep learning)。 在讨论深度模型的过程中,本书也将提及一些传统方法。

1.2.3. 目标函数

目标函数也可以被称为损失函数

  • 最常见的损失函数是平方误差(MSE), 有些目标函数(如平方误差)很容易被优化,有些目标(如错误率)由于不可微性或其他复杂性难以直接优化。 在这些情况下,通常会优化替代目标。

1.2.4. 优化算法

当我们获得了一些数据源及其表示、一个模型和一个合适的损失函数,接下来就需要一种算法,它能够搜索出最佳参数,以最小化损失函数。 深度学习中,大多流行的优化算法通常基于一种基本方法–梯度下降(gradient descent)。 简而言之,在每个步骤中,梯度下降法都会检查每个参数,看看如果仅对该参数进行少量变动,训练集损失会朝哪个方向移动。 然后,它在可以减少损失的方向上优化参数。

1.3. 各种机器学习问题

1.3.1. 监督学习

用概率论术语来说,我们希望预测“估计给定输入特征的标签”的条件概率。

1.3.1.1. regression

总而言之,判断回归问题的一个很好的经验法则是,任何有关“有多少”的问题很可能就是回归问题。比如:

  • 这个手术需要多少小时;

  • 在未来6小时,这个镇会有多少降雨量。

1.3.1.2. 分类

分类可能变得比二项分类、多项分类复杂得多。 例如,有一些分类任务的变体可以用于寻找层次结构,层次结构假定在许多类之间存在某种关系。 因此,并不是所有的错误都是均等的。 人们宁愿错误地分入一个相关的类别,也不愿错误地分入一个遥远的类别,这通常被称为层次分类(hierarchical classification)。 早期的一个例子是卡尔·林奈,他对动物进行了层次分类。

1.3.1.3. 标记问题

  • 无论模型有多精确,当分类器遇到新的动物时可能会束手无策。当一个图片中有多个动物时, 让模型去分辨图片属于哪一类动物没有意义, 这时候我们只关注如何让模型描绘输入图片的内容.
  • 学习预测不相互排斥的类别的问题称为多标签分类(multi-label classification)。 举个例子,人们在技术博客上贴的标签,比如“机器学习”“技术”“小工具”“编程语言”“Linux”“云计算”“AWS”。 一篇典型的文章可能会用5~10个标签,因为这些概念是相互关联的。

    1.3.1.4. 搜索

    有时,我们不仅仅希望输出一个类别或一个实值。 在信息检索领域,我们希望对一组项目进行排序。 以网络搜索为例,目标不是简单的“查询(query)-网页(page)”分类,而是在海量搜索结果中找到用户最需要的那部分。 搜索结果的排序也十分重要,学习算法需要输出有序的元素子集。

  • 即使结果集是相同的,集内的顺序有时却很重要。

该问题的一种可能的解决方案:首先为集合中的每个元素分配相应的相关性分数,然后检索评级最高的元素。PageRank,谷歌搜索引擎背后最初的秘密武器就是这种评分系统的早期例子,但它的奇特之处在于它不依赖于实际的查询。

2. 预备知识

2.1 数据操作

首先,我们介绍n维数组,也称为张量(tensor)。 使用过Python中NumPy计算包的读者会对本部分很熟悉。 无论使用哪个深度学习框架,它的张量类(在MXNet中为ndarray, 在PyTorch和TensorFlow中为Tensor)都与Numpy的ndarray类似。 但深度学习框架又比Numpy的ndarray多一些重要功能: 首先,GPU很好地支持加速计算,而NumPy仅支持CPU计算; 其次,张量类支持自动微分。 这些功能使得张量类更适合深度学习。 如果没有特殊说明,本书中所说的张量均指的是张量类的实例。

2.1.1. 入门

首先,我们可以使用 arange 创建一个行向量 x。这个行向量包含以0开始的前12个整数,它们默认创建为整数。也可指定创建类型为浮点数。张量中的每个值都称为张量的 元素(element)。例如,张量 x 中有 12 个元素。除非额外指定,新的张量将存储在内存中,并采用基于CPU的计算。

x = torch.arange(12)
x
tensor([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
  • 可以通过张量的shape属性来访问张量(沿每个轴的长度)的形状 。
    x.shape
    torch.Size([12])
  • 检查张量中的元素总数:x.numel() -> 12
    要想改变一个张量的形状而不改变元素数量和元素值,可以调用reshape函数。 例如,可以把张量x从形状为(12,)的行向量转换为形状为(3,4)的矩阵。 这个新的张量包含与转换前相同的值,但是它被看成一个3行4列的矩阵。 要重点说明一下,虽然张量的形状发生了改变,但其元素值并没有变。 注意,通过改变张量的形状,张量的大小不会改变。不要妄图使用reshape为张量添加一行或者一列
X = x.reshape(3, 4)
X
tensor([[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]])

我们不需要通过手动指定每个维度来改变形状。 也就是说,如果我们的目标形状是(高度,宽度), 那么在知道宽度后,高度会被自动计算得出,不必我们自己做除法。 在上面的例子中,为了获得一个3行的矩阵,我们手动指定了它有3行和4列。 幸运的是,我们可以通过-1来调用此自动计算出维度的功能。 即我们可以用x.reshape(-1,4)或x.reshape(3,-1)来取代x.reshape(3,4)。

torch.zeros((2, 3, 4))
tensor([[[0., 0., 0., 0.],
         [0., 0., 0., 0.],
         [0., 0., 0., 0.]],

        [[0., 0., 0., 0.],
         [0., 0., 0., 0.],
         [0., 0., 0., 0.]]]) 

torch.ones((2, 3, 4))
tensor([[[1., 1., 1., 1.],
         [1., 1., 1., 1.],
         [1., 1., 1., 1.]],

        [[1., 1., 1., 1.],
         [1., 1., 1., 1.],
         [1., 1., 1., 1.]]])

torch.randn(3, 4)

tensor([[-0.0135,  0.0665,  0.0912,  0.3212],
        [ 1.4653,  0.1843, -1.6995, -0.3036],
        [ 1.7646,  1.0450,  0.2457, -0.7732]])

我们还可以通过提供包含数值的Python列表(或嵌套列表),来为所需张量中的每个元素赋予确定值。 在这里,最外层的列表对应于轴0,内层的列表对应于轴1。

torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

tensor([[2, 1, 4, 3],
        [1, 2, 3, 4],
        [4, 3, 2, 1]])

2.1.2. 运算符

“按元素”方式可以应用更多的计算,包括像求幂这样的一元运算符

torch.exp(x)

我们也可以把多个张量连结(concatenate)在一起, 把它们端对端地叠起来形成一个更大的张量。 我们只需要提供张量列表,并给出沿哪个轴连结。 下面的例子分别演示了当我们沿行(轴-0,形状的第一个元素) 和按列(轴-1,形状的第二个元素)连结两个矩阵时,会发生什么情况。

X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)

#output
(tensor([[ 0.,  1.,  2.,  3.],
         [ 4.,  5.,  6.,  7.],
         [ 8.,  9., 10., 11.],
         [ 2.,  1.,  4.,  3.],
         [ 1.,  2.,  3.,  4.],
         [ 4.,  3.,  2.,  1.]]),
 tensor([[ 0.,  1.,  2.,  3.,  2.,  1.,  4.,  3.],
         [ 4.,  5.,  6.,  7.,  1.,  2.,  3.,  4.],
         [ 8.,  9., 10., 11.,  4.,  3.,  2.,  1.]]))

有时,我们想通过逻辑运算符构建二元张量。 以X == Y为例: 对于每个位置,如果X和Y在该位置相等,则新张量中相应项的值为1。 这意味着逻辑语句X == Y在该位置处为真,否则该位置为0。

X == Y

tensor([[False,  True, False,  True],
        [False, False, False, False],
        [False, False, False, False]])

对张量中的所有元素求和, 会产生一个单元素张量

X.sum()
Copy to clipboard
tensor(66.)

2.1.3. 广播机制

在上面的部分中,我们看到了如何在相同形状的两个张量上执行按元素操作。 在某些情况下,即使形状不同,我们仍然可以通过调用 广播机制(broadcasting mechanism)来执行按元素操作。 这种机制的工作方式如下:

  1. 通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状;

  2. 对生成的数组执行按元素操作。

在大多数情况下,我们将沿着数组中长度为1的轴进行广播,如下例子:

a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b

#output
(tensor([[0],
         [1],
         [2]]),
 tensor([[0, 1]]))

a + b
#output
tensor([[0, 1],
        [1, 2],
        [2, 3]])

2.1.4. 索引与切片

2.1.5. 节省内存

运行一些操作可能会导致为新结果分配内存。 例如,如果我们用Y = X + Y,我们将取消引用Y指向的张量,而是指向新分配的内存处的张量。

在下面的例子中,我们用Python的id()函数演示了这一点, 它给我们提供了内存中引用对象的确切地址。 运行Y = Y + X后,我们会发现id(Y)指向另一个位置。 这是因为Python首先计算Y + X,为结果分配新的内存,然后使Y指向内存中的这个新位置。

  • 幸运的是,执行原地操作非常简单。 我们可以使用切片表示法将操作的结果分配给先前分配的数组,例如Y[:] = 。为了说明这一点,我们首先创建一个新的矩阵Z,其形状与另一个Y相同, 使用zeros_like来分配一个全0的块。
Z = torch.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))
Copy to clipboard
id(Z): 140327634811696
id(Z): 140327634811696

如果在后续计算中没有重复使用X, 我们也可以使用X[:] = X + Y或X += Y来减少操作的内存开销。

before = id(X)
X += Y
id(X) == before
Copy to clipboard
True

2.1.6. 转换为其他Python对象

将深度学习框架定义的张量转换为NumPy张量(ndarray)很容易,反之也同样容易。 torch张量和numpy数组将共享它们的底层内存,就地操作更改一个张量也会同时更改另一个张量。

A = X.numpy()
B = torch.tensor(A)
type(A), type(B)
#output
(numpy.ndarray, torch.Tensor)

要将大小为1的张量转换为Python标量,我们可以调用item函数或Python的内置函数。

a = torch.tensor([3.5])
a, a.item(), float(a), int(a)

2.2 数据预处理

2.2.1. 读取数据集

使用pandas, 它可以和tensor兼容
要从创建的CSV文件中加载原始数据集,我们导入pandas包并调用read_csv函数。该数据集有四行三列。其中每行描述了房间数量(“NumRooms”)、巷子类型(“Alley”)和房屋价格(“Price”)。

import pandas as pd

data = pd.read_csv(data_file)
print(data)

#output
   NumRooms Alley   Price
0       NaN  Pave  127500
1       2.0   NaN  106000
2       4.0   NaN  178100
3       NaN   NaN  140000

2.3. 线性代数

矩阵范数

  • 范数定义:

c = A * b hence ||c|| < ||A|| * ||b||
对于矩阵A, 其长度||A||等于A中所有元素的平方和的开根
取决于如何衡量b和c的长度

  • 常见范数
    矩阵范数: 最小满足上边公式的值
    Frobenius范数:
    file

正定矩阵

||x||^2 >= 0 generalizes to zhuanzhi(x) A x >= 0

梯度

file

  1. 标量对于向量的导数
    file

    求导后翻转是因为dy/dx dx = dy, 而dy是一个标量*
    file

    这里求得的向量其实就是梯度
  2. 向量对于向量的导数
    file

2.4. 微积分

2.5. 自动微分

深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。 实际中,根据设计好的模型,系统会构建一个计算图(computational graph), 来跟踪计算是哪些数据通过哪些操作组合起来产生输出。 自动微分使系统能够随后反向传播梯度。 这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。

2.5.1. 一个简单的例子

import torch

x = torch.arange(4.0) # tensor([0., 1., 2., 3.])
x.requires_grad_(True) # 等价于x=torch.arange(4.0,requires_grad=True)
x,grad # 默认值是None

y = 2 * torch.dot(x, x)
# y -> tensor(28., grad_fn=<MulBackword0>)
y.backward()
x.grad # tensor([0., 4., 8., 12.])

函数y = 2 x * x 关于x的梯度因为4x, 可以通过以下方法验证

x.grad == 4 * x
#output
tensor([True, True, True, True])

2.5.2. 非标量变量的反向传播

当y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。 对于高阶和高维的y和x,求导的结果可以是一个高阶张量。

然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括深度学习中), 但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。 这里,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。

# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_() # 默认情况下,pytorch会累积梯度,需要手动清空
y = x * x # 对应位置的元素相乘
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
#output
tensor([0., 2., 4., 6.])

2.5.2. 分离计算

有时,我们希望将某些计算移动到记录的计算图之外。 例如,假设y是作为x的函数计算的,而z则是作为y和x的函数计算的。 想象一下,我们想计算z关于x的梯度,但由于某种原因,希望将y视为一个常数, 并且只考虑到x在y被计算后发挥的作用。

这里可以分离y来返回一个新变量u,该变量与y具有相同的值, 但丢弃计算图中如何计算y的任何信息。 换句话说,梯度不会向后流经u到x。

x.grad.zero_()
y = x * x
u = y.detach()
z = u * x

z.sum().backward()
x.grad == u
#output
tensor([True, True, True, True])

2.5.4. Python控制流的梯度计算

用自动微分的一个好处是: 即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),我们仍然可以计算得到的变量的梯度。
pytorch会自动为我们完成计算图的搭建

def f(a):
    b = a * 2
    while b.norm() < 1000:
        b = b * 2
    if b.sum() > 0:
        c = b
    else:
        c = 100 * b
    return c
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()

我们现在可以分析上面定义的f函数。 请注意,它在其输入a中是分段线性的。 换言之,对于任何a,存在某个常量标量k,使得f(a)=k*a,其中k的值取决于输入a,因此可以用d/a验证梯度是否正确。

a.grad == d / a
tensor(True)

2.6. 概率

3. 线性神经网络

3.1. 线性回归

线性回归基于几个简单的假设:

  • 首先,假设自变量x和因变量y之间的关系是线性的, 即y可以表示为x中元素的加权和,这里通常允许包含观测值的一些噪声;
  • 其次,我们假设任何噪声都比较正常,如噪声遵循正态分布。

3.1.1. 随机梯度下降

梯度下降

file

yita就是学习率
file

总结

  • 梯度下降通过不断沿着反梯度方向更新参数求解
  • 小批量随机梯度下降时深度学习的默认求解算法
  • 两个重要的超参数是批量大小和学习率

    3.2. 线性回归的从零开始实现

    3.2.1. 生成数据集

def synthetic_data(w, b, num_examples):  #@save
    """生成y=Xw+b+噪声"""
    X = torch.normal(0, 1, (num_examples, len(w)))
    y = torch.matmul(X, w) + b
    y += torch.normal(0, 0.01, y.shape) # 增加难度, 为y添加一点噪声
    return X, y.reshape((-1, 1)) # 转成一列

true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)

#output
features: tensor([1.4632, 0.5511])
label: tensor([5.2498])

通过生成第二个特征features[:, 1]和labels的散点图, 可以直观观察到两者之间的线性关系。

d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1);

file

3.2.2. 读取数据集

def data_iter(batch_size, features, labels):
    num_examples = len(features)
    indices = list(range(num_examples))
    # 这些样本是随机读取的,没有特定的顺序
    random.shuffle(indices)
    for i in range(0, num_examples, batch_size):
        batch_indices = torch.tensor(
            indices[i: min(i + batch_size, num_examples)])
        yield features[batch_indices], labels[batch_indices]

通常,我们利用GPU并行运算的优势,处理合理大小的“小批量”。 每个样本都可以并行地进行模型计算,且每个样本损失函数的梯度也可以被并行计算。 GPU可以在处理几百个样本时,所花费的时间不比处理一个样本时多太多。

3.2.3. 初始化模型参数

下面的代码中, 我们通过从均值为0, 标准差为0.01的正太分布中采样随机数来初始化权重, 并将bias初始化为0;

w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)

3.2.4. 定义模型

def linreg(X, w, b):
    return torch.matmul(X, w) + b

3.2.5. 定义损失函数

def squared_loss(y_hat, y): # y:真实值 y_hat:预测值
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2

思考:为什么需要使用reshape (因为经过net得到的为行向量, 形状不同)

3.2.6. 定于优化算法

使用小批量随机梯度下降

def sgd(params, lr, batch_size):
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

3.2.7. Train

现在我们已经准备好了模型训练所有许哟啊的要素, 乡可以实现主要的训练过程的部分了.
再每个迭代周期(epoch)中, 我们使用data_iter函数遍历整个数据集, 这里的num_epochs和学习率lr都是超参, 设置超参很棘手, 需要通过反复实验进行调整

lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss

for epoch in range(num_epochs):
    for X, y in data_iter(batch_size, features, labels):
        l = loss(net(X, w, b), y)  # X和y的小批量损失
        # 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
        # 并以此计算关于[w,b]的梯度
        l.sum().backward() #调用计算梯度
        sgd([w, b], lr, batch_size)  # 使用参数的梯度更新参数
    with torch.no_grad():
        train_l = loss(net(features, w, b), labels)
        print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')

幸运的是,即使是在复杂的优化问题上,随机梯度下降通常也能找到非常好的解。 其中一个原因是,在深度网络中存在许多参数组合能够实现高度精确的预测。

3.3. 线性回归的简洁实现

实际上, 由于数据迭代器, 损失函数, 优化器和神经网络层很常用, 现代深度学习库也为我们实现了这些组件.

3.3.1. 生成数据集

import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l

true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)

3.3.2. 读取数据集

is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据。

ef load_array(data_arrays, batch_size, is_train=True):  #@save
    """构造一个PyTorch数据迭代器"""
    dataset = data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset, batch_size, shuffle=is_train)

batch_size = 10
data_iter = load_array((features, labels), batch_size)

# 打印date_iter
[array([[ 0.6631022 , -0.16627775],
        [ 1.4011933 , -0.19310263],
        [-0.9567186 , -1.6827176 ],
        [-0.7114856 , -2.0320427 ],
        [ 0.8230793 , -0.26465464],
        [-1.0424827 ,  0.35005474],
        [-0.594405  ,  0.7975923 ],
        [ 1.1555725 , -0.13389243],
        [-0.1386715 ,  0.7079162 ],
        [ 1.0653706 , -0.02712403]]),
 array([[6.0939207 ],
        [7.659188  ],
        [8.01163   ],
        [9.69425   ],
        [6.749446  ],
        [0.91951895],
        [0.30207413],
        [6.9719377 ],
        [1.5070254 ],
        [6.423725  ]])]

3.3.3. 定义模型

对于标准深度学习模型,我们可以使用框架的预定义好的层。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。 我们首先定义一个模型变量net,它是一个Sequential类的实例。 Sequential类将多个层串联在一起。 当给定输入数据时,Sequential实例将数据传入到第一层, 然后将第一层的输出作为第二层的输入,以此类推。 在下面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential。 但是由于以后几乎所有的模型都是多层的,在这里使用Sequential会让你熟悉“标准的流水线”。

  • 这一单层被称为全连接层(fully-connected layer), 因为它的每一个输入都通过矩阵-向量乘法得到它的每个输出。
    # nn是神经网络的缩写
    from torch import nn
    net = nn.Sequential(nn.Linear(2, 1))

3.3.4. 初始化模型参数

net[0].weight.data.normal_(0, 0.01) # 0表示去模型的第一层, weight表示取权重
net[0].bias.data.fill_(0)

3.3.5. 定义损失函数

loss = nn.MSELoss()

3.3.6. 定义优化算法

  • 小批量随机梯度下降算法, 即SGD
  • 我们实例化一个SGD实例时,我们要指定优化的参数 (可通过net.parameters()从我们的模型中获得)以及优化算法所需的超参数字典。
trainer = torch.optim.SGD(net.parameters(), lr=0.03)

3.3.7. 训练

  • 通过调用net(X)生成预测并计算损失l(前向传播)。
  • 通过进行反向传播来计算梯度。
  • 通过调用优化器来更新模型参数。

trainer通过net.parameters()与net绑定, loss或许通过加入计算图与net绑定

num_epochs = 3
for epoch in range(num_epochs):
    for X, y in data_iter:
        l = loss(net(X) ,y)
        trainer.zero_grad()
        l.backward()
        trainer.step()
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')

#output
epoch 1, loss 0.000248
epoch 2, loss 0.000103
epoch 3, loss 0.000103

3.4. softmax回归

实际上就是解决分类问题

3.4.1. 分类问题

  • 使用one-hot编码

3.4.2. 网络架构

  • 为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。
  • 为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数(affine function)。

file

softmax回归是一种单层神经网络

3.4.4. softmax运算

要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。 此外,我们需要一个训练的目标函数,来激励模型精准地估计概率。这个属性叫做校准(calibration)。
因此引入softmax函数

file

  • 尽管softmax是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。 因此,softmax回归是一个线性模型(linear model)。

3.4.6. 损失函数

使用最大似然估计, 这与在线性回归中的方法相同

3.4.6.1. 对数似然

3.4.6.2. softmax及其导数

file

换句话说,导数是我们softmax模型分配的概率与实际发生的情况(由独热标签向量表示)之间的差异。 从这个意义上讲,这与我们在回归中看到的非常相似, 其中梯度是观测值
和估计值
之间的差异。 这不是巧合,在任何指数族分布模型中 (参见本书附录中关于数学分布的一节), 对数似然的梯度正是由此得出的。 这使梯度计算在实践中变得容易很多。

3.4.7. 信息论基础

信息论(information theory)涉及编码、解码、发送以及尽可能简洁地处理信息或数据。

3.4.7.1. 熵

信息论的核心思想是量化数据中的信息内容。 在信息论中,该数值被称为分布P的熵(entropy)。可以通过以下方程得到:

file

file

3.4.7.2. 信息量

压缩与预测有什么关系呢? 想象一下,我们有一个要压缩的数据流。 如果我们很容易预测下一个数据,那么这个数据就很容易压缩。
但是,如果我们不能完全预测每一个事件,那么我们有时可能会感到“惊异”。
克劳德·香农决定用信息量来量化这种惊异程度, 在观察一个事件j时, 并赋予它(主观)概率P(j), 当我们赋予一个事件较低的概率时, 我们的惊异会更大, 该事件的信息量也就更大.
信息量的数学形式:

file

3.4.7.3. 重新审视交叉熵

如果把熵H(P)想象为"知道真是概率的人所经历的惊异程度
, 拿什么是交叉熵? 交叉熵从P到Q, 记为H(P, Q), 我们可以把交叉熵想象为"主观概率为Q的观察者在看到根据概率P生成的数据时的预期惊异".
简而言之,我们可以从两方面来考虑交叉熵分类目标: (i)最大化观测数据的似然;(ii)最小化传达标签所需的惊异。

3.6. softmax回归的从零开始实现

import torch
from IPython import display
from d2l import torch as d2l

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

3.6.1. 初始化模型参数

和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。 原始数据集中的每个样本都是28*28的图像。 本节将展平每个图像,把它们看作长度为784的向量。 在后面的章节中,我们将讨论能够利用图像空间结构的特征, 但现在我们暂时只把每个像素位置看作一个特征。
权重将构成一个784 * 10的矩阵, 偏执将构成一个1 * 10的行向量

num_inputs = 784
num_outputs = 10

W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)

3.6.2. 定义softmax操作

回想一下,实现softmax由三个步骤组成:

  1. 每个项求幂(使用exp);

  2. 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;

  3. 将每一行除以其规范化常数,确保结果的和为1。

在查看代码之前,我们回顾一下这个表达式:

file

这里输入的是一个矩阵

X = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
def softmax(X):
    X_exp = torch.exp(X)
    partition = X_exp.sum(1, keepdim=True)
    return X_exp / partition  # 这里应用了广播机制

无论输入的矩阵形状, 该函数均能处理

3.6.3. 定义模型

def net(X):
    return softmax(torch.matmul(X.reshape((-1, W,shape[0])), w) + b)

这里reshape的作用: 由于输入的是一批次256个图片, 并且为28 * 28的矩阵, 将其flatten为256 * 784的输入

3.6.4. 定义损失函数

接下来,我们实现 3.4节中引入的交叉熵损失函数。 这可能是深度学习中最常见的损失函数,因为目前分类问题的数量远远超过回归问题的数量。
回顾一下,交叉熵采用真实标签的预测概率的负对数似然。
这里我们不使用Python的for循环迭代预测(这往往是低效的), 而是通过一个运算符选择所有元素。 下面,我们创建一个数据样本y_hat,其中包含2个样本在3个类别的预测概率, 以及它们对应的标签y。 有了y,我们知道在第一个样本中,第一类是正确的预测; 而在第二个样本中,第三类是正确的预测。 然后使用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]

3.6.5. 分类精度

为了计算精度,我们执行以下操作。 首先,如果y_hat是矩阵,那么假定第二个维度存储每个类的预测分数。 我们使用argmax获得每行中最大元素的索引来获得预测类别。 然后我们将预测类别与真实y元素进行比较。 由于等式运算符“==”对数据类型很敏感, 因此我们将y_hat的数据类型转换为与y的数据类型一致。 结果是一个包含0(错)和1(对)的张量。 最后,我们求和会得到正确预测的数量。

def accuracy(y_hat, y):  #@save
    """计算预测正确的数量"""
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
        y_hat = y_hat.argmax(axis=1)
    cmp = y_hat.type(y.dtype) == y
    return float(cmp.type(y.dtype).sum())

# 用来评估任意模型的精度
def evaluate_accuracy(net, data_iter):  #@save
    """计算在指定数据集上模型的精度"""
    if isinstance(net, torch.nn.Module):
        net.eval()  # 将模型设置为评估模式
    metric = Accumulator(2)  # 正确预测数、预测总数
    with torch.no_grad():
        for X, y in data_iter:
            metric.add(accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]

# utils
class Accumulator:  #@save
    """在n个变量上累加"""
    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]

3.6.6. 训练

def train_epoch_ch3(net, train_iter, loss, updater):  #@save
    """训练模型一个迭代周期(定义见第3章)"""
    # 将模型设置为训练模式
    if isinstance(net, torch.nn.Module):
        net.train()
    # 训练损失总和、训练准确度总和、样本数
    metric = Accumulator(3)
    for X, y in train_iter:
        # 计算梯度并更新参数
        y_hat = net(X)
        l = loss(y_hat, y)
        if isinstance(updater, torch.optim.Optimizer):
            # 使用PyTorch内置的优化器和损失函数
            updater.zero_grad()
            l.mean().backward()
            updater.step()
        else:
            # 使用定制的优化器和损失函数
            l.sum().backward()
            updater(X.shape[0])
        metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
    # 返回训练损失和训练精度
    return metric[0] / metric[2], metric[1] / metric[2]

# 总训练函数
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):  #@save
    """训练模型(定义见第3章)"""
    animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
                        legend=['train loss', 'train acc', 'test acc'])
    for epoch in range(num_epochs):
        train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
        test_acc = evaluate_accuracy(net, test_iter)
        animator.add(epoch + 1, train_metrics + (test_acc,))
    train_loss, train_acc = train_metrics
    assert train_loss < 0.5, train_loss
    assert train_acc <= 1 and train_acc > 0.7, train_acc
    assert test_acc <= 1 and test_acc > 0.7, test_acc

设置优化器和学习率:

lr = 0.1
def updater(batch_size):
    return d2l.sgd([W, b], lr, batch_size)

num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)

3.6.7. 预测

def predict_ch3(net, test_iter, n=6):  #@save
    """预测标签(定义见第3章)"""
    for X, y in test_iter:
        break
    trues = d2l.get_fashion_mnist_labels(y)
    preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
    titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
    d2l.show_images(
        X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])

predict_ch3(net, test_iter)

file

3.7. softmax回归的简洁实现

import torch
from torch import nn
from d2l import torch as d2l

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

3.7.1. 初始化模型参数

Sequential的意思是序列, 预备添加多个层
如我们在 3.4节所述, softmax回归的输出层是一个全连接层。 因此,为了实现我们的模型, 我们只需在Sequential中添加一个带有10个输出的全连接层。 同样,在这里Sequential并不是必要的, 但它是实现深度模型的基础。 我们仍然以均值0和标准差0.01随机初始化权重。

# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);

nn.Flatten()将输入除第一维以外的维度全部压缩为一维, 用于将28 * 28的图片压缩为784维度的向量.

3.7.2. 重新审视softmax的实现

file

file

loss = nn.CrossEntropyLoss(reduction='none')

3.7.3. 优化算法

trainer = torch.optim.SGD(net.parameters(), lr=0.1)

3.7.4. 训练

trainer = torch.optim.SGD(net.parameters(), lr=0.1)

4. 多层感知机

在多层感知机之前曾有单层感知机又来处理分类问题, 单层感知机采用的方法可以视为batch_size为1的随机梯度下降, 并且在最后经过一个sigma函数将输出处理为0, 1两种(感知机模型为单输出, 用以处理二分类问题)
但是感知机模型很快被证明不能处理简单的异或分类问题, 原因就在于这是一个纯线性的单层网络, 只能在线性空间中产生一个单一的分割面.

4.1. 多层感知机

4.1.1. 隐藏层

我们在 3.1.1.1节中描述了仿射变换, 它是一种带有偏置项的线性变换。 首先,回想一下如 图3.4.1中所示的softmax回归的模型架构。 该模型通过单个仿射变换将我们的输入直接映射到输出,然后进行softmax操作。 如果我们的标签通过仿射变换后确实与我们的输入数据相关,那么这种方法确实足够了。 但是,仿射变换中的线性是一个很强的假设。

线性模型可能会出错

例如,线性意味着单调假设: 任何特征的增大都会导致模型输出的增大(如果对应的权重为正), 或者导致模型输出的减小(如果对应的权重为负)。
我们可以认为,在其他条件不变的情况下, 收入较高的申请人比收入较低的申请人更有可能偿还贷款。 但是,虽然收入与还款概率存在单调性,但它们不是线性相关的。 收入从0增加到5万,可能比从100万增加到105万带来更大的还款可能性。 处理这一问题的一种方法是对我们的数据进行预处理, 使线性变得更合理,如使用收入的对数作为我们的特征。
但是,如何对猫和狗的图像进行分类呢? 增加位置(13, 17)处像素的强度是否总是增加(或降低)图像描绘狗的似然? 对线性模型的依赖对应于一个隐含的假设, 即区分猫和狗的唯一要求是评估单个像素的强度。 在一个倒置图像后依然保留类别的世界里,这种方法注定会失败。
与我们前面的例子相比,这里的线性很荒谬, 而且我们难以通过简单的预处理来解决这个问题。 这是因为任何像素的重要性都以复杂的方式取决于该像素的上下文(周围像素的值)。 我们的数据可能会有一种表示,这种表示会考虑到我们在特征之间的相关交互作用。 在此表示的基础上建立一个线性模型可能会是合适的, 但我们不知道如何手动计算这么一种表示。 对于深度神经网络,我们使用观测数据来联合学习隐藏层表示和应用于该表示的线性预测器。

在网络中添加隐藏层

我们可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制, 使其能处理更普遍的函数关系类型。 要做到这一点,最简单的方法是将许多全连接层堆叠在一起。 每一层都输出到上面的层,直到生成最后的输出。 我们可以把前L - 1层看作表示,把最后一层看作线性预测器。 这种架构通常称为多层感知机(multilayer perceptron),通常缩写为MLP。

file

从线性到非线性

注意在添加隐藏层之后,模型现在需要跟踪和更新额外的参数。 可我们能从中得到什么好处呢?在上面定义的模型里,我们没有好处! 原因很简单:上面的隐藏单元由输入的仿射函数给出, 而输出(softmax操作前)只是隐藏单元的仿射函数。 仿射函数的仿射函数本身就是仿射函数, 但是我们之前的线性模型已经能够表示任何仿射函数。

这个也叫做梯度坍塌,因为通过数学可以证明,多个线性累加,最后仍然可以用一个线性公式来表示

file

为了发挥多层架构的潜力, 我们还需要一个额外的关键要素: 在仿射变换之后对每个隐藏单元应用非线性的激活函数(activation function)

通用近似定理

例如,在一对输入上进行基本逻辑操作,多层感知机是通用近似器。即使是网络只有一个隐藏层,给定足够的神经元和正确的权重, 我们可以对任意函数建模,尽管实际中学习该函数是很困难的。
虽然一个单隐层网络能学习任何函数, 但并不意味着我们应该尝试使用单隐藏层网络来解决所有问题。 事实上,通过使用更深(而不是更广)的网络,我们可以更容易地逼近许多函数。 我们将在后面的章节中进行更细致的讨论。

4.1.2. 激活函数

激活函数(activation function)通过计算加权和并加上偏置来确定神经元是否应该被激活, 它们将输入信号转换为输出的可微运算。 大多数激活函数都是非线性的.
激活函数是深度学习的基础

ReLU函数

最受欢迎的激活函数是修正线性单元(Rectified linear unit,ReLU), 因为它实现简单,同时在各种预测任务中表现良好。 ReLU提供了一种非常简单的非线性变换。

file

通俗地说,ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。
当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。 注意,当输入值精确等于0时,ReLU函数不可导。 在此时,我们默认使用左侧的导数,即当输入为0时导数为0。 我们可以忽略这种情况,因为输入可能永远都不会是0。你不用去在意那些微妙的便捷条件

x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.relu(x)
d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))

file

使用ReLU的原因是,它求导表现得特别好:要么让参数消失,要么让参数通过。 这使得优化表现得更好,并且ReLU减轻了困扰以往神经网络的梯度消失问题(稍后将详细介绍)。
意,ReLU函数有许多变体,包括参数化ReLU(Parameterized ReLU,pReLU) 函数 (He et al., 2015)。 该变体为ReLU添加了一个线性项,因此即使参数是负的,某些信息仍然可以通过

sigmoid函数

file

sigmoid在隐藏层中已经较少使用, 它在大部分时候被更简单、更容易训练的ReLU所取代。 在后面关于循环神经网络的章节中,我们将描述利用sigmoid单元来控制时序信息流的架构。
当输入接近0时, sigmoid函数接近线性变换
file

tanh函数

与sigmoid函数类似, tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上。 tanh函数的公式如下:

file

file

4.2. 多层感知机从零开始实现

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

npx.set_np()

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

4.2.1. 初始化模型参数

Fashion-MNIST中的每个图像由 28 * 28 = 784 个灰度像素值组成。 所有图像共分为10个类别, 隐藏层设置256个隐藏单元(我们选择2的若干次幂作为层的宽度。 因为内存在硬件中的分配和寻址方式,这么做往往可以在计算上更高效)

num_inputs, num_outputs, num_hiddens = 784, 10, 256

W1 = np.random.normal(scale=0.01, size=(num_inputs, num_hiddens))
b1 = np.zeros(num_hiddens)
W2 = np.random.normal(scale=0.01, size=(num_hiddens, num_outputs))
b2 = np.zeros(num_outputs)
params = [W1, b1, W2, b2]

for param in params:
    param.attach_grad()

4.2.2. 激活函数

def relu(X):
    a = torch.zeros_like(X)
    return torch.max(X, a)

4.2.3. 模型

将二位图像reshape为以为输入

def net(X):
    X = X.reshape((-1, num_inputs))
    H = relu(X@W1 + b1)  # 这里“@”代表矩阵乘法
    return (H@W2 + b2)

损失函数:(softmax)

loss = nn.CrossEntropyLoss(reduction='none')
```### 4.2.5. 训练
可以直接调用d2l包的train_ch3函数(参见 3.6节 ), 将迭代周期数设置为10,并将学习率设置为0.1.
```python
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)

4.3. 多层感知机的简洁实现

import torch
from torch import nn
from d2l import torch as d2l

net = nn.Sequential(nn.Flatten(),
                    nn.Linear(784, 256),
                    nn.ReLU(),
                    nn.Linear(256, 10))

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);

batch_size, lr, num_epochs = 256, 0.1, 10
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)

train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

4.4. 模型选择, 过拟合与欠拟合

将模型在训练数据上拟合的比在潜在分布中更接近的现象称为过拟合(overfitting), 用于对抗过拟合的技术称为正则化(regularization)

4.5. 权重衰减

前一节我们描述了过拟合的问题,本节我们将介绍一些正则化模型的技术。
在训练参数化机器学习模型时, 权重衰减(weight decay)是最广泛使用的正则化的技术之一, 它通常也被称为正则化。

file

4.6. 抛弃法(Dropout)

在 4.5节 中, 我们介绍了通过惩罚权重的L2范数来正则化统计模型的经典方法。 在概率角度看,我们可以通过以下论证来证明这一技术的合理性: 我们已经假设了一个先验,即权重的值取自均值为0的高斯分布。 更直观的是,我们希望模型深度挖掘特征,即将其权重分散到许多特征中, 而不是过于依赖少数潜在的虚假关联。

4.8. 数值稳定性与模型初始化

初始化方案的选择在神经网络学习中起着举足轻重的作用, 它对保持数值稳定性至关重要。 此外,这些初始化方案的选择可以与非线性激活函数的选择有趣的结合在一起。 我们选择哪个函数以及如何初始化参数可以决定优化算法收敛的速度有多快。 糟糕选择可能会导致我们在训练时遇到梯度爆炸或梯度消失。

4.8.1. 梯度爆照与梯度消失

4.8.1.1. 梯度消失

选用sigmoid函数作为激活函数是导致梯度消失问题的一个常见的原因, 让我们仔细看看sigmoid函数为什么会导致梯度消失。

%matplotlib inline
import torch
from d2l import torch as d2l

x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.sigmoid(x)
y.backward(torch.ones_like(x)) # 使得针对x的每一个元素的和计算梯度而不是针对每一个元素单独计算梯度
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度

d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()],
         legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5

file

正如上图,当sigmoid函数的输入很大或是很小时, 它的梯度都会消失, 因此更稳定的ReLU系列函数已经成为从业者的默认选择.

4.8.1.2. 梯度爆炸

我们生成100个高斯随机矩阵,并将它们与某个初始矩阵相乘。

M = torch.normal(0, 1, size=(4,4))
print('一个矩阵 \n',M)
for i in range(100):
    M = torch.mm(M,torch.normal(0, 1, size=(4, 4)))

print('乘以100个矩阵后\n', M)

#output
一个矩阵
 tensor([[-0.7872,  2.7090,  0.5996, -1.3191],
        [-1.8260, -0.7130, -0.5521,  0.1051],
        [ 1.1213,  1.0472, -0.3991, -0.3802],
        [ 0.5552,  0.4517, -0.3218,  0.5214]])
乘以100个矩阵后
 tensor([[-2.1897e+26,  8.8308e+26,  1.9813e+26,  1.7019e+26],
        [ 1.3110e+26, -5.2870e+26, -1.1862e+26, -1.0189e+26],
        [-1.6008e+26,  6.4559e+26,  1.4485e+26,  1.2442e+26],
        [ 3.0943e+25, -1.2479e+26, -2.7998e+25, -2.4050e+25]])

4.8.1.3. 打破对称性

神经网络设计中的另一个问题是其参数化所固有的对称性。假设我们有一个简单的多层感知机,它有一个隐藏层和两个隐藏单元。 在这种情况下,我们可以对第一层的权重W进行重排列, 并且同样对输出层的权重进行重排列,可以获得相同的函数。
如果两个隐藏单元的参数被初始化为同一值c, 那么在反向传播中两个隐藏单元的权重永远会保持一致, 行为上就好像只有一个单元.

4.8.2. 参数初始化

解决(或至少减轻)上述问题的一种方法是进行参数初始化, 优化期间的注意和适当的正则化也可以进一步提高稳定性。

  • 目标: 让梯度保持在合理范围内, 例如[1e-6, 1e3]
  • 让乘法变加法: ResNet, LSTM
  • 归一化: 梯度归一化 / 梯度裁剪
  • 合理的权重初始化和激活函数

    保证梯度在一个合理范围内

    • 将每层的输出和梯度都看作随机变量
    • 让他们的均值和方差都保持一致
      file

为什么要注意权重初始化:

  • 在训练开始的时候更容易有权重不稳定
    • 最优解附近表面会比较平
    • 远离最优解的地方情况会比较复杂

      4.8.2.1. 默认初始化

      即使用正态分布来初始化权重值.

      4.8.2.2. Xavier初始化

file

file

Xavier初始化表明,对于每一层,输出的方差不受输入数量的影响,任何梯度的方差不受输出数量的影响.
补充: 添加上激活函数的情况:
file

检查常用的激活函数:
file

relu和tanh在原点附近都是近似于y=x的, 而sigmoid不符合这一点

4.9. 环境和分布偏移

4.9.1. 分布偏移的类型

4.9.1.1. 协变量偏移

在不同分布偏移中,协变量偏移可能是最为广泛研究的。 这里我们假设:虽然输入的分布可能随时间而改变, 但标签函数(即条件分布P(y|X))没有改变。 统计学家称之为协变量偏移(covariate shift). 因为这个问题是由于协变量(特征)分布的变化产生的.

4.9.1.2. 标签偏移

标签偏移(label shift)描述了与协变量偏移相反的问题。 这里我们假设标签边缘概率P(y)可以改变, 但是类别条件分布P(X|y)在不同的领域之间保持不变。 当我们认为y导致X时,标签偏移是一个合理的假设。 例如,预测患者的疾病,我们可能根据症状来判断, 即使疾病的相对流行率随着时间的推移而变化。

4.9.1.3. 概念偏移

我们也可能会遇到概念偏移(concept shift): 当标签的定义发生变化时,就会出现这种问题。 这听起来很奇怪——一只猫就是一只猫,不是吗? 然而,其他类别会随着不同时间的用法而发生变化。 精神疾病的诊断标准、所谓的时髦、以及工作头衔等等,都是概念偏移的日常映射。

4.9.3. 分布偏移纠正

4.9.3.1. 经验风险与实际风险

file

其中l就是损失函数, 用来度量: 给定标签yi, 预测f(Xi</sub)>)的"糟糕程度", 统计学家称其为经验风险, 经验风险是为了近似真实风险, 即从真实分布p(X, y)中抽取的所有数据的总体损失的期望值.

4.9.3.2. 协变量偏移纠正

TODO: 太数学了, 之后再细看吧()

4.9.3.3. 标签偏移纠正

TODO: 同理

5. 深度学习计算

5.1. 层和块

5.1.1. 自定义块

class MLP(nn.Module):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self):
        # 调用MLP的父类Module的构造函数来执行必要的初始化。
        # 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
        super().__init__() # unnecessary in python3
        self.hidden = nn.Linear(20, 256)  # 隐藏层
        self.out = nn.Linear(256, 10)  # 输出层

    # 定义模型的前向传播,即如何根据输入X返回所需的模型输出
    def forward(self, X):
        # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
        return self.out(F.relu(self.hidden(X)))

5.1.2. 顺序块

现在我们可以更仔细地看看Sequential类是如何工作的, 回想一下Sequential的设计是为了把其他模块串起来。 为了构建我们自己的简化的MySequential, 我们只需要定义两个关键函数:

  1. 一种将块逐个追加到列表中的函数;
  2. 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
class MySequential(nn.Module):
    def __init__(self, *args):
        super().__init__()
        for idx, module in enumerate(args):
            # 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
            # 变量_modules中。_module的类型是OrderedDict
            self._modules[str(idx)] = module

    def forward(self, X):
        # OrderedDict保证了按照成员添加的顺序遍历它们
        for block in self._modules.values():
            X = block(X)
        return X

读者可能会好奇为什么每个Module都有一个_modules属性? 以及为什么我们使用它而不是自己定义一个Python列表? 简而言之,_modules的主要优点是: 在模块的参数初始化过程中, 系统知道在_modules字典中查找需要初始化参数的子块。

5.2. 参数管理

本届将介绍以下内容:

  • 访问参数,用于调试、诊断和可视化;
  • 参数初始化;
  • 在不同模型组件间共享参数。

5.2.1. 参数访问

net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
print(net[2].state_dict())
# output:
OrderedDict([('weight', tensor([[-0.0427, -0.2939, -0.1894,  0.0220, -0.1709, -0.1522, -0.0334, -0.2263]])), ('bias', tensor([0.0887]))])

目标参数:

print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data) # 通过data直接访问数据本身
# output:
<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([0.0887], requires_grad=True)
tensor([0.0887])

一次性访问所有参数:

print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])

# output:

('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))

另一种方式:

net.state_dict()['2.bias'].data # tensor([0.0887])

5.2.2. 参数初始化

使用内置初始化器:

def init_normal(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, mean=0, std=0.01)
        nn.init.zeros_(m.bias)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]

#output:
(tensor([-0.0214, -0.0015, -0.0100, -0.0058]), tensor(0.))

# 初始化为给定常数:
def init_constant(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 1)
        nn.init.zeros_(m.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0]

# 对不同的块使用不同方法:

def init_xavier(m):
    if type(m) == nn.Linear:
        nn.init.xavier_uniform_(m.weight) # 前面提过
def init_42(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 42)

net[0].apply(init_xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)

5.2.3. 参数绑定

有时我们希望在多个层间共享参数: 我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。

# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                    shared, nn.ReLU(),
                    shared, nn.ReLU(),
                    nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])

这个例子表明第三个和第五个神经网络层的参数是绑定的。 它们不仅值相等,而且由相同的张量表示。 因此,如果我们改变其中一个参数,另一个参数也会改变。 这里有一个问题:当参数绑定时,梯度会发生什么情况? 答案是由于模型参数包含梯度,因此在反向传播期间第二个隐藏层 (即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。

5.5. 读写文件

import torch
from torch import nn
from torch.nn import functional as F

x = torch.arange(4)
torch.save(x, 'x-file')
x2 = torch.load('x-file')

y = torch.zeros(4)
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')

mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇