CV baseline概览|AlexNet:ImageNet Classification with Deep Convolutional Neural Networks基于深度卷积神经网络的图像分类

CV baseline重要性

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

CNN发展历程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

演化路径

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

AlexNet概览

在这里插入图片描述

AlexNet全文翻译

在这里插入图片描述

摘要

在这里插入图片描述
我们训练了一个大型深度卷积神经网络,将 ImageNet LSVRC-2010 竞赛中的 120 万张高分辨率图像分类为 1000 个不同的类别。 在测试数据上,我们实现了 37.5% 和 17.0% 的 top-1 和 top-5 错误率,这比之前的最先进水平要好得多。

该神经网络拥有 6000 万个参数和 650,000 个神经元由五个卷积层(其中一些后面是最大池层)和三个全连接层(最终为 1000 路 softmax)组成。

为了加快训练速度,我们使用非饱和神经元和非常高效的 GPU 实现卷积运算

为了减少全连接层中的过度拟合,我们采用了最近开发的称为“dropout”的正则化方法,事实证明该方法非常有效。 我们还在 ILSVRC-2012 竞赛中输入了该模型的一个变体,并取得了 15.3% 的获胜前 5 名测试错误率,而第二名的测试错误率为 26.2%。
在这里插入图片描述
在这里插入图片描述

1 引言

在这里插入图片描述
在这里插入图片描述

当前的物体识别方法充分利用了机器学习方法。 为了提高它们的性能,我们可以收集更大的数据集,学习更强大的模型,并使用更好的技术来防止过度拟合。 直到最近,标记图像的数据集还相对较小——大约有数万张图像(例如,NORB [16]、Caltech-101/256 [8, 9] 和 CIFAR-10/100 [12])。 使用这种大小的数据集可以很好地解决简单的识别任务,特别是如果通过保留标签的转换来增强它们。 例如,MNIST 数字识别任务当前的最佳错误率 (<0.3%) 接近人类表现 [4]。 但现实环境中的物体表现出相当大的可变性,因此要学习识别它们,有必要使用更大的训练集。 事实上,小型图像数据集的缺点已被广泛认识到(例如,Pinto 等人[21]),但直到最近才成为可能收集包含数百万张图像的标记数据集。 新的更大数据集包括 LabelMe [23](由数十万张完全分割的图像组成)和 ImageNet [6](由 22,000 多个类别的超过 1500 万张带标签的高分辨率图像组成)。

为了从数百万张图像中了解数千个物体,我们需要一个具有强大学习能力的模型。 然而,对象识别任务的巨大复杂性意味着即使像 ImageNet 这样大的数据集也无法指定这个问题,因此我们的模型还应该拥有大量先验知识来弥补我们没有的所有数据。 卷积神经网络(CNN)就是这样一类模型[16,11,13,18,15,22,26]。 它们的容量可以通过改变深度和广度来控制,并且它们还对图像的性质(即统计的平稳性和像素依赖性的局部性)做出强有力且基本正确的假设。 因此,与具有类似大小层的标准前馈神经网络相比,CNN 的连接和参数要少得多,因此它们更容易训练,而它们理论上的最佳性能可能只是稍差一些。

尽管 CNN 具有吸引人的品质,并且尽管其本地架构相对高效,但大规模应用于高分辨率图像仍然昂贵得令人望而却步。 幸运的是,当前的 GPU 与高度优化的 2D 卷积实现相结合,功能强大,足以促进有趣的大型 CNN 的训练,并且 ImageNet 等最近的数据集包含足够的标记示例来训练此类模型,而不会出现严重的过度拟合

本文的具体贡献如下:我们在 ILSVRC-2010 和 ILSVRC-2012 竞赛 [2] 中使用的 ImageNet 子集上训练了迄今为止最大的卷积神经网络之一,并取得了迄今为止报道过的最佳结果在这些数据集。 我们编写了高度优化的 2D 卷积 GPU 实现以及训练卷积神经网络固有的所有其他操作,并将其公开。 我们的网络包含许多新的和不寻常的功能,这些功能可以提高其性能并减少训练时间,第 3 节对此进行了详细介绍。即使有 120 万个标记训练样本,我们网络的规模也使过度拟合成为一个严重问题,因此我们使用了几种有效的技术来防止过度拟合,这将在第 4 节中进行描述。我们的最终网络包含五个卷积层和三个全连接层,并且 这个深度似乎很重要:我们发现删除任何卷积层(每个卷积层包含不超过模型参数的 1%)都会导致性能下降。

最后,网络的大小主要受到当前 GPU 上可用内存量以及我们愿意容忍的训练时间的限制。 我们的网络需要五到六天的时间在两个 GTX 580 3GB GPU 上进行训练。 我们所有的实验都表明,只需等待更快的 GPU 和更大的数据集可用,我们的结果就可以得到改善。

2 数据集

在这里插入图片描述
ImageNet 是一个包含超过 1500 万张带标签的高分辨率图像的数据集,属于大约 22,000 个类别。 这些图像是从网络上收集的,并由人工贴标员使用亚马逊的 Mechanical Turk 众包工具进行标记。 从 2010 年开始,作为 Pascal 视觉对象挑战赛的一部分,每年举办一次名为 ImageNet 大规模视觉识别挑战赛 (ILSVRC) 的比赛。 ILSVRC 使用 ImageNet 的子集,每个类别包含大约 1000 个图像。 总共大约有 120 万张训练图像、5 万张验证图像和 15 万张测试图像。

ILSVRC-2010 是唯一可用测试集标签的 ILSVRC 版本,因此我们在该版本上执行了大部分实验。 由于我们还在 ILSVRC-2012 竞赛中输入了我们的模型,因此在第 6 节中,我们也报告了此版本数据集的结果,其中测试集标签不可用。 在 ImageNet 上,通常报告两个错误率:top-1 和 top-5,其中 top-5 错误率是测试图像中正确标签不在模型认为最有可能的五个标签中的比例 。

