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

【深度学习】PyTorch框架(3):优化与初始化

1.引言

在本文中,我们将探讨神经网络的优化与初始化技术。随着神经网络深度的增加,我们会遇到多种挑战。最关键的是确保网络中梯度流动的稳定性,否则可能会遭遇梯度消失或梯度爆炸的问题。因此,我们将深入探讨以下两个核心概念:网络参数的初始化和优化算法的选择。
本文的前半部分,我们将介绍不同的参数初始化方法,从最基本的初始化策略开始,逐步深入到当前在极深网络中应用的高级技术。在后半部分,我们将聚焦于优化算法的比较,分析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_ok=True)# 对于每个文件,检查它是否已经存在。如果不存在,尝试下载。
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(root=DATASET_PATH, train=True, transform=transform, download=True)
train_set, val_set = torch.utils.data.random_split(train_dataset, [50000, 10000])# 加载测试集
test_set = FashionMNIST(root=DATASET_PATH, train=False, transform=transform, download=True)# 我们定义一组数据加载器,我们稍后可以用于不同的目的。
# 注意,对于实际训练模型,我们将使用具有较小批量大小的不同数据加载器。
train_loader = data.DataLoader(train_set, batch_size=1024, shuffle=True, drop_last=False)
val_loader = data.DataLoader(val_set, batch_size=1024, shuffle=False, drop_last=False)
test_loader = data.DataLoader(test_set, batch_size=1024, shuffle=False, drop_last=False)

与之前的文章相比,我们更改了归一化转换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(f"Mean: {imgs.mean().item():5.3f}")
print(f"Standard deviation: {imgs.std().item():5.3f}")
print(f"Maximum: {imgs.max().item():5.3f}")
print(f"Minimum: {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_size=784, num_classes=10, 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, color="C0", xlabel=None, stat="count", use_kde=True):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], ax=key_ax, color=color, bins=50, stat=stat,  # 绘制直方图kde=use_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(wspace=0.4)  # 调整子图之间的间隔return fig# 可视化模型权重分布
def visualize_weight_distribution(model, color="C0"):weights = {}for name, param in model.named_parameters():  # 遍历模型的参数if name.endswith(".bias"):  # 如果是偏置,则跳过continuekey_name = f"Layer {name.split('.')[1]}"  # 为权重创建键名weights[key_name] = param.detach().view(-1).cpu().numpy()  # 将权重转换为numpy数组# 绘图fig = plot_dists(weights, color=color, xlabel="Weight vals")  # 使用plot_dists函数绘制权重分布图fig.suptitle("Weight distribution", fontsize=14, y=1.05)  # 设置图表标题plt.show()  # 显示图表plt.close()  # 关闭图表# 可视化模型梯度分布
def visualize_gradients(model, color="C0", print_variance=False):# 设置模型为评估模式model.eval()small_loader = data.DataLoader(train_set, batch_size=1024, shuffle=False)  # 创建数据加载器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, color=color, xlabel="Grad magnitude")  # 使用plot_dists函数绘制梯度分布图fig.suptitle("Gradient distribution", fontsize=14, y=1.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, color="C0", print_variance=False):model.eval()  # 设置模型为评估模式small_loader = data.DataLoader(train_set, batch_size=1024, shuffle=False)  # 创建数据加载器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[f"Layer {layer_index}"] = feats.view(-1).detach().cpu().numpy()  # 将激活值转换为numpy数组# 绘图fig = plot_dists(activations, color=color, stat="density", xlabel="Activation vals")  # 使用plot_dists函数绘制激活值分布图fig.suptitle("Activation distribution", fontsize=14, y=1.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_fn=Identity()).to(device)

3.1 常数初始化

接下来,我们考虑一种最简单的初始化方法——常数初始化。直观上,将所有权重设置为零并不理想,因为这会导致传播的梯度也为零。但是,如果我们将所有权重设置为一个接近零的非零常数,情况会如何呢?为了探究这一点,我们可以编写一个函数来实现这一初始化,并可视化梯度的分布情况。
定义了一个名为const_init的函数,它接受一个模型和一个默认为0的常数值c,将模型中所有的参数(权重)填充为这个常数值。然后,我们调用这个函数将模型的权重初始化为0.005,接着使用visualize_gradientsvisualize_activations函数来可视化梯度和激活值的分布,并打印出它们的方差。这有助于我们理解在这种初始化策略下,网络的梯度和激活值的行为。

def const_init(model, c=0.0):for name, param in model.named_parameters():param.data.fill_(c)const_init(model, c=0.005)
visualize_gradients(model)
visualize_activations(model, print_variance=True)

添加图片注释,不超过 140 字(可选)
添加图片注释,不超过 140 字(可选)

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, std=0.01):for name, param in model.named_parameters():param.data.normal_(std=std)var_init(model, std=0.01)
visualize_activations(model, print_variance=True)

