赞
踩
事实上,在高维情形下 现的数据样本稀疏、 距离计算困 难等问是所有机器学习方法共同面 的严重障碍, 被称为" 维数灾难" (curse of dimensionality) . 缓解维数灾难的一个重要途径是降维(dimension reduction) 亦称" 维数约简 “ ,即通过某种数学变换将原始高维属 性空间转变为 一个低维"子空间" (subspace) ,在这 子空 中样本密 大幅提高 计算 变得更为容易。为什么进行降维?这是因为在很多时候, 人们观测或收集到的数据样本虽是高维的,但与学习任务密切相关的也许仅是某个低维分布,即高 空间中个低维"嵌入" (embedding) 如图给出直观的例子,原始高空间中的样本点,在这个低维嵌入子空间中更容易进行学习。
常见的数据降维方法有:PCA、LDA、MDS、ISOMAP、SNE、T-SNE、AutoEncoder等
MDS算法要求原始空间中样本之间的距离在低维空间中得以保持。但是为了有效降维,我们往往只需要降维后的距离与原始空间距离尽可能接近即可。
- def calculate_distance(x, y):
- d = np.sqrt(np.sum((x - y) ** 2))
- return d
-
-
- # 计算矩阵各行之间的欧式距离;x矩阵的第i行与y矩阵的第0-j行继续欧式距离计算,构成新矩阵第i行[i0、i1...ij]
- def calculate_distance_matrix(x, y):
- d = metrics.pairwise_distances(x, y)
- return d
-
-
- def cal_B(D):
- (n1, n2) = D.shape
- DD = np.square(D) # 矩阵D 所有元素平方
- Di = np.sum(DD, axis=1) / n1 # 计算dist(i.)^2
- Dj = np.sum(DD, axis=0) / n1 # 计算dist(.j)^2
- Dij = np.sum(DD) / (n1 ** 2) # 计算dist(ij)^2
- B = np.zeros((n1, n1))
- for i in range(n1):
- for j in range(n2):
- B[i, j] = (Dij + DD[i, j] - Di[i] - Dj[j]) / (-2) # 计算b(ij)
- return B
-
-
- def MDS(data, n=2):
- D = calculate_distance_matrix(data, data)
- # print(D)
- B = cal_B(D)
- Be, Bv = np.linalg.eigh(B) # Be矩阵B的特征值,Bv归一化的特征向量
- # print numpy.sum(B-numpy.dot(numpy.dot(Bv,numpy.diag(Be)),Bv.T))
- Be_sort = np.argsort(-Be)
- Be = Be[Be_sort] # 特征值从大到小排序
- Bv = Bv[:, Be_sort] # 归一化特征向量
- Bez = np.diag(Be[0:n]) # 前n个特征值对角矩阵
- # print Bez
- Bvz = Bv[:, 0:n] # 前n个归一化特征向量
- Z = np.dot(np.sqrt(Bez), Bvz.T).T
- # print(Z)
- return Z
对于流形(Manifold,局部具有欧式空间性质的空间),两点之间的距离并非欧氏距离。而是采用“局部具有欧式空间性质”的原因,让两点之间的距离近似等于依次多个临近点的连线的长度之和。通过这个方式,将多维空间“展开”到低维空间。
Isomap算法是在MDS算法的基础上衍生出的一种算法,MDS算法是保持降维后的样本间距离不变,Isomap算法引进了邻域图,样本只与其相邻的样本连接,他们之间的距离可直接计算,较远的点可通过最小路径算出距离,在此基础上进行降维保距。
计算流程如下:
- def calculate_distance(x, y):
- d = np.sqrt(np.sum((x - y) ** 2))
- return d
-
-
- # 计算矩阵各行之间的欧式距离;x矩阵的第i行与y矩阵的第0-j行继续欧式距离计算,构成新矩阵第i行[i0、i1...ij]
- def calculate_distance_matrix(x, y):
- d = metrics.pairwise_distances(x, y)
- return d
-
-
- def cal_B(D):
- (n1, n2) = D.shape
- DD = np.square(D) # 矩阵D 所有元素平方
- Di = np.sum(DD, axis=1) / n1 # 计算dist(i.)^2
- Dj = np.sum(DD, axis=0) / n1 # 计算dist(.j)^2
- Dij = np.sum(DD) / (n1 ** 2) # 计算dist(ij)^2
- B = np.zeros((n1, n1))
- for i in range(n1):
- for j in range(n2):
- B[i, j] = (Dij + DD[i, j] - Di[i] - Dj[j]) / (-2) # 计算b(ij)
- return B
-
-
- def MDS(data, n=2):
- D = calculate_distance_matrix(data, data)
- # print(D)
- B = cal_B(D)
- Be, Bv = np.linalg.eigh(B) # Be矩阵B的特征值,Bv归一化的特征向量
- # print numpy.sum(B-numpy.dot(numpy.dot(Bv,numpy.diag(Be)),Bv.T))
- Be_sort = np.argsort(-Be)
- Be = Be[Be_sort] # 特征值从大到小排序
- Bv = Bv[:, Be_sort] # 归一化特征向量
- Bez = np.diag(Be[0:n]) # 前n个特征值对角矩阵
- # print Bez
- Bvz = Bv[:, 0:n] # 前n个归一化特征向量
- Z = np.dot(np.sqrt(Bez), Bvz.T).T
- # print(Z)
- return Z
参考
它是一个线性变换。这个变换把数据变换到一个新的坐标系统中,使得任何数据投影的第一大方差在第一个坐标(称为第一主成分)上,第二大方差在第二个坐标(第二主成分)上,依次类推。主成分分析经常用于减少数据集的维数,同时保持数据集的对方差贡献最大的特征。
- def pca(data, n):
- data = np.array(data)
-
- # 均值
- mean_vector = np.mean(data, axis=0)
-
- # 协方差
- cov_mat = np.cov(data - mean_vector, rowvar=0)
-
- # 特征值 特征向量
- fvalue, fvector = np.linalg.eig(cov_mat)
-
- # 排序
- fvaluesort = np.argsort(-fvalue)
-
- # 取前几大的序号
- fValueTopN = fvaluesort[:n]
-
- # 保留前几大的数值
- newdata = fvector[:, fValueTopN]
-
- new = np.dot(data, newdata)
-
- return new
与PCA一样,是一种线性降维算法。不同于PCA只会选择数据变化最大的方向,由于LDA是有监督的(分类标签),所以LDA会主要以类别为思考因素,使得投影后的样本尽可能可分。它通过在k维空间选择一个投影超平面,使得不同类别在该超平面上的投影之间的距离尽可能近,同时不同类别的投影之间的距离尽可能远。从而试图明确地模拟数据类之间的差异。
LDA方法的考虑是,对于一个多类别的分类问题,想要把它们映射到一个低维空间,如一维空间从而达到降维的目的,我们希望映射之后的数据间,两个类别之间“离得越远”,且类别内的数据点之间“离得越近”,这样两个类别就越好区分。因此LDA方法分别计算“within-class”的分散程度Sw和“between-class”的分散程度Sb,而我们希望的是Sb/Sw越大越好,从而找到最合适的映射向量w。
假设我们的数据集$D={(x_1,y_1), (x_2,y_2), ...,((x_m,y_m))}$,其中任意样本xixi为n维向量,yi∈{0,1}。我们定义Nj(j=0,1)为第j类样本的个数,Xj(j=0,1)为第j类样本的集合,而μj(j=0,1)为第j类样本的均值向量,定义Σj(j=0,1)为第j类样本的协方差矩阵(严格说是缺少分母部分的协方差矩阵)。
μj的表达式为:
Σj的表达式为:
- import numpy as np
- import csv
- import matplotlib.pyplot as plt
-
-
- def loadDataset(filename):
- data1 ,data2 = [], []
- with open(filename, 'r') as f:
- lines = csv.reader(f)
- data = list(lines)
- for i in range(len(data)):
- del(data[i][0])
- for j in range(len(data[i])):
- data[i][j] = float(data[i][j])
- if data[i][-1]:
- del(data[i][-1])
- data1.append(data[i])
- else:
- del(data[i][-1])
- data2.append(data[i])
-
- return np.array(data1), np.array(data2)
-
-
-
- def lda_num2(data1, data2, n=2):
- mu0 = data2.mean(0)
- mu1 = data1.mean(0)
- print(mu0)
- print(mu1)
-
- sum0 = np.zeros((mu0.shape[0], mu0.shape[0]))
- for i in range(len(data2)):
- sum0 += np.dot((data2[i] - mu0).T, (data2[i] - mu0))
- sum1 = np.zeros(mu1.shape[0])
- for i in range(len(data1)):
- sum1 += np.dot((data1[i] - mu1).T, (data1[i] - mu1))
-
- s_w = sum0 + sum1
- print(s_w)
- w = np.linalg.pinv(s_w) * (mu0 - mu1)
-
- new_w = w[:n].T
-
- new_data1 = np.dot(data1, new_w)
- new_data2 = np.dot(data2, new_w)
-
- return new_data1, new_data2
-
-
- def view(data):
- x = [i[0] for i in data]
- y = [i[1] for i in data]
-
- plt.figure()
- plt.scatter(x, y)
- plt.show()
-
-
- if __name__ == '__main__':
- data1, data2 = loadDataset("Pima.csv")
-
- newdata1, newdata2 = lda_num2(data1, data2, 2)
-
- print(newdata1)
- print(newdata2)
- view(np.concatenate((newdata1, newdata2))*10**7)
- view(newdata1 * 10 ** 7)
- view(newdata2 * 10 ** 7)
SNE是通过仿射(affinitie)变换将数据点映射到概率分布上,主要包括两个步骤:
我们看到SNE模型是非监督的降维,他跟kmeans等不同,他不能通过训练得到一些东西之后再用于其它数据(比如kmeans可以通过训练得到k个点,再用于其它数据集,而t-SNE只能单独的对数据做操作,也就是说他只有fit_transform,而没有fit操作)
SNE是先将欧几里得距离转换为条件概率来表达点与点之间的相似度。具体来说,给定一个N个高维的数据 x1,...,xN(注意N不是维度), SNE首先是计算概率pij,正比于xi和xj之间的相似度(这种概率是我们自主构建的),即以x_i自己为中心,以高斯分布选择x_j作为近邻点的条件概率::
这里的有一个参数是σi,对于不同的点xi取值不一样,后续会讨论如何设置。此外设置px∣x=0因为我们关注的是两两之间的相似度。
那对于低维度下的yi,我们可以指定高斯分布为方差为根号二分之一,因此它们之间的相似度如下:
同样,设定qi∣i=0
如果降维的效果比较好,局部特征保留完整,那么 pi∣j=qi∣j, 因此我们优化两个分布之间的距离-KL散度(Kullback-Leibler divergences),那么目标函数(cost function)如下:
首先不同的点具有不同的σi,Pi的熵(entropy)会随着σi的增加而增加。SNE使用困惑度(perplexity)的概念,用二分搜索的方式来寻找一个最佳的σ。其中困惑度指:
这里的H(Pi)是Pi的熵,即:
困惑度可以解释为一个点附近的有效近邻点个数。SNE对困惑度的调整比较有鲁棒性,通常选择5-50之间,给定之后,使用二分搜索的方式寻找合适的σ
在初始化中,可以用较小的σ下的高斯分布来进行初始化。为了加速优化过程和避免陷入局部最优解,梯度中需要使用一个相对较大的动量(momentum)。即参数更新中除了当前的梯度,还要引入之前的梯度累加的指数衰减项,如下:
这里的Y(t)表示迭代t次的解,η表示学习速率,α(t)表示迭代t次的动量。
此外,在初始优化的阶段,每次迭代中可以引入一些高斯噪声,之后像模拟退火一样逐渐减小该噪声,可以用来避免陷入局部最优解。因此,SNE在选择高斯噪声,以及学习速率,什么时候开始衰减,动量选择等等超参数上,需要跑多次优化才可以。
尽管SNE提供了很好的可视化方法,但是他很难优化,而且存在”crowding problem”(拥挤问题)。后续中,Hinton等人又提出了t-SNE的方法。与SNE不同,主要如下:
t-SNE在低维空间下使用更重长尾分布的t分布来避免crowding问题和优化问题。
我们对比一下高斯分布和t分布(如上图,code见probability/distribution.md), t分布受异常值影响更小,拟合结果更为合理,较好的捕获了数据的整体特征。
使用了t分布之后的q变化,如下:
其中y~i~=x~i~/2σ
此外,t分布是无限多个高斯分布的叠加,计算上不是指数的,会方便很多。优化的梯度如下:
t-sne的有效性,也可以从上图中看到:横轴表示距离,纵轴表示相似度, 可以看到,对于较大相似度的点,t分布在低维空间中的距离需要稍小一点;而对于低相似度的点,t分布在低维空间中的距离需要更远。这恰好满足了我们的需求,即同一簇内的点(距离较近)聚合的更紧密,不同簇之间的点(距离较远)更加疏远。
总结一下,t-SNE的梯度更新有两大优势:
资料来源
- import numpy as np
-
-
- def cal_pairwise_dist(x):
- '''计算pairwise 距离, x是matrix
- (a-b)^2 = a^w + b^2 - 2*a*b
- '''
- sum_x = np.sum(np.square(x), 1)
- dist = np.add(np.add(-2 * np.dot(x, x.T), sum_x).T, sum_x)
- return dist
-
-
- def cal_perplexity(dist, idx=0, beta=1.0):
- '''计算perplexity, D是距离向量,
- idx指dist中自己与自己距离的位置,beta是高斯分布参数
- 这里的perp仅计算了熵,方便计算
- '''
- prob = np.exp(-dist * beta)
- # 设置自身prob为0
- prob[idx] = 0
- sum_prob = np.sum(prob)
- perp = np.log(sum_prob) + beta * np.sum(dist * prob) / sum_prob
- prob /= sum_prob
- return perp, prob
-
-
- def seach_prob(x, tol=1e-5, perplexity=30.0):
- '''二分搜索寻找beta,并计算pairwise的prob
- '''
-
- # 初始化参数
- print("Computing pairwise distances...")
- (n, d) = x.shape
- dist = cal_pairwise_dist(x)
- pair_prob = np.zeros((n, n))
- beta = np.ones((n, 1))
- # 取log,方便后续计算
- base_perp = np.log(perplexity)
-
- for i in range(n):
- if i % 500 == 0:
- print("Computing pair_prob for point %s of %s ..." % (i, n))
-
- betamin = -np.inf
- betamax = np.inf
- perp, this_prob = cal_perplexity(dist[i], i, beta[i])
-
- # 二分搜索,寻找最佳sigma下的prob
- perp_diff = perp - base_perp
- tries = 0
- while np.abs(perp_diff) > tol and tries < 50:
- if perp_diff > 0:
- betamin = beta[i].copy()
- if betamax == np.inf or betamax == -np.inf:
- beta[i] = beta[i] * 2
- else:
- beta[i] = (beta[i] + betamax) / 2
- else:
- betamax = beta[i].copy()
- if betamin == np.inf or betamin == -np.inf:
- beta[i] = beta[i] / 2
- else:
- beta[i] = (beta[i] + betamin) / 2
-
- # 更新perb,prob值
- perp, this_prob = cal_perplexity(dist[i], i, beta[i])
- perp_diff = perp - base_perp
- tries = tries + 1
- # 记录prob值
- pair_prob[i,] = this_prob
- print("Mean value of sigma: ", np.mean(np.sqrt(1 / beta)))
- return pair_prob
-
-
- def pca(x, no_dims=50):
- ''' PCA算法
- 使用PCA先进行预降维
- '''
- print("Preprocessing the data using PCA...")
- (n, d) = x.shape
- x = x - np.tile(np.mean(x, 0), (n, 1))
- l, M = np.linalg.eig(np.dot(x.T, x))
- y = np.dot(x, M[:, 0:no_dims])
- return y
-
-
- def tsne(x, no_dims=2, initial_dims=50, perplexity=30.0, max_iter=1000):
- """Runs t-SNE on the dataset in the NxD array x
- to reduce its dimensionality to no_dims dimensions.
- The syntaxis of the function is Y = tsne.tsne(x, no_dims, perplexity),
- where x is an NxD NumPy array.
- """
-
- # Check inputs
- if isinstance(no_dims, float):
- print("Error: array x should have type float.")
- return -1
- if round(no_dims) != no_dims:
- print("Error: number of dimensions should be an integer.")
- return -1
-
- # 初始化参数和变量
- x = pca(x, initial_dims).real
- (n, d) = x.shape
- initial_momentum = 0.5
- final_momentum = 0.8
- eta = 500
- min_gain = 0.01
- y = np.random.randn(n, no_dims)
- dy = np.zeros((n, no_dims))
- iy = np.zeros((n, no_dims))
- gains = np.ones((n, no_dims))
-
- # 对称化
- P = seach_prob(x, 1e-5, perplexity)
- P = P + np.transpose(P)
- P = P / np.sum(P)
- # early exaggeration
- P = P * 4
- P = np.maximum(P, 1e-12)
-
- # Run iterations
- for iter in range(max_iter):
- # Compute pairwise affinities
- sum_y = np.sum(np.square(y), 1)
- num = 1 / (1 + np.add(np.add(-2 * np.dot(y, y.T), sum_y).T, sum_y))
- num[range(n), range(n)] = 0
- Q = num / np.sum(num)
- Q = np.maximum(Q, 1e-12)
-
- # Compute gradient
- PQ = P - Q
- for i in range(n):
- dy[i, :] = np.sum(np.tile(PQ[:, i] * num[:, i], (no_dims, 1)).T * (y[i, :] - y), 0)
-
- # Perform the update
- if iter < 20:
- momentum = initial_momentum
- else:
- momentum = final_momentum
- gains = (gains + 0.2) * ((dy > 0) != (iy > 0)) + (gains * 0.8) * ((dy > 0) == (iy > 0))
- gains[gains < min_gain] = min_gain
- iy = momentum * iy - eta * (gains * dy)
- y = y + iy
- y = y - np.tile(np.mean(y, 0), (n, 1))
- # Compute current value of cost function
- if (iter + 1) % 100 == 0:
- if iter > 100:
- C = np.sum(P * np.log(P / Q))
- else:
- C = np.sum(P / 4 * np.log(P / 4 / Q))
- print("Iteration ", (iter + 1), ": error is ", C)
- # Stop lying about P-values
- if iter == 100:
- P = P / 4
- print("finished training!")
- return y
-
-
- if __name__ == "__main__":
- # Run Y = tsne.tsne(X, no_dims, perplexity) to perform t-SNE on your dataset.
- X = np.loadtxt("mnist2500_X.txt")
- labels = np.loadtxt("mnist2500_labels.txt")
- Y = tsne(X, 2, 50, 20.0)
- from matplotlib import pyplot as plt
-
- plt.scatter(Y[:, 0], Y[:, 1], 20, labels)
- plt.show()
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。