本系列为 斯坦福CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。


引言

通过ShowMeAI前序文章 深度学习与CV教程(3) | 损失函数与最优化深度学习与CV教程(4) | 神经网络与反向传播深度学习与CV教程(5) | 卷积神经网络 我们已经学习掌握了以下内容:

  • 计算图:计算前向传播、反向传播
  • 神经网络:神经网络的层结构、非线性函数、损失函数
  • 优化策略:梯度下降使损失最小
  • 批梯度下降:小批量梯度下降,每次迭代只用训练数据中的一个小批量计算损失和梯度
  • 卷积神经网络:多个滤波器与原图像独立卷积得到多个独立的激活图

【本篇】【下篇】 ShowMeAI讲解训练神经网络的核心方法与关键点,主要包括:

  • 初始化:激活函数选择、数据预处理、权重初始化、正则化、梯度检查
  • 训练动态:监控学习过程、参数更新、超参数优化
  • 模型评估:模型集成(model ensembles)

本篇重点

  • 激活函数
  • 数据预处理
  • 权重初始化
  • 批量归一化
  • 监控学习过程
  • 超参数调优

1.激活函数

关于激活函数的详细知识也可以参考阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 浅层神经网络 里【激活函数】板块内容。

在全连接层或者卷积层,输入数据与权重相乘后累加的结果送给一个非线性函数,即激活函数(activation function)。每个激活函数的输入都是一个数字,然后对其进行某种固定的数学操作。

下面是在实践中可能遇到的几种激活函数:

1.1 Sigmoid函数

数学公式:\(\sigma(x) = 1 / (1 + e^{-x})\)

求导公式:\(\frac{d\sigma(x)}{dx} = \left( 1 - \sigma(x) \right) \sigma(x)\) (不小于 \(0\) )

特点:把输入值「挤压」到 \(0\) 到 \(1\) 范围内。Sigmoid 函数把输入的实数值「挤压」到 \(0\) 到 \(1\) 范围内,很大的负数变成 \(0\),很大的正数变成 \(1\),在历史神经网络中,Sigmoid 函数很常用,因为它对神经元的激活频率有良好的解释:从完全不激活(\(0\))到假定最大频率处的完全饱和(saturated)的激活(\(1\))

然而现在 Sigmoid 函数已经很少使用了,因为它有三个主要缺点:

缺点①:Sigmoid 函数饱和时使梯度消失

  • 当神经元的激活在接近 \(0\) 或 \(1\) 处时(即门单元的输入过或过大时)会饱和:在这些区域,梯度几乎为 \(0\)。
  • 在反向传播的时候,这个局部梯度要与损失函数关于这个门单元输出的梯度相乘。因此,如果局部梯度非常小,那么相乘的结果也会接近零,这会「杀死」梯度,几乎就有没有信号通过神经元传到权重再到数据了。
  • 还有,为了防止饱和,必须对于权重矩阵初始化特别留意。比如,如果初始权重过大,那么大多数神经元将会饱和,导致网络就几乎不学习了。

