深度学习
- 线性回归恰好是一个在整个域中只有一个最小值的学习问题。 但是对像深度神经网络这样复杂的模型来说,损失平面上通常包含多个最小值。 深度学习实践者很少会去花费大力气寻找这样一组参数,使得在训练集上的损失达到最小。
多层感知机¶
- 单层的神经网络只能处理线性关系(线性回归模型),通过添加隐藏层可以处理更加普遍的函数关系类型
- 这种架构就成为多层感知机 MLP
- 为了让多层结构真正提升模型的表示能力(而不是只能表示线性的仿射关系),对每个隐藏单元应用非线性的激活函数(如ReLU、Sigmoid),从而阻止多层感知机退化成线性模型 \(\mathbf{H}=\sigma(\mathbf{X}\mathbf{W}^{(1)}+\mathbf{b}^{(1)})\)
- ReLU: 修正线性单元 \(\mathrm{ReLU}(x)=\max(x,0)\)。它求导表现得特别好:要么让参数消失,要么让参数通过。这使得优化表现得更好,并且ReLU减轻了困扰以往神经网络的梯度消失问题。
- sigmoid:机爱你个 R 上的输入映射到 \((0,1)\),\(\mathrm{sigmoid}(x)=\frac1{1+\exp(-x)}.\)在隐藏层中已经较少使用, 它在大部分时候被更简单、更容易训练的ReLU所取代。
- tanh:将输入压缩到 \((-1,1)\),\(\tanh(x)=\frac{1-\exp(-2x)}{1+\exp(-2x)}\)
- 过使用更深的网络,可以更容易地逼近许多函数。
实现¶
# 创建多层网络
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)
暂退法¶
- 希望模型深度挖掘特征,将权重分散到许多特征中,而不是过于依赖少数潜在的虚假关联
- 线性模型没有考虑到特征之间的交互作用,对于每个特征,线性模型必须指定正的或负的权重,而忽略其他特征。
- 泛化性和灵活性之间的这种基本权衡被描述为偏差-方差权衡
- 线性模型
- 很高的偏差:它们只能表示一小类函数
- 方差很低:它们在不同的随机数据样本上可以得出相似的结果。
-
深度神经网络:不局限于单独查看每个特征,而是学习特征之间的交互
-
在标准暂退法正则化中,通过按保留(未丢弃)的节点的分数进行规范化来消除每一层的偏差
- 每个中间活性值 \(h\) 以暂退概率 \(p\) 由随机变量 \(h`\) 替代
- \(h^{\prime}=\begin{cases}0&\text{概率为 }p\\\frac h{1-p}&\text{其他情况}&\end{cases}\)
def dropout_layer(X, dropout): assert 0 <= dropout <= 1 # 在本情况中,所有元素都被丢弃 if dropout == 1: return torch.zeros_like(X) # 在本情况中,所有元素都被保留 if dropout == 0: return X mask = (torch.rand(X.shape) > dropout).float() return mask * X / (1.0 - dropout)
- 只需在每个全连接层之后添加一个
Dropout
层,将暂退概率作为唯一的参数传递给它的构造函数。在训练时,Dropout
层将根据指定的暂退概率随机丢弃上一层的输出。在测试时,Dropout
层仅传递数据(测试时通常不进行丢弃)。net = nn.Sequential(nn.Flatten(), nn.Linear(784, 256), nn.ReLU(), # 在第一个全连接层之后添加一个dropout层 nn.Dropout(dropout1), nn.Linear(256, 256), nn.ReLU(), # 在第二个全连接层之后添加一个dropout层 nn.Dropout(dropout2), nn.Linear(256, 10))
计算图与传播¶
前向传播¶
- 从输入层到输出层按顺序来计算和存储神经网络中每层的结果
反向传播¶
- 计算神经网络采纳数梯度的方法,根据微积分中的链式规则,按早相反的顺序从输出层到输入层遍历网络,存储了计算某些参数梯度时所需的任何中间变量
数值稳定性和模型初始化¶
- 神经网络的结构如下:每一层都是上一层输出和本层参数为变量的函数 \(\mathbb{H}^{(l)}=f(\mathbb{H}^{(l-1)};\mathbb{W}^{(l)})\)
- 那么网络的输出就可以表示为 \(\mathbb{H}^{(L)}=f(f(\cdots f(\mathbb{X};\mathbb{W}^{(1)})\cdots;\mathbb{W}^{(L-1)});\mathbb{W}^{(L)})\)
- 对 \(l\) 参数的更新就依赖梯度 \(\frac{\partial\mathcal{L}}{\partial\mathbb{W}^{(l)}}=\frac{\partial\mathcal{L}}{\partial\mathbb{H}^{(L)}}\cdot\prod_{i=l}^{L-1}\frac{\partial\mathbb{H}^{(i+1)}}{\partial\mathbb{H}^{(i)}}\)
- 梯度是一个累积乘积,每一层的梯度都依赖于前面所有层的梯度,通过反向传播算法计算梯度时,梯度会随着层数增加而出现指数性衰减或增长
- 如果每一层的梯度的值非常小或非常大,那么它们的乘积会导致梯度整体上指数性地减小或增大。
- 梯度消失
- 当每一层的梯度都小于 1,就可能导致梯度消失
- 当sigmoid函数的输入很大或是很小时,它的梯度都会消失,因此更稳定的ReLU系列函数已经成为从业者的默认选择
- 靠近输入层的参数更新非常缓慢:因为梯度接近 0,导致这些层的权重几乎不会变化,模型无法有效学习深层特征。
- 训练时间变长或停止:模型可能无法收敛。
- 梯度爆炸
- 如果权重初始化得过大,或者激活函数(如 ReLU)在大范围值上表现不稳定,那么每一层的梯度值会变大。
- 如果网络的权重在初始化时值很大,正向传播中的激活值会不断增大,反向传播的梯度也会随之增大,导致梯度爆炸。
- 参数更新过大:导致模型的损失函数值变得不稳定,甚至出现 NaN。
- 优化难以收敛:爆炸的梯度会破坏优化算法的稳定性。
参数初始化¶
- 假设我们有一个简单的多层感知机,它有一个隐藏层和两个隐藏单元,如果我们在隐藏层的所有单元的参数初始化时,给定相同的值,这就会导致一种对称性
- 由于隐藏单元的输出完全相同,网络的行为就好像隐藏层只有一个单元,失去了隐藏单元的多样性。
- 可以通过随机初始化和暂退法正则化来解决
- 更好的初始化方案:Xavier 初始化
- 通过控制权重的方差,确保前向传播和反向传播中方差的稳定性,从而减轻梯度消失或梯度爆炸问题。
环境和分布偏移¶
- 协变量偏移:输入分布 \(P(X)\) 发生变化,但分布条件 \(P(Y|X)\) 和标签分布 \(P(Y)\) 不变
-
为训练集中的每个样本赋予权重,来调整训练分布使其更加接近测试分布 \(w(X)=\frac{P_{\mathrm{target}}(X)}{P_{\mathrm{source}}(X)}\)
- 这个系数可以通过混合输入输出用对数几率回归进行计算
-
标签偏移:标签分布发生变化,但分布条件和输入分布不变
- 在训练数据上构造混淆矩阵(行表示预测结果类型,列表示实际标签,用于表示模型预测结果)
- 在测试数据集上做预测
- 结合混淆矩阵反推测试数据的真实标签分布
- 利用估计的真实标签分布结合训练数据集调整训练数据集的权重
-
概念偏移:条件分布本身发生变化,其他条件不变
- 使用新数据更新现有的网络权重,而不是从头开始训练。
深度学习计算¶
层和块¶
- 块:可以描述单个层、由多个层组成的组件或整个模型本身
- 自定义块 (下面实现一个顺序块)
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 # 使用创建的网络 net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10)) net(X)
- 自定义层
# 无参数的层 class CenteredLayer(nn.Block): def __init__(self, **kwargs): super().__init__(**kwargs) def forward(self, X): return X - X.mean() # 有参数的层 class MyDense(nn.Block): def __init__(self, units, in_units, **kwargs): super().__init__(**kwargs) self.weight = self.params.get('weight', shape=(in_units, units)) self.bias = self.params.get('bias', shape=(units,)) def forward(self, x): linear = np.dot(x, self.weight.data(ctx=x.ctx)) + self.bias.data( ctx=x.ctx) return npx.relu(linear)
参数管理¶
- 获取模型一层的参数
net[2].state_dict()
OrderedDict([('weight', tensor([[-0.0427, -0.2939, -0.1894, 0.0220, -0.1709, -0.1522, -0.0334, -0.2263]])), ('bias', 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()])
- 通过下标访问
net.state_dict()['2.bias'].data
- 对具有复杂层级的块也可以嵌套访问
rgnet[0][1][0].bias.data
参数初始化¶
- 正态分布初始化
nn.init.normal_(m.weight, mean=0, std=0.01)
- 初始化为 0
nn.init.normal_(m.weight, mean=0, std=0.01)
- 自定义初始化
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]
延后初始化¶
- 使得在没有指定输入维度等信息时就可以先建立网络
- 直到数据第一次通过模型传递时,框架才会动态地推断出每个层的大小
- 延后初始化使框架能够自动推断参数形状,使修改模型架构变得容易
读写文件¶
- 实现将模型保存到文件中
- 加载和保存张量
x = torch.arange(4) torch.save(x, 'x-file') x2 = torch.load('x-file') # 存储一个列表(同理也可以存储字典等) torch.save([x, y],'x-files') x2, y2 = torch.load('x-files')
- 加载和保存模型参数
torch.save(net.state_dict(), 'mlp.params') clone.load_state_dict(torch.load('mlp.params'))
GPU¶
- 获取 GPU 的数目
torch.cuda.device_count()
- 查看张量所在的位置
x.device
- 将张量存储在 GPU 上
X = torch.ones(2, 3, device=try_gpu())
- 指定存储的 GPU
torch.rand(2, 3, device=try_gpu(1))
- 指定存储的 GPU
- 进行张量运算之前,要决定在哪里执行这个操作(将其他数据移动过去, 否则找不到数据会发生异常)
- 将数据复制到 GPU 1 上
Z = X.cuda(1)
- 之后就可以进行计算
- 将数据复制到 GPU 1 上
- 指定神经网络模型运行到 GPU 上
net = net.to(device=try_gpu())
卷积神经网络 CNN¶
- 强大的、为处理图像数据而设计的神经网络
- 图片中有太多的像素点,传统的机器学习算法无法处理如此海量的参数
- 卷积神经网络正是将空间不变性的这一概念系统化,从而基于这个模型使用较少的参数来学习有用的表示。
- 平移不变性:不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。(即如果输入图像中的某个对象从左侧移动到了右侧,模型应该能够识别到这是同一个对象,而不因为其位置的改变对输出产生完全不同的影响)
- 局部性:神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终可以聚合这些局部特征,以在整个图像级别进行预测。
- 这个原则与现实中图像的性质相符:一个像素的特征通常与其周围的像素紧密相关,而与距离较远的像素关系较弱。
-
输入的图像表示为矩阵 \(X\),形状为 \((H, W)\)
- 隐藏层中的表示也都为相同形状的二维矩阵
- 因此对于全连接层每层的参数可以使用一个四阶张量来表示
- \(W(h
,w
,h,w)\) 表示连接输入图像中位置 \((h,w)\) 的像素与隐藏表示中位置 \((h,w
)\) 的像素权重 \(\mathbf{H}(h^{\prime},w^{\prime})=\sum_{h=1}^H\sum_{w=1}^W\mathbf{X}(h,w)\cdot\mathbf{W}(h^{\prime},w^{\prime},h,w)+\mathbf{b}(h^{\prime},w^{\prime}),\) - 传统的多层感知机全连接具有太多的参数,而且并不具有平移不变形,难以处理平移
-
卷积操作通过一个小的权重窗口(卷积核)对图像的局部区域进行加权求和。
- 每一个输出像素值只依赖于输入图像的局部像素区域,而不是整个图像 \(\mathbf{H}(i,j)=\sum_{m,n}\mathbf{X}(i+m,j+n)\cdot\mathbf{K}(m,n)\) 其中 \(K\) 表示卷积核的权重
- 卷积操作中的权重在整个输入图像中共享。无论输入图像的某个特征出现在左上角还是右下角,卷积核都能对该特征进行相同的加权计算。
- 当输入图像中的对象发生平移时,卷积操作产生的隐藏表示也会随之平移,而不会改变表示的内容。
- 有点:参数数量少,平移不变形,捕获局部特征