ImageNet 由可变分辨率图像组成,而我们的系统需要恒定的输入维度。 因此,我们将图像下采样到固定分辨率 256 × 256。给定一个矩形图像,我们首先重新缩放图像,使短边的长度为 256 ,然后从生成的图像中裁剪出中央 256 × 256 的patch。 除了从每个像素中减去训练集的mean activity之外,我们没有以任何其他方式预处理图像。 因此,我们根据像素的(居中)原始 RGB 值来训练我们的网络。

3 网络结构

在这里插入图片描述
我们的网络架构如图 2 所示。它包含八个学习层——五个卷积层和三个全连接层。 下面,我们描述了我们网络架构的一些新颖或不寻常的特征。 第 3.1-3.4 节根据我们对其重要性的估计进行排序,最重要的排在最前面。
在这里插入图片描述

3.1 ReLU 非线性激活函数

在这里插入图片描述
图 1:使用 ReLU(实线)的四层卷积神经网络在 CIFAR-10 上达到 25% 的训练错误率,比使用 tanh 神经元(虚线)的等效网络快六倍。 每个网络的学习率都是独立选择的,以使训练尽可能快。 没有采用任何形式的正则化。 这里展示的效果大小因网络架构而异,但使用 ReLU 的网络的学习速度始终比使用饱和神经元的网络快几倍。

对神经元输出进行建模的标准方法是使用双曲正切函数 (tanh) 或 sigmoid 函数 ( 1 + e − x ) − 1 (1 + e^{-x}) ^{-1} (1+ex)1就梯度下降的训练时间而言,这些饱和非线性比非饱和非线性 f ( x ) = m a x ( 0 , x ) f(x)=max(0,x) f(x)=max(0,x)慢得多。 遵循 Nair 和 Hinton [20],我们将具有这种非线性的神经元称为整流线性单元 (ReLU)。 使用 ReLU 的深度卷积神经网络的训练速度比使用 tanh 单元的深度卷积神经网络快几倍。 图 1 对此进行了演示,该图显示了特定四层卷积网络在 CIFAR-10 数据集上达到 25% 训练误差所需的迭代次数。 该图表明,如果我们使用传统的饱和神经元模型,我们将无法在这项工作中试验如此大型的神经网络。

我们并不是第一个考虑替代 CNN 中传统神经元模型的人。 例如,贾勒特等人 [11] 声称非线性 f ( x ) = ∣ t a n h ( x ) ∣ {f(x) = |tanh(x)|} f(x)=tanh(x)在 Caltech-101 数据集上使用其对比度归一化类型和局部平均池化效果特别好。 然而,在这个数据集上,主要关注的是防止过度拟合,因此他们观察到的效果与我们在使用 ReLU 时报告的加速拟合训练集的能力不同。 更快的学习对在大型数据集上训练的大型模型的性能有很大影响。

在这里插入图片描述
在这里插入图片描述

3.2 在多GPU上训练

在这里插入图片描述
单个 GTX 580 GPU 仅具有 3GB 内存,这限制了可在其上训练的网络的最大大小。 事实证明,120 万个训练样本足以训练一个 GPU 无法容纳的庞大网络。 因此,我们将网络分布在两个 GPU 上。 当前的 GPU 特别适合跨 GPU 并行化,因为它们能够直接读取和写入彼此的内存,而无需通过主机内存。

我们采用的并行化方案本质上是将一半的内核(或神经元)放在每个 GPU 上还有一个额外的技巧:GPU 仅在某些层中进行通信。 这意味着,例如,第 3 层的内核从第 2 层中的所有内核映射获取输入。但是,第 4 层中的内核仅从驻留在同一 GPU 上的第 3 层中的那些内核映射获取输入。 选择连接模式是交叉验证的一个问题,但这使我们能够精确调整通信量,直到它达到计算量的可接受的分数。

由此产生的架构有点类似于 Cire¸san 等人采用的“柱状”CNN。 [5],除了我们的列不是独立的(见图 2)。 与在一个 GPU 上训练的每个卷积层中内核数量减半的网络相比,该方案将 top-1 和 top-5 错误率分别降低了 1.7% 和 1.2%。 两 GPU 网络的训练时间比一 GPU 网络2略少。

2 ^2 2在最终卷积层中,单 GPU 网络实际上具有与双 GPU 网络相同数量的内核。 这是因为网络的大部分参数都在第一个全连接层中,该层将最后一个卷积层作为输入。 因此,为了使两个网络具有大致相同数量的参数,我们没有将最终卷积层(也没有随后的全连接层)的大小减半。 因此,这种比较偏向于单 GPU 网络,因为它比双 GPU 网络的“一半大小”还要大。

3.3 局部响应标准化LRN

在这里插入图片描述
LRN这种思想后来在2014年被证明没有什么效果

ReLU 具有理想的特性,即它们不需要输入标准化来防止它们饱和。 如果至少有一些训练样例对 ReLU 产生正输入,学习就会在该神经元中发生。 然而,我们仍然发现以下局部标准化方案有助于泛化。 用 a x , y i a^i_{x,y} ax,yi 表示通过在位置 (x, y) 处应用内核 i 然后应用 ReLU 非线性计算的神经元活动,响应归一化活动 b x , y i b^i_{x,y} bx,yi 由以下表达式给出
在这里插入图片描述
其中总和运行在同一空间位置的 n 个“相邻”核映射上,N 是层中核的总数。 内核映射的顺序当然是任意的,并且在训练开始之前确定。 这种响应标准化实现了一种受真实神经元中发现的类型启发的侧向抑制形式,在使用不同内核计算的神经元输出之间创造了对大活动的竞争。 常数 k、n、α 和 β 是超参数,其值是使用验证集确定的; 我们使用 k = 2、n = 5、α = 1 0 − 4 10^{−4} 104 和 β = 0.75。 我们在某些层中应用 ReLU 非线性之后应用了这种归一化(参见第 3.5 节)。