缺点②:Sigmoid 函数的输出不是零中心的

  • 这个性质会导致神经网络后面层中的神经元得到的数据不是零中心的。
  • 这一情况将影响梯度下降的运作,因为如果输入神经元的数据总是正数(比如在 \(\sigma(\sum_{i}w_ix_i+b)\) )中每个输入 \(x\) 都有 \(x > 0\)),那么关于 \(w\) 的梯度在反向传播的过程中,将会要么全部是正数,要么全部是负数(根据该 Sigmoid 门单元的回传梯度来定,回传梯度可正可负,而 \(\frac{d\sigma}{dW}=X^T \cdot\sigma'\) 在 \(X\) 为正时恒为非负数)。
  • 这将会导致梯度下降权重更新时出现 \(z\) 字型的下降。该问题相对于上面的神经元饱和问题来说只是个小麻烦,没有那么严重。

缺点③: 指数型计算量比较大

1.2 tanh函数

数学公式:\(\tanh(x) = 2 \sigma(2x) -1\)

特点:将实数值压缩到 \([-1,1]\) 之间

和 \(Sigmoid\) 神经元一样,它也存在饱和问题,但是和 \(Sigmoid\) 神经元不同的是,它的输出是零中心的。因此,在实际操作中,\(tanh\) 非线性函数比 \(Sigmoid\) 非线性函数更受欢迎。注意 \(tanh\) 神经元是一个简单放大的 \(Sigmoid\) 神经元。

1.3 ReLU 函数

数学公式:\(f(x) = \max(0, x)\)

特点:一个关于 \(0\) 的阈值

优点

  • ReLU 只有负半轴会饱和;节省计算资源,不含指数运算,只对一个矩阵进行阈值计算;更符合生物学观念;加速随机梯度下降的收敛。
  • Krizhevsky 论文指出比 Sigmoid 和 tanh 函数快6倍之多,据称这是由它的线性,非饱和的公式导致的。

缺点

  • 仍有一半会饱和;非零中心;
  • 训练时,ReLU 单元比较脆弱并且可能「死掉」。
    • 举例来说,当一个很大的梯度流过 ReLU 的神经元的时候,由于梯度下降,可能会导致权重更新到一种特别的状态(比如大多数的 \(w\) 都小于 \(0\) ),在这种状态下神经元将无法被其他任何数据点再次激活。如果这种情况发生,那么从此所有流过这个神经元的梯度将都变成 \(0\),也就是说,这个 ReLU 单元在训练中将不可逆转的死亡,因为这导致了数据多样化的丢失。
    • 例如,如果学习率设置得太高(本来大多数大于 \(0\) 的 \(w\) 更新后都小于 \(0\) 了),可能会发现网络中40%的神经元都会死掉(在整个训练集中这些神经元都不会被激活)。
    • 通过合理设置学习率,这种情况的发生概率会降低。

1.4 Leaky ReLU

公式:\(f(x) = \mathbb{1}(x < 0) (\alpha x) + \mathbb{1}(x>=0) (x)\),\(\alpha\) 是小常量

特点:解决「 ReLU 死亡」问题,\(x<0\) 时给出一个很小的梯度值,比如 \(0.01\)。

Leaky ReLU 修正了 \(x<0\) 时 ReLU 的问题,有研究指出这个激活函数表现很不错,但是其效果并不是很稳定。Kaiming He等人在2015年发布的论文 Delving Deep into Rectifiers 中介绍了一种新方法 PReLU,把负区间上的斜率当做每个神经元中的一个参数,然而无法确定该激活函数在不同任务中均有益处。

1.5 指数线性单元(Exponential Linear Units,ELU)

公式:\(f(x)=\begin{cases} x & if \space\space x>0 \\ \alpha(exp(x)-1) & otherwise \end{cases}\)

特点:介于 ReLU 和Leaky ReLU 之间

具有 ReLU 的所有优点,但是不包括计算量;介于 ReLU 和 Leaky ReLU 之间,有负饱和的问题,但是对噪声有较强的鲁棒性。

1.6 Maxout

\[\max \left(w_{1}^{T} x+b_{1}, w_{2}^{T} x+b_{2}\right)
\]

公式:\(max(w_1^Tx+b_1, w_2^Tx + b_2)\)

特点:是对 ReLU 和 leaky ReLU 的一般化归纳

对于权重和数据的内积结果不再使用非线性函数,直接比较两个线性函数。ReLU 和 Leaky ReLU 都是这个公式的特殊情况,比如 ReLU 就是当 \(w_1=1\),\(b_1=0\) 的时候。

Maxout 拥有 ReLU 单元的所有优点(线性操作和不饱和),而没有它的缺点(死亡的 ReLU 单元)。然而和 ReLU 对比,它每个神经元的参数数量增加了一倍,这就导致整体参数量激增。

实际应用Tips

  • 用 ReLU 函数。注意设置好学习率,你可以监控你的网络中死亡的神经元占的比例。
  • 如果单元死亡问题困扰你,就试试Leaky ReLU 或者 Maxout,不要再用 Sigmoid 了。也可以试试 tanh,但是其效果应该不如 ReLU 或者 Maxout。

2.数据预处理

关于深度学习数据预处理的知识也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章深度学习的实用层面里【标准化输入】板块内容。

关于数据预处理有 3 个常用的符号,数据矩阵 \(X\),假设其尺寸是 \([N \times D]\)(\(N\) 是数据样本的数量,\(D\) 是数据的维度)。

2.1 减均值(Mean Subtraction)

减均值法是数据预处理最常用的形式。它对数据中每个独立特征减去平均值,在每个维度上都将数据的中心都迁移到原点。

在 numpy 中,该操作可以通过代码 X -= np.mean(X, axis=0) 实现。而对于图像,更常用的是对所有像素都减去一个值,可以用 X -= np.mean(X) 实现,也可以在 3 个颜色通道上分别操作。

具体来讲,假如训练数据是 \(50000\) 张 \(32 \times 32 \times 3\) 的图片:

  • 第一种做法是减去均值图像,即将每张图片拉成长为 \(3072\) 的向量,\(50000 \times 3072\) 的矩阵按列求平均,得到一个含有 \(3072\) 个数的均值图像,训练集测试集验证集都要减去这个均值,AlexNet 是这种方式;
  • 第二种做法是按照通道求平均,RGB三个通道每个通道一个均值,即每张图片的 \(3072\) 个数中,RGB各有 \(32 \times 32\) 个数,要在 \(50000 \times 32 \times 32\) 个数中求一个通道的均值,最终的均值有 \(3\) 个数字,然后所有图片每个通道都要减去对应的通道均值,VGGNet是这种方式。

之所以执行减均值操作,是因为解决输入数据大多数都是正或者负的问题。虽然经过这种操作,数据变成零中心的,但是仍然只能第一层解决 Sigmoid 非零均值的问题,后面会有更严重的问题。

2.2 归一化(Normalization)

归一化是指将数据的所有维度都归一化,使其数值范围都近似相等。

有两种常用方法可以实现归一化。

  • 第一种是先对数据做零中心化(zero-centered)处理,然后每个维度都除以其标准差,实现代码为 X /= np.std(X, axis=0)
  • 第二种是对每个维度都做归一化,使得每个维度的最大和最小值是 \(1\) 和 \(-1\)。这个预处理操作只有在确信不同的输入特征有不同的数值范围(或计量单位)时才有意义,但要注意预处理操作的重要性几乎等同于学习算法本身

在图像处理中,由于像素的数值范围几乎是一致的(都在0-255之间),所以进行这个额外的预处理步骤并不是很必要。

  • 左边:原始的 2 维输入数据。
  • 中间:在每个维度上都减去平均值后得到零中心化数据,现在数据云是以原点为中心的。
  • 右边:每个维度都除以其标准差来调整其数值范围,红色的线指出了数据各维度的数值范围。

在中间的零中心化数据的数值范围不同,但在右边归一化数据中数值范围相同。

2.3 主成分分析(PCA)

这是另一种机器学习中比较常用的预处理形式,但在图像处理中基本不用。在这种处理中,先对数据进行零中心化处理,然后计算协方差矩阵,它展示了数据中的相关性结构。

# 假设输入数据矩阵X的尺寸为[N x D]
X -= np.mean(X, axis = 0) # 对数据进行零中心化(重要)
cov = np.dot(X.T, X) / X.shape[0] # 得到数据的协方差矩阵,DxD

数据协方差矩阵的第 \((i, j)\) 个元素是数据第 \(i\) 个和第 \(j\) 个维度的协方差。具体来说,该矩阵的对角线上的元素是方差。还有,协方差矩阵是对称和半正定的。我们可以对数据协方差矩阵进行 SVD(奇异值分解)运算。

U,S,V = np.linalg.svd(cov)

\(U\) 的列是特征向量,\(S\) 是装有奇异值的1维数组(因为 cov 是对称且半正定的,所以S中元素是特征值的平方)。为了去除数据相关性,将已经零中心化处理过的原始数据投影到特征基准上:

Xrot = np.dot(X,U) # 对数据去相关性

np.linalg.svd 的一个良好性质是在它的返回值U中,特征向量是按照特征值的大小排列的。我们可以利用这个性质来对数据降维,只要使用前面的小部分特征向量,丢弃掉那些包含的数据没有方差的维度,这个操作也被称为 主成分分析(Principal Component Analysis 简称PCA) 降维:

Xrot_reduced = np.dot(X, U[:,:100]) # Xrot_reduced 变成 [N x 100]

经过上面的操作,将原始的数据集的大小由 \([N \times D]\) 降到了 \([N \times 100]\),留下了数据中包含最大方差的的 100 个维度。通常使用 PCA 降维过的数据训练线性分类器和神经网络会达到非常好的性能效果,同时还能节省时间和存储器空间。

有一问题是为什么使用协方差矩阵进行 SVD 分解而不是使用原 \(X\) 矩阵进行?

其实都是可以的,只对数据 \(X\)(可以不是方阵)进行 SVD 分解,做 PCA 降维(避免了求协方差矩阵)的话一般用到的是右奇异向量 \(V\),即 \(V\) 的前几列是需要的特征向量(注意 np.linalg.svd 返回的是 V.T)。\(X\) 是\(N \times D\),则 \(U\) 是 \(N \times N\),\(V\) 是 \(D \times D\);而对协方差矩阵(\(D \times D\))做 SVD 分解用于 PCA 降维的话,可以随意取左右奇异向量\(U\)、\(V\)(都是 \(D \times D\))之一,因为两个向量是一样的。

2.4 白化(Whitening)

最后一个在实践中会看见的变换是白化(whitening)。白化操作的输入是特征基准上的数据,然后对每个维度除以其特征值来对数值范围进行归一化。

白化变换的几何解释是:如果数据服从多变量的高斯分布,那么经过白化后,数据的分布将会是一个均值为零,且协方差相等的矩阵。

该操作的代码如下:

# 对数据进行白化操作:
# 除以特征值
Xwhite = Xrot / np.sqrt(S + 1e-5)

注意分母中添加了 1e-5(或一个更小的常量)来防止分母为 \(0\),该变换的一个缺陷是在变换的过程中可能会夸大数据中的噪声,这是因为它将所有维度都拉伸到相同的数值范围,这些维度中也包含了那些只有极少差异性(方差小)而大多是噪声的维度。

在实际操作中,这个问题可以用更强的平滑来解决(例如:采用比 1e-5 更大的值)。

下图为 CIFAR-10 数据集上的 PCA白化等操作结果可视化。

从左往右4张子图:

  • 第1张:一个用于演示的图片集合,含 49 张图片。
  • 第2张:3072 个特征向量中的前 144 个。靠前面的特征向量解释了数据中大部分的方差。
  • 第3张:49 张经过了PCA降维处理的图片,只使用这里展示的这 144 个特征向量。为了让图片能够正常显示,需要将 144 维度重新变成基于像素基准的 3072 个数值。因为U是一个旋转,可以通过乘以 U.transpose()[:144,:] 来实现,然后将得到的 3072 个数值可视化。可以看见图像变得有点模糊了,然而,大多数信息还是保留了下来。
  • 第4张:将「白化」后的数据进行显示。其中 144个 维度中的方差都被压缩到了相同的数值范围。然后 144 个白化后的数值通过乘以 U.transpose()[:144,:] 转换到图像像素基准上。

2.5 实际应用

实际上在卷积神经网络中并不会采用PCA和白化,对数据进行零中心化操作还是非常重要的,对每个像素进行归一化也很常见。

补充说明

进行预处理很重要的一点是:任何预处理策略(比如数据均值)都只能在训练集数据上进行计算,然后再应用到验证集或者测试集上

  • 一个常见的错误做法是先计算整个数据集图像的平均值然后每张图片都减去平均值,最后将整个数据集分成训练/验证/测试集。正确的做法是先分成训练/验证/测试集,只是从训练集中求图片平均值,然后各个集(训练/验证/测试集)中的图像再减去这个平均值

3.权重初始化

关于神经网络权重初始化的知识也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章深度学习的实用层面里【权重初始化缓解梯度消失和爆炸】板块内容。

初始化网络参数是训练神经网络里非常重要的一步,有不同的初始化方式,我们来看看他们各自的特点。

3.1 全零初始化

对一个两层的全连接网络,如果输入给网络的所有参数初始化为 \(0\) 会怎样?

这种做法是错误的。 因为如果网络中的每个神经元都计算出同样的输出,然后它们就会在反向传播中计算出同样的梯度,从而进行同样的参数更新。换句话说,如果权重被初始化为同样的值,神经元之间就失去了不对称性的源头。

3.2 小随机数初始化

现在权重初始值要非常接近 \(0\) 又不能等于 \(0\),解决方法就是将权重初始化为很小的数值,以此来打破对称性。

思路是:如果神经元刚开始的时候是随机且不相等的,那么它们将计算出不同的更新,并将自身变成整个网络的不同部分。

实现方法是:W = 0.01 * np.random.randn(D,H)。其中 randn 函数是基于零均值和标准差的一个高斯分布来生成随机数的。

小随机数初始化在简单的网络中效果比较好,但是网络结构比较深的情况不一定会得到好的结果。比如一个 10 层的全连接网络,每层 500 个神经元,使用 \(tanh\) 激活函数,用小随机数初始化。

代码与输出图像如下:

import numpy as np
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt # 假设一些高斯分布单元
D = np.random.randn(1000, 500)
hidden_layer_sizes = [500]*10 # 隐藏层尺寸都是500,10层
nonlinearities = ['tanh']*len(hidden_layer_sizes) # 非线性函数都是用tanh函数 act = {'relu': lambda x: np.maximum(0, x), 'tanh': lambda x: np.tanh(x)}
Hs = {}
for i in range(len(hidden_layer_sizes)):
X = D if i == 0 else Hs[i-1] # 当前隐藏层的输入
fan_in = X.shape[1]
fan_out = hidden_layer_sizes[i]
W = np.random.randn(fan_in, fan_out) * 0.01 # 权重初始化 H = np.dot(X, W) # 得到当前层输出
H = act[nonlinearities[i]](H) # 激活函数
Hs[i] = H # 保存当前层的结果并作为下层的输入 # 观察每一层的分布
print('输入层的均值:%f 方差:%f'% (np.mean(D), np.std(D)))
layer_means = [np.mean(H) for i,H in Hs.items()]
layer_stds = [np.std(H) for i,H in Hs.items()]
for i,H in Hs.items():
print('隐藏层%d的均值:%f 方差:%f' % (i+1, layer_means[i], layer_stds[i])) # 画图
plt.figure()
plt.subplot(121)
plt.plot(list(Hs.keys()), layer_means, 'ob-')
plt.title('layer mean')
plt.subplot(122)
plt.plot(Hs.keys(), layer_stds, 'or-')
plt.title('layer std') # 绘制分布图
plt.figure()
for i,H in Hs.items():
plt.subplot(1, len(Hs), i+1)
plt.hist(H.ravel(), 30, range=(-1,1)) plt.show()

可以看到只有第一层的输出均值方差比较好,输出接近高斯分布,后面几层均值方差基本为 \(0\),这样导致的后果是正向传播的激活值基本为 \(0\),反向传播时就会计算出非常小的梯度(因权重的梯度就是层的输入,输入接近 \(0\),梯度接近 \(0\) ),参数基本不会更新。

如果上面的例子不用小随机数,即 W = np.random.randn(fan_in, fan_out) * 1,此时会怎样呢?

此时,由于权重较大并且使用的 tanh 函数,所有神经元都会饱和,输出为 \(+1\) 或 \(-1\),梯度为 \(0\),如下图所示,均值在 \(0\) 附近波动,方差较大在 \(0.98\) 附近波动,神经元输出大多为 \(+1\) 或 \(-1\)。

3.3 Xavier/He初始化(校准方差)

上述分析可以看出,权重过小可能会导致网络崩溃,权重过大可能会导致网络饱和,所以都在研究出一种合理的初始化方式。一种很好的经验是使用Xavier初始化:

W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in)

这是Glorot等在2010年发表的 论文。这样就保证了网络中所有神经元起始时有近似同样的输出分布。实践经验证明,这样做可以提高收敛的速度。

原理:假设神经元的权重 \(w\) 与输入 \(x\) 的内积为 \(s = \sum_i^n w_i x_i\),这是还没有进行非线性激活函数运算之前的原始数值。此时 \(s\) 的方差:

\[\begin{aligned}
\text{Var}(s) &= \text{Var}(\sum_i^n w_ix_i) \\
&= \sum_i^n \text{Var}(w_ix_i) \\
&= \sum_i^n [E(w_i)]^2\text{Var}(x_i) + E[(x_i)]^2\text{Var}(w_i) + \text{Var}(x_i)\text{Var}(w_i) \\
&= \sum_i^n \text{Var}(x_i)\text{Var}(w_i) \\
&= n \text{Var}(w) \text{Var}(x)
\end{aligned}
\]

前三步使用的是方差的性质(累加性、独立变量相乘);

第三步中,假设输入和权重的均值都是 \(0\),即 \(E[x_i] = E[w_i] = 0\),但是 ReLU 函数中均值应该是正数。在最后一步,我们假设所有的 \(w_i,x_i\) 都服从同样的分布。从这个推导过程我们可以看见,如果想要 \(s\) 有和输入 \(x\) 一样的方差,那么在初始化的时候必须保证每个权重 \(w\) 的方差是\(1/n\) 。

又因为对于一个随机变量 \(X\) 和标量 \(a\),有 \(\text{Var}(aX) = a^2\text{Var}(X)\),这就说明可以让 \(w\) 基于标准高斯分布(方差为1)取样,然后乘以 \(a = \sqrt{1/n}\),即 \(\text{Var}( \sqrt{1/n}\cdot w) = 1/n\text{Var}(w)=1/n\),此时就能保证 \(\text{Var}(s) =\text{Var}(x)\)。

代码为:W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in),其中fan_in就是上文的 \(n\)。