添加图片注释,不超过 140 字(可选)

在神经网络的层与层之间,激活值的方差呈现出逐渐减小的趋势,到了最后一层,方差几乎趋近于零。这种情况下,一个可能的解决办法是增加标准差的数值。通过提高初始化时的标准差,我们可以尝试维持网络深层的激活方差,避免其在传播过程中消失。

var_init(model, std=0.1)
visualize_activations(model, print_variance=True)

在这里插入图片描述

通过使用更高的标准差进行初始化,我们可以观察到网络各层激活值的分布情况,特别是它们的方差,以评估这种策略是否有效。这种方法可能有助于解决深层网络中的梯度消失问题,但同时也要警惕不要导致梯度爆炸,这需要我们在实践中仔细调整和平衡。

3.3.如何找到合适的初始化值

从我们之前的实验中,我们可以看到需要从某个概率分布中对权重进行采样,但具体选择哪个分布我们还不确定。下一步,我们将尝试从激活值分布的角度出发,寻找最优的初始化方法。为此,我们提出两个要求:
(1)激活值的均值应该为零。
(2)激活值的方差应该在每一层都保持不变。
假设我们要为以下层设计一个初始化方法:要设计一个满足上述两个要求的初始化方法,我们需要考虑权重矩阵和激活函数的特性。对于一个全连接层,如果使用恒等激活函数(即线性激活函数),输出的均值和方差将取决于输入的均值和方差以及权重矩阵。为了使激活值的均值为零,我们可以选择一个合适的均值。而为了保持激活值的方差在每一层都相同,我们需要选择一个合适的标准差。
一种常见的方法是使用与输入维度的平方根成反比的标准差。这样,无论输入的维度如何变化,权重的标准差都会相应调整,以保持激活值的方差大致相同。这种方法通常被称为Xavier初始化或Glorot初始化。
我们的目标是让每个元素的方差与输入的方差相同,即每个权重更新后的方差应该保持与输入数据的方差一致。这是为了确保在多层网络中,信息能够稳定地从前层传递到后层,避免出现梯度消失或爆炸的问题。

