🐋 损失函数和激活函数的选择
type
status
date
slug
summary
tags
category
icon
password
Property
 
DNN可以使用的损失函数和激活函数不少。这些损失函数和激活函数如何选择呢?
首先我们来看看均方差+Sigmoid的组合有什么问题。
Sigmoid激活函数的表达式为:
图像如下:
notion image
对于Sigmoid,当的取值越来越大后,函数曲线变得越来越平缓,意味着此时的导数也越来越小。同样的,当的取值越来越小时,也有这个问题。仅仅在 取值为0附近时,导数 的取值较大。
均方差+Sigmoid的反向传播算法中,每一层向前递推都要乘以 ,得到梯度变化值。Sigmoid的这个曲线意味着在大多数时候梯度变化值很小,导致 更新到极值的速度较慢,也就是算法收敛速度较慢。那么有什么什么办法可以改进呢?
 

使用交叉熵损失函数+Sigmoid激活函数改进DNN算法收敛速度

换掉Sigmoid?这当然是一种选择。另一种常见的选择是用交叉熵损失函数来代替均方差损失函数。
二分类时每个样本的交叉熵损失函数的形式:
🐋 参数初始化
type
status
date
slug
summary
tags
category
icon
password
Property
 
 

默认初始化

前面的部分,我们使用正态分布来初始化权重值。如果不指定初始化方法, 框架将使用默认的随机初始化方法,对于中等难度的问题,这种方法通常很有效。
 

Xavier初始化

没有非线性的全连接层输出,对于该层输入及其相关权重
权重 都是从同一分布中独立抽取的。 此外,假设该分布具有零均值和方差。 注意,这并不意味着分布必须是高斯的,只是均值和方差需要存在。 现在,让我们假设层的输入也具有零均值和方差, 并且它们独立于并且彼此独立。 在这种情况下,可以按如下方式计算的平均值和方差:
保持方差不变的一种方法是设置 。 现在考虑反向传播过程,我们面临着类似的问题,尽管梯度是从更靠近输出的层传播的。 使用与前向传播相同的推断,我们可以看到,除非, 否则梯度的方差可能会增大,这使得我们进退两难:不可能同时满足这两个条件。 相反,只需满足:
🐋 多层感知机的实现
type
status
date
slug
summary
tags
category
icon
password
Property
 

多层感知机从零开始实现

 
 

多层感知机的简洁实现

 
 
 
notion image
 
🐋 正则化
type
status
date
slug
summary
tags
category
icon
password
Property
 
和普通的机器学习算法一样,DNN也会遇到过拟合的问题,需要考虑泛化
 

模型复杂性

notion image
notion image

代码

 
 
🐋 K折交叉验证
type
status
date
slug
summary
tags
category
icon
password
Property
 

K折划分

 

K-Flod训练

 
🐋 环境和分布偏移
type
status
date
slug
summary
tags
category
icon
password
Property
 
 
数据最初从哪里来?计划最终如何处理模型的输出? 通常情况下,开发人员会拥有一些数据且急于开发模型,而不关注这些基本问题。
许多失败的机器学习部署(即实际应用)都可以追究到这种方式。 有时,根据测试集的精度衡量,模型表现得非常出色。 但是当数据分布突然改变时,模型在部署中会出现灾难性的失败。 更隐蔽的是,有时模型的部署本身就是扰乱数据分布的催化剂。 举一个有点荒谬却可能真实存在的例子。 假设我们训练了一个贷款申请人违约风险模型,用来预测谁将偿还贷款或违约。 这个模型发现申请人的鞋子与违约风险相关(穿牛津鞋申请人会偿还,穿运动鞋申请人会违约)。 此后,这个模型可能倾向于向所有穿着牛津鞋的申请人发放贷款,并拒绝所有穿着运动鞋的申请人。
这种情况可能会带来灾难性的后果。 首先,一旦模型开始根据鞋类做出决定,顾客就会理解并改变他们的行为。 不久,所有的申请者都会穿牛津鞋,而信用度却没有相应的提高。 总而言之,机器学习的许多应用中都存在类似的问题: 通过将基于模型的决策引入环境,可能会破坏模型。
 