不过作者在论文中推荐的是: W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in + fan_out),使 \(\text{Var}(w) = 2/(n_{in} + n_{out})\),其中 \(n_{in}, n_{out}\) 是前一层和后一层中单元的个数,这是基于妥协和对反向传播中梯度的分析得出的结论)

输出结果为:

图上可以看出,后面几层的输入输出分布很接近高斯分布。

但是使用 ReLU 函数这种关系会被打破,同样 \(w\) 使用单位高斯并且校准方差,然而使用 ReLU 函数后每层会消除一半的神经元(置 \(0\) ),结果会使方差每次减半,会有越来越多的神经元失活,输出为 \(0\) 的神经元越来越多。如下图所示:

解决方法是 W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in/2)。因为每次有一半的神经元失活,校准时除2即可,这样得到的结果会比较好。

这是2015年何凯明的论文 Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification 提到的方法,这个形式是神经网络算法使用 ReLU 神经元时的当前最佳推荐。结果如下:

3.4 稀疏初始化

另一个处理非标定方差的方法是将所有权重矩阵设为 \(0\),但是为了打破对称性,每个神经元都同下一层固定数目的神经元随机连接(其权重数值由一个小的高斯分布生成)。一个比较典型的连接数目是10个。

偏置项(biases)的初始化:通常将偏置初始化为 \(0\)。