在数学上,如果我们考虑一个全连接层 y = W x + b , y ∈ R d y , x ∈ R d x y=Wx+b,y\in\mathbb{R}^{d_y},x\in\mathbb{R}^{d_x} y=Wx+b,yRdy,xRdx,其输出可以表示为 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}) WN(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=jwijxj=Var(jwijxj)=jVar(wijxj)=jVar(wij)Var(xj)=dxVar(wij)Var(xj)=σx2dxVar(wij)=dx1

基于上述理论,我们的权重初始化策略应该是使用一个具有适当方差的分布。具体来说,权重的方差应该是输入维度倒数的方差。这样的初始化有助于保持网络各层激活值的方差大致相同,从而有助于梯度在网络中的稳定流动。

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_(std=1.0/math.sqrt(param.shape[1]))# 应用Equal Variance Initialization到模型
equal_var_init(model)# 可视化权重分布
visualize_weight_distribution(model)# 可视化激活值分布,并打印每层激活值的方差
visualize_activations(model, print_variance=True)

添加图片注释,不超过 140 字(可选)
添加图片注释,不超过 140 字(可选)

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+nnext2或者 1 d x \frac{1}{d_x} dx1方差的其他分布。通常你会看到使用均匀分布进行初始化。使用均匀分布而不是正态分布的一个小小好处是,我们可以排除初始化非常大或非常小的权重的可能性。

除了激活值的方差之外,我们希望稳定的另一个方差是梯度的方差。这确保了深层网络的稳定优化。结果表明,我们可以从 Δ x = W Δ y \Delta x=W\Delta y Δx=WΔy开始进行与上述相同的计算,并得出我们应该使用 1 d y \frac{1}{d_y} dy1来初始化我们的层的结论,其中 是输出神经元的数量。你可以将这个计算作为练习来做,或者在这个博客文章中查看详尽的解释。作为两种约束之间的折衷,Glorot和Bengio(2010年)提议使用这两个值的调和平均值。这引导我们得到了众所周知的Xavier初始化:
W ∼ N ( 0 , 2 d x + d y ) W\sim \mathcal{N}\left(0,\frac{2}{d_x+d_y}\right) WN(0,dx+dy2)
如果我们使用均匀分布来初始化权重,我们会这样设置:
W ∼ U [ − 6 d x + d y , 6 d x + d y ] W\sim U\left[-\frac{\sqrt{6}}{\sqrt{d_x+d_y}}, \frac{\sqrt{6}}{\sqrt{d_x+d_y}}\right] WU[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_variance=True)
visualize_activations(model, print_variance=True)

添加图片注释,不超过 140 字(可选)

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.017655

添加图片注释,不超过 140 字(可选)

Layer 0 - Variance: 1.216592
Layer 2 - Variance: 1.719161
Layer 4 - Variance: 1.714506
Layer 6 - Variance: 2.224779
Layer 8 - Variance: 5.297660

Xavier初始化方法旨在保持网络中梯度和激活值方差的一致性。我们注意到,输出层的方差之所以显著增加,是因为输入层和输出层的维度存在较大差异。例如,输入层可能有1024个神经元,而输出层可能仅有10个神经元。目前,我们的讨论假设了激活函数是线性的。引入非线性激活函数,如tanh或ReLU,会改变激活值的分布,进而影响梯度的方差。
在基于tanh的网络中,一个普遍的假设是,在训练初期,对于接近零的小值,tanh函数可以近似为线性函数。这意味着,在训练的早期阶段,我们不需要调整初始化策略的计算。然而,随着训练的进行,权重的更新可能会导致激活值的分布发生变化,从而使得tanh的非线性特性变得更加显著。
为了验证我们的初始化策略是否适用于非线性激活函数,我们可以在训练的早期阶段检查激活值的分布。如果激活值主要集中在tanh的线性区域(即接近零点),那么我们的初始化方法可能仍然有效。如果激活值分布远离零点,我们可能需要考虑调整初始化策略,以适应激活函数的非线性特性。

model = BaseNetwork(act_fn=nn.Tanh()).to(device)
xavier_init(model)
visualize_gradients(model, print_variance=True)
visualize_activations(model, print_variance=True)

添加图片注释,不超过 140 字(可选)

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.000455

添加图片注释,不超过 140 字(可选)

Layer 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 W=0 W=0 b = 0 b=0 b=0 ,输出的期望值就是零。ReLU初始化的计算与恒等激活函数不同之处在于确定权重的标准差 Var ( w i j x j ) \text{Var}(w_{ij}x_{j}) Var(wijxj)
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(wijxj)==Var(wij) E[wij2]E[xj2]=0 E[wij]2E[xj]2=Var(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]=21E[y~2]y~ 是以零为中心且对称的=21Var(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=0y~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_fn=nn.ReLU()).to(device)
kaiming_init(model)
visualize_gradients(model, print_variance=True)
visualize_activations(model, print_variance=True)

添加图片注释,不超过 140 字(可选)

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.005548

添加图片注释,不超过 140 字(可选)

Layer 0 - Variance: 1.012342
Layer 2 - Variance: 1.092432
Layer 4 - Variance: 1.268176
Layer 6 - Variance: 1.193706
Layer 8 - Variance: 1.760064

Kaiming初始化通过特别考虑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_slope=0.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, mean=0.0, std=std)

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, net=None):# 构造配置文件和模型文件的路径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_fn=act_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_ok=True)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_epochs=50, batch_size=256, overwrite=False):"""在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}, ...) wt=f(wt1,gt,...),其中 是时间步 t 的参数, g t = ∇ w ( t − 1 ) L ( t ) g^{t} = \nabla_{w^{(t-1)}} \mathcal{L}^{(t)} gt=w(t1)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(t1)η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)=β1m(t1)+(1β1)g(t)=w(t1)ηm(t)

class SGDMomentum(OptimizerTemplate):# 初始化函数,添加动量参数def __init__(self, params, lr, momentum=0.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)=β1m(t1)+(1β1)g(t)=β2v(t1)+(1β2)(g(t))2=1β1tm(t),v^(t)=1β2tv(t)=w(t1)v^(t) +ϵηm^(t)
Epsilon是一个非常小的常数,用于提高梯度范数非常小的情况下的数值稳定性。请记住,自适应学习率并不替代学习率超参数 η \eta η ,而是作为一个额外的因素,确保不同参数的梯度具有相似的范数。

class Adam(OptimizerTemplate):# 初始化函数,添加了Adam优化器所需的参数def __init__(self, params, lr, beta1=0.9, beta2=0.999, eps=1e-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_mom

4.1.优化器比较

在实现了三种优化器(SGD、带动量的SGD和Adam)之后,我们可以开始分析并比较它们。首先,我们测试它们在优化FashionMNIST数据集上的神经网络方面的表现。我们再次使用我们的线性网络,这次使用ReLU激活函数和Kaiming初始化,这是我们之前发现适用于基于ReLU的网络的。请注意,该模型对于此任务来说是过度参数化的,我们可以使用更小的网络(例如100,100,100)实现类似的性能。然而,我们的主要兴趣在于优化器能够多好地训练深度神经网络,因此采用了过度参数化。

base_model = BaseNetwork(act_fn=nn.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, lr=1e-1),  # 使用SGD优化器max_epochs=40, batch_size=256)  # 训练参数

