对于机器学习来说,数据的质量很大程度上决定了模型的质量,因此对于几乎所有的情况下都需要对于数据进行预处理。其中较为常用的数据处理方式是数据的标准化,进一步还可以通过主成分分析 Principal components analysis, PCA,随机投影 Random Projection 和独立成分分析 Independent Conponent Analysis 来对数据进行降维 dimensionality reduction。
数据的标准化
数据标准化最基本目的是对于将要进行运算的数据的取值进行缩放,使得数据在取值范围上处于同一个数量级,避免在运算过程中取值较小的特征被取值较大的特征“吃掉”。但将不同特征缩放到同一数量级的前提是各自特征对于最终分类和判断的重要性是等同的,否则就不应该做这一处理。
常用的标准化方式:
采用最大值最小值缩放 Min-Max Scaler 的方式:x' = [ x - min(x) ] / [ max(x) - min(x) ]
采用正态分布的标准值 standard value 的方式:z = (x - x̄) / σ
-采用整体缩放的方式:在图像处理中常常将图片数组整体除以 255 以将取值缩放在 [0, 1] 之间
这里 x 和 x', z 代表向量,采用最大值最小缩放标准化后的取值范围是 [0, 1],而标准值方法标准化的结果是将原本服从正态分布的元素进一步标准化成服从均值为 0, 方差为 1 的标准正态分布,且这两种方法在使用过程中必须要注意 排除异常值 Outlier 的影响。
数据的标准化可以通过借助 skitlearn 的预处理模块很容易的完成:
In[3]:
import numpy as np
from sklearn.preprocessing import MinMaxScaler
# sklearn prefers your inputs as float
# otherwise will pop out a warning BUT still do the calculation
weights = np.array([[115], [140], [175]])
scaler = MinMaxScaler()
rescaled_weight = scaler.fit_transform(weights)
# rescaled_weight = MinMaxScaler().fit_transform(weights)
rescaled_weight
Out[3]:
array([[ 0. ],
[ 0.41666667],
[ 1. ]])
In [4]:
from sklearn.preprocessing import StandardScaler
weights = np.array([[115], [140], [175]])
scaler = StandardScaler()
rescaled_weight = scaler.fit_transform(weights)
# rescaled_weight = StandardScaler().fit_transform(weights)
rescaled_weight
Out[4]:
array([[-1.15138528],
[-0.13545709],
[ 1.28684238]])
矩阵的数据处理
在实际的工作中,尤其是计算机视觉方面的应用, 更多的数据是以矩阵的形式存储的。对于一个形如 [N, D] 的矩阵 X,其中 N 为样本的数量,D 为特征的数量。
特征的去均值化 Mean subtraction
对于矩阵中的每一列特征都减去相应特征的均值,这种处理使得数据在各个特征维度上都更加趋近于中心位置,使得数据更加密集。
相应的在 Numpy 中的处理方式为:X = X - np.mean(X, axis=0)
而对于图像矩阵来说最常做的一个数据处理是在图像矩阵的各个通道上减去相应通道上全部训练样本的像素值的均值,例如在 VGG16 中的 Imagenet 数据前处理部分就是将输入图像的三个 RGB 通道上分别减去 103.939,116.779,123.68,后面这三个数是所有 Imagenet 中的图片在三个通道上分别计算得到的均值。
相应的在 Numpy 中的处理方式为:X = X - np.mean(X)
在去均值化的基础上,如果有必要还可以进一步除以相应特征的标准差使得数据进一步标准化:X /= np.std(X, axis=0)
在这里需要注意的是,这里的均值和方差都是针对训练数据集而言的,也即应该在划分训练数据集、验证数据集和测试数据集后在训练数据集上进行计算,再用训练数据集中的均值和方差来处理验证数据集和测试数据集。
主成分分析 Principal Components Analysis
对于本部分需要的数学知识,如 基的变换、本征值分解和奇异值分解,协方差 请见链接中的笔记内容,这里直接进入主题。
之所以要做主成分分析,是因为大多数实践中的数据都是默认基于自然基 naive basis 进行记录和表示的,并且这些数据当中通常有大量的干扰噪声 noise 和冗余特征 redundancy,而寻找主成分的过程就是希望通过对于基的线性变换来找到对于被观察的数据更简洁的表示形式,从中提取最为重要的特征,即主成分,而忽略次要特征,这对简化模型复杂度和提高模型的稳健性具有重要的意义。
如果我们通过对自然基下的特征数据做线性变换后发现其在某一个方向上变动的离散程度很大,也即方差最大,则这个方向就可以认为是特征取值变动的方向,通常也就是我们感兴趣的方向,或者称信号的方向,而与之垂直的方向则可以理解为噪声的方向。评估数据质量的一个重要指标是信噪比 Signal-to-noise-ratio, SNR,其数学定义为:
- SNR = σ2signal / σ2noise ,这个公式也隐含(一般情况下)高信号方差对应高信噪比
除噪声外,多个特征之间很可能存在直接的相关性,也即我们只需要包含其中的部分特征就可以推导出其他的全部特征,因此可以在数据处理的过程中去除冗余特征,借此降低特征矩阵的维数,以减小模型需要处理的数据量。
在机器学习中对于被研究对象的多个特征的多次观测的结果通常会以一个矩阵的形式表示,称为特征矩阵,因此为了便于区分,后续涉及到利用本征值对矩阵进行的分解我都称之为本征值分解,而不是国内很多教材上的特征分解。借由统计相关知识,两个特征的相关性可以通过协方差 Covariance 来衡量,对于多个特征来说,则可以基于原有的特征矩阵构建协方差矩阵 Covariance matrix。在协方差矩阵中,对角线元素为同一个特征的方差,较大的方差值则意味着其可能是我们需要主要关注的重要变动元素。而非对角线元素则对应不同的两个特征之间的协方差,较大的协方差数值意味着两个特征之间具有较大的线性相关性,也即存在较大可能的冗余。
如果期望可以最大程度的降低冗余,则希望这个协方差矩阵可以通过线性变换变成一个对角矩阵。从协方差矩阵的构建过程可以看到它是一个实对称矩阵,而对于任意实对称矩阵来说都可以进行本征值分解,其结果为 C = QΛQT = QΛQ-1,其中 Q 为本征向量构成的正交矩阵 Quadrature matrix,Λ 为本征值构成的对角矩阵。对于本征向量来说,如果一个向量是矩阵的本征向量,则其任意非零 k 倍也是本征向量,这也可以理解为在本征向量的方向上可以有最多的信号聚集,在机器学习和深度学习的语境中,协方差矩阵的本征向量构成的正交矩阵就是特征数据集的主成分 Principal components。
同时,为突出具有较大方差的特征的重要性,可以将 Λ 对角线上的元素按照从大到小的顺序进行布置。对本征值从大到小的排列后,为了满足本征分解的运算条件,本征值对应的本征向量也要在正交矩阵中保持相同的顺序,这也使得我们可以容易的识别出哪些本征向量的方向最为重要,进而舍弃掉不重要的特征实现维度缩减。
对于已有的输入特征矩阵 X,在通过本征值分解进行主元素分析时,需要采用以下几个步骤:
对特征矩阵的每一列 x 进行去均值化得到标准化后的矩阵 X:
X -= np.mean(X, axis = 0)
-
通过计算每一列特征 x 与其他特征的协方差来构造协方差矩阵
两个特征向量的协方差计算公式为:Cov(x, y) = sx,y = (x - x̄) ⋅ (y - ȳ) / n - 1,其中 n 为每一个特征的样本数量,n - 1 是为了实现误差校正,即减少因样本方差少于总体方差带来的估计误差,并且采用代码实现时分母采用向量内积 np.dot(x - x̄, y - ȳ)
covariance_matrix = np.dot(X.T, X) / (X.shape[0] - 1)
-
在 Numpy 中实施本征分解的方法为:
eigen_values, eigen_vectors = np.linalg.eig(convariance_matrix)
-
在本征分解后,将本征值和本征向量配对,并将本征值按照从大到小的方式排列(Numpy 默认不是按照本征值大小进行排列的):
eigen_pairs = [(np.abs(eigen_values[i]), eigen_vectors[:, i]) for i in range(len(eigen_values))]
eigen_paris.sort(reverse=True) # sort the pairs with eigen_values
此时在 Numpy 中本征向量会以行向量的方式进行存储,构造本征向量构成的投影矩阵 P
用之前的特征矩阵乘以这个投影矩阵得到 Y = XPT 即为基变换后的矩阵,如果只选取前 n' 行,n' ≤ n 即可以实现降维
Y = np.dot(X, P.T)
在 Numpy 中的 PCA 具体实现举例如下:
import numpy as np
In [42]:
X = np.array([[1, 2, 3], [4, 6, 1], [6, 2, 0], [7, 3, 1]], dtype='float64')
X
Out[42]:
array([[ 1., 2., 3.],
[ 4., 6., 1.],
[ 6., 2., 0.],
[ 7., 3., 1.]])
In [44]:
X -= np.mean(X, axis=0)
X
Out[44]:
array([[-3.5 , -1.25, 1.75],
[-0.5 , 2.75, -0.25],
[ 1.5 , -1.25, -1.25],
[ 2.5 , -0.25, -0.25]])
In [45]:
cov = np.dot(X.T, X)
cov
Out[45]:
array([[ 21. , 0.5 , -8.5 ],
[ 0.5 , 10.75, -1.25],
[ -8.5 , -1.25, 4.75]])
In [46]:
eigen_values, eigen_vectors = np.linalg.eig(cov)
In [47]:
eigen_values
Out[47]:
array([ 24.6986712 , 1.02265454, 10.77867426])
In [48]:
eigen_vectors
Out[48]:
array([[ 0.91627689, 0.38756163, -0.10115656],
[ 0.06821481, 0.0978696 , 0.99285864],
[-0.39469406, 0.9166338 , -0.06323821]])
In [49]:
eigen_pairs = [(np.abs(eigen_values[i]), eigen_vectors[:, i]) for i in range(len(eigen_values))]
eigen_pairs.sort(reverse=True)
In [50]:
eigen_pairs
Out[50]:
[(24.698671197292434, array([ 0.91627689, 0.06821481, -0.39469406])),
(10.778674258520375, array([-0.10115656, 0.99285864, -0.06323821])),
(1.0226545441871755, array([ 0.38756163, 0.0978696 , 0.9166338 ]))]
In [51]:
projection = np.array([element[1] for element in eigen_pairs[:2]])
projection
Out[51]:
array([[ 0.91627689, 0.06821481, -0.39469406],
[-0.10115656, 0.99285864, -0.06323821]])
In [52]:
Y = np.dot(X, projection.T)
Y
Out[52]:
array([[-3.98295224, -0.99769222],
[-0.17187419, 2.79674909],
[ 1.78251439, -1.31376037],
[ 2.37231203, -0.4852965 ]])
由于 Numpy 中 SVD 分解后会默认的将奇异值按照从大到小的方式进行排列,因此上述 PCA 过程还可以利用 Numpy 的 SVD 分解来进行:
In [53]:
cov
Out[53]:
array([[ 21. , 0.5 , -8.5 ],
[ 0.5 , 10.75, -1.25],
[ -8.5 , -1.25, 4.75]])
In [54]:
U, S, V = np.linalg.svd(cov)
U
Out[54]:
array([[-0.91627689, 0.10115656, 0.38756163],
[-0.06821481, -0.99285864, 0.0978696 ],
[ 0.39469406, 0.06323821, 0.9166338 ]])
In [55]:
S
Out[55]:
array([ 24.6986712 , 10.77867426, 1.02265454])
In [56]:
V
Out[56]:
array([[-0.91627689, -0.06821481, 0.39469406],
[ 0.10115656, -0.99285864, 0.06323821],
[ 0.38756163, 0.0978696 , 0.9166338 ]])
In [57]:
Z = np.dot(X, U[:, :2])
Z
Out[57]:
array([[ 3.98295224, 0.99769222],
[ 0.17187419, -2.79674909],
[-1.78251439, 1.31376037],
[-2.37231203, 0.4852965 ]])
上述两种方法计算得到的特征向量有一个互为相反数,是因为特征向量不唯一导致的。
进一步地,如果输入特征本身不可以通过自然基线性表示、不服从正态分布,或者特征之间不能正交分离,那么则无法有效的通过上述方法进行降维。
随机投影 Random Projection
当发现数据集中的特征维数过高时,此时如果通过 PCA 来实现降维可能所需的计算量非常大,此时可以考虑通过矩阵乘积的形式对原始数据进行随机投影,这一投影降维的过程可以理解为一种 Embedding 实现。
在 Scikit-Learn 中实现随机投影的代码如下:
from sklearn import random_projection
rp = random_projection.SparseRandomProjection()
projected = rp.fit_transform(X)
独立成分分析 ICA
PCA 通过分离出输入数据中方差变化较大的项而实现降维,ICA 则从另一个角度,其认为输入的高维数据是由多个不同的独立成分混合而成的,因此这一算法试图分离出这些独立的成分以实现降维。
在 Scikit-Learn 中实现 ICA 的代码如下:
from sklearn.decomposition import FastICA
ica = FastICA(n_components=3)
components = ica.fit(X)