该方案与 Jarrett 等人的局部对比度归一化方案有一些相似之处。 [11],但我们的方法更正确地称为“亮度归一化”,因为我们不减去平均活动。 响应归一化将 top-1 和 top-5 错误率分别降低了 1.4% 和 1.2%。 我们还在 CIFAR-10 数据集上验证了该方案的有效性:四层 CNN 在没有归一化的情况下实现了 13% 的测试错误率,在归一化的情况下达到了 11%。
在这里插入图片描述

3.4 重叠池化

在这里插入图片描述

CNN 中的池化层汇总了同一内核映射中相邻神经元组的输出。 传统上,相邻池单元汇总的邻域不重叠(例如,[17,11,4])。 更准确地说,池化层可以被认为由间隔 s 个像素的池化单元网格组成,每个池化单元汇总了以池化单元位置为中心的大小为 z × z 的邻域。 如果我们设置 s = z,我们就获得了 CNN 中常用的传统局部池化。 如果我们设置 s < z,我们就获得重叠池化。 这是我们在整个网络中使用的,其中 s = 2 和 z = 3。与非重叠方案 s = 相比,该方案将 top-1 和 top-5 错误率分别降低了 0.4% 和 0.3% 2、z = 2,产生同等维度的输出。 我们通常在训练过程中观察到,具有重叠池化的模型发现过拟合稍微困难一些。
在这里插入图片描述

3.5 整体架构

在这里插入图片描述
在这里插入图片描述
图 2:我们的 CNN 架构图,明确显示了两个 GPU 之间的职责划分。 一个 GPU 运行图顶部的层部分,而另一个 GPU 运行底部的层部分。 GPU 仅在某些层进行通信。 网络的输入为 150,528 维,网络剩余层中的神经元数量为 253,440–186,624–64,896–64,896–43,264–4096–4096–1000

现在我们准备好描述 CNN 的整体架构了。 如图 2 所示,该网络包含八个带有权重的层; 前五个是卷积的,其余三个是全连接的。

最后一个全连接层的输出被馈送到 1000 路 softmax,生成 1000 个类标签的分布。

我们的网络最大化多项式逻辑回归目标,这相当于最大化预测分布下正确标签的对数概率的训练案例的平均值。

第二、第四和第五卷积层的内核仅连接到驻留在同一 GPU 上的前一层中的内核映射(参见图 2)。 第三卷积层的内核连接到第二层中的所有内核映射。 全连接层中的神经元与前一层中的所有神经元连接。 响应归一化层(其实就是LRN局部响应标准化)位于第一和第二卷积层之后。 最大池层(第 3.4 节中描述的那种)位于响应归一化层和第五个卷积层之后。 ReLU 非线性应用于每个卷积层和全连接层的输出。

第一个卷积层使用 96 个大小为 11×11×3 的核,步幅为 4 个像素(这是核图中相邻神经元的感受野中心之间的距离)对 224×224×3 输入图像进行过滤。 第二个卷积层将第一个卷积层的(响应归一化和池化)输出作为输入,并使用 256 个大小为 5 × 5 × 48 的内核对其进行过滤。第三、第四和第五卷积层相互连接,无需任何中间的池化或标准化层。 第三个卷积层有 384 个大小为 3 × 3 × 256 的内核,连接到第二个卷积层的(归一化、池化)输出。 第四个卷积层有 384 个大小为 3 × 3 × 192 的内核,第五个卷积层有 256 个大小为 3 × 3 × 192 的内核。每个全连接层有 4096 个神经元。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
F i F_i Fi为图片通道数, K s K_s Ks为卷积核大小, K n K_n Kn为卷积核数量

4 减轻过拟合

在这里插入图片描述
我们的神经网络架构有 6000 万个参数。 尽管 ILSVRC 的 1000 个类别使每个训练示例对从图像到标签的映射施加 10 位约束,但事实证明,这不足以在不出现严重过拟合的情况下学习如此多的参数。 下面,我们描述了对抗过度拟合的两种主要方法。

注意:这里的10位约束是由于 2 10 > 1000 2^{10} > 1000 210>1000而得出来的

4.1 数据增强

在这里插入图片描述
减少图像数据过度拟合的最简单和最常见的方法是使用标签保留变换人为地扩大数据集(例如,[25,4,5])我们采用两种不同形式的数据增强,这两种形式都允许通过很少的计算从原始图像生成转换后的图像,因此转换后的图像不需要存储在磁盘上。 (相当于online的技术))在我们的实现中,转换后的图像是在 CPU 上用 Python 代码生成的,而 GPU 则对前一批图像进行训练。 因此,这些数据增强方案实际上是无需计算的。