3.5 实际应用

合适的初始化设置仍然是现在比较活跃的研究领域,经典的论文有:

当前的推荐是使用 ReLU 激活函数,并且使用 w = np.random.randn(n) * sqrt(2.0/n) 来进行权重初始化,n 是上一层神经元的个数,这是何凯明的论文得出的结论,也称作 He初始化

4.批量归一化(Batch Normalization)

关于Batch Normalization的详细图示讲解也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章网络优化:超参数调优、正则化、批归一化和程序框架里【Batch Normalization】板块内容。

4.1 概述

批量归一化 是 loffe 和 Szegedy 最近才提出的方法,该方法一定程度解决了如何合理初始化神经网络这个棘手问题,其做法是让激活数据在训练开始前通过一个网络,网络处理数据使其服从标准高斯分布。

归一化是一个简单可求导的操作,所以上述思路是可行的。在实现层面,应用这个技巧通常意味着全连接层(或者是卷积层,后续会讲)与激活函数之间添加一个BatchNorm层。在神经网络中使用批量归一化已经变得非常常见,在实践中使用了批量归一化的网络对于不好的初始值有更强的鲁棒性。

4.2 原理

具体来说,我们希望每一层网络的输入都近似符合标准高斯分布,考虑有 \(N\) 个激活数据的小批量输入,每个输入 \(x\) 有 \(D\) 维,即 \(x = (x^{(1)} \cdots x^{(d)})\),那么对这个小批量数据的每个维度进行归一化,使符合单位高斯分布,应用下面的公式:

\[\hat{x}^{(k)} =\frac{x^{(k)}-\text{E}[x^{(k)}]}{\sqrt{\text{Var}[x^{(k)}]}}
\]
  • 其中的均值和方差是根据整个训练集计算出来的;
  • 这个公式其实就是随机变量转化为标准高斯分布的公式,是可微的;
  • 前向传播与反向传播也是利用小批量梯度下降(SGD),也可以利用这个小批量进行归一化;
  • 在训练开始前进行归一化,而不是在初始化时;
  • 卷积层每个激活图都有一个均值和方差;
  • 对每个神经元分别进行批量归一化。

批量归一化会把输入限制在非线性函数的线性区域,有时候我们并不想没有一点饱和,所以希望能控制饱和程度,即在归一化完成后,我们在下一步添加两个参数去缩放和平移归一化后的激活数据:

\[y^{(k)} = \gamma ^{(k)}\hat{x} ^{(k)}+\beta ^{(k)}
\]

这两个参数可以在网络中学习,并且能实现我们想要的效果。的确,通过设置:\(\gamma ^{(k)}=\sqrt{\text{Var}[x^{(k)}]}\),\(\beta ^{(k)}=\text{E}[x^{(k)}]\) 可以恢复原始激活数据,如果这样做的确最优的话。现在网络有了为了让网络达到较好的训练效果而去学习控制让 tanh 具有更高或更低饱和程度的能力。

当使用随机优化时,我们不能基于整个训练集去计算。我们会做一个简化:由于我们在 SGD 中使用小批量,每个小批量都可以得到激活数据的均值和方差的估计。这样,用于归一化的数据完全可以参与梯度反向传播。

批量归一化的思想:考虑一个尺寸为 \(m\) 的小批量B。由于归一化被独立地应用于激活数据 \(x\) 的每个维度,因此让我们关注特定激活数据维度 \(x(k)\) 并且为了清楚起见省略 \(k\)。在小批量中共有 \(m\) 个这种激活数据维度 \(x(k)\):\(\text{B} ={x_{1 \cdots m}}\)

归一化后的值为:\(\hat{x}_{1 \cdots m}\)

线性转化后的值为:\(y_{1 \cdots m}\)

这种线性转化是批量归一化转化:\(\text{BN}_{\gamma, \beta} : x_{1 \cdots m} → y_{1 \cdots m}\)

于是,我们的小批量激活数据 \(\text{B} ={x_{1 \cdots m}}\) 通过BN层,有两个参数需要学习:\(\gamma\),\(\beta\) (\(\varepsilon\) 是为了维持数值稳定在小批量方差上添加的小常数)。

该BN层的输出为:\({y_i=\text{BN}_{\gamma, \beta}(x_i)},i=1 \cdots m\),该层的计算有:

  • 小批量均值:\(\mu _B\leftarrow \frac{1}{m} \sum_{i=1}^m x_i\)

  • 小批量方差:\(\sigma^2 _B\leftarrow \frac{1}{m} \sum_{i=1}^m (x_i-\mu _B)^2\)

  • 归一化:\(\hat{x} _i\leftarrow \frac{x_i-\mu _B}{\sqrt{\sigma^2 _B+\varepsilon } }\)

  • 缩放和平移:\(y_i\leftarrow \gamma \hat{x} _i+\beta \equiv \text{BN}_{\gamma,\beta }(x_i)\)

4.3 优势

  • 改善通过网络的梯度流
  • 具有更高的鲁棒性:允许更大的学习速率范围、减少对初始化的依赖
  • 加快学习速率衰减,更容易训练
  • 可以看作是一种正则方式,在原始输入 \(X\) 上抖动
  • 可以不使用Dropout,加快训练

补充说明:测试时不使用小批量中计算的均值和方差,相反,使用训练期间激活数据的一个固定的经验均值,例如可以使用在训练期间的平均值作为估计。

总结:批量归一化可以理解为在网络的每一层之前都做预处理,将输入数据转化为单位高斯数据或者进行平移伸缩,只是这种操作以另一种方式与网络集成在了一起。

5.层归一化(Layer Normalization)

事实证明,批量归一化能使网络更容易训练,但是对批量的大小有依赖性,批量太小效果不好,批量太大又受到硬件的限制。所以在对输入批量大小具有上限的复杂网络中不太有用。

目前已经提出了几种批量归一化的替代方案来缓解这个问题,其中一个就是层归一化。我们不再对这个小批量进行归一化,而是对特征向量进行归一化。换句话说,当使用层归一化时,基于该特征向量内的所有项的总和来归一化对应于单个数据点。

层归一化测试与训练的行为相同,都是计算每个样本的归一。可用于循环神经网络。

6.卷积神经网络中归一化

空间批量归一化(Spatial Batch Normalization)是对深度进行归一化。

  • 全连接网络中的批量归一化输入尺寸为 \((N,D)\) 输出是 \((N,D)\),其中我们在小批量维度 \(N\) 上计算统计数据用于归一化 \(N\) 个特征点。
  • 卷积层输入的数据,批量归一化的输入尺寸是 \((N,C,H,W)\) 并产生尺寸为 \((N,C,H,W)\) 的输出,其中N是小批量大小,\((H,W)\) 是输出特征图的空间大小。
  • 如果使用卷积生成特征图,我们期望每个特征通道的统计在不同图像和同一图像内的不同位置之间相对一致。因此,空间批量归一化通过计算小批量维度N和空间维度 \(H\) 和 \(W\) 的统计量来计算每个 \(C\) 特征通道的均值和方差。

卷积神经网络中的层归一化是对每张图片进行归一化。

  • 然而在卷积神经网络中,层归一化效果不好。因为对于全连接层,层中的所有隐藏单元倾向于对最终预测做出类似的贡献,并且对层的求和输入重新定中心和重新缩放效果很好;而对于卷积神经网络,贡献类似的假设不再适用。其感受野位于图像边界附近的大量隐藏单元很少打开,因此与同一层内其余隐藏单元的统计数据非常不同(图片中间的位置贡献比较大,边缘的位置可能是背景或噪声)。

实例归一化既对图片又对数据进行归一化;