在上述代码中,我们首先定义了一个基础模型 base_model,它是一个具有ReLU激活函数和特定隐藏层大小的 BaseNetwork 的实例。然后,我们使用 kaiming_init 函数对这个模型的权重进行初始化。
接下来,我们使用 copy.deepcopy 来创建 base_model 的一个深拷贝,以确保在训练过程中不会影响原始模型。我们将这个模型移动到适当的设备上(例如GPU),然后使用 train_model 函数来训练模型。在这个例子中,我们使用学习率为0.1的SGD优化器进行训练,最大周期数设置为40,批量大小设置为256。
通过这种方式,我们可以比较不同优化器在相同条件下的性能。类似的步骤可以用于测试带有动量的SGD和Adam优化器,只需更改 train_model 函数中的优化器参数即可。
添加图片注释,不超过 140 字(可选)

SGDMom_model = copy.deepcopy(base_model).to(device)
SGDMom_results = train_model(SGDMom_model, "FashionMNIST_SGDMom",lambda params: SGDMomentum(params, lr=1e-1, momentum=0.9),max_epochs=40, batch_size=256)

添加图片注释,不超过 140 字(可选)

Adam_model = copy.deepcopy(base_model).to(device)
Adam_results = train_model(Adam_model, "FashionMNIST_Adam",lambda params: Adam(params, lr=1e-3),max_epochs=40, batch_size=256)

添加图片注释,不超过 140 字(可选)

结果是,所有优化器在给定模型上的表现都相当好。差异太小,以至于无法得出任何重大结论。然而,请记住,这也可以归因于我们选择的初始化方式。当将初始化方式改为较差的(例如,常数初始化)时,由于其自适应学习率,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_3d=False, cmap=cm.viridis, title="Pathological curvature"):# 创建图形fig = plt.figure()# 根据plot_3d参数选择创建3D轴还是2D轴ax = plt.axes(projection='3d') 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, indexing='xy')# 计算曲面的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, cmap=cmap, linewidth=1, color="#000", antialiased=False)ax.set_zlabel("loss")  # 设置Z轴标签为"loss"else:ax.imshow(z[::-1], cmap=cmap, 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_3d=True)
plt.show()  # 显示图形

添加图片注释,不超过 140 字(可选)

在优化方面,你可以将 和 想象成权重参数,而曲率则代表了 和 空间上的损失曲面。请注意,在典型的网络中,我们拥有的参数数量远远超过两个,这种曲率也可能以多维空间中出现。
理想情况下,我们的优化算法会找到峡谷的中心,并专注于沿着 方向优化参数。然而,如果我们在山脊沿线遇到某点, 方向的梯度将远大于 ​,我们可能会从一个侧面跳到另一个侧面。由于梯度较大,我们将不得不降低学习率,从而显著减慢学习速度。
为了测试我们的算法,我们可以实现一个简单的函数,在这样一个曲面上训练两个参数:

def train_curve(optimizer_func, curve_func=pathological_curve_loss, num_updates=100, 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_grad=True)# 创建优化器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(dim=0).detach()], dim=0))# 清零梯度optimizer.zero_grad()# 反向传播计算梯度loss.backward()# 更新参数optimizer.step()# 将记录的点转换为NumPy数组并返回points = torch.stack(list_points, dim=0).numpy()return points

下一步,让我们在曲率上应用不同的优化器。注意,我们为优化算法设置了一个比标准神经网络更高的学习率。
这是因为我们只有两个参数,而不是数万甚至数百万。

SGD_points = train_curve(lambda params: SGD(params, lr=10))  # 使用SGD优化器
SGDMom_points = train_curve(lambda params: SGDMomentum(params, lr=10, momentum=0.9))  # 使用带动量的SGD优化器
Adam_points = train_curve(lambda params: Adam(params, lr=1))  # 使用Adam优化器# 为了最好地理解不同算法的工作方式,我们通过损失曲面绘制更新步骤的折线图。
# 为了可读性,我们将坚持使用2D表示。# 将所有优化器的点合并到一个数组中
all_points = np.concatenate([SGD_points, SGDMom_points, Adam_points], axis=0)
# 绘制损失曲面并标记不同优化器的路径
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_3d=False
)
ax.plot(SGD_points[:, 0], SGD_points[:, 1], color="red", marker="o", zorder=1, label="SGD")  # SGD路径
ax.plot(SGDMom_points[:, 0], SGDMom_points[:, 1], color="blue", marker="o", zorder=2, label="SGDMom")  # 带动量的SGD路径
ax.plot(Adam_points[:, 0], Adam_points[:, 1], color="grey", marker="o", zorder=3, label="Adam")  # Adam路径
plt.legend()  # 显示图例
plt.show()  # 显示图形