数据增强的第一种形式包括生成图像平移和水平反射。 我们通过从 256×256 图像中提取随机 224×224 块(及其水平反射)并在这些提取的块上训练我们的网络来实现此目的。 这将我们的训练集大小增加了 2048 倍,(这里的2048是怎么得来的呢,256-224=32,32*32=1024,这是一张图像可以得到1024倍的图像,同时我们还可以通过水平反射再得到1024倍图像,所以就增加了2048倍)尽管最终的训练示例当然是高度相互依赖的。 如果没有这个方案,我们的网络就会遭受严重的过度拟合,这将迫使我们使用更小的网络。 **在测试时,网络通过提取五个 224 × 224 补丁(四个角补丁和中心补丁)及其水平反射(因此总共十个patches)来进行预测,**并对网络的 softmax 层做出的预测进行平均在十个patches上。
在这里插入图片描述
数据增强的第二种形式包括改变训练图像中 RGB 通道的强度。 具体来说,我们对整个 ImageNet 训练集中的 RGB 像素值集执行 PCA。 对于每个训练图像,我们添加多个找到的主成分,其大小与相应的特征值乘以从均值为零和标准差为 0.1 的高斯分布中抽取的随机变量成正比。 因此,对于每个 RGB 图像像素 I x y I_{xy} Ixy = [ I x y R I^R_{xy} IxyR, I x y G I^G_{xy} IxyG, I x y B ] T I^B_{xy}]^T IxyB]T,我们添加以下数量
在这里插入图片描述
其中 p i p_i pi λ i λ_i λi分别是RGB像素值的3×3协方差矩阵的第i个特征向量和特征值, α i α_i αi是上述随机变量。 每个 α i α_i αi 仅针对特定训练图像的所有像素绘制一次,直到该图像再次用于训练,此时将重新绘制。 该方案大致捕捉了自然图像的一个重要属性,即对象身份对于照明强度和颜色的变化是不变的。 该方案将top-1错误率降低了1%以上。

相当于对色彩空间进行一个扰动
在这里插入图片描述

4.2 Dropout 随机失活

在这里插入图片描述
结合许多不同模型的预测是减少测试错误的一种非常成功的方法[1, 3],但对于已经需要几天时间来训练的大型神经网络来说似乎太昂贵了。 然而,有一个非常有效的模型组合版本,在训练期间仅花费大约两倍的成本。 最近推出的技术称为“dropout”[10],包括以 0.5 的概率将每个隐藏神经元的输出设置为零。 以这种方式“丢弃”的神经元不会对前向传播做出贡献,也不参与反向传播。 因此,每次出现输入时,神经网络都会对不同的架构进行采样,但所有这些架构都共享权重。 这项技术减少了神经元复杂的共同适应,因为神经元不能依赖于特定其他神经元的存在。 因此,它被迫学习更强大的特征,这些特征与其他神经元的许多不同的随机子集结合使用是有用的。 在测试时,我们使用所有神经元,但将它们的输出乘以 0.5,这是采用指数多丢失网络产生的预测分布的几何平均值的合理近似值。

我们在图 2 的前两个全连接层中使用了 dropout。如果没有 dropout,我们的网络就会表现出严重的过度拟合。 Dropout 大约使收敛所需的迭代次数增加了一倍。
在这里插入图片描述

5 超参数设置

在这里插入图片描述
图 3:第一个卷积层在 224×224×3 输入图像上学习了 96 个大小为 11×11×3 的卷积核。 前 48 个内核是在 GPU 1 上学习的,而后 48 个内核是在 GPU 2 上学习的。有关详细信息,请参阅第 6.1 节

我们使用随机梯度下降来训练模型,批量大小为 128 个示例,动量为 0.9,权重衰减为 0.0005。 我们发现这种少量的权重衰减对于模型的学习很重要。 换句话说,这里的权重衰减不仅仅是一个正则化器:它减少了模型的训练误差。 权重w的更新规则为
在这里插入图片描述
其中 i 是迭代索引,v 是动量变量, ϵ \epsilon ϵ是学习率, < ∂ L ∂ w ∣ w i > D i <\frac{\partial L}{\partial w}|_{w_i}>_{D_i} <wLwi>Di是在 w i w_i wi 处评估的目标相对于 w 的导数第 i 个批次 D i D_i Di 的平均值

SGD:
在这里插入图片描述
我们从标准差为 0.01 的零均值高斯分布初始化每层的权重。 我们使用常量 1 初始化第二、第四和第五卷积层以及全连接隐藏层中的神经元偏差。这种初始化通过为 ReLU 提供正输入来加速早期阶段的学习。 我们用常量 0 初始化剩余层中的神经元偏差。

我们对所有层使用相同的学习率,并在整个训练过程中手动调整。 我们遵循的启发式方法是,当验证错误率不再随当前学习率提高时,将学习率除以 10。 学习率初始化为 0.01,并在终止前降低三倍。 我们通过 120 万张图像的训练集对网络进行了大约 90 个周期的训练,这在两个 NVIDIA GTX 580 3GB GPU 上花费了五到六天的时间。

6 结果

在这里插入图片描述
表 1 总结了我们在 ILSVRC-2010 上的结果。我们的网络实现了 37.5% 和 17.0%5 的 top-1 和 top-5 测试集错误率。 ILSVRC-2010 竞赛期间取得的最佳性能为 47.1% 和 28.2%,采用的方法是对在不同特征上训练的六个稀疏编码模型产生的预测进行平均 [2],此后发布的最佳结果为 45.7% 和 25.7 % 采用一种方法,对根据两种密集采样特征计算的费舍尔向量 (FV) 训练的两个分类器的预测进行平均 [24]。

5 :如第 4.1 节所述,在不对 10 个补丁进行平均预测的情况下,错误率分别为 39.0% 和 18.3%

表 1:ILSVRC-2010 测试集结果比较。 斜体字是其他人取得的最佳结果

我们还在 ILSVRC-2012 竞赛中输入了我们的模型,并在表 2 中报告了我们的结果。由于 ILSVRC-2012 测试集标签未公开,因此我们无法报告我们尝试过的所有模型的测试错误率。 在本段的其余部分中,我们交替使用验证错误率和测试错误率,因为根据我们的经验,它们的差异不会超过 0.1%(参见表 2)。 本文描述的 CNN 实现了 18.2% 的 top-5 错误率。 对 5 个相似 CNN 的预测进行平均,错误率为 16.4%。 训练一个 CNN,在最后一个池化层上增加一个额外的第六个卷积层,对整个 ImageNet Fall 2011 版本(15M 图像,22K 类别)进行分类,然后在 ILSVRC-2012 上对其进行“微调”,得到的错误率为 16.6 %。 将在整个 2011 年秋季版本中预训练的两个 CNN 与上述五个 CNN 的预测进行平均,得出的错误率为 15.3%。 第二佳参赛作品的错误率达到了 26.2%,其方法是对根据不同类型的密集采样特征计算出的 FV 训练的几个分类器的预测进行平均 [7]