组归一化(Group Normalization)2018年何凯明的论文 Group Normalization 提出了一种中间技术。

  • 与层归一化在每个数据点的整个特征上进行标准化相比,建议将每个数据点特征拆分为相同的 \(G\) 组,然后对每个数据点的每个数据组的标准化(简单来说,相对于层归一化将整张图片归一,这个将整张图片裁成 \(G\) 组,然后对每个组进行归一)。
  • 这样就可以假设每个组仍然做出相同的贡献,因为分组就是根据视觉识别的特征。比如将传统计算机视觉中的许多高性能人为特征在一起。其中一个定向梯度直方图就是在计算每个空间局部块的直方图之后,每个直方图块在被连接在一起形成最终特征向量之前被归一化。

7.监控学习过程

7.1 监控学习过程的步骤

1) 数据预处理,减均值

2) 选择网络结构

两层神经网络,一个隐藏层有 50 个神经元,输入图像是 3072 维的向量,输出层有 10 个神经元,代表10种分类。

3) 合理性(Sanity)检查

使用小参数进行初始化,使正则损失为 \(0\),确保得到的损失值与期望一致。

例如,输入数据集为CIFAR-10的图像分类

  • 对于Softmax分类器,一般期望它的初始损失值是 \(2.302\),这是因为初始时预计每个类别的概率是 \(0.1\)(因为有10个类别),然后Softmax损失值正确分类的负对数概率 \(-ln(0.1) = 2.302\)。
  • 对于多类 SVM,假设所有的边界都被越过(因为所有的分值都近似为零),所以损失值是9(因为对于每个错误分类,边界值是1)。
  • 如果没看到这些损失值,那么初始化中就可能有问题。

提高正则化强度,损失值会变大。

def init_two_layer_model(input_size, hidden_size, output_size):
model = {}
model["W1"] = 0.0001 * np.random.randn(input_size, hidden_size)
model['b1'] = np.zeros(hidden_size)
model['W2'] = 0.0001 * np.random.randn(hidden_size, output_size)
model['b2'] = np.zeros(output_size)
return model model = init_two_layer_model(32*32*3, 50, 10)
loss, grad = two_layer_net(X_train, model, y_train, 0) # 0没有正则损失
print(loss)

对小数据子集过拟合

  • 这一步很重要,在整个数据集进行训练之前,尝试在一个很小的数据集上进行训练(比如20个数据),然后确保能到达0的损失值。此时让正则化强度为0,不然它会阻止得到0的损失。除非能通过这一个正常性检查,不然进行整个数据集训练是没有意义的。
  • 但是注意,能对小数据集进行过拟合依然有可能存在不正确的实现。比如,因为某些错误,数据点的特征是随机的,这样算法也可能对小数据进行过拟合,但是在整个数据集上跑算法的时候,就没有任何泛化能力。
model = init_two_layer_model(32*32*3, 50, 10)
trainer = ClassifierTrainer()
X_tiny = X_train[:20] # 选前20个作为样本
y_tiny = y_train[:20]
best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny,
model, two_layer_net, verbose=True,
num_epochs=200, reg=0.0, update='sgd',
learning_rate=1e-3, learning_rate_decay=1,
sample_batchs=False)

4) 梯度检查(Gradient Checks)

理论上将进行梯度检查很简单,就是简单地把解析梯度和数值计算梯度进行比较。然而从实际操作层面上来说,这个过程更加复杂且容易出错。下面是一些常用的技巧:

① 使用中心化公式

在使用有限差值近似来计算数值梯度的时候,常见的公式是:\(\frac{df(x)}{dx} = \frac{f(x + h) - f(x)}{h}\) 其中 \(h\) 是一个很小的数字,在实践中近似为 1e-5。但是在实践中证明,使用中心化公式效果更好:\(\frac{df(x)}{dx} = \frac{f(x + h) - f(x - h)}{2h}\) 该公式在检查梯度的每个维度的时候,会要求计算两次损失函数(所以计算资源的耗费也是两倍),但是梯度的近似值会准确很多。

② 使用相对误差来比较

数值梯度 \(f'_n\) 和解析梯度 \(f'_a\) 的绝对误差并不能准确的表明二者的差距,应当使用相对误差。\(\frac{\mid f'_a - f'_n \mid}{\max(\mid f'_a \mid, \mid f'_n \mid)}\) 在实践中:相对误差大于 1e-2 通常就意味着梯度可能出错;小于 1e-7 才是比较好的结果。但是网络的深度越深,相对误差就越高。所以对于一个10层网络,1e-2的相对误差值可能就行,因为误差一直在累积。相反,如果一个可微函数的相对误差值是 1e-2,那么通常说明梯度实现不正确。

③ 使用双精度

一个常见的错误是使用单精度浮点数来进行梯度检查,这样会导致即使梯度实现正确,相对误差值也会很高(比如1e-2)。保持在浮点数的有效范围。把原始的解析梯度和数值梯度数据打印出来,确保用来比较的数字的值不是过小。

④ 注意目标函数的不可导点(kinks)

在进行梯度检查时,一个导致不准确的原因是不可导点问题。不可导点是指目标函数不可导的部分,由 ReLU 函数、SVM损失、Maxout神经元等引入。考虑当 x=-1e-6 时,对 ReLU 函数进行梯度检查。因为 \(x<0\),所以解析梯度在该点的梯度为0。然而,在这里数值梯度会突然计算出一个非零的梯度值,因为 \(f(x+h)\) 可能越过了不可导点(例如:如果 h>1e-6),导致了一个非零的结果。解决这个问题的有效方法是使用少量数据点。这样不可导点会减少,并且如果梯度检查对2-3个数据点都有效,那么基本上对整个批量数据也是没问题的。

⑤ 谨慎设置h

并不是越小越好,如果无法进行梯度检查,可以试试试试将 \(h\) 调到 1e-4 或者 1e-6

在操作的特性模式中梯度检查。为了安全起见,最好让网络学习(「预热」)一小段时间,等到损失函数开始下降的之后再进行梯度检查。在第一次迭代就进行梯度检查的危险就在于,此时可能正处在不正常的边界情况,从而掩盖了梯度没有正确实现的事实。

⑥ 关闭正则损失

推荐先关掉正则化对数据损失做单独检查,然后对正则化做单独检查,防止正则化损失吞没掉数据损失。

5) 正式训练,数值跟踪,特征可视化

