当前位置: 首页 > news >正文

专门做红酒的网站邯郸信息港发布信息

专门做红酒的网站,邯郸信息港发布信息,h5商城网站是什么意思,网站方案原则1.引言 在本文中#xff0c;我们将探讨神经网络的优化与初始化技术。随着神经网络深度的增加#xff0c;我们会遇到多种挑战。最关键的是确保网络中梯度流动的稳定性#xff0c;否则可能会遭遇梯度消失或梯度爆炸的问题。因此#xff0c;我们将深入探讨以下两个核心概念我们将探讨神经网络的优化与初始化技术。随着神经网络深度的增加我们会遇到多种挑战。最关键的是确保网络中梯度流动的稳定性否则可能会遭遇梯度消失或梯度爆炸的问题。因此我们将深入探讨以下两个核心概念网络参数的初始化和优化算法的选择。 本文的前半部分我们将介绍不同的参数初始化方法从最基本的初始化策略开始逐步深入到当前在极深网络中应用的高级技术。在后半部分我们将聚焦于优化算法的比较分析SGD、动量SGD以及Adam这几种优化器的性能差异。 首先让我们开始导入所需的标准库。 ## 标准库 import os import json import math import numpy as np import copy## 绘图所需导入 import matplotlib.pyplot as plt from matplotlib import cm %matplotlib inline from IPython.display import set_matplotlib_formats set_matplotlib_formats(svg, pdf) # 用于导出 import seaborn as sns sns.set()## 进度条 from tqdm.notebook import tqdm## PyTorch import torch import torch.nn as nn import torch.nn.functional as F import torch.utils.data as data import torch.optim as optim #我们将使用与教程3相同的set_seed函数以及路径变量DATASET_PATH和CHECKPOINT_PATH。如有必要请调整路径。# 数据集下载存放的文件夹路径例如MNIST DATASET_PATH ../data # 预训练模型保存的文件夹路径 CHECKPOINT_PATH ../saved_models/tutorial4# 设置种子的函数 def set_seed(seed):np.random.seed(seed)torch.manual_seed(seed)if torch.cuda.is_available():torch.cuda.manual_seed(seed)torch.cuda.manual_seed_all(seed) set_seed(42)# 确保在GPU上的所有操作都是确定性的如果使用以实现可复现性 torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False# 获取将在此笔记本中使用整个过程中使用的设备 device torch.device(cpu) if not torch.cuda.is_available() else torch.device(cuda:0) print(Using device, device) 使用设备 cuda:0 ##在本文的最后部分我们将使用三种不同的优化器训练模型。以下是这些模型的预训练版本下载链接。import urllib.request from urllib.error import HTTPError # 存储本教程预训练模型的Github URL base_url https://raw.githubusercontent.com/phlippe/saved_models/main/tutorial4/ # 需要下载的文件 pretrained_files [FashionMNIST_SGD.config, FashionMNIST_SGD_results.json, FashionMNIST_SGD.tar,FashionMNIST_SGDMom.config, FashionMNIST_SGDMom_results.json, FashionMNIST_SGDMom.tar,FashionMNIST_Adam.config, FashionMNIST_Adam_results.json, FashionMNIST_Adam.tar ] # 如果检查点路径不存在则创建 os.makedirs(CHECKPOINT_PATH, exist_okTrue)# 对于每个文件检查它是否已经存在。如果不存在尝试下载。 for file_name in pretrained_files:file_path os.path.join(CHECKPOINT_PATH, file_name)if not os.path.isfile(file_path):file_url base_url file_nameprint(f正在下载 {file_url}...)try:urllib.request.urlretrieve(file_url, file_path)except HTTPError as e:print(下载过程中出现问题。请尝试从GDrive文件夹下载文件或联系作者并附上包括以下错误的完整输出\n, e)2.准备工作 在本文中我们将使用一个深度全连接网络与我们之前的文章类似。我们还将再次将网络应用于FashionMNIST我们首先加载FashionMNIST数据集 from torchvision.datasets import FashionMNIST from torchvision import transforms# 应用于每张图片的转换 首先将它们转换为张量然后使用均值为0和标准差为1进行归一化 transform transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.2861,), (0.3530,)) ])# 加载训练数据集。我们需要将其分割为训练部分和验证部分 train_dataset FashionMNIST(rootDATASET_PATH, trainTrue, transformtransform, downloadTrue) train_set, val_set torch.utils.data.random_split(train_dataset, [50000, 10000])# 加载测试集 test_set FashionMNIST(rootDATASET_PATH, trainFalse, transformtransform, downloadTrue)# 我们定义一组数据加载器我们稍后可以用于不同的目的。 # 注意对于实际训练模型我们将使用具有较小批量大小的不同数据加载器。 train_loader data.DataLoader(train_set, batch_size1024, shuffleTrue, drop_lastFalse) val_loader data.DataLoader(val_set, batch_size1024, shuffleFalse, drop_lastFalse) test_loader data.DataLoader(test_set, batch_size1024, shuffleFalse, drop_lastFalse)与之前的文章相比我们更改了归一化转换transforms.Normalize的参数。现在归一化的设计是让我们在像素上获得预期的均值为0和标准差为1。这将特别适用于我们下面将要讨论的初始化问题因此我们在这里进行更改。应当指出在大多数分类任务中两种归一化技术介于-1和1之间或均值为0和标准差为1都已被证明效果良好。我们可以通过在原始图像上确定均值和标准差来计算归一化参数 print(Mean, (train_dataset.data.float() / 255.0).mean().item()) print(Std, (train_dataset.data.float() / 255.0).std().item())输出显示为 Mean 0.2860923707485199 Std 0.3530242443084717我们可以通过查看单个批次的统计数据来验证转换 imgs, _ next(iter(train_loader)) print(fMean: {imgs.mean().item():5.3f}) print(fStandard deviation: {imgs.std().item():5.3f}) print(fMaximum: {imgs.max().item():5.3f}) print(fMinimum: {imgs.min().item():5.3f})输出 Mean: 0.002 Standard deviation: 1.001 Maximum: 2.022 Minimum: -0.810请注意最大值和最小值不再是1和-1而是向正值偏移。这是因为FashionMNIST包含许多黑色像素与MNIST类似。接下来我们将创建一个线性神经网络。 class BaseNetwork(nn.Module):def __init__(self, act_fn, input_size784, num_classes10, hidden_sizes[512, 256, 256, 128]):输入:act_fn - 应该在网络中作为非线性使用的激活函数的对象。input_size - 输入图像的像素尺寸num_classes - 我们想要预测的类别数量hidden_sizes - 一个整数列表指定神经网络中隐藏层的大小super().__init__()# 根据指定的隐藏大小创建网络layers []layer_sizes [input_size] hidden_sizesfor layer_index in range(1, len(layer_sizes)):layers [nn.Linear(layer_sizes[layer_index-1], layer_sizes[layer_index]),act_fn]layers [nn.Linear(layer_sizes[-1], num_classes)]self.layers nn.ModuleList(layers) # 模块列表将模块列表注册为子模块例如用于参数self.config {act_fn: act_fn.__class__.__name__, input_size: input_size, num_classes: num_classes, hidden_sizes: hidden_sizes}def forward(self, x):x x.view(x.size(0), -1)for l in self.layers:x l(x)return x对于激活函数我们使用PyTorch的torch.nn库而不是自己实现。当然我们也定义了一个Identity激活函数。尽管这种激活函数会大大限制网络的建模能力但我们将在我们的初始化讨论的第一步中使用它(为了简化)。 class Identity(nn.Module):def forward(self, x):return xact_fn_by_name {tanh: nn.Tanh,relu: nn.ReLU,identity: Identity }最后我们定义了一些绘图函数我们将在讨论中使用它们。这些函数帮助我们 (1)可视化网络内部的权重/参数分布 (2)可视化不同层的参数接收的梯度以及 (3)激活值即线性层的输出。 # 绘制值的分布图 def plot_dists(val_dict, colorC0, xlabelNone, statcount, use_kdeTrue):columns len(val_dict) # 图表的列数等于val_dict的键的数量fig, ax plt.subplots(1, columns, figsize(columns*3, 2.5)) # 创建子图fig_index 0for key in sorted(val_dict.keys()): # 遍历val_dict的键key_ax ax[fig_index % columns] # 获取当前的子图轴sns.histplot(val_dict[key], axkey_ax, colorcolor, bins50, statstat, # 绘制直方图kdeuse_kde and ((val_dict[key].max()-val_dict[key].min())1e-8)) # 如果有方差则绘制核密度估计key_ax.set_title(f{key} (r(%i $\to$ %i) % (val_dict[key].shape[1], val_dict[key].shape[0]) if len(val_dict[key].shape) 1 else )) # 设置标题if xlabel is not None:key_ax.set_xlabel(xlabel) # 设置x轴标签fig_index 1fig.subplots_adjust(wspace0.4) # 调整子图之间的间隔return fig# 可视化模型权重分布 def visualize_weight_distribution(model, colorC0):weights {}for name, param in model.named_parameters(): # 遍历模型的参数if name.endswith(.bias): # 如果是偏置则跳过continuekey_name fLayer {name.split(.)[1]} # 为权重创建键名weights[key_name] param.detach().view(-1).cpu().numpy() # 将权重转换为numpy数组# 绘图fig plot_dists(weights, colorcolor, xlabelWeight vals) # 使用plot_dists函数绘制权重分布图fig.suptitle(Weight distribution, fontsize14, y1.05) # 设置图表标题plt.show() # 显示图表plt.close() # 关闭图表# 可视化模型梯度分布 def visualize_gradients(model, colorC0, print_varianceFalse):# 设置模型为评估模式model.eval()small_loader data.DataLoader(train_set, batch_size1024, shuffleFalse) # 创建数据加载器imgs, labels next(iter(small_loader)) # 获取一批数据imgs, labels imgs.to(device), labels.to(device) # 将数据移动到设备上# 将一批数据通过网络前向传播并计算权重的梯度model.zero_grad() # 清空梯度preds model(imgs) # 前向传播loss F.cross_entropy(preds, labels) # 计算交叉熵损失loss.backward() # 反向传播计算梯度# 限制可视化为权重参数不包括偏置以减少图表数量grads {name: params.grad.view(-1).cpu().clone().numpy() for name, params in model.named_parameters() if weight in name}model.zero_grad() # 清空梯度# 绘图fig plot_dists(grads, colorcolor, xlabelGrad magnitude) # 使用plot_dists函数绘制梯度分布图fig.suptitle(Gradient distribution, fontsize14, y1.05) # 设置图表标题plt.show() # 显示图表plt.close() # 关闭图表if print_variance: # 如果需要打印方差for key in sorted(grads.keys()): # 遍历梯度字典的键print(f{key} - Variance: {np.var(grads[key])}) # 打印方差# 可视化模型激活值分布 def visualize_activations(model, colorC0, print_varianceFalse):model.eval() # 设置模型为评估模式small_loader data.DataLoader(train_set, batch_size1024, shuffleFalse) # 创建数据加载器imgs, labels next(iter(small_loader)) # 获取一批数据imgs, labels imgs.to(device), labels.to(device) # 将数据移动到设备上# 将一批数据通过网络前向传播并计算权重的梯度feats imgs.view(imgs.shape[0], -1) # 重塑特征activations {}with torch.no_grad(): # 不计算梯度for layer_index, layer in enumerate(model.layers): # 遍历模型的每一层feats layer(feats) # 应用层if isinstance(layer, nn.Linear): # 如果是线性层activations[fLayer {layer_index}] feats.view(-1).detach().cpu().numpy() # 将激活值转换为numpy数组# 绘图fig plot_dists(activations, colorcolor, statdensity, xlabelActivation vals) # 使用plot_dists函数绘制激活值分布图fig.suptitle(Activation distribution, fontsize14, y1.05) # 设置图表标题plt.show() # 显示图表plt.close() # 关闭图表if print_variance: # 如果需要打印方差for key in sorted(activations.keys()): # 遍历激活值字典的键print(f{key} - Variance: {np.var(activations[key])}) # 打印方差3.初始化 在深入讨论神经网络的初始化问题之前有必要指出关于这一主题网络上已经有许多精彩的博客文章例如deeplearning.ai提供的资源或者那些更侧重于数学分析的文章。如果在阅读完本教程后仍有疑惑我们建议您也浏览一下这些博客文章以获得更深入的理解。 初始化神经网络时我们希望其具备一些特定的属性。首先输入数据的方差应能通过整个网络传递到输出层以保证输出神经元具有相似的标准差。如果我们在网络深层发现方差逐渐消失那么模型将难以优化因为下一层的输入将变得几乎等同于一个恒定值。同样地如果方差随着网络深度的增加而增大那么梯度可能会变得非常大导致数值稳定性问题。其次我们希望在初始化时各层的梯度分布具有相同的方差。如果第一层得到的梯度远小于最后一层我们可能就会在选择合适的学习速率时遇到困难。 为了寻找合适的初始化方法我们首先以一个没有激活函数的线性神经网络作为起点进行分析即网络中仅使用恒等激活函数。之所以这样做是因为不同的激活函数对初始化方法有特定的要求我们可以根据所使用的激活函数调整初始化策略。 model BaseNetwork(act_fnIdentity()).to(device)3.1 常数初始化 接下来我们考虑一种最简单的初始化方法——常数初始化。直观上将所有权重设置为零并不理想因为这会导致传播的梯度也为零。但是如果我们将所有权重设置为一个接近零的非零常数情况会如何呢为了探究这一点我们可以编写一个函数来实现这一初始化并可视化梯度的分布情况。 定义了一个名为const_init的函数它接受一个模型和一个默认为0的常数值c将模型中所有的参数权重填充为这个常数值。然后我们调用这个函数将模型的权重初始化为0.005接着使用visualize_gradients和visualize_activations函数来可视化梯度和激活值的分布并打印出它们的方差。这有助于我们理解在这种初始化策略下网络的梯度和激活值的行为。 def const_init(model, c0.0):for name, param in model.named_parameters():param.data.fill_(c)const_init(model, c0.005) visualize_gradients(model) visualize_activations(model, print_varianceTrue)Layer 0 - Variance: 2.058276 Layer 2 - Variance: 13.489119 Layer 4 - Variance: 22.100567 Layer 6 - Variance: 36.209572 Layer 8 - Variance: 14.831439从我们的观察来看只有第一层和最后一层展现出了多样化的梯度分布而中间的三层则显示出所有权重具有相同梯度的现象注意这个值并不为零但往往非常接近零。如果用相同值初始化的参数最终获得了相同的梯度这就意味着这些参数的值将始终一致。这样的结果会让我们网络中的这一层失去作用实际上将我们网络的参数数量减少到了单一的一个值。因此我们不能采用常数值初始化的方法来训练我们的网络。 3.2.关于方差的恒定性 在上述实验中我们已经发现单一的常数值初始化策略是行不通的。那么如果我们改为从诸如高斯分布的某种概率分布中随机采样来初始化参数情况会怎样呢最直接的方法可能是为网络中的所有层选择一个相同的方差值。接下来我们将实现这种方法并可视化各层的激活分布情况。 def var_init(model, std0.01):for name, param in model.named_parameters():param.data.normal_(stdstd)var_init(model, std0.01) visualize_activations(model, print_varianceTrue)在神经网络的层与层之间激活值的方差呈现出逐渐减小的趋势到了最后一层方差几乎趋近于零。这种情况下一个可能的解决办法是增加标准差的数值。通过提高初始化时的标准差我们可以尝试维持网络深层的激活方差避免其在传播过程中消失。 var_init(model, std0.1) visualize_activations(model, print_varianceTrue)通过使用更高的标准差进行初始化我们可以观察到网络各层激活值的分布情况特别是它们的方差以评估这种策略是否有效。这种方法可能有助于解决深层网络中的梯度消失问题但同时也要警惕不要导致梯度爆炸这需要我们在实践中仔细调整和平衡。 3.3.如何找到合适的初始化值 从我们之前的实验中我们可以看到需要从某个概率分布中对权重进行采样但具体选择哪个分布我们还不确定。下一步我们将尝试从激活值分布的角度出发寻找最优的初始化方法。为此我们提出两个要求 1激活值的均值应该为零。 2激活值的方差应该在每一层都保持不变。 假设我们要为以下层设计一个初始化方法要设计一个满足上述两个要求的初始化方法我们需要考虑权重矩阵和激活函数的特性。对于一个全连接层如果使用恒等激活函数即线性激活函数输出的均值和方差将取决于输入的均值和方差以及权重矩阵。为了使激活值的均值为零我们可以选择一个合适的均值。而为了保持激活值的方差在每一层都相同我们需要选择一个合适的标准差。 一种常见的方法是使用与输入维度的平方根成反比的标准差。这样无论输入的维度如何变化权重的标准差都会相应调整以保持激活值的方差大致相同。这种方法通常被称为Xavier初始化或Glorot初始化。 我们的目标是让每个元素的方差与输入的方差相同即每个权重更新后的方差应该保持与输入数据的方差一致。这是为了确保在多层网络中信息能够稳定地从前层传递到后层避免出现梯度消失或爆炸的问题。 在数学上如果我们考虑一个全连接层 y W x b , y ∈ R d y , x ∈ R d x yWxb,y\in\mathbb{R}^{d_y},x\in\mathbb{R}^{d_x} yWxb,y∈Rdy​,x∈Rdx​其输出可以表示为 Var ( y i ) Var ( x i ) σ x 2 \text{Var}(y_i)\text{Var}(x_i)\sigma_x^{2} Var(yi​)Var(xi​)σx2​其中 是权重矩阵 W W W 是输入 X X X 是偏置 b b b。对于激活函数 σ \sigma σ 激活输出 a a a以表示为 a σ ( z ) a \sigma(z) aσ(z) 。我们希望 a a a的方差保持与 x x x的方差相同。 对于偏置项 b b b通常初始化为0因为它们不影响激活值的方差。对于权重 W W W如果我们假设输入 x x x的方差为 σ x 2 \sigma_x^2 σx2​并且我们希望输出 y y y的方差也为 σ x 2 \sigma_x^2 σx2​那么我们可以通过以下方式初始化权重 W ∼ N ( 0 , σ x 2 n ) W \sim \mathcal{N}(0, \frac{\sigma_x^2}{n}) W∼N(0,nσx2​​) 这里是输入特征的数量。这种初始化方法确保了在没有激活函数的情况下输出 的方差与输入 的方差相同。 如果使用激活函数我们需要根据激活函数的特性调整初始化策略。例如对于ReLU激活函数He初始化是一种流行的选择它使用稍微不同的公式来初始化权重以保持激活值的方差。 在实践中我们通常会使用现成的初始化方法如Xavier初始化适用于tanh激活函数或He初始化适用于ReLU激活函数这些方法已经考虑了保持激活值方差的需要。 接下来我们需要计算用于初始化权重参数所需的方差。在计算过程中我们需要使用以下方差规则给定两个独立的随机变量它们的乘积的方差是 Var [ X Y ] E [ X 2 ] ⋅ E [ Y 2 ] − ( E [ X ] ⋅ E [ Y ] ) 2 \text{Var}[XY] \text{E}[X^2] \cdot \text{E}[Y^2] - (\text{E}[X] \cdot \text{E}[Y])^2 Var[XY]E[X2]⋅E[Y2]−(E[X]⋅E[Y])2 这里 x x x和 y y y不是指特定的随机变量而是任意随机变量。 所需权重 W W W的方差 Var [ W i j ] \text{Var}[W_{ij}] Var[Wij​] 计算如下 y i ∑ j w i j x j 单个输出神经元的计算不含偏置项 Var ( y i ) σ x 2 Var ( ∑ j w i j x j ) ∑ j Var ( w i j x j ) 输入和权重是彼此独立的。  ∑ j Var ( w i j ) ⋅ Var ( x j ) 方差规则见上文期望值为零 d x ⋅ Var ( w i j ) ⋅ Var ( x j ) 对于所有的 d x 元素方差相等 σ x 2 ⋅ d x ⋅ Var ( w i j ) ⇒ Var ( w i j ) σ W 2 1 d x \begin{split}\begin{split} y_i \sum_{j} w_{ij}x_{j}\hspace{10mm}\\\text{单个输出神经元的计算不含偏置项}\\ \text{Var}(y_i) \sigma_x^{2} \text{Var}\left(\sum_{j} w_{ij}x_{j}\right)\\ \sum_{j} \text{Var}(w_{ij}x_{j}) \hspace{10mm}\\\text{输入和权重是彼此独立的。 }\\ \sum_{j} \text{Var}(w_{ij})\cdot\text{Var}(x_{j}) \hspace{10mm}\\\text{方差规则见上文期望值为零}\\ d_x \cdot \text{Var}(w_{ij})\cdot\text{Var}(x_{j}) \hspace{10mm}\\\text{对于所有的$d_x$元素方差相等}\\ \sigma_x^{2} \cdot d_x \cdot \text{Var}(w_{ij})\\ \Rightarrow \text{Var}(w_{ij}) \sigma_{W}^2 \frac{1}{d_x}\\ \end{split}\end{split} yi​单个输出神经元的计算不含偏置项Var(yi​)σx2​输入和权重是彼此独立的。 方差规则见上文期望值为零对于所有的dx​元素方差相等⇒Var(wij​)σW2​​j∑​wij​xj​Var(j∑​wij​xj​)j∑​Var(wij​xj​)j∑​Var(wij​)⋅Var(xj​)dx​⋅Var(wij​)⋅Var(xj​)σx2​⋅dx​⋅Var(wij​)dx​1​​​ 基于上述理论我们的权重初始化策略应该是使用一个具有适当方差的分布。具体来说权重的方差应该是输入维度倒数的方差。这样的初始化有助于保持网络各层激活值的方差大致相同从而有助于梯度在网络中的稳定流动。 def equal_var_init(model):for name, param in model.named_parameters():if name.endswith(.bias): # 如果是偏置项则初始化为0param.data.fill_(0)else:# 对权重使用特定的标准差进行正态分布初始化# 标准差为1除以输入特征数量的平方根param.data.normal_(std1.0/math.sqrt(param.shape[1]))# 应用Equal Variance Initialization到模型 equal_var_init(model)# 可视化权重分布 visualize_weight_distribution(model)# 可视化激活值分布并打印每层激活值的方差 visualize_activations(model, print_varianceTrue)Layer 0 - Variance: 1.020319 Layer 2 - Variance: 1.049295 Layer 4 - Variance: 1.031418 Layer 6 - Variance: 1.025792 Layer 8 - Variance: 0.872356正如我们所预期的方差确实在各层之间保持恒定。请注意我们的初始化方法并不限制我们只能使用正态分布而是允许使用任何具有0均值和 2 n x n next \frac{2}{n_x n_{\text{next}}} nx​nnext​2​或者 1 d x \frac{1}{d_x} dx​1​方差的其他分布。通常你会看到使用均匀分布进行初始化。使用均匀分布而不是正态分布的一个小小好处是我们可以排除初始化非常大或非常小的权重的可能性。 除了激活值的方差之外我们希望稳定的另一个方差是梯度的方差。这确保了深层网络的稳定优化。结果表明我们可以从 Δ x W Δ y \Delta xW\Delta y ΔxWΔy开始进行与上述相同的计算并得出我们应该使用 1 d y \frac{1}{d_y} dy​1​来初始化我们的层的结论其中 是输出神经元的数量。你可以将这个计算作为练习来做或者在这个博客文章中查看详尽的解释。作为两种约束之间的折衷Glorot和Bengio2010年提议使用这两个值的调和平均值。这引导我们得到了众所周知的Xavier初始化 W ∼ N ( 0 , 2 d x d y ) W\sim \mathcal{N}\left(0,\frac{2}{d_xd_y}\right) W∼N(0,dx​dy​2​) 如果我们使用均匀分布来初始化权重我们会这样设置 W ∼ U [ − 6 d x d y , 6 d x d y ] W\sim U\left[-\frac{\sqrt{6}}{\sqrt{d_xd_y}}, \frac{\sqrt{6}}{\sqrt{d_xd_y}}\right] W∼U[−dx​dy​ ​6 ​​,dx​dy​ ​6 ​​] def xavier_init(model):for name, param in model.named_parameters():if name.endswith(.bias):param.data.fill_(0)else:bound math.sqrt(6)/math.sqrt(param.shape[0]param.shape[1])param.data.uniform_(-bound, bound)xavier_init(model) visualize_gradients(model, print_varianceTrue) visualize_activations(model, print_varianceTrue)layers.0.weight - Variance: 0.000436 layers.2.weight - Variance: 0.000747 layers.4.weight - Variance: 0.001149 layers.6.weight - Variance: 0.001744 layers.8.weight - Variance: 0.017655Layer 0 - Variance: 1.216592 Layer 2 - Variance: 1.719161 Layer 4 - Variance: 1.714506 Layer 6 - Variance: 2.224779 Layer 8 - Variance: 5.297660Xavier初始化方法旨在保持网络中梯度和激活值方差的一致性。我们注意到输出层的方差之所以显著增加是因为输入层和输出层的维度存在较大差异。例如输入层可能有1024个神经元而输出层可能仅有10个神经元。目前我们的讨论假设了激活函数是线性的。引入非线性激活函数如tanh或ReLU会改变激活值的分布进而影响梯度的方差。 在基于tanh的网络中一个普遍的假设是在训练初期对于接近零的小值tanh函数可以近似为线性函数。这意味着在训练的早期阶段我们不需要调整初始化策略的计算。然而随着训练的进行权重的更新可能会导致激活值的分布发生变化从而使得tanh的非线性特性变得更加显著。 为了验证我们的初始化策略是否适用于非线性激活函数我们可以在训练的早期阶段检查激活值的分布。如果激活值主要集中在tanh的线性区域即接近零点那么我们的初始化方法可能仍然有效。如果激活值分布远离零点我们可能需要考虑调整初始化策略以适应激活函数的非线性特性。 model BaseNetwork(act_fnnn.Tanh()).to(device) xavier_init(model) visualize_gradients(model, print_varianceTrue) visualize_activations(model, print_varianceTrue)layers.0.weight - Variance: 0.000016 layers.2.weight - Variance: 0.000027 layers.4.weight - Variance: 0.000036 layers.6.weight - Variance: 0.000049 layers.8.weight - Variance: 0.000455Layer 0 - Variance: 1.295969 Layer 2 - Variance: 0.583388 Layer 4 - Variance: 0.291432 Layer 6 - Variance: 0.265237 Layer 8 - Variance: 0.274929尽管随着深度的增加方差有所减小但很明显激活值的分布更加集中在低值上。因此如果我们进一步加深网络方差将稳定在0.25左右。因此我们可以得出结论Xavier初始化对于Tanh网络效果很好。但是对于ReLU网络呢在这里我们不能采用之前对于小值时非线性趋近线性的假设。ReLU激活函数按期望将一半的输入设置为0因此输入的期望值也不是零。然而只要 W 0 W0 W0 和 b 0 b0 b0 输出的期望值就是零。ReLU初始化的计算与恒等激活函数不同之处在于确定权重的标准差 Var ( w i j x j ) \text{Var}(w_{ij}x_{j}) Var(wij​xj​) Var ( w i j x j ) E [ w i j 2 ] ⏟ Var ( w i j ) E [ x j 2 ] − E [ w i j ] 2 ⏟ 0 E [ x j ] 2 Var ( w i j ) E [ x j 2 ] \text{Var}(w_{ij}x_{j})\underbrace{\mathbb{E}[w_{ij}^2]}_{\text{Var}(w_{ij})}\mathbb{E}[x_{j}^2]-\underbrace{\mathbb{E}[w_{ij}]^2}_{0}\mathbb{E}[x_{j}]^2\text{Var}(w_{ij})\mathbb{E}[x_{j}^2] Var(wij​xj​)Var(wij​) E[wij2​]​​E[xj2​]−0 E[wij​]2​​E[xj​]2Var(wij​)E[xj2​] 如果我们现在假设 是前一层经过ReLU激活函数的输出即 我们可以按照以下方式计算期望值 E [ x 2 ] E [ max ⁡ ( 0 , y ~ ) 2 ] 1 2 E [ y ~ 2 ] y ~ 是以零为中心且对称的 1 2 Var ( y ~ ) \begin{split}\begin{split} \mathbb{E}[x^2] \mathbb{E}[\max(0,\tilde{y})^2]\\ \frac{1}{2}\mathbb{E}[{\tilde{y}}^2]\hspace{2cm}\tilde{y}\text{ 是以零为中心且对称的}\\ \frac{1}{2}\text{Var}(\tilde{y}) \end{split}\end{split} E[x2]​E[max(0,y~​)2]21​E[y~​2]y~​ 是以零为中心且对称的21​Var(y~​)​​ 由于ReLU函数的定义为 max ⁡ ( 0 , y ~ ) \max(0, \tilde{y}) max(0,y~​)它将所有负值置为0而所有正值保持不变。因此对于输入 y ~ \tilde{y} y~​的任意小的正期望 μ y ~ \mu_{\tilde{y}} μy~​​输出 y y y 的期望 μ y ~ \mu_{\tilde{y}} μy~​​将是 μ y E [ y ] E [ max ⁡ ( 0 , y ~ ) ] \mu_y \mathbb{E}[y] \mathbb{E}[\max(0, \tilde{y})] μy​E[y]E[max(0,y~​)] 由于 y ~ \tilde{y} y~​的负部分被置为0只有当 y ~ \tilde{y} y~​大于0时它才对期望有贡献。假设 y ~ \tilde{y} y~​的概率密度函数是对称的那么其正负部分的期望将抵消只有正值部分对期望有贡献。因此我们可以简化计算为 μ y ∫ 0 ∞ y ~ p ( y ~ ) d y ~ \mu_y \int_0^\infty \tilde{y} p(\tilde{y}) d\tilde{y} μy​∫0∞​y~​p(y~​)dy~​ 这里 p ( y ~ ) p(\tilde{y}) p(y~​)是 的概率密度函数。如果 是从标准正态分布 初始化的那么 μ y σ y ~ 2 π \mu_y \sigma_{\tilde{y}} \sqrt{\frac{2}{\pi}} μy​σy~​​π2​ ​ 这个结果表明即使输入 y ~ \tilde{y} y~​的期望是0经过ReLU激活函数后输出 的期望也会是一个正的小数值。这个正值来自于正态分布的正尾部分的积分。 在初始化权重时我们需要考虑到这一点以确保在ReLU激活下网络的输出和梯度的期望保持在合理的范围内。这就是为什么He初始化也称为Kaiming初始化为ReLU激活专门设计了权重的初始化策略。 因此我们发现在方程中有一个额外的1/2因子所以我们期望的权重方差变为 。这给我们提供了Kaiming初始化见He, K. 等人 (2015) 的论文。请注意Kaiming初始化不使用输入和输出大小之间的调和平均值。在他们的论文第2.2节反向传播最后一段中他们争论说使用 或 都可以在整个网络中得到稳定的梯度并且只依赖于网络的整体输入和输出大小。因此我们这里只使用输入 def kaiming_init(model):for name, param in model.named_parameters():if name.endswith(.bias):param.data.fill_(0)elif name.startswith(layers.0): # The first layer does not have ReLU applied on its inputparam.data.normal_(0, 1/math.sqrt(param.shape[1]))else:param.data.normal_(0, math.sqrt(2)/math.sqrt(param.shape[1]))model BaseNetwork(act_fnnn.ReLU()).to(device) kaiming_init(model) visualize_gradients(model, print_varianceTrue) visualize_activations(model, print_varianceTrue)layers.0.weight - Variance: 0.000075 layers.2.weight - Variance: 0.000108 layers.4.weight - Variance: 0.000185 layers.6.weight - Variance: 0.000444 layers.8.weight - Variance: 0.005548Layer 0 - Variance: 1.012342 Layer 2 - Variance: 1.092432 Layer 4 - Variance: 1.268176 Layer 6 - Variance: 1.193706 Layer 8 - Variance: 1.760064Kaiming初始化通过特别考虑ReLU激活函数的特性确保了在基于ReLU的网络中权重的方差能够在每一层保持稳定。这种初始化方法对于保持深层网络在训练过程中梯度的稳定性至关重要。 然而对于其他变体的ReLU激活函数比如Leaky-ReLU其中负值不会被置为零而是乘以一个小的正斜率例如0.01我们需要对Kaiming初始化的方差因子进行调整。这是因为Leaky-ReLU的输出不会像标准的ReLU那样有一半的零值因此期望值和方差的计算会有所不同。 PyTorch框架提供了一个内置函数 calculate_gain它可以根据激活函数的不同自动计算所需的初始化增益。这个函数可以自动为Leaky-ReLU等激活函数计算合适的初始化因子从而简化了初始化过程。 import torch.nn.init as init# 假设我们使用的是Leaky-ReLU激活函数 def leaky_relu_gain(negative_slope0.01):# 使用PyTorch的calculate_gain函数计算Leaky-ReLU的增益return init.calculate_gain(leaky_relu, negative_slope)# 计算Leaky-ReLU的增益 gain leaky_relu_gain() std gain / math.sqrt(fan_in) # fan_in是输入特征的数量# 使用计算出的增益来初始化权重 for param in model.parameters():init.normal_(param.data, mean0.0, stdstd)4.优化算法 除了初始化之外为深度神经网络选择一个合适的优化算法也是一个重要的选择。在深入研究这些算法之前我们应该定义训练模型的代码。 # 根据模型路径和名称获取配置文件的路径 def _get_config_file(model_path, model_name):return os.path.join(model_path, model_name .config)# 根据模型路径和名称获取模型文件的路径 def _get_model_file(model_path, model_name):return os.path.join(model_path, model_name .tar)# 根据模型路径和名称获取结果文件的路径 def _get_result_file(model_path, model_name):return os.path.join(model_path, model_name _results.json)# 加载模型 def load_model(model_path, model_name, netNone):# 构造配置文件和模型文件的路径config_file, model_file _get_config_file(model_path, model_name), _get_model_file(model_path, model_name)# 确保配置文件和模型文件存在assert os.path.isfile(config_file), f找不到配置文件\{config_file}\。请确认路径正确并且模型配置已存储在此位置。assert os.path.isfile(model_file), f找不到模型文件\{model_file}\。请确认路径正确并且模型已存储在此位置。# 读取配置文件with open(config_file, r) as f:config_dict json.load(f)# 如果没有提供网络结构则根据配置文件创建网络if net is None:act_fn_name config_dict[act_fn].pop(name).lower()assert act_fn_name in act_fn_by_name, f未知的激活函数\{act_fn_name}\。请将其添加到\act_fn_by_name\字典中。act_fn act_fn_by_name[act_fn_name]()net BaseNetwork(act_fnact_fn, **config_dict)# 加载模型状态net.load_state_dict(torch.load(model_file))return net# 保存模型 def save_model(model, model_path, model_name):config_dict model.config# 创建模型保存路径os.makedirs(model_path, exist_okTrue)config_file, model_file _get_config_file(model_path, model_name), _get_model_file(model_path, model_name)# 保存配置文件和模型状态with open(config_file, w) as f:json.dump(config_dict, f)torch.save(model.state_dict(), model_file)# 训练模型 def train_model(net, model_name, optim_func, max_epochs50, batch_size256, overwriteFalse):在FashionMNIST的训练集上训练模型输入net - BaseNetwork类型的对象model_name - str模型名称用于创建检查点名称max_epochs - 我们想要最大训练的周期数patience - 如果在#patience个周期内验证集上的性能没有改善我们将提前停止训练batch_size - 训练中使用的批次大小overwrite - 确定如何处理已经存在检查点的情况。如果为True将被覆盖。否则我们将跳过训练。# 省略了部分代码...由于代码过长这里省略了部分内容实际使用时不应省略# 测试模型 def test_model(net, data_loader):在指定的数据集上测试模型。输入net - 训练好的BaseNetwork类型的模型data_loader - 要在其上测试的数据集的DataLoader对象验证或测试net.eval()true_preds, count 0., 0for imgs, labels in data_loader:imgs, labels imgs.to(device), labels.to(device)with torch.no_grad():preds net(imgs).argmax(dim-1)true_preds (preds labels).sum().item()count labels.shape[0]test_acc true_preds / countreturn test_acc首先我们需要理解优化器实际上是做什么的。优化器负责根据梯度更新网络的参数。因此我们实际上实现了一个函数 w t f ( w t − 1 , g t , . . . ) w^{t} f(w^{t-1}, g^{t}, ...) wtf(wt−1,gt,...)其中 是时间步 t 的参数 g t ∇ w ( t − 1 ) L ( t ) g^{t} \nabla_{w^{(t-1)}} \mathcal{L}^{(t)} gt∇w(t−1)​L(t)是时间步 t 的梯度。这个函数的常见额外参数是学习率这里用 η \eta η 表示。通常学习率可以看作是更新的“步长”。较高的学习率意味着我们更大幅度地根据梯度方向改变权重较小的学习率意味着我们采取更短的步长。 由于大多数优化器只在 f 的实现上有所不同我们可以在PyTorch中定义一个优化器的模板如下。我们输入模型的参数和一个学习率。函数 zero_grad 将所有参数的梯度设置为零这是在调用 loss.backward() 之前我们必须做的。最后step() 函数告诉优化器根据它们的梯度更新所有权重。模板设置如下 class OptimizerTemplate:# 初始化函数接受模型的参数和学习率def __init__(self, params, lr):self.params list(params) # 将传入的参数转换为列表self.lr lr # 学习率# 清零梯度的函数def zero_grad(self):# 遍历所有参数for p in self.params:# 如果参数的梯度存在if p.grad is not None:p.grad.detach_() # 对于二阶优化器这很重要p.grad.zero_() # 将梯度置为零# 应用更新步骤的函数使用torch.no_grad()上下文管理器来禁用梯度计算torch.no_grad()def step(self):# 遍历所有参数for p in self.params:# 如果参数没有梯度则跳过if p.grad is None:continueself.update_param(p) # 更新参数# 更新参数的函数需要在具体的优化器子类中实现def update_param(self, p):raise NotImplementedError(Parameter update method should be implemented in optimizer-specific classes)我们将要实现的第一个优化器是标准的随机梯度下降SGD。SGD使用以下公式更新参数 w ( t ) w ( t − 1 ) − η ⋅ g ( t ) \begin{split} w^{(t)} w^{(t-1)} - \eta \cdot g^{(t)} \end{split} w(t)​w(t−1)−η⋅g(t)​ class SGD(OptimizerTemplate):# 初始化函数调用父类的初始化函数def __init__(self, params, lr):super().__init__(params, lr)# 实现SGD参数更新的方法def update_param(self, p):# 计算参数更新的值这里是根据SGD的更新规则p_update -self.lr * p.grad# 原地更新参数即直接在原参数上减去计算出的更新值# 使用add_()方法可以节省内存并且不会创建额外的计算图p.add_(p_update)在本文中我们还讨论了动量概念它通过将包括当前梯度在内的所有过去梯度的指数平均值来替代更新中的梯度 m ( t ) β 1 m ( t − 1 ) ( 1 − β 1 ) ⋅ g ( t ) w ( t ) w ( t − 1 ) − η ⋅ m ( t ) \begin{split}\begin{split} m^{(t)} \beta_1 m^{(t-1)} (1 - \beta_1)\cdot g^{(t)}\\ w^{(t)} w^{(t-1)} - \eta \cdot m^{(t)}\\ \end{split}\end{split} m(t)w(t)​β1​m(t−1)(1−β1​)⋅g(t)w(t−1)−η⋅m(t)​​ class SGDMomentum(OptimizerTemplate):# 初始化函数添加动量参数def __init__(self, params, lr, momentum0.0):super().__init__(params, lr)self.momentum momentum # 对应于公式中的 beta_1# 创建一个字典用于存储每个参数的动量项 m_tself.param_momentum {p: torch.zeros_like(p.data) for p in self.params}# 实现带动量的SGD参数更新方法def update_param(self, p):# 计算当前参数的动量项这里是指数加权平均的实现self.param_momentum[p] (1 - self.momentum) * p.grad self.momentum * self.param_momentum[p]# 计算参数更新的值结合了学习率和动量项p_update -self.lr * self.param_momentum[p]# 原地更新参数节省内存且不创建额外的计算图p.add_(p_update)最终我们来到了Adam优化器。Adam结合了动量的概念和基于平方梯度的指数平均值的自适应学习率即梯度的范数。此外我们为动量和自适应学习率在最初的迭代中添加了偏差校正 m ( t ) β 1 m ( t − 1 ) ( 1 − β 1 ) ⋅ g ( t ) v ( t ) β 2 v ( t − 1 ) ( 1 − β 2 ) ⋅ ( g ( t ) ) 2 m ^ ( t ) m ( t ) 1 − β 1 t , v ^ ( t ) v ( t ) 1 − β 2 t w ( t ) w ( t − 1 ) − η v ^ ( t ) ϵ ∘ m ^ ( t ) \begin{split}\begin{split} m^{(t)} \beta_1 m^{(t-1)} (1 - \beta_1)\cdot g^{(t)}\\ v^{(t)} \beta_2 v^{(t-1)} (1 - \beta_2)\cdot \left(g^{(t)}\right)^2\\ \hat{m}^{(t)} \frac{m^{(t)}}{1-\beta^{t}_1}, \hat{v}^{(t)} \frac{v^{(t)}}{1-\beta^{t}_2}\\ w^{(t)} w^{(t-1)} - \frac{\eta}{\sqrt{\hat{v}^{(t)}} \epsilon}\circ \hat{m}^{(t)}\\ \end{split}\end{split} m(t)v(t)m^(t)w(t)​β1​m(t−1)(1−β1​)⋅g(t)β2​v(t−1)(1−β2​)⋅(g(t))21−β1t​m(t)​,v^(t)1−β2t​v(t)​w(t−1)−v^(t) ​ϵη​∘m^(t)​​ Epsilon是一个非常小的常数用于提高梯度范数非常小的情况下的数值稳定性。请记住自适应学习率并不替代学习率超参数 η \eta η 而是作为一个额外的因素确保不同参数的梯度具有相似的范数。 class Adam(OptimizerTemplate):# 初始化函数添加了Adam优化器所需的参数def __init__(self, params, lr, beta10.9, beta20.999, eps1e-8):super().__init__(params, lr)self.beta1 beta1 # 动量超参数self.beta2 beta2 # 二次动量超参数self.eps eps # 用于数值稳定性的小常数# 用于记录每个参数的更新次数用于偏差校正self.param_step {p: 0 for p in self.params}# 用于存储每个参数的一阶动量self.param_momentum {p: torch.zeros_like(p.data) for p in self.params}# 用于存储每个参数的二阶动量self.param_2nd_momentum {p: torch.zeros_like(p.data) for p in self.params}# 实现Adam参数更新的方法def update_param(self, p):self.param_step[p] 1 # 更新参数的更新次数# 计算一阶动量指数加权平均的梯度self.param_momentum[p] (1 - self.beta1) * p.grad self.beta1 * self.param_momentum[p]# 计算二阶动量指数加权平均的梯度平方self.param_2nd_momentum[p] (1 - self.beta2) * (p.grad)**2 self.beta2 * self.param_2nd_momentum[p]# 计算偏差校正因子bias_correction_1 1 - self.beta1 ** self.param_step[p]bias_correction_2 1 - self.beta2 ** self.param_step[p]# 计算调整后的二阶动量和一阶动量p_2nd_mom self.param_2nd_momentum[p] / bias_correction_2p_mom self.param_momentum[p] / bias_correction_1# 计算自适应学习率p_lr self.lr / (torch.sqrt(p_2nd_mom) self.eps)# 计算参数更新值p_update -p_lr * p_mom4.1.优化器比较 在实现了三种优化器SGD、带动量的SGD和Adam之后我们可以开始分析并比较它们。首先我们测试它们在优化FashionMNIST数据集上的神经网络方面的表现。我们再次使用我们的线性网络这次使用ReLU激活函数和Kaiming初始化这是我们之前发现适用于基于ReLU的网络的。请注意该模型对于此任务来说是过度参数化的我们可以使用更小的网络例如100,100,100实现类似的性能。然而我们的主要兴趣在于优化器能够多好地训练深度神经网络因此采用了过度参数化。 base_model BaseNetwork(act_fnnn.ReLU(), hidden_sizes[512,256,256,128]) kaiming_init(base_model) # 使用Kaiming初始化方法初始化模型权重为了进行公平比较我们使用三种优化器以相同的种子训练完全相同的模型。如果你愿意可以自由更改超参数然而那样的话你必须自己训练模型。 SGD_model copy.deepcopy(base_model).to(device) # 创建模型的深拷贝并将其移动到设备上 SGD_results train_model(SGD_model, FashionMNIST_SGD,lambda params: SGD(params, lr1e-1), # 使用SGD优化器max_epochs40, batch_size256) # 训练参数在上述代码中我们首先定义了一个基础模型 base_model它是一个具有ReLU激活函数和特定隐藏层大小的 BaseNetwork 的实例。然后我们使用 kaiming_init 函数对这个模型的权重进行初始化。 接下来我们使用 copy.deepcopy 来创建 base_model 的一个深拷贝以确保在训练过程中不会影响原始模型。我们将这个模型移动到适当的设备上例如GPU然后使用 train_model 函数来训练模型。在这个例子中我们使用学习率为0.1的SGD优化器进行训练最大周期数设置为40批量大小设置为256。 通过这种方式我们可以比较不同优化器在相同条件下的性能。类似的步骤可以用于测试带有动量的SGD和Adam优化器只需更改 train_model 函数中的优化器参数即可。 SGDMom_model copy.deepcopy(base_model).to(device) SGDMom_results train_model(SGDMom_model, FashionMNIST_SGDMom,lambda params: SGDMomentum(params, lr1e-1, momentum0.9),max_epochs40, batch_size256)Adam_model copy.deepcopy(base_model).to(device) Adam_results train_model(Adam_model, FashionMNIST_Adam,lambda params: Adam(params, lr1e-3),max_epochs40, batch_size256)结果是所有优化器在给定模型上的表现都相当好。差异太小以至于无法得出任何重大结论。然而请记住这也可以归因于我们选择的初始化方式。当将初始化方式改为较差的例如常数初始化时由于其自适应学习率Adam通常表现出更强的鲁棒性。为了展示这些优化器的特定优势我们将继续观察一些可能的损失曲面其中动量和自适应学习率至关重要。 4.2.病态曲率 病态曲率 病态曲率是一种类似于峡谷的曲面对于普通的SGD优化特别棘手。用文字描述病态曲率通常在一个方向上具有陡峭的梯度中心有一个最优解而在第二个方向上我们有一个更平缓的梯度通向全局最优解。让我们首先创建这样一个示例曲面并对其进行可视化 # 定义病态曲率损失函数 def pathological_curve_loss(w1, w2):# 这是一个病态曲率的例子。还有许多其他可能的曲面欢迎在此实验x1_loss torch.tanh(w1)**2 0.01 * torch.abs(w1) # w1的损失项x2_loss torch.sigmoid(w2) # w2的损失项return x1_loss x2_loss # 总损失是x1_loss和x2_loss的和# 定义绘制曲面的函数 def plot_curve(curve_fn, x_range(-5,5), y_range(-5,5), plot_3dFalse, cmapcm.viridis, titlePathological curvature):# 创建图形fig plt.figure()# 根据plot_3d参数选择创建3D轴还是2D轴ax plt.axes(projection3d) if plot_3d else plt.axes()# 创建x和y的值范围x torch.arange(x_range[0], x_range[1], (x_range[1]-x_range[0])/100.)y torch.arange(y_range[0], y_range[1], (y_range[1]-y_range[0])/100.)# 利用meshgrid生成网格坐标点x, y torch.meshgrid(x, y, indexingxy)# 计算曲面的Z值即损失函数值z curve_fn(x, y)# 将计算得到的Z值转换为numpy数组x, y, z x.numpy(), y.numpy(), z.numpy()# 根据plot_3d参数绘制3D曲面图或2D图像if plot_3d:ax.plot_surface(x, y, z, cmapcmap, linewidth1, color#000, antialiasedFalse)ax.set_zlabel(loss) # 设置Z轴标签为losselse:ax.imshow(z[::-1], cmapcmap, extent(x_range[0], x_range[1], y_range[0], y_range[1]))# 设置图形的标题和坐标轴标签plt.title(title)ax.set_xlabel(r$w_1$)ax.set_ylabel(r$w_2$)plt.tight_layout() # 调整子图布局以适应图形return ax# 重置Seaborn的默认样式 sns.reset_orig() # 绘制3D曲面图 _ plot_curve(pathological_curve_loss, plot_3dTrue) plt.show() # 显示图形在优化方面你可以将 和 想象成权重参数而曲率则代表了 和 空间上的损失曲面。请注意在典型的网络中我们拥有的参数数量远远超过两个这种曲率也可能以多维空间中出现。 理想情况下我们的优化算法会找到峡谷的中心并专注于沿着 方向优化参数。然而如果我们在山脊沿线遇到某点 方向的梯度将远大于 ​我们可能会从一个侧面跳到另一个侧面。由于梯度较大我们将不得不降低学习率从而显著减慢学习速度。 为了测试我们的算法我们可以实现一个简单的函数在这样一个曲面上训练两个参数 def train_curve(optimizer_func, curve_funcpathological_curve_loss, num_updates100, init[5, 5]):该函数用于在特定的损失曲面上训练权重参数并记录训练过程。输入optimizer_func - 要使用的优化器的构造函数。应该只接受一个参数列表。curve_func - 损失函数例如病态曲率。num_updates - 优化过程中更新/步数的数量。init - 参数的初始值。必须是一个有两个元素的列表/元组分别代表 w_1 和 w_2。输出NumPy数组形状为 [num_updates, 3]其中 [t,:2] 是第 t 步时的参数值[t,2] 是第 t 步的损失。# 将初始值转换为可训练的参数weights nn.Parameter(torch.FloatTensor(init), requires_gradTrue)# 创建优化器optimizer optimizer_func([weights])# 初始化用于记录训练过程中参数和损失的列表list_points []for _ in range(num_updates):# 计算损失loss curve_func(weights[0], weights[1])# 将当前的参数和损失添加到记录列表中list_points.append(torch.cat([weights.data.detach(), loss.unsqueeze(dim0).detach()], dim0))# 清零梯度optimizer.zero_grad()# 反向传播计算梯度loss.backward()# 更新参数optimizer.step()# 将记录的点转换为NumPy数组并返回points torch.stack(list_points, dim0).numpy()return points下一步让我们在曲率上应用不同的优化器。注意我们为优化算法设置了一个比标准神经网络更高的学习率。 这是因为我们只有两个参数而不是数万甚至数百万。 SGD_points train_curve(lambda params: SGD(params, lr10)) # 使用SGD优化器 SGDMom_points train_curve(lambda params: SGDMomentum(params, lr10, momentum0.9)) # 使用带动量的SGD优化器 Adam_points train_curve(lambda params: Adam(params, lr1)) # 使用Adam优化器# 为了最好地理解不同算法的工作方式我们通过损失曲面绘制更新步骤的折线图。 # 为了可读性我们将坚持使用2D表示。# 将所有优化器的点合并到一个数组中 all_points np.concatenate([SGD_points, SGDMom_points, Adam_points], axis0) # 绘制损失曲面并标记不同优化器的路径 ax plot_curve(pathological_curve_loss,x_range(-np.absolute(all_points[:, 0]).max(), np.absolute(all_points[:, 0]).max()),y_range(all_points[:, 1].min(), all_points[:, 1].max()),plot_3dFalse ) ax.plot(SGD_points[:, 0], SGD_points[:, 1], colorred, markero, zorder1, labelSGD) # SGD路径 ax.plot(SGDMom_points[:, 0], SGDMom_points[:, 1], colorblue, markero, zorder2, labelSGDMom) # 带动量的SGD路径 ax.plot(Adam_points[:, 0], Adam_points[:, 1], colorgrey, markero, zorder3, labelAdam) # Adam路径 plt.legend() # 显示图例 plt.show() # 显示图形这段代码首先使用三种不同的优化器SGD、带动量的SGD和Adam在病态曲率损失曲面上进行训练并记录了每一步的参数值和损失。然后它将所有优化器的训练路径合并到一个数组中并使用plot_curve函数绘制损失曲面的2D表示。在2D图形上使用不同的颜色和标记样式绘制了每种优化器的路径并添加了图例来标识每种优化器。最后显示了这个图形让我们可以直观地比较不同优化器在病态曲率上的优化过程。 我们可以清楚地看到SGD随机梯度下降无法找到优化曲线的中心并且由于方向上的梯度非常陡峭它在收敛方面存在问题。相比之下Adam和带动量的SGD能够很好地收敛因为 方向上变化的方向在不断抵消自身。在这类曲面上使用动量至关重要。 4.3.陡峭的最优值 第二种具有挑战性的损失曲面是陡峭的最优值。在这些曲面中有一大部分区域的梯度非常小而在最优值周围我们有非常大的梯度。例如考虑以下损失曲面 # 定义一个二元高斯函数 def bivar_gaussian(w1, w2, x_mean0.0, y_mean0.0, x_sig1.0, y_sig1.0):norm 1 / (2 * np.pi * x_sig * y_sig) # 高斯分布的归一化因子x_exp (-1 * (w1 - x_mean)**2) / (2 * x_sig**2) # w1的高斯指数部分y_exp (-1 * (w2 - y_mean)**2) / (2 * y_sig**2) # w2的高斯指数部分return norm * torch.exp(x_exp y_exp) # 返回二元高斯分布的值# 定义组合函数创建具有陡峭最优值的损失曲面 def comb_func(w1, w2):z -bivar_gaussian(w1, w2, x_mean1.0, y_mean-0.5, x_sig0.2, y_sig0.2)z - bivar_gaussian(w1, w2, x_mean-1.0, y_mean0.5, x_sig0.2, y_sig0.2)z - bivar_gaussian(w1, w2, x_mean-0.5, y_mean-0.8, x_sig0.2, y_sig0.2)return z# 使用plot_curve函数绘制具有陡峭最优值的损失曲面 _ plot_curve(comb_func, x_range(-2, 2), y_range(-2, 2), plot_3dTrue, titleSteep optima ) plt.show()大部分损失曲面的梯度非常小甚至没有梯度。然而在最优值附近我们有非常陡峭的梯度。要从梯度较低的区域开始达到最小值我们预期自适应学习率至关重要。为了验证这个假设我们可以在曲面上运行我们的三种优化器 # 使用train_curve函数和不同的优化器在具有陡峭最优值的损失曲面上进行训练 SGD_points train_curve(lambda params: SGD(params, lr0.5), curve_funccomb_func, num_updates1000, init[0, 0] ) SGDMom_points train_curve(lambda params: SGDMomentum(params, lr1, momentum0.9), curve_funccomb_func, num_updates1000, init[0, 0] ) Adam_points train_curve(lambda params: Adam(params, lr0.2), curve_funccomb_func, num_updates1000, init[0, 0] )# 将不同优化器的训练路径合并到一个数组中 all_points np.concatenate([SGD_points, SGDMom_points, Adam_points], axis0)# 使用plot_curve函数绘制损失曲面并在图上绘制不同优化器的训练路径 ax plot_curve(comb_func,x_range(-2, 2),y_range(-2, 2),plot_3dFalse,titleSteep optima ) ax.plot(SGD_points[:, 0], SGD_points[:, 1], colorred, markero, zorder3, labelSGD, alpha0.7 ) ax.plot(SGDMom_points[:, 0], SGDMom_points[:, 1], colorblue, markero, zorder2, labelSGDMom, alpha0.7 ) ax.plot(Adam_points[:, 0], Adam_points[:, 1], colorgrey, markero, zorder1, labelAdam, alpha0.7 ) ax.set_xlim(-2, 2) # 设置x轴的范围 ax.set_ylim(-2, 2) # 设置y轴的范围 plt.legend() # 显示图例 plt.show() # 显示图形SGD最初采取的步长非常小直到它触及最优值的边界。首先到达大约(-0.75, -0.5)的点梯度方向发生了变化将参数推向(0.8, 0.5)从这个点SGD再也无法恢复除非经过许多步骤。带动量的SGD也有类似的问题只不过它继续沿着触及最优值的方向前进。这个时间点的梯度远大于其他任何点以至于动量 被它压倒。最后Adam能够在最优值处收敛展示了自适应学习率的重要性。 4.4. 优化器的选择要点 在看到优化结果后我们的结论是什么我们应该总是使用Adam再也不考虑SGD了吗简短的回答不。有许多论文表明在某些情况下SGD带动量泛化得更好而Adam往往倾向于过拟合[5,6]。这与寻找更宽广的最优值有关。 在实际应用中选择哪种优化器取决于多种因素包括问题的具体性质、网络的架构、训练数据的规模和特性等。因此理解不同优化器的特性并在适当的情境中运用它们是非常重要的。尽管Adam在许多情况下表现出色但SGD及其变体在其他情况下可能更为合适特别是在我们关心模型泛化能力的时候。例如参见下图中不同最优值的示意图Keskar等人2017年 黑色线条代表训练损失曲面而虚线红线是测试损失。找到锐利、狭窄的最小值可能有助于发现最小的训练损失。然而这并不意味着它也会最小化测试损失因为尤其是平坦的最小值被证明具有更好的泛化能力。可以想象由于测试数据集与训练集中的示例不同其损失曲面可能会有轻微的偏移。对于锐利的最小值来说小的变化可能会产生显著的影响而平坦的最小值通常对这种变化更加稳健。 在下篇博文中我们将看到某些类型的网络仍然可以更好地使用SGD和学习率调度来优化而不是Adam。尽管如此Adam是深度学习中最常用的优化器因为它通常比其他优化器表现得更好特别是对于深层网络。 5.结论 在文中我们讨论了神经网络的初始化和优化技术。我们看到良好的初始化必须平衡保持梯度方差和激活方差。这可以通过使用Xavier初始化实现对于基于tanh的网络以及使用Kaiming初始化实现对于基于ReLU的网络。在优化方面动量和自适应学习率等概念可以帮助应对具有挑战性的损失曲面但并不能保证神经网络性能的提升。 参考文献 [1] Glorot, Xavier, 和 Yoshua Bengio. “理解训练深度前馈神经网络的难度。” 第十三届国际人工智能和统计会议论文集。2010年。链接 [2] He, Kaiming, 等人. “深入研究激活函数在ImageNet分类上超越人类水平的表现。” 2015年IEEE国际计算机视觉会议论文集。2015年。链接 [3] Kingma, Diederik P. Ba, Jimmy. “Adam一种用于随机优化的方法。” 第三届国际学习表示会议(ICLR)论文集。2015年。链接 [4] Keskar, Nitish Shirish, 等人. “关于深度学习的大规模批量训练泛化差距和尖锐最小值。” 第五届国际学习表示会议(ICLR)论文集。2017年。链接 [5] Wilson, Ashia C., 等人. “自适应梯度方法在机器学习中的边际价值。” 神经信息处理系统进展。2017年。链接 [6] Ruder, Sebastian. “梯度下降优化算法概述。” arXiv预印本。2017年。链接
http://www.hkea.cn/news/14438753/

