前言:
本篇内容记录笔者学习深度学习的学习过程,如果你有任何想询问的问题,欢迎在以下任何平台提问!
在本章中,我们将深入探索深度学习计算的关键组件, 即模型构建、参数访问与初始化、设计自定义层和块、将模型读写到磁盘, 以及利用GPU实现显著的加速。
这些知识将使读者从深度学习”基础用户”变为”高级用户”。
虽然本章不介绍任何新的模型或数据集, 但后面的高级模型章节在很大程度上依赖于本章的知识。
参考书:《动手学深度学习》
Bilibili:space.bilibili.com/57089326
层和块:
从这里也可以看出,层和块的关系:块里面包含单个或多个层。
我们简要说明一下每个块必须提供的基本功能。
- 将输入数据作为其前向传播函数的参数。
- 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收一个20维的输入,但是返回一个维度为256的输出。
- 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。
- 存储和访问前向传播计算所需的参数。
- 根据需要初始化模型参数。
如图:多个层被组合成块,形成更大的模型。
简要总结。
- 一个块可以由许多层组成;一个块可以由许多块组成。
- 块可以包含代码。
- 块负责大量的内部处理,包括参数初始化和反向传播。
- 层和块的顺序连接由
Sequential
块处理。
参数管理:
在选择了架构并设置了超参数后,我们就进入了训练阶段。
此时,我们的目标是找到使损失函数最小化的模型参数值。
经过训练后,我们将需要使用这些参数来做出未来的预测。
此外,有时我们希望提取参数,以便在其他环境中复用它们;
将模型保存下来,以便它可以在其他软件中执行, 或者为了获得科学的理解而进行检查。
这是成为调参工程师的第一步!
首先,我们来明确一点,超参数和模型参数有什么区别?如何区分它们?
(已经了解的读者可以跳过)
两种不同的参数:
(该部分引用自zhuanlan.zhihu.com/p/37476536,…
模型参数是模型内部的配置变量,其值可以根据数据进行估计。
- 模型在进行预测时需要它们。
- 它们的值,定义了可使用的模型。
- 他们是从数据估计或获悉的。
- 它们通常不由编程者手动设置。
- 他们通常被保存为学习模型的一部分。
模型超参数是模型外部的配置,其值无法从数据中估计。
- 它们通常用于帮助估计模型参数。
- 它们通常由人工指定。
- 他们通常可以使用启发式设置。
- 他们经常被调整为给定的预测建模问题。
我们虽然无法知道给定问题的模型超参数的最佳值,但是我们可以使用经验法则,在其他问题上使用复制值,或通过反复试验来搜索最佳值。
如何区分?
如果必须手动指定模型参数,那么它可能是一个模型超参数。
接下来,我们进入正题。
访问参数:
我们可以通过一些方式来访问参数(也就是查看):
print(net[2].state_dict())
# 输出:
# OrderedDict([('weight', tensor([[ 0.3016, -0.1901, -0.1991, -0.1220, 0.1121, -0.1424, -0.3060, 0.3400]])), ('bias', tensor([-0.0291]))])
注意,state_dict()
并不是一定要这么写,具体要看情况。
不过我们也可以采用一种更加简便的方式访问所有的参数,而不用每一层都问一次:
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
# 输出:
# ('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]))
当然我们也可以从嵌套块收集参数,但因为我们的终点不是讲某一个细节,而是作为一个初学者,对于每一部分的知识形成一个整体的印象和框架,所以我们并不深入地去看这到底是怎么实现的。
可能会有较真的严谨一些的读者对此表示嗤之以鼻,的确,如果能第一次学习就将所有的细节都掌握那自然是更好的,不过笔者并不能做到这一点,相信也有一部分读者赞同这个观点,总之大家可以各取所需就是了。
参数初始化:
良好的初始化是很有必要的,有两种参数初始化的办法:
- 深度学习框架提供的默认随机初始化;
- 自定义初始化方法, 满足我们通过其他规则实现初始化权重。
下面直接看两种办法在Pytorch里面的代码对比:
# 默认
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]
# 自定义
def my_init(m):
if type(m) == nn.Linear:
print("Init", *[(name, param.shape)
for name, param in m.named_parameters()][0])
nn.init.uniform_(m.weight, -10, 10)
m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
net[0].weight[:2]
参数绑定:
有时我们希望在多个层间共享参数: 我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。
特别地,当参数绑定时,梯度会发生什么情况?
答案是由于模型参数包含梯度,因此在反向传播期间,绑定层的梯度会加在一起。
延后初始化与自定义层:
延后初始化:
深度学习框架无法判断网络的输入维度是什么,不过有延后初始化(defers initialization):
- 直到数据第一次通过模型传递时,框架才会动态地推断出每个层的大小。
延后初始化使框架能够自动推断参数形状,使修改模型架构变得容易,避免了一些常见的错误。
我们可以通过模型传递数据,使框架最终初始化参数。
自定义层:
深度学习成功背后的一个因素是神经网络的灵活性:
- 我们可以用创造性的方式组合不同的层,从而设计出适用于各种任务的架构。
关于文件读写和GPU的介绍就不在本文给出了,读者可以自行查找相关内容。
本文正在参加 人工智能创作者扶持计划