这段代码首先使用三种不同的优化器(SGD、带动量的SGD和Adam)在病态曲率损失曲面上进行训练,并记录了每一步的参数值和损失。然后,它将所有优化器的训练路径合并到一个数组中,并使用plot_curve函数绘制损失曲面的2D表示。在2D图形上,使用不同的颜色和标记样式绘制了每种优化器的路径,并添加了图例来标识每种优化器。最后,显示了这个图形,让我们可以直观地比较不同优化器在病态曲率上的优化过程。

添加图片注释,不超过 140 字(可选)

我们可以清楚地看到,SGD(随机梯度下降)无法找到优化曲线的中心,并且由于方向上的梯度非常陡峭,它在收敛方面存在问题。相比之下,Adam和带动量的SGD能够很好地收敛,因为 方向上变化的方向在不断抵消自身。在这类曲面上,使用动量至关重要。

4.3.陡峭的最优值

第二种具有挑战性的损失曲面是陡峭的最优值。在这些曲面中,有一大部分区域的梯度非常小,而在最优值周围,我们有非常大的梯度。例如,考虑以下损失曲面:

# 定义一个二元高斯函数
def bivar_gaussian(w1, w2, x_mean=0.0, y_mean=0.0, x_sig=1.0, y_sig=1.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_mean=1.0, y_mean=-0.5, x_sig=0.2, y_sig=0.2)z -= bivar_gaussian(w1, w2, x_mean=-1.0, y_mean=0.5, x_sig=0.2, y_sig=0.2)z -= bivar_gaussian(w1, w2, x_mean=-0.5, y_mean=-0.8, x_sig=0.2, y_sig=0.2)return z# 使用plot_curve函数绘制具有陡峭最优值的损失曲面
_ = plot_curve(comb_func, x_range=(-2, 2), y_range=(-2, 2), plot_3d=True, title="Steep optima"
)
plt.show()

添加图片注释,不超过 140 字(可选)

大部分损失曲面的梯度非常小,甚至没有梯度。然而,在最优值附近,我们有非常陡峭的梯度。要从梯度较低的区域开始达到最小值,我们预期自适应学习率至关重要。为了验证这个假设,我们可以在曲面上运行我们的三种优化器:

# 使用train_curve函数和不同的优化器在具有陡峭最优值的损失曲面上进行训练
SGD_points = train_curve(lambda params: SGD(params, lr=0.5), curve_func=comb_func, num_updates=1000, init=[0, 0]
)
SGDMom_points = train_curve(lambda params: SGDMomentum(params, lr=1, momentum=0.9), curve_func=comb_func, num_updates=1000, init=[0, 0]
)
Adam_points = train_curve(lambda params: Adam(params, lr=0.2), curve_func=comb_func, num_updates=1000, init=[0, 0]
)# 将不同优化器的训练路径合并到一个数组中
all_points = np.concatenate([SGD_points, SGDMom_points, Adam_points], axis=0)# 使用plot_curve函数绘制损失曲面,并在图上绘制不同优化器的训练路径
ax = plot_curve(comb_func,x_range=(-2, 2),y_range=(-2, 2),plot_3d=False,title="Steep optima"
)
ax.plot(SGD_points[:, 0], SGD_points[:, 1], color="red", marker="o", zorder=3, label="SGD", alpha=0.7
)
ax.plot(SGDMom_points[:, 0], SGDMom_points[:, 1], color="blue", marker="o", zorder=2, label="SGDMom", alpha=0.7
)
ax.plot(Adam_points[:, 0], Adam_points[:, 1], color="grey", marker="o", zorder=1, label="Adam", alpha=0.7
)
ax.set_xlim(-2, 2)  # 设置x轴的范围
ax.set_ylim(-2, 2)  # 设置y轴的范围
plt.legend()  # 显示图例
plt.show()  # 显示图形

添加图片注释,不超过 140 字(可选)
SGD最初采取的步长非常小,直到它触及最优值的边界。首先到达大约(-0.75, -0.5)的点,梯度方向发生了变化,将参数推向(0.8, 0.5),从这个点SGD再也无法恢复(除非经过许多步骤)。带动量的SGD也有类似的问题,只不过它继续沿着触及最优值的方向前进。这个时间点的梯度远大于其他任何点,以至于动量 被它压倒。最后,Adam能够在最优值处收敛,展示了自适应学习率的重要性。

4.4. 优化器的选择要点

在看到优化结果后,我们的结论是什么?我们应该总是使用Adam,再也不考虑SGD了吗?简短的回答:不。有许多论文表明,在某些情况下,SGD(带动量)泛化得更好,而Adam往往倾向于过拟合[5,6]。这与寻找更宽广的最优值有关。
在实际应用中,选择哪种优化器取决于多种因素,包括问题的具体性质、网络的架构、训练数据的规模和特性等。因此,理解不同优化器的特性并在适当的情境中运用它们是非常重要的。尽管Adam在许多情况下表现出色,但SGD及其变体在其他情况下可能更为合适,特别是在我们关心模型泛化能力的时候。例如,参见下图中不同最优值的示意图(Keskar等人,2017年):