表 2:ILSVRC-2012 验证集和测试集的错误率比较。 斜体字是其他人取得的最佳结果。 带星号*的模型经过“预训练”,可以对整个 ImageNet 2011 秋季版本进行分类。 详细信息请参见第 6 节。

最后,我们还报告了 ImageNet 2009 年秋季版本(包含 10,184 个类别和 890 万张图像)的错误率。 在此数据集上,我们遵循文献中的惯例,使用一半图像进行训练,一半图像进行测试。 由于没有建立的测试集, 我们的分割必然与之前作者使用的分割不同,但这不会明显影响结果。 我们在此数据集上的 top-1 和 top-5 错误率分别为 67.4% 和 40.9%,这是通过上述网络实现的,但在最后一个池化层上增加了第六个卷积层。 该数据集上已发表的最佳结果为 78.1% 和 60.9% [19]
在这里插入图片描述

6.1 定性评估

在这里插入图片描述
图 3 显示了网络的两个数据连接层学习的卷积核。 该网络已经学习了各种频率和方向选择性内核,以及各种彩色斑点。 请注意两个 GPU 表现出的专业化,这是第 3.5 节中描述的受限连接的结果。 GPU 1 上的内核很大程度上与颜色无关,而 GPU 2 上的内核主要与颜色相关。 这种专门化发生在每次运行期间,并且独立于任何特定的随机权重初始化(以 GPU 重新编号为模)。
在这里插入图片描述
在这里插入图片描述
图 4:(左)八张 ILSVRC-2010 测试图像和我们的模型认为最有可能的五个标签。 正确的标签写在每个图像下方,分配给正确标签的概率也用红色条显示(如果它恰好在前 5 个中)。 (右)第一列中的五张 ILSVRC-2010 测试图像。 其余列显示了六个训练图像,它们在最后一个隐藏层中生成特征向量,与测试图像的特征向量的欧几里得距离最小。

在图 4 的左图中,我们通过计算 8 个测试图像的前 5 个预测来定性评估网络所学到的内容。 请注意,即使是偏离中心的物体,例如左上角的螨虫,也可以被网络识别。 大多数前 5 名的标签看起来都是合理的。 例如,只有其他类型的猫才被认为是豹子的合理标签。 在某些情况下(格栅、樱桃),照片的预期焦点确实存在模糊性。

探测网络视觉知识的另一种方法是考虑图像在最后 4096 维隐藏层引起的特征激活。 如果两个图像产生具有较小欧几里德分离的特征激活向量,我们可以说神经网络的更高层认为它们是相似的。 图 4 显示了测试集中的 5 张图像和训练集中的 6 张图像,根据此测量,它们与每张图像最相似。 请注意,在像素级别,检索到的训练图像在 L2 中通常与第一列中的查询图像不接近。 例如,被回收的狗和大象以各种姿势出现。 我们在补充材料中提供了更多测试图像的结果。

使用两个 4096 维实值向量之间的欧几里德距离计算相似度效率较低,但可以通过训练自动编码器将这些向量压缩为短二进制代码来提高效率。 与将自动编码器应用于原始像素[14]相比,这应该产生更好的图像检索方法,自动编码器不使用图像标签,因此倾向于检索具有相似边缘模式的图像,无论它们在语义上是否相似。

在这里插入图片描述

7 探讨

在这里插入图片描述
我们的结果表明,大型深度卷积神经网络能够使用纯监督学习在极具挑战性的数据集上取得破纪录的结果。 值得注意的是,如果删除单个卷积层,我们的网络性能就会下降。 例如,删除任何中间层都会导致网络的 top-1 性能损失约 2%。 因此,深度对于实现我们的成果确实很重要。

为了简化我们的实验,我们没有使用任何无监督的预训练,尽管我们期望它会有所帮助,特别是如果我们获得足够的计算能力来显著增加网络的大小而无需获得标记数据量的相应增加。 到目前为止,我们的结果已经有所改善,因为我们扩大了网络并训练了更长的时间,但为了匹配人类视觉系统的下颞叶路径,我们仍然有许多数量级的工作要做。 最终,我们希望在视频序列上使用非常大和深的卷积网络,其中时间结构提供了静态图像中缺失或不太明显的非常有用的信息。

AlexNet研究背景

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Top 5error

在这里插入图片描述
对于每张图像,算法将生成最多 5 个对象类别的列表,按置信度降序排列。 将根据与图像的真实标签最匹配的标签来评估标签的质量。 这个想法是允许算法识别图像中的多个对象,并且如果识别出的对象之一实际上存在但未包含在基本事实中,则不会受到惩罚

如果y = x,即产生的标签等于真实标签,则返回值为0,否则为1

算法的总体误差得分是所有测试图像的平均误差
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

AlexNet研究成果意义

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

AlexNet训练技巧

在这里插入图片描述

AlexNet实验结果及分析

在这里插入图片描述

论文总结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

论文代码复现准备工作

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

AlexNet推理代码结构

在这里插入图片描述
在这里插入图片描述

# -*- coding: utf-8 -*-
import os
os.environ['NLS_LANG'] = 'SIMPLIFIED CHINESE_CHINA.UTF8'
import time
import json
import torch.nn as nn
import torch
import torchvision.transforms as transforms
from PIL import Image
from matplotlib import pyplot as plt
import torchvision.models as models
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")


