我们将学习无监督学习模型中降低数据维度的方法。
不同于我们之前学习的回归、分类和聚类模型,降维方法并不是用来做模型预测的。降维方法从一个D维的数据输入提取出一个远小于D的k维表示。因此,降维本身是一种预处理方法,或者说特征转换的方法。
降维方法中最重要的是:被抽取出的维度表示应该仍能捕捉大部分的原始数据的变化和结构。这源于一个基本思想:大部分数据源包含某种内部结构,这种结构一般来说应该是未知的(常称为隐含特征或潜在特征),但如果能发现结构中的一些特征,我们的模型就可以学习这种结构并从中预测,而不用从大量无关的充满噪音特征的原始数据中去学习预测。简言之,缩减维度可以排除数据中的噪音并保留数据原有的隐含结构。
有时候,原始数据的维度远高于我们拥有的数据点数目,如基因数据,可能只有少数人的基因数据,但是一个人的基因数据十分庞大。不降维,直接使用分类、回归等方法进行机器学习建模将非常困难。因为需要拟合的参数数目远大于训练样本的数目(从这个意义上讲,这个方法和我们在分类和回归中用的正则化方法相似)
以下是一些使用降维技术的场景:
探索性数据分析
提取特征去训练其他机器学习模型
降低大型模型在预测阶段的存储和计算需求(例如,一个执行预测的生产系统)
把大量文档缩减为一组隐含话题
当数据维度很高时,使得学习和推广更加容易(例如,处理文本、声音、图片、视频等非常高维的数据时)
MLlib提供两种相似的降低维度的模型:PCA(Principal Components Analysis,主成分分析法)和SVD(Singular Value Decomposition,奇异值分解法)
以下为Scala代码
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
import org.apache.spark.mllib.feature.StandardScaler
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.linalg.distributed.RowMatrix
import org.apache.spark.{SparkConf, SparkContext}
/***
* 我们将利用户外脸部标注LFW数据集,这些数据集包含13000多张从互联网上获得的公众人物的面部图片
* http://vis-www.cs.umass.edu/lfw/lfw-a.tgz
*/
object Spark应用于数据降维 {
def main(args: Array[String]): Unit = {
//连接SparkMaster
val conf = new SparkConf().setAppName("Spark机器学习:聚类").setMaster("local")
val sc = new SparkContext(conf)
val path = "file:///home/chenjie/lfw/*"
val rdd = sc.wholeTextFiles(path)
val first = rdd.first()
println(first)
//(file:/home/chenjie/lfw/Alicia_Silverstone/Alicia_Silverstone_0001.jpg,各种乱码)
val files = rdd.map{ case (fileName, content) =>
fileName.replace("file:", "")
}
println(files.first())
///home/chenjie/lfw/Alicia_Silverstone/Alicia_Silverstone_0001.jpg
println(files.count())
//1054
//表明我们有1054张图片要进行处理
//(1)载入图片
val aePath = "/home/chenjie/lfw/Alicia_Silverstone/Alicia_Silverstone_0001.jpg"
val aeImage = loadImageFromFile(aePath)//从文件中读取图片
println(aeImage)//输出信息
//BufferedImage@54755dd9:
// type = 5
// ColorModel:
// #pixelBits = 24
// numComponents = 3
// color space = java.awt.color.ICC_ColorSpace@f1f7db2
// transparency = 1
// has alpha = false
// isAlphaPre = false
// ByteInterleavedRaster:
// width = 250
// height = 250
// #numDataElements 3
// dataOff[0] = 2
//观察到图片是250*250,RGB数为3
//(2)转换灰度图片并改变图片尺寸
val grayImage = processImage(aeImage, 100, 100)
println(grayImage)
//BufferedImage@6801b414:
// type = 10
// ColorModel:
// #pixelBits = 8
// numComponents = 1
// color space = java.awt.color.ICC_ColorSpace@177c41d7
// transparency = 1
// has alpha = false
// isAlphaPre = false
// ByteInterleavedRaster:
// width = 100
// height = 100
// #numDataElements 1
// dataOff[0] = 0
//观察到图像宽度和高度以及RGB数目已经改变
ImageIO.write(grayImage, "jpg", new File("/home/chenjie/lfw_chenjie/Alicia_Silverstone_0001.jpg"))
//使用Python的matplotlib库观察图片
//(3)提取特征向量
val pixels = files.map(f => extractPixles(f, 50, 50))
println(pixels.take(10).map(_.take(10).mkString("", ", ", ", ...")).mkString("\n"))
/* 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ...
129.0, 114.0, 107.0, 101.0, 95.0, 92.0, 64.0, 101.0, 108.0, 54.0, ...
25.0, 25.0, 29.0, 23.0, 56.0, 29.0, 26.0, 56.0, 29.0, 20.0, ...
24.0, 23.0, 24.0, 24.0, 25.0, 26.0, 27.0, 29.0, 31.0, 28.0, ...
1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, ...
130.0, 128.0, 128.0, 129.0, 129.0, 128.0, 129.0, 127.0, 127.0, 125.0, ...
202.0, 201.0, 198.0, 199.0, 198.0, 200.0, 202.0, 203.0, 198.0, 200.0, ...
111.0, 108.0, 111.0, 109.0, 110.0, 110.0, 111.0, 112.0, 108.0, 111.0, ...
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ...
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ...*/
//为每一张图片创建MLlib向量对象。我们将缓存RDD来加速我们之后的计算
val vectors = pixels.map(p => Vectors.dense(p))
vectors.setName("image-vectors")
vectors.cache()
//4、正则化
//在运行降维模型尤其是PCA之前,通常会对输入数据进行标准化
//在这里我们只从数据中提取平均值
val scalar = new StandardScaler(withMean = true, withStd = false).fit(vectors)
//我们使用返回的scaler来转换原始的图像向量
val scaledVectors = vectors.map(v => scalar.transform(v))
//8、3 训练降维模型
val matrix = new RowMatrix(scaledVectors)
val K = 10
val pc = matrix.computePrincipalComponents(K)
//computePrincipalComponents计算分布式矩阵的前K个主成份
//1、可视化特征脸
val rows = pc.numRows
val cols = pc.numCols
println(rows, cols)
//(2500,10)
// import breeze.linalg.DenseMatrix
val pcBreeze = new org.apache.spark.mllib.linalg.DenseMatrix(rows, cols, pc.toArray)
/*import breeze.linalg.csvwrite
csvwrite(new File("/home/chenjie/pc.csv"), pcBreeze)*/
//8、4 使用降维模型
//降维方法最终的目标是要得到数据更加压缩化的表示,并能包含原始数据之中重要的特征和变化。
//为了做到这一点,我们需要通过训练好的模型,把原始数据投影到用主成分表示的新的低纬空间上。
//8.4.1 在LFW数据集上使用PCA投影数据
//我们将通过把每一个LFW图像投影到10维的向量上来演示这个概念。
//用矩阵乘法把图像矩阵和主成份矩阵相乘来实现投影
//因为图像矩阵是分布式的MLlib RowMatrix,Spark帮助我们实现了分布式计算的multiply函数
val projected = matrix.multiply(pc)
println(projected.numRows(), projected.numCols())
//(1054,10)
//每幅2500维度的图像已经被转换成一个大小为10的向量
println(projected.rows.take(5).mkString(","))
//[368.67703747427214,634.9574055886793,-720.0805946150181,-183.33178649806726,-185.35006151607868,-247.5679906183762,465.2185200095564,262.3178924253854,111.4533174989094,-117.83070817441184],[134.62507014854992,-241.275146028033,533.7812361831355,-417.5785573695592,272.8090786827052,172.63853228474193,130.14531618547025,-47.77723740561575,-67.2302636292146,-110.17504777263377],[2512.7665859365693,-693.5244838877622,101.61275144312586,118.65365662645617,152.1439876448207,-939.4510971748716,-473.3949108755985,257.9149518866237,370.59668128619967,491.7412753164988],[-219.3496822812356,-445.6572375018637,-940.0797632637393,388.0131847147831,292.161994489455,-360.63417027491164,-314.53475189487324,-772.2721936293738,-316.2826333820594,-237.87174535357053],[-733.3942273439329,-1058.6218138921884,384.1019040089379,968.9920364148038,-1009.2728630673803,-904.3695549330403,-331.2880163745114,856.648334529545,461.43138465517336,-293.00101932358973]
//这些以向量形式表示的投影后的数据可以用来作为另一个机器学习模型的输入
//例如我们可以使用这些投影后的脸的投影数据和一些没有脸的图像产生的投影数据的,共同训练一个面部识别模型
//8.4.2 PCA和SVD模型的关系
//PCA和SVD有着密切的联系。事实上,可以使用SVD恢复出相同的主成份向量,并且应用相应的投影矩阵投影到主成份空间
val svd = matrix.computeSVD(10, computeU = true)
println(s"U dimension: (${svd.U.numRows}, ${svd.U.numCols})")
println(s"S dimension: (${svd.s.size}")
println(s"V diemnsion: (${svd.V.numRows}, ${svd.V.numCols}")
println(approxEqual(Array(1.0, 2.0, 3.0), Array(1.0, 2.0, 3.0)))
println(approxEqual(Array(1.0, 2.0, 3.0), Array(3.0, 2.0, 1.0)))
println(approxEqual(svd.V.toArray, pc.toArray))
//说明矩阵V和PCA结果完全一样,不考虑正符号和浮点数误差
val breezeS = breeze.linalg.DenseVector(svd.s.toArray)
val projectedSVD = svd.U.rows.map{ v =>
val breezeV = breeze.linalg.DenseVector(v.toArray)
val multV = breezeV :* breezeS
Vectors.dense(multV.data)
}
projected.rows.zip(projectedSVD).map{ case (v1, v2) =>
approxEqual(v1.toArray, v2.toArray)
}.filter(b => true).count()
//1055
//表明投影后的每一行和SVD投影后的每一行相等
//8.5、评价降维模型
//PCA和SVD都是确定性模型,就是对于给定输入数据,总可以产生确定结果的模型。
//这两个模型都可以确定返回多个主成分或者奇异值,因此控制模型的唯一参数就是K
//就像聚类模型,增加K总是可以提高模型的表现
val sValues = (1 to 5).map{i => matrix.computeSVD(i, computeU = false).s}
sValues.foreach(println)
//为了估算SVD和PCA做聚类时的K值,以一个较大的K的变化范围绘制一个奇异值图是很有用的。
val svd300 = matrix.computeSVD(300, computeU = false)
import breeze.linalg.DenseMatrix
import breeze.linalg.csvwrite
val sMatrix = new DenseMatrix(1, 300, svd300.s.toArray)
csvwrite(new File("/home/chenjie/s.csv"), sMatrix)
}
/***
* 从文件中读取图片
* @param path
* @return
*/
def loadImageFromFile(path: String): BufferedImage = {
import javax.imageio.ImageIO
import java.io.File
ImageIO.read(new File(path))
}
/***
* 转换灰度图片并改变图片尺寸
* @param image
* @param width
* @param height
* @return
*/
def processImage(image: BufferedImage, width: Int, height: Int): BufferedImage = {
val bwImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY)
val g = bwImage.getGraphics()
g.drawImage(image, 0, 0, width, height, null)
g.dispose()
bwImage
}
/***
* 打平二维的像素矩阵来构造一维的向量
* @param image
* @return
*/
def getPixelsFromImage(image: BufferedImage): Array[Double] = {
val width = image.getWidth()
val height = image.getHeight()
val pixels = Array.ofDim[Double](width * height)
image.getData.getPixels(0, 0, width, height, pixels)
}
/***
* 组合
* @param path
* @param width
* @param height
* @return
*/
def extractPixles(path: String, width: Int, height: Int): Array[Double] = {
val raw = loadImageFromFile(path)
val processed = processImage(raw, width, height)
getPixelsFromImage(processed)
}
/***
* 比较两个矩阵的向量数据是否大致相等
* @param array1
* @param array2
* @param tolerance
* @return
*/
def approxEqual(array1: Array[Double], array2: Array[Double], tolerance: Double = 1e-6): Boolean = {
val bools = array1.zip(array2).map{ case (v1, v2) =>
if(math.abs(math.abs(v1) - math.abs(v2)) > tolerance) false else true
}
bools.fold(true)(_ & _)
}
}
使用jupyter notebook可视化
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
path = "/home/chenjie/lfw/Alicia_Silverstone/Alicia_Silverstone_0001.jpg"
img=mpimg.imread(path)
plt.imshow(img)
plt.show()
path2 = "/home/chenjie/lfw_chenjie/Alicia_Silverstone_0001.jpg"
img2=mpimg.imread(path2)
plt.imshow(img2, cmap=plt.cm.gray)
plt.show()