添加图片注释,不超过 140 字(可选)

黑色线条代表训练损失曲面,而虚线红线是测试损失。找到锐利、狭窄的最小值可能有助于发现最小的训练损失。然而,这并不意味着它也会最小化测试损失,因为尤其是平坦的最小值被证明具有更好的泛化能力。可以想象,由于测试数据集与训练集中的示例不同,其损失曲面可能会有轻微的偏移。对于锐利的最小值来说,小的变化可能会产生显著的影响,而平坦的最小值通常对这种变化更加稳健。
在下篇博文中,我们将看到某些类型的网络仍然可以更好地使用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年。链接

相关文章:

【深度学习】PyTorch框架(3):优化与初始化

1.引言 在本文中,我们将探讨神经网络的优化与初始化技术。随着神经网络深度的增加,我们会遇到多种挑战。最关键的是确保网络中梯度流动的稳定性,否则可能会遭遇梯度消失或梯度爆炸的问题。因此,我们将深入探讨以下两个核心概念&a…...

Go-知识测试-子测试

Go-知识测试-子测试 1. 介绍2. 例子3. 子测试命名规则4. 选择性执行5. 子测试并发6. testing.T.Run7. testing.T.Parallel8. 子测试适用于单元测试9. 子测试适用于性能测试10. 总结10.1 启动子测试 Run10.2 启动并发测试 Parallel 建议先看:https://blog.csdn.net/a…...

.net core IConfiguration 读 appsettings.json 数据,举例

在.NET Core中,IConfiguration 接口是用来读取配置数据的,包括从 appsettings.json 文件中读取。下面是一个如何在使用.NET Core时通过 IConfiguration 读取 appsettings.json 数据的示例。 首先,假设你的 appsettings.json 文件内容如下&am…...

全球Windows机器蓝屏,作为量化人,我的检讨来了

昨天下午,微软给大家放了个假。Windows又双叒死机了。不过,这一次不是几台机器,而是全球大范围宕机。这一刻,大家都是“正蓝旗”。 蓝瓶的,效果好! 现在根本原因已经找到,绝大多数人的机器都已修…...

部署和运维

目录 1.Git1.1. Git指令中merge和rebase的区别1. Commit 记录2. 合并方式3. 冲突处理4. 使用场景选择建议 1.2. cherry-pick的使用如何使用 git cherry-pick例子处理冲突撤销 cherry-pick其他选项 结论 2. 部署1. Nginx的使用场景 编译打包1. webpack2. webpack打包优化1. 代码…...

微信小程序基本语法

官网 https://developers.weixin.qq.com/miniprogram/dev/framework/ 视频教程:尚硅谷微信小程序开发教程,2024最新微信小程序项目实战! 仿慕尚花坊项目源码:https://gitee.com/abcdfdewrw/flower-workshop 目录 一,初…...

测试用例的设计方法

等价类 等价类概念:在所有测试的数据中,具有某种共同特征的数据子集 边界值 边界值分析是对程序输入或输出的边界值进行测试的一种黑盒测试方法 边界值是作为等价类的补充,其主要区别是: 边界值测试设计不是从某一个等价类中…...

Android10.0 锁屏分析-KeyguardPatternView图案锁分析

首先一起看看下面这张图: 通过前面锁屏加载流程可以知道在KeyguardSecurityContainer中使用getSecurityView()根据不同的securityMode inflate出来,并添加到界面上的。 我们知道,Pattern锁所使用的layout是 R.layout.keyguard_pattern_view&a…...

Python 装饰器:函数的函数,代码的艺术

引言 在Python中,装饰器是一种强大的功能,允许程序员在不修改原函数源码的情况下增强或修改函数行为。装饰器本质上是一个接收函数作为参数的高阶函数,并返回一个新的函数或修改原函数的行为。这种机制极大地提高了代码的复用性、可读性和模…...

安全防御2

实验要求: 实验过程: 7,办公区设备可以通过电信链路和移动链路上网(多对多的NAT,并且需要保留一个公网IP不能用来转换): 新建电信区: 新建移动区: 将对应接口划归到各自区域: 新建…...

C语言 ——— 打印水仙花数

目录 何为水仙花数 题目要求 代码实现 何为水仙花数 “水仙花数”是指一个n位数,其各位数字的n次方之和等于该数本身 如:153 1^3 5^3 3^3,则153就是一个“水仙花数” 题目要求 求出0~100000的所有“水仙花数”并输出 代码实现 #i…...

「Conda」在Linux系统中安装Conda环境管理器

在Linux系统中安装Conda环境管理器是一个相对简单的过程。 1. 准备工作 确保你的Linux系统已经更新到最新版本,并安装了基本的开发工具和库。打开终端,执行以下命令: sudo apt-get update sudo apt-get upgrade sudo apt-get install build-essential2. 安装Miniconda或An…...