def img_transform(img_rgb, transform=None):
    """
    将数据转换为模型读取的形式
    :param img_rgb: PIL Image
    :param transform: torchvision.transform
    :return: tensor
    """

    if transform is None:
        raise ValueError("找不到transform!必须有transform对img进行处理")

    img_t = transform(img_rgb)
    return img_t


def load_class_names(p_clsnames, p_clsnames_cn):
    """
    加载标签名
    :param p_clsnames:
    :param p_clsnames_cn:
    :return:
    """
    with open(p_clsnames, "r") as f:
        class_names = json.load(f)
    with open(p_clsnames_cn, encoding='UTF-8') as f:  # 设置文件对象
        class_names_cn = f.readlines()
    return class_names, class_names_cn


def get_model(path_state_dict, vis_model=False):
    """
    创建模型,加载参数
    :param path_state_dict:
    :return:
    """
    model = models.alexnet()
    pretrained_state_dict = torch.load(path_state_dict)
    model.load_state_dict(pretrained_state_dict)
    model.eval()

    if vis_model:
        from torchsummary import summary
        summary(model, input_size=(3, 224, 224), device="cpu")

    model.to(device)
    return model


def process_img(path_img):

    # hard code
    norm_mean = [0.485, 0.456, 0.406]
    norm_std = [0.229, 0.224, 0.225]
    inference_transform = transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize(norm_mean, norm_std),
    ])

    # path --> img
    img_rgb = Image.open(path_img).convert('RGB')

    # img --> tensor
    img_tensor = img_transform(img_rgb, inference_transform)
    img_tensor.unsqueeze_(0)        # chw --> bchw
    img_tensor = img_tensor.to(device)

    return img_tensor, img_rgb


if __name__ == "__main__":

    # config
    path_state_dict = os.path.join(BASE_DIR, "..", "data", "alexnet-owt-4df8aa71.pth")
    path_img = os.path.join(BASE_DIR, "..", "data", "Golden Retriever from baidu.jpg")
    # path_img = os.path.join(BASE_DIR, "..", "data", "tiger cat.jpg")
    path_classnames = os.path.join(BASE_DIR, "..", "data", "imagenet1000.json")
    path_classnames_cn = os.path.join(BASE_DIR, "..", "data", "imagenet_classnames.txt")

    # load class names
    cls_n, cls_n_cn = load_class_names(path_classnames, path_classnames_cn)

    # 1/5 load img
    img_tensor, img_rgb = process_img(path_img)

    # 2/5 load model
    alexnet_model = get_model(path_state_dict, True)

    # 3/5 inference  tensor --> vector
    # 只做前向传播
    with torch.no_grad():
        time_tic = time.time()
        outputs = alexnet_model(img_tensor)
        time_toc = time.time()

    # 4/5 index to class names
    _, pred_int = torch.max(outputs.data, 1)
    _, top5_idx = torch.topk(outputs.data, 5, dim=1)

    pred_idx = int(pred_int.cpu().numpy())
    pred_str, pred_cn = cls_n[pred_idx], cls_n_cn[pred_idx]
    print("img: {} is: {}\n{}".format(os.path.basename(path_img), pred_str, pred_cn))
    print("time consuming:{:.2f}s".format(time_toc - time_tic))

    # 5/5 visualization
    plt.imshow(img_rgb)
    plt.title("predict:{}".format(pred_str))
    top5_num = top5_idx.cpu().numpy().squeeze()
    text_str = [cls_n[t] for t in top5_num]
    for idx in range(len(top5_num)):
        plt.text(5, 15+idx*30, "top {}:{}".format(idx+1, text_str[idx]), bbox=dict(fc='yellow'))
    plt.show()

Pytorch中关于AlexNet的构建架构