分布偏移的类型

首先,我们考虑数据分布可能发生变化的各种方式,以及为挽救模型性能可能采取的措施。 在一个经典的情景中,假设训练数据是从某个分布 中采样的, 但是测试数据将包含从不同分布 中抽取的未标记样本。 一个清醒的现实是:如果没有任何关于 和之间 相互关系的假设, 学习到一个分类器是不可能的。
考虑一个二元分类问题:区分狗和猫。 如果分布可以以任意方式偏移,那么我们的情景允许病态的情况, 即输入的分布保持不变: , 但标签全部翻转: 。 换言之,如果将来所有的“猫”现在都是狗,而我们以前所说的“狗”现在是猫。 而此时输入 的分布没有任何改变, 那么我们就不可能将这种情景与分布完全没有变化的情景区分开。
幸运的是,在对未来我们的数据可能发生变化的一些限制性假设下, 有些算法可以检测这种偏移,甚至可以动态调整,提高原始分类器的精度。

协变量偏移

在不同分布偏移中,协变量偏移可能是最为广泛研究的。 这里我们假设:虽然输入的分布可能随时间而改变, 但标签函数(即条件分布)没有改变。 统计学家称之为协变量偏移(covariate shift), 因为这个问题是由于协变量(特征)分布的变化而产生的。 虽然有时我们可以在不引用因果关系的情况下对分布偏移进行推断, 但在我们认为 导致 的情况下,协变量偏移是一种自然假设。
🐡 层与块
type
status
date
slug
summary
tags
category
icon
password
Property
 
 
 
为了实现复杂的网络,引入了神经网络的概念。 块(block)可以描述单个层、由多个层组成的组件或整个模型本身。 使用块进行抽象的一个好处是可以将一些块组合成更大的组件, 这一过程通常是递归的,如下图所示。 通过定义代码来按需生成任意复杂度的块, 可以通过简洁的代码实现复杂的神经网络。
notion image
从编程的角度来看,块由类(class)表示。 它的任何子类都必须定义一个将其输入转换为输出的前向传播函数,并且必须存储任何必需的参数(有些块不需要参数)。
为了计算梯度,块必须具有反向传播函数。 由于自动微分提供了一些后端实现,所以在定义我们自己的块时,只需要考虑前向传播函数和必需的参数。
 
 
回顾一下多层感知机:
生成一个网络,包含一个具有256个单元和ReLU激活函数的全连接隐藏层,然后是一个具有10个隐藏单元且不带激活函数的全连接输出层。
🐡 参数管理
type
status
date
slug
summary
tags
category
icon
password
Property
 
在选择了架构并设置了超参数后,就进入了训练阶段。 此时,我们的目标是找到使损失函数最小化的模型参数值。 经过训练后,我们将需要使用这些参数来做出未来的预测。 此外,有时我们希望提取参数,以便在其他环境中复用它们, 将模型保存下来,以便它可以在其他软件中执行, 或者为了获得科学的理解而进行检查。
之前我们只依靠深度学习框架来完成训练的工作, 而忽略了操作参数的具体细节。 这里将介绍以下内容:
  • 访问参数,用于调试、诊断和可视化。
  • 参数初始化。
  • 在不同模型组件间共享参数。
 
首先看一下具有单隐藏层的多层感知机:
 

参数访问

当通过Sequential类定义模型时, 我们可以通过索引来访问模型的任意层。 这就像模型是一个列表一样,每层的参数都在其属性中。 如下所示,可以检查第二个全连接层的参数:
🐡 自定义层
type
status
date
slug
summary
tags
category
icon
password
Property
 

不带参数的层