9.11和9.9哪个大?GPT-4o也翻车了

今天刷到了这个问题,心血来潮去问下chatgpt-4o,没想到疯狂翻车... 第一次问: GPT一开始给出了难绷的解答,让我想起了某短视频软件评论区里对某歌手节目排名的质疑哈哈哈哈哈 但是在接下来的进一步询问和回答中它反应过来了。 第…...

[开源]语雀+Vercel:打造免费个人博客网站

大家好,我是白露。 今天我想和大家分享我的今年的第一个开源项目 —— 基于语雀+Nextjs+Vercel实现免费的博客系统。 简单来说,你在语雀写博客,然后直接一键同步到个人网站上,网站自动部署! 而且,整个过程几乎不需要额外的成本,也不用充值语雀超级会员,hh。这个项目…...

使用ElementUI和element-china-area-data库实现省市区三级联动组件封装

在前端开发中,省市区三级联动是一个常见的需求。今天我们将使用Vue.js和ElementUI组件库,结合element-china-area-data库,来实现一个省市区三级联动的组件。这个组件不仅可以提高用户体验,还能大大简化我们的代码。接下来&#xf…...

0718,TCP协议,三次握手,四次挥手

目录 上课喵: TCP(Transmission Control Protocol,传输控制协议)的状态迁移图 TCP连接的状态迁移图 状态迁移说明: 注意: big_htonl.c 字节序转换 addr.c IP地址的转换 作业喵: …...

如何安装Visual Studio Code

Visual Studio Code(简称 VS Code) Visual Studio Code 是一款由微软开发的免费、开源的现代化轻量级代码编辑器。 主要特点包括: 跨平台:支持 Windows、Mac 和 Linux 等主流操作系统,方便开发者在不同平台上保持一…...

vi 编辑器快捷生成 main 函数和基本框架

step1: 执行 sudo vi /etc/vim/vimrc &#xff08;修改vimrc需要管理员权限&#xff1a;sudo&#xff09; step2:输入用户密码&#xff0c;回车, 编辑vimrc文件 step3:在尾行输入以下代码&#xff08;可复制&#xff09; map mf i#include<stdio.h><ESC>o#includ…...

npm相关指令

​ 切换镜像 腾讯镜像 npm config set registry https://mirrors.cloud.tencent.com/npm/ 淘宝镜像&#xff08;新版&#xff09; npm config set registry https://registry.npmmirror.com 淘宝镜像&#xff08;旧版&#xff0c;已弃用&#xff09; npm config set regist…...

为什么不要碰自媒体

要是失业了&#xff0c;搞自媒体&#xff0c;可行吗&#xff1f;毫无希望&#xff01; 如今的自媒体早卷得不成样子了&#xff0c;很难再有机会&#xff0c;根本原因在于几乎没有增量用户的同时&#xff0c;存量用户也不再有剩余时间&#xff0c;全量用户的时间早已被几个自媒…...

酷炫末世意境背景404单页HTML源码

源码介绍 酷炫末世意境背景404单页HTML源码&#xff0c;背景充满着破坏一切的意境&#xff0c;彷佛末世的到来&#xff0c;可以做网站错误页或者丢失页面&#xff0c;将下面的代码放到空白的HTML里面&#xff0c;然后上传到服务器里面&#xff0c;设置好重定向即可 效果预览 …...

PHP 调用 1688 详情 API 接口的实战攻略

在电商领域&#xff0c;获取准确和详细的商品信息对于业务的发展至关重要。1688 作为国内知名的批发采购平台&#xff0c;其详情 API 接口为开发者提供了丰富的数据资源。本文将为您详细介绍如何使用 PHP 调用 1688 详情 API 接口。 一、前期准备 注册 1688 开放平台账号&#…...

SAP ABAP性能优化

1.前言 ABAP作为SAP的专用的开发语言&#xff0c;衡量其性能的指标主要有以下两个方面&#xff1a; 响应时间&#xff1a;对于某项特定的业务请求&#xff0c;系统在收到请求后需要多久返回结果 吞吐量&#xff1a;在给定的时间能&#xff0c;系统能够处理的数据量 2. ABAP语…...

【鸿蒙学习笔记】构建布局・选项卡 (Tabs)

官方文档&#xff1a;选项卡 (Tabs) 目录标题 底部导航顶部导航侧边导航限制导航栏的滑动切换固定导航栏・可滚动导航栏自定义导航栏切换至指定页签 底部导航 Entry Component struct Bujv_tabs {build() {Column() {Tabs({ barPosition: BarPosition.End }) {TabContent() {T…...

独立游戏《星尘异变》UE5 C++程序开发日志5——实现物流系统

