文章目录
一、数据归一化方法
1. 数据归一化计算公式
1.1 0-1 标准化
1.2 Z-Score 标准化
1.3 非线性标准化
2. 数据归一化算法执行过程
3. 数据归一化算法评价
4. Z-Score 标准化算法评价及横向对比
二、梯度下降算法优化初阶
1. 数据归一化与梯度下降算法优化
2. 学习率调度
3. 小批量梯度下降与迭代收敛速度
4. 梯度下降组合优化策略
我们知道,在引入了一定的样本随机性之后,能够帮助参数点跨越局部最小值点,但对于机器学习的任何算法,在引入随机性提升算法性能的时候也将面临随机性本身所造成的麻烦。
在上一小节中我们看到,随机梯度下降会伴随着收敛过程不稳定、收敛结果持续震荡等问题,当面对更加复杂的数据集,随机梯度下降还会面临收敛所需迭代次数过多等问题。
要真正帮助随机梯度下降和小批量梯度下降解决随机性所造成的麻烦,就必须采用一些围绕迭代过程的优化方法,而所有的围绕迭代过程的优化方法中,最基础也是最通用的两种方法,分别是数据归一化方法和学习率调度。
机器学习领域的优化方法分类很多类,包括模型结果优化、评估指标优化、收敛过程优化等等
# 科学计算模块 import numpy as np import pandas as pd # 绘图模块 import matplotlib as mpl import matplotlib.pyplot as plt # 自定义模块 from ML_basic_function import *
一、数据归一化方法
数据归一化方法的本质是一种对数据进行线性转换的方法,通过构建一种样本空间之间的线性映射关系来进行数据数值的转化,这种转化并不会影响数据分布,即不会影响数据的内在规律,只是对数据的数值进行调整。
数据归一化有很多方法,并且在机器学习领域有诸多用途,不仅是能够作为梯度下降的优化算法,同时还能帮助一些数据集避免量纲不一致等问题。
经典机器学习领域的数据归一化算法主要有两种,分别是 0-1标准化(Max-Min Normalization)和 Z-Score 标准化。我们先讨论归一化基本流程,再探讨归一化对机器学习算法在各方面的影响。
关于归一化和标准化的概念辨析
一般来说,归一化和标准化都是指对数据进行数值转化,根据维基百科的解释,都是Feature scaling(特征缩放)的方法,并且都可以称为 normalization。
但某些场景下也会有不同的称呼,例如将 0-1 标准化称为 normalization,也就是归一化,而把 Z-Score 标准化称为 Standardization,即标准化。
1. 数据归一化计算公式
1.1 0-1 标准化
- 0-1标准化也被称为离差标准化,是最简单同时也是最常用的标准化方法。
- 该方法通过在输入特征中逐列遍历其中里的每一个数据,将 Max 和 Min 的记录下来,并通过 Max-Min 作为基数(即 Min=0,Max=1)进行数据的归一化处理,基本公式为:
- Xnormalization=Max−Minx−Min
实际计算过程中需要逐列进行处理,即用每一列中的元素减去当前列的最小值,再除以该列的极差。例如:
a = np.arange(12).reshape(6, 2) a #array([[ 0, 1], # [ 2, 3], # [ 4, 5], # [ 6, 7], # [ 8, 9], # [10, 11]]) # 计算每列最大值 a.max(axis = 0) #array([10, 11]) np.max(a, 0) #array([10, 11]) # 计算每列最小值 a.min(axis = 0) #array([0, 1]) # 计算每列极差 a.max(axis = 0) - a.min(axis = 0) #array([10, 10]) (a - a.min(axis = 0)) #array([[ 0, 0], # [ 2, 2], # [ 4, 4], # [ 6, 6], # [ 8, 8], # [10, 10]]) # 对数据集a进行归一化计算 (a - a.min(axis = 0)) / (a.max(axis = 0) - a.min(axis = 0)) #array([[0. , 0. ], # [0.2, 0.2], # [0.4, 0.4], # [0.6, 0.6], # [0.8, 0.8], # [1. , 1. ]])
至此,我们将 a 的两列都放缩到了 0-1 区间内,这也是 0-1 标准化名称的由来。当然,我们可以定义一个函数完成上述过程:
def maxmin_norm(X): """ max—min normalization标准化函数 """ maxmin_range = X.max(axis=0) - X.min(axis=0) return (X - X.min(axis=0)) / maxmin_range
maxmin_norm(a) #array([[0. , 0. ], # [0.2, 0.2], # [0.4, 0.4], # [0.6, 0.6], # [0.8, 0.8], # [1. , 1. ]])
1.2 Z-Score 标准化
Z-Score 标准化也被称为 0 均值标准化
和 0-1 标准化不同,Z-score 标准化利用原始数据的均值(mean)和标准差(standard deviation)进行数据的标准化。
逐列进行操作,每一条数据都减去当前列的均值再除以当前列的标准差,在这种标准化操作下,如果原数据服从正态分布,处理之后的数据服从标准正态分布。
Z-Score标准化计算公式如下:
其中 μ 代表均值,σ 代表标准差。当然,我们也可通过如下方式对张量进行 Z-Score 标准化处理。
a #array([[ 0, 1], # [ 2, 3], # [ 4, 5], # [ 6, 7], # [ 8, 9], # [10, 11]]) # 每列均值 a.mean(0) #array([5., 6.]) # 每列标准差 a.std(0) #array([3.41565026, 3.41565026]) (a - a.mean(0)) #array([[-5., -5.], # [-3., -3.], # [-1., -1.], # [ 1., 1.], # [ 3., 3.], # [ 5., 5.]]) # Z-Score标准化 (a - a.mean(0)) / a.std(0) #array([[-1.46385011, -1.46385011], # [-0.87831007, -0.87831007], # [-0.29277002, -0.29277002], # [ 0.29277002, 0.29277002], # [ 0.87831007, 0.87831007], # [ 1.46385011, 1.46385011]])
- 和 0-1 标准化不同,Z-Score 标准化并不会将数据放缩在 0-1 之间,而是均匀地分布在 0 的两侧。
- 类似这种数据也被称为 Zero-Centered Data,在深度学习领域有重要应用。当然我们可以将上述过程封装为一个函数:
def z_score(X): """ Z-Score标准化函数 """ return (X - X.mean(axis=0)) / X.std(axis=0)
- 一种更加严谨的做法,是在分母项、也就是标准差上加上一个非常小的常数 μ,从而使得分母恒大于 0。
z_score(a) #array([[-1.46385011, -1.46385011], # [-0.87831007, -0.87831007], # [-0.29277002, -0.29277002], # [ 0.29277002, 0.29277002], # [ 0.87831007, 0.87831007], # [ 1.46385011, 1.46385011]])
1.3 非线性标准化
除了 0-1 标准化和 Z-Score 标准化外,还有一类使用非线性函数进行归一化操作的方法。其中最具代表性的是 Sigmoid 标准化。
Sigmoid 标准化其实非常好理解,就是利用 Sigmoid 函数对数据集的每一列进行处理,由于 Sigmoid 函数特性,处理之后的数据也将被压缩到 0-1 之间。
a #array([[ 0, 1], # [ 2, 3], # [ 4, 5], # [ 6, 7], # [ 8, 9], # [10, 11]]) # 借助此前定义的Sigmoid函数 sigmoid(a) #array([[0.5 , 0.73105858], # [0.88079708, 0.95257413], # [0.98201379, 0.99330715], # [0.99752738, 0.99908895], # [0.99966465, 0.99987661], # [0.9999546 , 0.9999833 ]])
当然,相比 Sigmoid 标准化,Z-Score 标准化实际用途更广。
2. 数据归一化算法执行过程
我们以 0-1 标准化为例,来探讨数据归一化处理对量纲的影响以及在实际建模过程中的计算流程。当然其他标准化也类似。
对于 0-1 标准化来说,表面上看起来只是将每一列数据都放缩至 0-1 区间内,但实际上却有着非常多的用途。
一个最简单的使用场景是,当数据集中不同列的量纲不一致时,通过对每一列的 0-1 标准化处理,能够消除因为这种不一致而引发的算法学习偏差。
例如,在鸢尾花数据中,每一列都是以厘米作为单位,整体数据分布相对统一,但如果把其中某一列改为毫米、而其他几列改为米作为单位,则以毫米为单位的列数值将特别大,而其他几列数值将特别小,如此一来就会对包括线性方程在内的一系列模型建模造成重大影响,模型将无法均匀的从各列中提取信息。
iris_df = pd.read_csv("iris.csv") iris_df
以线性回归为例进行说明,假设现在有数据集满足 y = 200 x 1 − x 2 + 1关系,数据基本情况如下:
# 设置随机数种子 np.random.seed(24) # 扰动项取值为0.01 features, labels = arrayGenReg(w = [200, -1, 1], delta=0.01) features #array([[ 1.32921217, -0.77003345, 1. ], # [-0.31628036, -0.99081039, 1. ], # [-1.07081626, -1.43871328, 1. ], # ..., # [ 1.5507578 , -0.35986144, 1. ], # [-1.36267161, -0.61353562, 1. ], # [-1.44029131, 0.50439425, 1. ]]) labels
数据的真实规律是第一个特征其实对标签的取值起到非常重大的作用,但在实际数据获取记录过程中,如果量纲错配,即给了第一个特征一个非常大的量纲、第二个特征一个非常小的量纲,那么数据情况和实际建模情况就将如下所示:
features[:,:1] = features[:,:1] * 0.1 features[:,1:2] = features[:,1:2] * 10 features #array([[ 0.13292122, -7.70033452, 1. ], # [ -0.03162804, -9.90810387, 1. ], # [ -0.10708163, -14.3871328 , 1. ], # ..., # [ 0.15507578, -3.5986144 , 1. ], # [ -0.13626716, -6.13535618, 1. ], # [ -0.14402913, 5.04394254, 1. ]])
- 则在进行线性回归建模时,算得各列线性关系如下:
np.linalg.lstsq(features, labels, rcond=-1)[0] #array([[ 1.99999619e+03], # [-9.99852807e-02], # [ 9.99705410e-01]]) np.linalg.lstsq(features, labels, rcond=-1)[0][0] #array([1999.99618924]) np.linalg.lstsq(features, labels, rcond=-1)[0][1] #array([-0.09998528]) 1999.99618924 / -0.09998528 #-20002.906320210335
此时模型为了捕捉第一列相对更加重要的特性,计算所得的第一列特征取值非常大,甚至是第二列特征系数的 10 的 5 次方倍左右。
尽管上述模型结果仍然是可以使用的结果,但特征系数差异性的增加(由 200 倍变成 20000 倍差异)会导致两个问题,其一是部分系数太小而导致计算精度问题,其二则是在特征重要性判别上会忽视系数较小的特征。
为了能够消除这种量纲差异所带来的规律挖掘方面的影响,我们就需要采用归一化方法。
例如对上述数据集进行归一化处理之后再进行建模过程如下:
features_max = features[:, :2].max(0) features_min = features[:, :2].min(0) features_range = features[:, :2].max(0) - features[:, :2].min(0) (features[:, :2] - features_min) / features_range #array([[0.72219451, 0.35489507], # [0.44561184, 0.32515777], # [0.31878564, 0.26482801], # ..., # [0.75943302, 0.41014271], # [0.26972912, 0.37597436], # [0.25668241, 0.52655264]]) maxmin_norm(features[:, :2]) #array([[0.72219451, 0.35489507], # [0.44561184, 0.32515777], # [0.31878564, 0.26482801], # ..., # [0.75943302, 0.41014271], # [0.26972912, 0.37597436], # [0.25668241, 0.52655264]]) features[:, :2] = maxmin_norm(features[:, :2]) features #array([[0.72219451, 0.35489507, 1. ], # [0.44561184, 0.32515777, 1. ], # [0.31878564, 0.26482801, 1. ], # ..., # [0.75943302, 0.41014271, 1. ], # [0.26972912, 0.37597436, 1. ], # [0.25668241, 0.52655264, 1. ]])
注意,关于标签是否需要归一化的问题,一般来说这并不是一个典型的操作,在绝大多数情况下我们也并不会对标签进行归一化操作。
但此处,由于需要在归一化后全都为正数的特征上进行回归类问题预测,且标签取值有正有负,因此可以考虑对标签进行归一化处理,以方便观测后续模型参数。不过尽管如此,此处的标签归一化也并不是必须的。
w = np.linalg.lstsq(features, maxmin_norm(labels), rcond=-1)[0] w #array([[ 0.99847747], # [-0.00622912], # [ 0.00410845]])
此时我们能发现,尽管参数取值和背后真实规律有差异,但基本能够体现第一个特征重要性远高于第二个特征的一般规律,并且二者基本相差 200 倍的关系。
-0.00622912 * 200 #-1.245824 0.00410845 * 200 #0.8216899999999999
很明显,该结果是一个相对更加准确的结果。而如果我们现在假设数据集满足上述关系,那么对于新进来的数据,应该如何预测呢?其实非常简单,只需要借助训练数据中计算出的极值对新数据进行处理即可,例如我们又获得数据如下:
features1, labels1 = arrayGenReg(num_examples = 100, w = [200, -1, 1], delta=0.01) features1[:,:1] = features1[:,:1] * 0.1 features1[:,1:2] = features1[:,1:2] * 10
相同总体中获取的满足相同规律的数据,当然新数据理论上应该是不带标签的。 features1
- 此时模型预测输出结果为:
yhat = features1.dot(w) yhat[:5] #array([[0.90058932], # [0.73377993], # [0.25493639], # [0.48001114], # [0.24441544]])
- 此时需要知道的是,由于在训练参数 w 的时候是对标签也进行了归一化处理的,所以当前预测结果和真实标签还差了一个归一化过程。
- 此时如果是面对完全未知情况进行预测,我们需要讲 yhat 逆向归一化处理,即
labels_min = labels.min(0) labels_max = labels.max(0) labels_range = labels.max(0) - labels.min(0) yhat = yhat * labels_range + labels_min yhat[:5] #array([[ 479.25093691], # [ 280.4665185 ], # [-290.1646276 ], # [ -21.94619293], # [-302.70229372]])
- 比较真实标签:
labels1[:5] #array([[ 479.25042102], # [ 280.45978588], # [-290.16538845], # [ -21.94008706], # [-302.70455066]])
能够看出,在经过归一化处理之后模型能够排除量纲差异所导致的学习偏倚问题,最终得到一个准确率较高的结果。
当然,如果是划分训练集和测试集进行建模并且进行归一化操作,那么在遵循在训练集上训练,在测试集上进行测试的基本原则下,我们首先在训练集上进行数据归一化处理并记录各列的极值,然后当模型训练完成之后,再借助训练集各列的极值来对测试机数据进行归一化,再带入模型进行测试。
当然,如果这个过程对标签也进行了归一化处理,则标签的归一化过程和特征归一化过程无异,唯一需要注意的是如果是对未知数据进行预测,即需要模型输出和真实采集到数据类似的结果,则需要在模型输出的归一化的标签基础上进行逆向归一化处理。
此外,一般来说如果是 Z-Score 标准化,则无需对标签进行标准化处理。
3. 数据归一化算法评价
此处我们通过量纲不一致问题引出归一化方法,但归一化方法却并不一定、且不仅仅应用于处理量纲不一致问题中。
并非所有模型都受到数据各列的绝对数值大小影响,在通用的模型中,线性模型和距离类模型是两类典型的会受到各列绝对数值大小影响的模型,例如线性回归、KNN、K-Means(一种无监督的聚类模型)等,并且逻辑回归在使用 ECOC 编码进行类别判别时也是利用距离来判别样本最终归属。
此时,由于各列的绝对数值会影响模型学习的偏重,模型会更加侧重于学习那些数值比较大的列,而无法均匀的从各列中提取有效信息,因此有时会出现较差的模型结果。
但有些模型却不受此影响,典型的如树模型。
我们需要知道,一旦对数据进行归一化处理,数据就将失去可解释性,也就是失去了量纲。
例如对于鸢尾花数据来说,原始数据代表花瓣花萼的长宽测量结果,而如果我们对其进行归一化处理,则每条数据就无法再给予明确的现实意义,这也是在很多要求可解释性的情况下我们应该避免使用归一化方法的原因。
不仅是归一化方法,其实所有的样本空间的映射都会改变数据集的可解释性。
归一化方法属于仿射变换的一种特殊形式,而所有的仿射变换其实都不会影响数据集原始分布,也就是并不影响数据集真实规律,只会影响某些算法挖掘规律的难度(也就是受到特征绝对数值影响的算法)。
例如对如下数据,我们可以观察其归一化前后的数据分布变化情况:
# 设置随机数种子 np.random.seed(24) # 扰动项取值为0.01 features, labels = arrayGenReg(delta=0.01) # 绘制图像进行观察 plt.subplot(121) plt.plot(features[:, 0], labels, 'o') plt.subplot(122) plt.plot(maxmin_norm(features[:, 0]), maxmin_norm(labels), 'o')
仿射变换指的是样本空间平移(加减某个数)和放缩(乘除某个数)的变换。0-1 标准化过程中,平移就是减去每一列最小值,放缩就是除以某一列的极差。
对于梯度下降算法来说,归一化能够提高收敛速度。
例如下图所示,经过归一化处理之后的数据,在进行损失函数构造时损失函数的等高线图将更加均匀,此时梯度下降的收敛速度也将更快,具体理论理解详见下文论述。
在实际使用过程中,经过归一化的数据在梯度下降过程中往往收敛更快,这其实是相比消除量纲影响,归一化方法更加重要应用场景。
- 在提高收敛速度方面,Z-Score 效果要好于 0-1 标准化。
- 此处可以举例说明。还是以前两节的等高线图案例进行说明,尝试对比在进行数据归一化前后两组数据损失函数的等高线图及收敛轨迹图。
# 创建数据及进行深拷贝 x = np.array([[1, 1], [3, 1]]) x_norm = np.copy(x) x_norm[:, :1] = z_score(x_norm[:, :1]) x_norm #array([[-1, 1], # [ 1, 1]]) y = np.array([[2], [4]]) y #array([[2], # [4]])
np.random.seed(24) w = np.random.randn(2, 1) w_norm = np.copy(w) w, w_res = w_cal_rec(x, w, y, gd_cal = lr_gd, lr = 0.1, itera_times = 100) w_norm, w_res_norm = w_cal_rec(x_norm, w_norm, y, gd_cal = lr_gd, lr = 0.1, itera_times = 100)
plt.subplot(121) # 网格点坐标 x1, x2 = np.meshgrid(np.arange(1, 2, 0.001), np.arange(-1, 1, 0.001)) # 绘制等高线图 plt.contour(x1, x2, (2-x1-x2)**2+(4-3*x1-x2)**2) # 参数点移动轨迹图 plt.plot(np.array(w_res)[:, 0], np.array(w_res)[:, 1], '-o', color='#ff7f0e') plt.subplot(122) # 网格点坐标 x1, x2 = np.meshgrid(np.arange(0, 4, 0.01), np.arange(-1, 3, 0.01)) # 绘制等高线图 plt.contour(x1, x2, (2+x1-x2)**2+(4-x1-x2)**2) # 绘制参数点移动轨迹图 plt.plot(np.array(w_res_norm)[:, 0], np.array(w_res_norm)[:, 1], '-o', color='#ff7f0e')
能够看出经过归一化处理的之后的数据损失函数等高线更加均匀,收敛效率更高。
关于归一化能够让等高线更加均匀从而加快迭代收敛过程的理解:
从理论角度出发,其实梯度下降过程每一步参数点移动的方向是能够让梯度最快速下降的方向,也就是图片上垂直于等高线的方向。
但这种所谓的最快速的方向只在开始移动的一瞬间满足,由于梯度是连续变化的函数,因此当移动了一小步之后最优方向其实就可能发生了变化,但参数只能在下次移动时再改变方向,因此中间其实很长一段距离参数并不不一定是沿着最优方向在进行移动。
这里需要注意,如果下一次移动的方向和上一次移动方向一致或者类似,那就说明这次移动过程中参数并没有偏离方向太多,反之则这次移动走了很多弯路。
当损失函数的等高线是均匀分布时,外圈的垂直线也就是内圈的垂直线,此时参数两次移动过程大概率最优方向一致,也就是说相同的移动能够更大程度降低损失函数值,而如果类似图 1 中的情况,内外圈分布不均匀,则参数两次迭代过程最优方向将发生偏移,也就是说明上一次迭代过程有很长一段距离没有沿着最优方向迭代,该次迭代只降低了有限的损失函数计算值。
经次过程不断迭代,由于经过归一化的损失函数每次迭代效率都更高,因此相比其他损失函数,经过归一化的数据只需要更少次的迭代就能抵达最小值点,这也就是加快收敛速度的根本原因
4. Z-Score 标准化算法评价及横向对比
从大类上来分,Z-Score 的使用场景要远高于 0-1 标准化使用场景。当然这也并不是绝对的,要区分二者使用情景,我们首先需要进一步了解二者算法性能。
生成 Zero-Centered Data
一般来说,由于 Z-Score 标准化生成数据的 Zero-Centered 特性,使得其在深度学习领域备受欢迎(是Batch Normalization的一种特殊情况)。
在机器学习领域对于标签同时存在正负值的回归类问题,使用 Z-Score 能够避免对标签进行归一化。
标准正态分布
由于该方法同时也是正态分布转换为标准正态分布的计算公式,因此如果原始数据满足正态分布,则经过 Z-Score 转化之后就能转化为标准正态分布,进而可以利用标准正态分布诸多统计性质。
保留极端值分布
还有一点非常实用的功能,就是相比 0-1 标准化,Z-Score 标准化能够保留极端值的分布。例如有数据如下:
a = np.arange(8).reshape(4, 2) a[1, 1] = 100 a #array([[ 0, 1], # [ 2, 100], # [ 4, 5], # [ 6, 7]])
其中第二列的第二个值就是极端值,如果对 a 进行 0-1 标准化处理,则计算结果如下:
maxmin_norm(a) #array([[0. , 0. ], # [0.33333333, 1. ], # [0.66666667, 0.04040404], # [1. , 0.06060606]])
- 我们会发现,由于极端值的存在,会将其他数值压缩在一个非常小的范围内。而如果此时我们采用 Z-Score 进行标准化,则计算结果如下:
z_score(a) #array([[-1.34164079, -0.65692457], # [-0.4472136 , 1.72970047], # [ 0.4472136 , -0.56049527], # [ 1.34164079, -0.51228063]])
- 我们发现,极端值仍然还是极端值(相对该列其他数值而言),此时我们即可采用极端值处理方法对其进行处理(删除或者盖帽)。
- 不过相比 0-1 标准化,Z-Score 标准化问题也比较明显,那就是需要进行均值和方差的计算,而该过程将耗费更大的计算量。