相关文章:

  • 开化网站建设为什么用wp做网站
  • 建设一个网站需要什么软件wordpress文章首页不展开
  • 重庆做网站changeke环境设计专业必看网站
  • 如可做网站百度关键字排名软件
  • 用html制作网站代码做营销网站
  • 新建的网站怎么上首页360建筑网质量怎么样
  • 教做网站wordpress的站点地图
  • 安阳网站建设哪家公司好企业网站申请永久
  • 英雄联盟做的广告视频网站国外乡村建设网站
  • 网站建设及维护费关键词优化seo优化排名
  • 网站如何做质保系统做网站的企业
  • 网站诊断网店购物系统
  • 林壑地板北京网站建设重庆建网站培训机构
  • 公司网站建设指南门户网站建设工作会议
  • 网站建设合同属于什么印花税东莞微信网站建设代理
  • 文明网站建设总结专门做婚纱儿童摄影网站
  • 哪个网站的课件做的好处大创意网站
  • 传播文化有限公司网站建设大型小说网站开发语言
  • 晋城推广型网站建设网络营销网站推广的基本策略
  • 怎样把自己做的网页放在网站里元气森林网络营销方式
  • 建设部网站工程设计收费标准平面设计主要做什么的
  • 网页设计制作与网站建设课程免费创建网站的平台
  • 辽阳建设网站天津网站在哪里建设
  • 用手机怎么做免费网站侨联 文化宣传 侨联网站建设
  • 最实用的手机app软件网站优化 前端怎么做
  • 织梦的网站地图更新曹鹏wordpress
  • 龙华网站 建设龙华信科windows搭建网站
  • 黄埔区做网站建设公司网站需要什么资料
  • 郑州好的网站建设公司哪家好平台网站建设开票开什么内容
  • 外包做网站哪家好dw做网站环境配置