目录 一、进出口清单 二、路径计算 三、包裹 1.包裹的数据结构 2.包裹在场景中的运动 四、道路 1.道路的数据结构 2.道路的建造 3.道路的销毁 4.某个有道路连接的建筑被删除 作为一个工厂类模拟经营游戏&#xff0c;各个工厂之间的运输必不可少&#xff0c;本游戏采用的…...

Web开发:<br>标签的作用

br作用 介绍基本用法常见用途注意事项使用CSS替代 介绍 在Web开发中&#xff0c;<br> 标签是一个用于插入换行符的HTML标签。它是“break”的缩写&#xff0c;常用于需要在文本中强制换行的地方。<br> 标签是一个空标签&#xff0c;这意味着它没有结束标签。 基本…...

DVC+Minio

由于参数文件比较大&#xff0c;因此onnx、engine等大文件弃用LFS管理&#xff0c;改用dvc管理&#xff1a; minio就是存储用的 启动miniosudo netstat -ntpl#查看端口号 sudo kill -9 $(sudo lsof -i:5061 -t) 关闭端口对应进程 ./minio server --console-address ":6570…...

C++内存管理(区别C语言)深度对比

欢迎来到我的Blog&#xff0c;点击关注哦&#x1f495; 前言 前面已经介绍了类和对象&#xff0c;对C面向对象编程已经有了全面认识&#xff0c;接下来要学习对语言学习比较重要的是对内存的管理。 一、内存的分区 代码区&#xff1a;存放程序的机器指令&#xff0c;通常是可…...

手把手带你写一个精简版 HashMap 的 put 方法

&#x1f446;&#x1f3fb;&#x1f446;&#x1f3fb;&#x1f446;&#x1f3fb;关注博主&#xff0c;让你的代码变得更加优雅。 前言 HashMap 大家工作中遇到的太多了&#xff0c;已经成了必须使用的类了&#xff0c; 在面试的时候 HashMap 基本是必问题&#xff0c;但是…...

【面试题】数据结构:堆排序的排序思想?

堆排序的排序思想&#xff1f; 堆排序是一种高效的排序算法&#xff0c;其基本思想是利用堆这种数据结构来实现排序。堆是一种特殊的完全二叉树&#xff0c;通常用数组来表示。堆排序的基本步骤如下&#xff1a; 1. 构建初始堆&#xff1a; 将待排序的数组转换成一个最大堆&a…...

凡客之家推广平台/seo快速排名软件平台

以前听这人说genymotion好&#xff0c;听那人说genymotion模拟器好&#xff0c;身为开发者&#xff0c;使用google原生模拟器确实有点慢&#xff0c;所以本人就到genymotion官网下了个带vitrualbox的安装包&#xff0c;然后下一步下一步安装&#xff0c;安装完成后启动genymoti…...

有哪些图片设计网站有哪些问题/百度推广登录入口登录

目录PLL1和PLL1控制器PLL2和PLL2控制器CSL的使用本文主要介绍TMS320C6455的时钟相关的内容&#xff0c;参考文档为&#xff1a; SPRS276M - TMS320C6455 Fixed-Point Digital Signal ProcessorSPRUE56 - TMS320C645x DSP Software-Programmable Phase-Locked Loop (PLL) Contr…...

华为官方网站专卖店/查网站

矩阵的Hadamard积与符号模式【摘要】&#xff1a;我们主要讨论了非负矩阵、M-阵的Hadamard积与Fan积问题&#xff0c;以及矩阵Hadamard积的一些范数不等式&#xff0e;同时也讨论了逆M-矩阵、零模式不变矩阵、符号模式矩阵、k-幂等阵和符号k-幂等阵等特殊矩阵的相关问题&#x…...

最新在线免费网站/电脑培训班有哪些科目

&#xfeff;为方便完成指定数据表的同步操作&#xff0c;可以采用dblink与merge结合的方法完成。操作环境&#xff1a;此数据库服务器ip为192.168.196.76,有center与branch两个库&#xff0c;一般需要将center的表数据同步到branch&#xff0c;center为源库&#xff0c;branch…...

wordpress安装演示不同/百度一下首页极简版

用matlab放大一个&#xff08;1&#xff0c;n&#xff09;的矩阵&#xff0c;这段代码的编写逻辑可以应用于图片的放大中 clear all arandi(100,1,10); binput(输入矩阵的列数(大于10)); czeros(1,b); %比例系数 bsb/10;for m1:b x1round(m/(bs)); %防溢出 if x<1y1; elsei…...

郑州建站费用/关键词搜索引擎又称为

在qt官网中推荐使用qxt解析csv文件 qxt在linux下并不支持qt5&#xff0c;主要原因是qt5使用了一些渲染和处理&#xff0c;这些都将qxt的原生性降低了。qxt的一部分仍然可以被复用&#xff0c;但目前并无复用的案例。 Excel解析&#xff0c;在windows下&#xff0c;qt提供了qta…...