设置一个较小的正则强度,找到使损失下降的学习率。

best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny,
model, two_layer_net, verbose=True,
num_epochs=10, reg=0.000001, update='sgd',
learning_rate=1e-6, learning_rate_decay=1,
sample_batchs=False)

学习率为 \(10^{-6}\) 时,损失下降缓慢,说明学习速率过小。

如果把学习率设为另一个极端:\(10^{6}\),如下图所示,会发生损失爆炸:

NaN通常意味着学习率过高,导致损失过大。设为 \(10^{-3}\) 时仍然爆炸,一个比较合理的范围是 \([10^{-5}, 10^{-3}]\)。

7.2 训练过程中的数值跟踪

1) 跟踪损失函数

训练期间第一个要跟踪的数值就是损失值,它在前向传播时对每个独立的批数据进行计算。

在下面的图表中,\(x\) 轴通常都是表示周期(epochs)单位,该单位衡量了在训练中每个样本数据都被观察过的次数的期望(一个 epoch 意味着每个样本数据都被观察过了一次)。相较于迭代次数(iterations) ,一般更倾向跟踪 epoch,这是因为迭代次数与数据的批尺寸(batchsize)有关,而批尺寸的设置又可以是任意的。

比如一共有 1000个 训练样本,每次 SGD 使用的小批量是 10 个样本,一次迭代指的是用这 10 个样本训练一次,而1000个样本都被使用过一次才是一次 epoch,即这 1000 个样本全部被训练过一次需要 100 次 iterations,一次 epoch。

下图展示的是损失值随时间的变化,曲线形状会给出学习率设置的情况:

左图展示了不同的学习率的效果。过低的学习率导致算法的改善是线性的。高一些的学习率会看起来呈几何指数下降,更高的学习率会让损失值很快下降,但是接着就停在一个不好的损失值上(绿线)。这是因为最优化的「能量」太大,参数随机震荡,不能最优化到一个很好的点上。过高的学习率又会导致损失爆炸。

右图显示了一个典型的随时间变化的损失函数值,在CIFAR-10数据集上面训练了一个小的网络,这个损失函数值曲线看起来比较合理(虽然可能学习率有点小,但是很难说),而且指出了批数据的数量可能有点太小(因为损失值的噪音很大)。损失值的震荡程度和批尺寸(batch size)有关,当批尺寸为1,震荡会相对较大。当批尺寸就是整个数据集时震荡就会最小,因为每个梯度更新都是单调地优化损失函数(除非学习率设置得过高)。

下图这种开始损失不变,然后开始学习的情况,说明初始值设置的不合理。

2) 跟踪训练集和验证集准确率

在训练分类器的时候,需要跟踪的第二重要的数值是验证集和训练集的准确率。这个图表能够展现知道模型过拟合的程度:

训练集准确率和验证集准确率间的间距指明了模型过拟合的程度。在图中,蓝色的验证集曲线比训练集准确率低了很多,这就说明模型有很强的过拟合。遇到这种情况,就应该增大正则化强度(更强的L2权重惩罚,更多的随机失活等)或收集更多的数据。另一种可能就是验证集曲线和训练集曲线很接近,这种情况说明模型容量还不够大:应该通过增加参数数量让模型容量更大些。

3) 跟踪权重更新比例

最后一个应该跟踪的量是权重中更新值的数量和全部值的数量之间的比例。注意:是更新的,而不是原始梯度(比如,在普通sgd中就是梯度乘以学习率)。需要对每个参数集的更新比例进行单独的计算和跟踪。一个经验性的结论是这个比例应该在 1e-3 左右。如果更低,说明学习率可能太小,如果更高,说明学习率可能太高。下面是具体例子:

# 假设参数向量为W,其梯度向量为dW
param_scale = np.linalg.norm(W.ravel()) # ravel将多维数组转化成一维;
# np.linalg.norm默认求L2范式
update = -learning_rate*dW # 简单SGD更新
update_scale = np.linalg.norm(update.ravel())
W += update # 实际更新
print update_scale / param_scale # 要得到1e-3左右

4) 第一层可视化

如果数据是图像像素数据,那么把第一层特征可视化会有帮助:

左图: 特征充满了噪音,这暗示了网络可能出现了问题:网络没有收敛,学习率设置不恰当,正则化惩罚的权重过低。

右图: 特征不错,平滑,干净而且种类繁多,说明训练过程进行良好。

8.超参数调优

关于超参数调优的讲解也可以对比阅读ShowMeAI深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章网络优化:超参数调优、正则化、批归一化和程序框架里【超参数调优】板块内容。

如何进行超参数调优呢?常需要设置的超参数有三个:

  • 学习率
  • 学习率衰减方式(例如一个衰减常量)
  • 正则化强度(L2 惩罚,随机失活强度)

下面介绍几个常用的策略:

1) 比起交叉验证最好使用一个验证集

在大多数情况下,一个尺寸合理的验证集可以让代码更简单,不需要用几个数据集来交叉验证。

2) 分散初值,几次周期(epoch)

选择几个非常分散的数值,然后使用几次 epoch(完整数据集训练一轮是1个epoch)去学习。经过几次 epoch,基本就能发现哪些数值较好哪些不好。比如很快就 nan(往往超过初始损失 3 倍就可以认为是 nan,就可以结束训练。),或者没有反应,然后进行调整。

3) 过程搜索:从粗到细

发现比较好的区间后,就可以精细搜索,epoch 次数更多,运行时间更长。比如之前的网络,每次进行 5 次 epoch,对较好的区间进行搜索,找到准确率比较高的值,然后进一步精确查找。注意,需要在对数尺度上进行超参数搜索

也就是说,我们从标准分布中随机生成了一个实数,然后让它成为 10 的次数。对于正则化强度,可以采用同样的策略。直观地说,这是因为学习率和正则化强度都对于训练的动态进程有乘的效果。

例如:当学习率是 0.001 的时候,如果对其固定地增加 0.01,那么对于学习进程会有很大影响。然而当学习率是 10 的时候,影响就微乎其微了。这就是因为学习率乘以了计算出的梯度。

比起加上或者减少某些值,思考学习率的范围是乘以或者除以某些值更加自然。但是有一些参数(比如随机失活)还是在原始尺度上进行搜索。