class AlexNet(nn.Module):
    def __init__(self, num_classes: int = 1000, dropout: float = 0.5) -> None:
        super().__init__()
        _log_api_usage_once(self)
        self.features = nn.Sequential(
            # conv1
            # 3通道,64卷积核个数=输出通道数
            # 原文是96个卷积核---> 64个
            # 舍弃LRN
            nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),

            # conv2
            # 256-->192
            nn.Conv2d(64, 192, kernel_size=5, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),

            # conv3
            nn.Conv2d(192, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),

            # conv4
            # 385-->256
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),

            # conv5
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
        )
        # trick
        # 自适应池化层
        # 保证输出都是6*6
        self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
        self.classifier = nn.Sequential(
            nn.Dropout(p=dropout),
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(inplace=True),

            nn.Dropout(p=dropout),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),

            nn.Linear(4096, num_classes),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # 特征提取器
        x = self.features(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        # 分类器
        x = self.classifier(x)
        return x

在这里插入图片描述
在这里插入图片描述

运行结果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
其中关于参数的计算,在3.5也解释了,比方说Conv1的参数计算就是 11 × 11 × 3 × 64 + 64 = 23296 11\times11\times3\times64+64=23296 11×11×3×64+64=23296
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

AlexNet可视化代码结构

在这里插入图片描述
在这里插入图片描述

# -*- coding: utf-8 -*-
import os
import torch
import torch.nn as nn
from PIL import Image
import torchvision.transforms as transforms
from torch.utils.tensorboard import SummaryWriter
import torchvision.utils as vutils
import torchvision.models as models
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

if __name__ == "__main__":

    log_dir = os.path.join(BASE_DIR, "..", "results")
    # ----------------------------------- kernel visualization -----------------------------------
    writer = SummaryWriter(log_dir=log_dir, filename_suffix="_kernel")

    # m1
    # alexnet = models.alexnet(pretrained=True)

    # m2
    path_state_dict = os.path.join(BASE_DIR, "..", "data", "alexnet-owt-4df8aa71.pth")
    alexnet = models.alexnet()
    pretrained_state_dict = torch.load(path_state_dict)
    alexnet.load_state_dict(pretrained_state_dict)

    kernel_num = -1
    vis_max = 1
    for sub_module in alexnet.modules():
        if not isinstance(sub_module, nn.Conv2d):
            continue
        kernel_num += 1
        if kernel_num > vis_max:
            break

        kernels = sub_module.weight
        # c_out: 64   c_int: 3   k_w: 11   k_h:11
        c_out, c_int, k_h, k_w = tuple(kernels.shape)

        # 拆分channel
        for o_idx in range(c_out):
            kernel_idx = kernels[o_idx, :, :, :].unsqueeze(1)  # 获得(3, h, w), 但是make_grid需要 BCHW,这里拓展C维度变为(3, 1, h, w)
            kernel_grid = vutils.make_grid(kernel_idx, normalize=True, scale_each=True, nrow=c_int)
            writer.add_image('{}_Convlayer_split_in_channel'.format(kernel_num), kernel_grid, global_step=o_idx)

        kernel_all = kernels.view(-1, 3, k_h, k_w)  # 3, h, w
        kernel_grid = vutils.make_grid(kernel_all, normalize=True, scale_each=True, nrow=8)  # c, h, w
        writer.add_image('{}_all'.format(kernel_num), kernel_grid, global_step=620)

        print("{}_convlayer shape:{}".format(kernel_num, tuple(kernels.shape)))

    # ----------------------------------- feature map visualization -----------------------------------
    writer = SummaryWriter(log_dir=log_dir, filename_suffix="_feature map")

    # 数据
    path_img = os.path.join(BASE_DIR, "..", "data", "tiger cat.jpg")  # your path to image
    # 图片预处理的操作
    normMean = [0.49139968, 0.48215827, 0.44653124]
    normStd = [0.24703233, 0.24348505, 0.26158768]
    norm_transform = transforms.Normalize(normMean, normStd)
    img_transforms = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        norm_transform
    ])

    img_pil = Image.open(path_img).convert('RGB')
    img_tensor = img_transforms(img_pil)
    img_tensor.unsqueeze_(0)  # chw --> bchw

    # 模型
    # alexnet = models.alexnet(pretrained=True)

    # forward
    convlayer1 = alexnet.features[0]
    fmap_1 = convlayer1(img_tensor)

    # 预处理
    fmap_1.transpose_(0, 1)  # bchw=(1, 64, 55, 55) --> (64, 1, 55, 55)
    fmap_1_grid = vutils.make_grid(fmap_1, normalize=True, scale_each=True, nrow=8)

    writer.add_image('feature map in conv1', fmap_1_grid, global_step=620)
    writer.close()

在这里插入图片描述
在这里插入图片描述
可以注意到step=37对应下面第一个卷积核的第37个图,R,G,B叠加正好就是图中是绿色的
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

AlexNet训练猫狗数据集

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

# -*- coding: utf-8 -*-
import os
import numpy as np
import torch.nn as nn
import torch
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torch.optim as optim
from matplotlib import pyplot as plt
import torchvision.models as models
from tools.my_dataset import CatDogDataset

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")


def get_model(path_state_dict, vis_model=False):
    """
    创建模型,加载参数
    :param path_state_dict:
    :return:
    """
    model = models.alexnet()
    pretrained_state_dict = torch.load(path_state_dict)
    model.load_state_dict(pretrained_state_dict)

    if vis_model:
        from torchsummary import summary
        summary(model, input_size=(3, 224, 224), device="cpu")

    model.to(device)
    return model


if __name__ == "__main__":

    # config
    data_dir = os.path.join(BASE_DIR, "..", "data", "train")
    path_state_dict = os.path.join(BASE_DIR, "..", "data", "alexnet-owt-4df8aa71.pth")
    num_classes = 2

    MAX_EPOCH = 30       # 可自行修改
    BATCH_SIZE = 128    # 可自行修改
    LR = 0.001          # 可自行修改
    log_interval = 1    # 可自行修改
    val_interval = 1    # 可自行修改
    classes = 2
    start_epoch = -1
    # 学习率衰减
    lr_decay_step = 10   # 可自行修改

    # ============================ step 1/5 数据 ============================
    norm_mean = [0.485, 0.456, 0.406]
    norm_std = [0.229, 0.224, 0.225]

    train_transform = transforms.Compose([
        # 表示按照长宽比让短边缩小为256
        transforms.Resize((256)),      # (256, 256) 区别
        transforms.CenterCrop(256),
        transforms.RandomCrop(224),
        transforms.RandomHorizontalFlip(p=0.5),
        transforms.ToTensor(),
        transforms.Normalize(norm_mean, norm_std),
    ])

    normalizes = transforms.Normalize(norm_mean, norm_std)
    valid_transform = transforms.Compose([
        transforms.Resize((256, 256)),
        transforms.TenCrop(224, vertical_flip=False),
        transforms.Lambda(lambda crops: torch.stack([normalizes(transforms.ToTensor()(crop)) for crop in crops])),
    ])

    # 构建MyDataset实例
    train_data = CatDogDataset(data_dir=data_dir, mode="train", transform=train_transform)
    valid_data = CatDogDataset(data_dir=data_dir, mode="valid", transform=valid_transform)

    # 构建DataLoder
    train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
    valid_loader = DataLoader(dataset=valid_data, batch_size=4)

    # ============================ step 2/5 模型 ============================
    alexnet_model = get_model(path_state_dict, False)

    num_ftrs = alexnet_model.classifier._modules["6"].in_features
    alexnet_model.classifier._modules["6"] = nn.Linear(num_ftrs, num_classes)

    alexnet_model.to(device)
    # ============================ step 3/5 损失函数 ============================
    criterion = nn.CrossEntropyLoss()
    # ============================ step 4/5 优化器 ============================
    # 冻结卷积层
    flag = 0
    # flag = 1
    if flag:
        fc_params_id = list(map(id, alexnet_model.classifier.parameters()))  # 返回的是parameters的 内存地址
        base_params = filter(lambda p: id(p) not in fc_params_id, alexnet_model.parameters())
        optimizer = optim.SGD([
            {'params': base_params, 'lr': LR * 0.1},  # 0
            {'params': alexnet_model.classifier.parameters(), 'lr': LR}], momentum=0.9)

    else:
        optimizer = optim.SGD(alexnet_model.parameters(), lr=LR, momentum=0.9)  # 选择优化器

    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=lr_decay_step, gamma=0.1)  # 设置学习率下降策略
    # scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(patience=5)