首先构造一个没有任何参数的自定义层。下面的CenteredLayer类要从其输入中减去均值。 要构建它,我们只需继承基础层类并实现前向传播功能
 
 

带参数的层

下面定义具有参数的层, 这些参数可以通过训练进行调整。 可以使用内置函数来创建参数,这些函数提供一些基本的管理功能。 比如管理访问、初始化、共享、保存和加载模型参数。 这样做的好处之一是:不需要为每个自定义层编写自定义的序列化程序。
 
现在实现自定义版本的全连接层,该层需要两个参数,一个用于表示权重,另一个用于表示偏置项。 在此实现中,使用修正线性单元作为激活函数。 该层需要输入参数:in_unitsunits,分别表示输入数和输出数。
🐡 读写文件
type
status
date
slug
summary
tags
category
icon
password
Property
 

加载和保存张量

对于单个张量,可以直接调用load和save函数分别读写它们
 
 

加载和保存模型参数

深度学习框架提供了内置函数来保存和加载整个网络。 需要注意的一个重要细节是,这将保存模型的参数而不是保存整个模型。 例如,如果有一个3层多层感知机,需要单独指定架构。 因为模型本身可以包含任意代码,所以模型本身难以序列化。 因此,为了恢复模型,需要用代码生成架构, 然后从磁盘加载参数。
接下来,将模型的参数存储在一个叫做“mlp.params”的文件中
为了恢复模型,实例化了原始多层感知机模型的一个备份。 这里不需要随机初始化模型参数,而是直接读取文件中存储的参数。
🐡 GPU加速
type
status
date
slug
summary
tags
category
icon
password
Property
 
使用nvidia-smi命令来查看显卡信息
notion image
 
 

计算设备

可以指定用于存储和计算的设备(CPU和GPU)。 默认情况下,张量是在内存中创建的,然后使用CPU计算它。在PyTorch中,CPU和GPU可以用torch.device('cpu')torch.device('cuda')表示。
  • cpu设备意味着所有物理CPU和内存,PyTorch的计算将尝试使用所有CPU核心
  • gpu设备只代表一个卡和相应的显存,如果有多个GPU,使用torch.device(f'cuda:{i}')来表示第块GPU( 从0开始),cuda:0cuda是等价的
 
🐡 异步计算
type
status
date
slug
summary
tags
category
icon
password
Property
 
 
计算机是高度并行的系统,由多个CPU核、多个GPU、多个处理单元组成。通常每个CPU核有多个线程,每个设备通常有多个GPU,每个GPU有多个处理单元。总之,可以同时处理许多不同的事情,并且通常是在不同的设备上。
不幸的是,Python并不善于编写并行和异步代码,至少在没有额外帮助的情况下不是好选择。归根结底,Python是单线程的,将来也是不太可能改变的。因此在诸多的深度学习框架中,MXNet和TensorFlow之类则采用了一种异步编程(asynchronous programming)模型来提高性能,而PyTorch则使用了Python自己的调度器来实现不同的性能权衡。
对于PyTorch来说,GPU操作在默认情况下是异步的。调用一个使用GPU的函数时,操作会排队到特定的设备上,但不一定要等到以后才执行。这允许并行执行更多的计算,包括在CPU或其他GPU上的操作。因此,了解异步编程如何工作,主动地减少计算需求和相互依赖,有助于开发更高效的程序,能够减少内存开销并提高处理器利用率。
 

通过后端异步处理

生成一个随机矩阵并将其相乘,在NumPy和PyTorch张量中都这样做,看看它们的区别:
NumPy点积是在CPU上执行的,PyTorch矩阵乘法是在GPU上执行的,后者的速度要快得多。
 
巨大的时间差距表明一定还有其他原因。默认情况下,GPU操作在PyTorch中是异步的。强制PyTorch在返回之前完成所有计算,这种强制说明了之前发生的情况:计算是由后端执行,而前端将控制权返回给了Python。