max_count = 100
for count in range(max_count):
reg = 10**uniform(-5, 5) # random模块的函数uniform,会在-5~5范围内随机选择一个实数
# reg在10^-5~10^5之间取值,指数函数
lr = 10**uniform(-3, -6) model = init_two_layer_model(32 * 32 * 3, 50, 10)
trainer = ClassifierTrainer()
best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny,
model, two_layer_net, verbose=False,
num_epochs=5, reg=reg, update='momentum',
learning_rate=lr, learning_rate_decay=0.9,
sample_batchs=True, batch_size=100)

比较好的结果在红框中,学习率在 10e-4 左右,正则强度在 10e-4~10e-1 左右,需要进一步精细搜索。修改代码:

max_count = 100
for count in range(max_count):
reg = 10**uniform(-4, 0)
lr = 10**uniform(-3, -4)

有一个相对较好的准确率:\(53\%\)。但是这里却有一个问题,这些比较高的准确率都是学习率在 10e-4附近,也就是说都在我们设置的区间边缘,或许 10e-510e-6 有更好的结果。所以在设置区间的时候,要把较好的值放在区间中间,而不是区间边缘

随机搜索优于网格搜索。Bergstra 和 Bengio 在文章 Random Search for Hyper-Parameter Optimization 中说「随机选择比网格化的选择更加有效」,而且在实践中也更容易实现。通常,有些超参数比其余的更重要,通过随机搜索,而不是网格化的搜索,可以让你更精确地发现那些比较重要的超参数的好数值。

上图中绿色函数部分是比较重要的参数影响,黄色是不重要的参数影响,同样取9个点,如果采用均匀采样就会错过很多重要的点,随机搜索就不会。

下一篇 深度学习与CV教程(7) | 神经网络训练技巧 (下) 会讲到的学习率衰减方案、更新类型、正则化、以及网络结构(深度、尺寸)等都需要超参数调优。

9.拓展学习

可以点击 B站 查看视频的【双语字幕】版本

10.要点总结

  • 激活函数选择折叶函数
  • 数据预处理采用减均值
  • 权重初始化采用 Xavier 或 He 初始化
  • 使用批量归一化
  • 梯度检查;合理性检查;跟踪损失函数、准确率、更新比例等
  • 超参数调优采用随机搜索,对数间隔,不断细化范围,增加 epoch

斯坦福 CS231n 全套解读

ShowMeAI 系列教程推荐

深度学习与CV教程(6) | 神经网络训练技巧 (上)的更多相关文章

  1. 深度学习与CV教程(4) | 神经网络与反向传播

    作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...

  2. 深度学习与CV教程(2) | 图像分类与机器学习基础

    作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...

  3. 深度学习与CV教程(8) | 常见深度学习框架介绍

    作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...

  4. 深度学习与CV教程(10) | 轻量化CNN架构 (SqueezeNet,ShuffleNet,MobileNet等)

    作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...

  5. 深度学习与CV教程(12) | 目标检测 (两阶段,R-CNN系列)

    作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...

  6. 深度学习与CV教程(13) | 目标检测 (SSD,YOLO系列)

    作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...

  7. 深度学习与CV教程(14) | 图像分割 (FCN,SegNet,U-Net,PSPNet,DeepLab,RefineNet)

    作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...

  8. 深度学习与计算机视觉教程(15) | 视觉模型可视化与可解释性(CV通关指南·完结)

    作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...

  9. [转]Caffe 深度学习框架上手教程

    Caffe 深度学习框架上手教程 机器学习Caffe caffe 原文地址:http://suanfazu.com/t/caffe/281   blink 15年1月 6   Caffe448是一个清 ...

随机推荐

  1. javaweb之浏览功能

    今天我们来写浏览功能,浏览主要是通过sql语句将数据库里的数据查出来,并显示在页面上. 一.dao层 在上一篇文章的基础上dao层加入浏览方法. public List<Course> l ...

  2. 惠普电脑win10系统中WLAN不见了

    原文链接:笔记本电脑win10系统中WLAN不见了 怎么解决? - 知乎 (zhihu.com)

  3. VMware workstation16 许可证

    ZF3R0-FHED2-M80TY-8QYGC-NPKYF YF390-0HF8P-M81RQ-2DXQE-M2UT6 ZF71R-DMX85-08DQY-8YMNC-PPHV8 若资金允许,请购买正 ...

  4. myEclipse开发内存溢出解决办法myEclipse调整jvm内存大小 java.lang.OutOfMemoryError: PermGen space及其解决方法

    Window->Preferences->MyEclipse->Servers->Tomcat x.x->JDK->Optional Java VM argumen ...

  5. LINUX执行shutdown.sh提示:-bash: ./startup.sh: Permission denied

    在执行./startup.sh,或者./shutdown.sh的时候,爆出了Permission denied, 其实很简单,就是今天在执行tomcat的时候,用户没有权限,而导致无法执行, 用命令c ...

  6. spring原始注解(value)-03

    本博客依据是是spring原始注解-02的代码 注入普通数据类型:@Value注解的使用 1.添加driver属性,使用value注解 @Service("userService" ...

  7. vue 项目build后部署上去页面空白

    默认情况下vue-cli 会认为项目是部署在域名的根路径上. 但是当项目被部署到了一个子路径上,就要自己选定子路径. 比如项目被部署在了 https://www.ujapp.com/my-app, 则 ...

  8. Educational Codeforces Round 113 (Rated for Div. 2)

    多拿纸画画 ! ! ! Problem - B - Codeforces 题意 给出n个数字(数字为1或2), 1代表这第i个选手没有输过,  2代表这第i个选手至少赢一次 输出为n*n矩阵( i行j ...

  9. mouseenter 和 mouseover 的区别

    当鼠标移动到元素上时就会触发mouseenter事件 类似mouseover,它们两者之间的差别是 mouseover鼠标经过自身盒子会触发,经过子盒子还会触发.mouseenter只会经过自身盒子触 ...

  10. Linux的软件安装tomcat 以及jdk

    因为tomcat的启动需要jdk,所以我们先安装jdk,安装完成后再安装tomcat 具体的文件大家可以到官网下载,下面介绍安装步骤 目录 jdk安装 1.通过xftp或者其他方式将安装包传到我们的L ...