# ============================ step 5/5 训练 ============================
    train_curve = list()
    valid_curve = list()

    for epoch in range(start_epoch + 1, MAX_EPOCH):

        loss_mean = 0.
        correct = 0.
        total = 0.

        alexnet_model.train()
        for i, data in enumerate(train_loader):

            # if i > 1:
            #     break

            # forward
            inputs, labels = data
            inputs, labels = inputs.to(device), labels.to(device)
            outputs = alexnet_model(inputs)

            # backward
            optimizer.zero_grad()
            loss = criterion(outputs, labels)
            loss.backward()

            # update weights
            optimizer.step()

            # 统计分类情况
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).squeeze().cpu().sum().numpy()

            # 打印训练信息
            loss_mean += loss.item()
            train_curve.append(loss.item())
            if (i+1) % log_interval == 0:
                loss_mean = loss_mean / log_interval
                print("Training:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                    epoch, MAX_EPOCH, i+1, len(train_loader), loss_mean, correct / total))
                loss_mean = 0.

        scheduler.step()  # 更新学习率

        # validate the model
        if (epoch+1) % val_interval == 0:

            correct_val = 0.
            total_val = 0.
            loss_val = 0.
            alexnet_model.eval()
            with torch.no_grad():
                for j, data in enumerate(valid_loader):
                    inputs, labels = data
                    inputs, labels = inputs.to(device), labels.to(device)

                    bs, ncrops, c, h, w = inputs.size()     # [4, 10, 3, 224, 224]
                    # 为了把五维张量转换为四维张量
                    # outputs为[40, 3, 224, 224]
                    outputs = alexnet_model(inputs.view(-1, c, h, w))
                    outputs_avg = outputs.view(bs, ncrops, -1).mean(1)

                    loss = criterion(outputs_avg, labels)

                    _, predicted = torch.max(outputs_avg.data, 1)
                    total_val += labels.size(0)
                    correct_val += (predicted == labels).squeeze().cpu().sum().numpy()

                    loss_val += loss.item()

                loss_val_mean = loss_val/len(valid_loader)
                valid_curve.append(loss_val_mean)
                print("Valid:\t Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                    epoch, MAX_EPOCH, j+1, len(valid_loader), loss_val_mean, correct_val / total_val))
            alexnet_model.train()

    train_x = range(len(train_curve))
    train_y = train_curve

    train_iters = len(train_loader)
    valid_x = np.arange(1, len(valid_curve)+1) * train_iters*val_interval # 由于valid中记录的是epochloss,需要对记录点进行转换到iterations
    valid_y = valid_curve

    plt.plot(train_x, train_y, label='Train')
    plt.plot(valid_x, valid_y, label='Valid')

    plt.legend(loc='upper right')
    plt.ylabel('loss value')
    plt.xlabel('Iteration')
    plt.show()
# -*- coding: utf-8 -*-

import numpy as np
import torch
import os
import random
from PIL import Image
from torch.utils.data import Dataset

random.seed(1)
rmb_label = {"1": 0, "100": 1}


class CatDogDataset(Dataset):
    def __init__(self, data_dir, mode="train", split_n=0.9, rng_seed=620, transform=None):
        """
        rmb面额分类任务的Dataset
        :param data_dir: str, 数据集所在路径
        :param transform: torch.transform,数据预处理
        """
        self.mode = mode
        self.data_dir = data_dir
        self.rng_seed = rng_seed
        self.split_n = split_n
        self.data_info = self._get_img_info()  # data_info存储所有图片路径和标签,在DataLoader中通过index读取样本
        self.transform = transform

    def __getitem__(self, index):
        path_img, label = self.data_info[index]
        img = Image.open(path_img).convert('RGB')     # 0~255

        if self.transform is not None:
            img = self.transform(img)   # 在这里做transform,转为tensor等等

        return img, label

    def __len__(self):
        if len(self.data_info) == 0:
            raise Exception("\ndata_dir:{} is a empty dir! Please checkout your path to images!".format(self.data_dir))
        return len(self.data_info)

    def _get_img_info(self):
        # This method returns the list of all files and directories in the specified path.
        # The return type of this method is list
        img_names = os.listdir(self.data_dir)
        img_names = list(filter(lambda x: x.endswith('.jpg'), img_names))

        random.seed(self.rng_seed)
        random.shuffle(img_names)

        img_labels = [0 if n.startswith('cat') else 1 for n in img_names]
        # 划分训练集和验证集
        split_idx = int(len(img_labels) * self.split_n)  # 25000* 0.9 = 22500
        # split_idx = int(100 * self.split_n)
        if self.mode == "train":
            img_set = img_names[:split_idx]     # 数据集90%训练
            # img_set = img_names[:22500]     #  hard code 数据集90%训练
            label_set = img_labels[:split_idx]
        elif self.mode == "valid":
            img_set = img_names[split_idx:]
            label_set = img_labels[split_idx:]
        else:
            raise Exception("self.mode 无法识别,仅支持(train, valid)")

        path_img_set = [os.path.join(self.data_dir, n) for n in img_set]
        data_info = [(n, l) for n, l in zip(path_img_set, label_set)]

        return data_info

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

  • 32
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值