文章列表

奇异值分解(SVD)

返回 [Spark MLlib入门教程](http://mocom.xmu.edu.cn/article/show/5858ab782b2730e00d70fa08/0/1) ## MLlib中的特征降维方法 --- **降维(Dimensionality Reduction)** 是机器学习中的一种重要的特征处理手段,它可以减少计算过程中考虑到的随机变量(即特征)的个数,其被广泛应用于各种机器学习问题中,用于消除噪声、对抗数据稀疏问题。它在尽可能维持原始数据的内在结构的前提下,得到一组描述原数据的,低维度的隐式特征(或称主要特征)。 MLlib机器学习库提供了两个常用的降维方法:**奇异值分解(Singular Value Decomposition,SVD)** 和 **主成分分析(Principal Component Analysis,PCA)**,下面我们将通过实例介绍其具体的使用方法。 ## 一、奇异值分解(SVD) ## 1、概念介绍 奇异值分解(SVD)** 来源于代数学中的矩阵分解问题,对于一个方阵来说,我们可以利用矩阵特征值和特征向量的特殊性质(矩阵点乘特征向量等于特征值数乘特征向量),通过求特征值与特征向量来达到矩阵分解的效果: $$A = Q\Sigma Q^{-1} $$ 这里,$Q$是由特征向量组成的矩阵,而 $\Sigma$是特征值降序排列构成的一个对角矩阵(对角线上每个值是一个特征值,按降序排列,其他值为0),特征值的数值表示对应的特征的重要性。 在很多情况下,最大的一小部分特征值的和即可以约等于所有特征值的和,而通过矩阵分解的降维就是通过在$Q$、$\Sigma$中删去那些比较小的特征值及其对应的特征向量,使用一小部分的特征值和特征向量来描述整个矩阵,从而达到降维的效果。 但是,实际问题中大多数矩阵是以奇异矩阵形式,而不是方阵的形式出现的,奇异值分解是特征值分解在奇异矩阵上的推广形式,它将一个维度为$m\times n$奇异矩阵$A$分解成三个部分 : $$A = U\Sigma V^{T}$$ 其中$U$、$V$是两个正交矩阵,其中的每一行(每一列)分别被称为 **左奇异向量** 和 **右奇异向量**,他们和 $\Sigma$中对角线上的奇异值相对应,通常情况下我们只需要取一个较小的值$k$,保留前$k$个奇异向量和奇异值即可,其中$U$的维度是$m\times k$、$V$的维度是$n \times k$、$\Sigma$是一个$k \times k$的方阵,从而达到降维效果。 ## 2、SVD变换的例子 Mllib内置的奇异值分解功能位于`org.apache.spark.mllib.linalg`包下的`RowMatrix`和`IndexedRowMatrix`类中,所以,我们必须先通过已有数据创建出相应矩阵类型的对象,然后调用该类的成员方法来进行SVD分解,这里以`RowMatrix`为例: 首先,引入需要的类: ```scala import org.apache.spark.mllib.linalg.distributed.RowMatrix ``` 准备好一个矩阵,这里我们采用一个简单的文件`a.mat`来存储一个尺寸为(4,9)的矩阵,其内容如下: ``` 1 2 3 4 5 6 7 8 9 5 6 7 8 9 0 8 6 7 9 0 8 7 1 4 3 2 1 6 4 2 1 3 4 2 1 5 ``` 随后,将该文本文件读入成`RDD[Vector]`,并转换成`RowMatrix`,即可调用`RowMatrix`自带的`computeSVD`方法计算分解结果,这一结果保存在类型为`SingularValueDecomposition`的`svd`对象中: ```scala scala> val data = sc.textFile("a.mat").map(_.split(" ").map(_.toDouble)).map(line => Vectors.dense(line)) data: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.Vector] = MapPartitionsRDD[3] at map at :31 //通过RDD[Vectors]创建行矩阵 scala> val rm = new RowMatrix(data) rm: org.apache.spark.mllib.linalg.distributed.RowMatrix = org.apache.spark.mllib.linalg.distributed.RowMatrix@4397952a //保留前3个奇异值 scala> val svd = rm.computeSVD(3) svd: org.apache.spark.mllib.linalg.SingularValueDecomposition[o.... ``` 通过访问`svd`对象的`V`、`s`、`U`成员分别拿到进行SVD分解后的右奇异矩阵、奇异值向量和左奇异矩阵: ```scala scala> svd.s res7: org.apache.spark.mllib.linalg.Vector = [28.741265581939565,10.847941223452608,7.089519467626695] scala> svd.V res8: org.apache.spark.mllib.linalg.Matrix = -0.32908987300830383 0.6309429972945555 0.16077051991193514 -0.2208243332000108 -0.1315794105679425 -0.2368641953308101 -0.35540818799208057 0.39958899365222394 -0.147099615168733 -0.37221718676772064 0.2541945113699779 -0.25918656625268804 -0.3499773046239524 -0.24670052066546988 -0.34607608172732196 -0.21080978995485605 0.036424486072344636 0.7867152486535043 -0.38111806017302313 -0.1925222521055529 -0.09403561250768909 -0.32751631238613577 -0.3056795887065441 0.09922623079118417 -0.3982876638452927 -0.40941282445850646 0.26805622896042314 scala> svd.U res9: org.apache.spark.mllib.linalg.distributed.RowMatrix = null ``` 这里可以看到,由于限定了取前三个奇异值,所以奇异值向量`s`包含有三个从大到小排列的奇异值,而右奇异矩阵`V`中的每一列都代表了对应的右奇异向量。`U`成员得到的是一个`null`值,这是因为在实际运用中,只需要`V`和`S`两个成员,即可通过矩阵计算达到降维的效果,其具体原理可以参看这篇博文:[机器学习中的数学(5)-强大的矩阵奇异值分解(SVD)及其应用](http://www.cnblogs.com/LeftNotEasy/archive/2011/01/19/svd-and-applications.html),这里不再赘述。 如果需要获得`U`成员,可以在进行SVD分解时,指定`computeU`参数,令其等于`True`,即可在分解后的`svd`对象中拿到`U`成员,如下文所示: ```scala scala> val svd = rm.computeSVD(3, computeU = true) svd: org.apache.spark.mllib.linalg.SingularValueDecomposition[o ```
[技术] Tue Dec 27 2016 22:24:36 GMT+0800 (中国标准时间)

支持向量机SVM分类器 -- spark.mllib

返回 [Spark MLlib入门教程](http://mocom.xmu.edu.cn/article/show/5858ab782b2730e00d70fa08/0/1) --- ## 方法简介 ​ 支持向量机是一种二分类模型。它的基本模型是定义在特征空间上的间隔最大的线性分类器。支持向量机学习方法包含3种模型:线性可分支持向量机、线性支持向量机及非线性支持向量机。当训练数据线性可分时,通过硬间隔最大化,学习一个线性的分类器,即线性可分支持向量机;当训练数据近似线性可分时,通过软间隔最大化,也学习一个线性的分类器,即线性支持向量机;当训练数据线性不可分时,通过使用核技巧及软间隔最大化,学习非线性支持向量机。线性支持向量机支持L1和L2的正则化变型。关于正则化,可以参见http://spark.apache.org/docs/1.6.2/mllib-linear-methods.html#regularizers ## 基本原理 ​ 支持向量机,因其英文名为support vector machine,故一般简称SVM。SVM从线性可分情况下的最优分类面发展而来。最优分类面就是要求分类线不但能将两类正确分开(训练错误率为0),且使分类间隔最大。SVM考虑寻找一个满足分类要求的超平面,并且使训练集中的点距离分类面尽可能的远,也就是寻找一个分类面使它两侧的空白区域(margin)最大。这两类样本中离分类面最近,且平行于最优分类面的超平面上的点,就叫做支持向量(下图中红色的点)。 ![svm](http://mocom.xmu.edu.cn/blog/585cb051e5fef7440a425648.png) 假设超平面可描述为: $$ wx+b=0, w\in R^n, b\in R $$ 其分类间隔等于$\frac{2}{||w||}$。其学习策略是使数据间的间隔最大化,最终可转化为一个凸二次规划问题的求解。 分类器的损失函数(hinge loss铰链损失)如下所示: $$ L(w;x,y):=max(0,1-yw^Tx) $$ 默认情况下,线性SVM是用L2 正则化来训练的,但也支持L1正则化。在这种情况下,这个问题就变成了一个线性规划。 ​ 线性SVM算法输出一个SVM模型。给定一个新的数据点,比如说$x$,这个模型就会根据$w^Tx$ 的值来进行预测。默认情况下,如果$w^Tx \ge 0$ ,则输出预测结果为正(因为我们想要损失函数最小,如果预测为负,则会导致损失函数大于1),反之则预测为负。 ## 示例代码 ​ 下面的例子具体介绍了如何读入一个数据集,然后用SVM对训练数据进行训练,然后用训练得到的模型对测试集进行预测,并计算错误率。以iris数据集([https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data](https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data))为例进行分析。iris以鸢尾花的特征作为数据来源,数据集包含150个数据集,分为3类,每类50个数据,每个数据包含4个属性,是在数据挖掘、数据分类中非常常用的测试集、训练集。 ### 1. 导入需要的包: ​ 首先,我们导入需要的包: ```scala import org.apache.spark.SparkConf import org.apache.spark.SparkContext import org.apache.spark.mllib.regression.LabeledPoint import org.apache.spark.mllib.linalg.{Vectors,Vector} import org.apache.spark.mllib.classification.{SVMModel, SVMWithSGD} import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics ``` ### 2. 读取数据: ​ 首先,读取文本文件;然后,通过map将每行的数据用“,”隔开,在我们的数据集中,每行被分成了5部分,前4部分是鸢尾花的4个特征,最后一部分是鸢尾花的分类。把这里我们用LabeledPoint来存储标签列和特征列。LabeledPoint在监督学习中常用来存储标签和特征,其中要求标签的类型是double,特征的类型是Vector。所以,我们把莺尾花的分类进行了一下改变,"Iris-setosa"对应分类0,"Iris-versicolor"对应分类1,其余对应分类2;然后获取莺尾花的4个特征,存储在Vector中。 ```scala scala> val data = sc.textFile("G:/spark/iris.data") data: org.apache.spark.rdd.RDD[String] = G:/spark/iris.data MapPartitionsRDD[1] at textFile at :28 scala> val parsedData = data.map { line => | val parts = line.split(',') | LabeledPoint(if(parts(4)=="Iris-setosa") 0.toDouble else if (parts(4) =="Iris-versicolor") 1.toDouble else | 2.toDouble, Vectors.dense(parts(0).toDouble,parts(1).toDouble,parts (2).toDouble,parts(3).toDouble)) | } parsedData: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPo int] = MapPartitionsRDD[2] at map at :30 ``` ### 3. 构建模型 ​ 因为SVM只支持2分类,所以我们要进行一下数据抽取,这里我们通过filter过滤掉第2类的数据,只选取第0类和第1类的数据。然后,我们把数据集划分成两部分,其中训练集占60%,测试集占40%: ```scala scala> val splits = parsedData.filter { point => point.label != 2 }.randomSplit( Array(0.6, 0.4), seed = 11L) splits: Array[org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.Labeled Point]] = Array(MapPartitionsRDD[4] at randomSplit at :32, MapPartition sRDD[5] at randomSplit at :32) scala> val training = splits(0).cache() training: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoin t] = MapPartitionsRDD[4] at randomSplit at :32 scala> val test = splits(1) test: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint] = MapPartitionsRDD[5] at randomSplit at :32 ``` ​ 接下来,通过训练集构建模型SVMWithSGD。这里的SGD即著名的随机梯度下降算法(Stochastic Gradient Descent)。设置迭代次数为1000,除此之外还有stepSize(迭代步伐大小),regParam(regularization正则化控制参数),miniBatchFraction(每次迭代参与计算的样本比例),initialWeights(weight向量初始值)等参数可以进行设置。 ```scala scala> val numIterations = 1000 numIterations: Int = 1000 scala> val model = SVMWithSGD.train(training, numIterations) model: org.apache.spark.mllib.classification.SVMModel = org.apache.spark.mllib.c lassification.SVMModel: intercept = 0.0, numFeatures = 4, numClasses = 2, thresh old = 0.0 ``` ### 4. 模型评估 ​ 接下来,我们清除默认阈值,这样会输出原始的预测评分,即带有确信度的结果。 ```scala scala> model.clearThreshold() res0: model.type = org.apache.spark.mllib.classification.SVMModel: intercept = 0 .0, numFeatures = 4, numClasses = 2, threshold = None scala> val scoreAndLabels = test.map { point => | val score = model.predict(point.features) | (score, point.label) | } scoreAndLabels: org.apache.spark.rdd.RDD[(Double, Double)] = MapPartitionsRDD[11 ] at map at :42 scala> scoreAndLabels.foreach(println) (-3.0127314882950778,0.0) (-2.4596261094505403,0.0) (-2.64505513159329,0.0) (-3.503342620026854,0.0) (-2.717199557755541,0.0) (-2.6779191149350754,0.0) ... ... ``` ​ 那如果设置了阈值,则会把大于阈值的结果当成正预测,小于阈值的结果当成负预测。 ```scala scala> model.setThreshold(0.0) res0: model.type = org.apache.spark.mllib.classification.SVMModel: intercept = 0 .0, numFeatures = 4, numClasses = 2, threshold = 0 scala> scoreAndLabels.foreach(println) (0.0,0.0) (0.0,0.0) (0.0,0.0) (0.0,0.0) (0.0,0.0) (0.0,0.0) ... ... ``` ​ 最后,我们构建评估矩阵,把模型预测的准确性打印出来: ```scala scala> val metrics = new BinaryClassificationMetrics(scoreAndLabels) metrics: org.apache.spark.mllib.evaluation.BinaryClassificationMetrics = org.apa che.spark.mllib.evaluation.BinaryClassificationMetrics@12468ced scala> val auROC = metrics.areaUnderROC() auROC: Double = 1.0 scala> println("Area under ROC = " + auROC) Area under ROC = 1.0 ``` ​ 其中, `SVMWithSGD.train()` 方法默认的通过把正则化参数设为1来执行来范数。如果我们想配置这个算法,可以通过创建一个新的 `SVMWithSGD`对象然后调用他的setter方法来进行重新配置。下面这个例子,我们构建了一个正则化参数为0.1的L1正则化SVM方法 ,然后迭代这个训练算法2000次。 ```scala import org.apache.spark.mllib.optimization.L1Updater scala> val svmAlg = new SVMWithSGD() svmAlg: org.apache.spark.mllib.classification.SVMWithSGD = org.apache.spark.mlli b.classification.SVMWithSGD@475774a9 scala> svmAlg.optimizer. | setNumIterations(2000). | setRegParam(0.1). | setUpdater(new L1Updater) res3: svmAlg.optimizer.type = org.apache.spark.mllib.optimization.GradientDescen t@62f740b7 scala> val modelL1 = svmAlg.run(training) modelL1: org.apache.spark.mllib.classification.SVMModel = org.apache.spark.mllib .classification.SVMModel: intercept = 0.0, numFeatures = 4, numClasses = 2, thre shold = 0.0 ```
[技术] Fri Dec 23 2016 13:04:39 GMT+0800 (中国标准时间)

特征抽取:CountVectorizer -- spark.ml

返回 [Spark MLlib入门教程](http://mocom.xmu.edu.cn/article/show/5858ab782b2730e00d70fa08/0/1) --- CountVectorizer和CountVectorizerModel旨在通过计数来将一个文档转换为向量。当不存在先验字典时,Countvectorizer可作为Estimator来提取词汇,并生成一个CountVectorizerModel。该模型产生文档关于词语的稀疏表示,其表示可以传递给其他算法如LDA。 在fitting过程中,countvectorizer将根据语料库中的词频排序从高到低进行选择,词汇表的最大含量由vocabsize参数来指定。一个可选的参数minDF也影响fitting过程,它指定词汇表中的词语至少要在多少个不同文档中出现。 我们接下来通过一个例子来进行介绍。首先,导入CountVectorizer所需要的包: ```scala import org.apache.spark.SparkConf import org.apache.spark.SparkContext import org.apache.spark.sql.SQLContext import org.apache.spark.ml.feature.{CountVectorizer, CountVectorizerModel} ``` 接下来,根据SparkContext来创建一个SQLContext,其中sc是一个已经存在的SparkContext;然后导入sqlContext.implicits._来实现RDD到Dataframe的隐式转换。 ```scala scala> val sqlContext = new SQLContext(sc)sqlContext: org.apache.spark.sql.SQLContext = org.apache.spark.sql.SQLContext@225a9fc6 scala> import sqlContext.implicits._ import sqlContext.implicits._ ``` 假设有如下的DataFrame包含id和words两列,一共有两个文档。 ```scala scala> val df = sqlContext.createDataFrame(Seq( | (0, Array("a", "b", "c")), | (1, Array("a", "b", "b", "c", "a")) | )).toDF("id", "words") df: org.apache.spark.sql.DataFrame = [id: int, words: array] ``` 创建一个CountVectorizerModel,设定词汇表的最大size为3,设定词汇表中的词至少要在2个文档中出现过。 ```scala scala> val cvModel: CountVectorizerModel = new CountVectorizer(). | setInputCol("words"). | setOutputCol("features"). | setVocabSize(3). | setMinDF(2). | fit(df) cvModel: org.apache.spark.ml.feature.CountVectorizerModel = cntVec_237a080886a2 scala> cvModel.transform(df).select("features").foreach { println } [(3,[0,1,2],[1.0,1.0,1.0])] [(3,[0,1,2],[2.0,2.0,1.0])] ``` 从打印结果我们可以看到,词汇表中有“a”,“b”,“c”三个词,且这三个词都在2个文档中出现过。其中结果中前面的3代表的是vocabsize;“a”和“b”都出现了3次,而“c”出现两次,所以在结果中0和1代表“a”和“b”,2代表“c”;后面的数组是相应词语在各个文档中出现次数的统计。倘若把vocabsize设为2,则不会出现“c”。 也可以用下面的方式来创建一个CountVectorizerModel,通过指定一个数组来预定义一个词汇表,在本例中即只包含“a”,“b”,“c”三个词。 ```scala scala> val cvm = new CountVectorizerModel(Array("a", "b", "c")). | setInputCol("words"). | setOutputCol("features") cvm: org.apache.spark.ml.feature.CountVectorizerModel = cntVecModel_c6a17c2befee scala> cvm.transform(df).select("features").foreach { println } [(3,[0,1,2],[1.0,1.0,1.0])] [(3,[0,1,2],[2.0,2.0,1.0])] ```
[技术] Tue Dec 20 2016 15:55:02 GMT+0800 (中国标准时间)

Spark MLlib 教程

最近,我们小组(赖永炫、林志捷、吕铮)在学习spark的机器学习库Mllib,将会陆续的推出一序列博文,请大家关注。 本博文还同步发表在 厦门大学数据库实验室的[Spark教程](http://dblab.xmu.edu.cn/blog/spark/)。 返回 [Spark MLlib入门教程](http://mocom.xmu.edu.cn/article/show/5858ab782b2730e00d70fa08/0/1) ## Spark MLlib 入门 ### [第12.1节 Spark MLlib简介](http://mocom.xmu.edu.cn/article/show/584816afe083c990247075a2/0/1) ### 第12.2节 spark.mllib库 [第12.2.1节 MLlib基本数据类型(1)](http://mocom.xmu.edu.cn/article/show/58481eb2e083c990247075a5) [第12.2.2节 MLlib基本数据类型(2)](http://mocom.xmu.edu.cn/article/show/58482057e083c990247075a7) [第12.2.3节 基本的统计工具(1)](http://mocom.xmu.edu.cn/article/show/58482e8be083c990247075a8) [第12.2.4节 基本的统计工具(2)](http://mocom.xmu.edu.cn/article/show/584d1fc5bd8177b41ebbd8bc) 第12.2.5节 降维操作 [第12.2.5.1节 奇异值分解(SVD)](http://mocom.xmu.edu.cn/article/show/586279a4aa2c3f280956e7ad) [第12.2.5.2节 主成分分析(PCA)](http://mocom.xmu.edu.cn/article/show/58627a2faa2c3f280956e7ae) [第12.2.6节 分类与回归](http://mocom.xmu.edu.cn/article/show/584d1cf7bd8177b41ebbd8bb) [第12.2.6.1节 逻辑斯蒂回归的分类器](http://mocom.xmu.edu.cn/article/show/58578f482b2730e00d70f9fc) [第12.2.6.2节 决策树分类器](http://mocom.xmu.edu.cn/article/show/585808932b2730e00d70fa02) [第12.2.6.3节 支持向量机SVM分类器](http://mocom.xmu.edu.cn/article/show/585808932b2730e00d70fa02) 第12.2.7节 推荐算法 [第12.2.7.1节 协同过滤算法](http://mocom.xmu.edu.cn/article/show/586cac76aa2c3f280956e7b2) 第12.2.8节 聚类算法 [第12.2.8.1节 KMeans聚类算法](http://mocom.xmu.edu.cn/article/show/586df21caa2c3f280956e7b3) ### 第12.3节 spark.ml库 ## 第12.3.1节 工作流 [第12.3.1.1节 机器学习工作流](http://mocom.xmu.edu.cn/article/show/5858a6062b2730e00d70fa06) [第12.3.1.2节 构建一个机器学习工作流](http://mocom.xmu.edu.cn/article/show/5858aa292b2730e00d70fa07) 12.3.2 特征抽取、转化和选择 [13.3.2.1 特征抽取: TF-IDF -- spark.ml](http://mocom.xmu.edu.cn/article/show/58588d4e2b2730e00d70fa03) [13.3.2.2 特征抽取:CountVectorizer -- spark.ml](http://mocom.xmu.edu.cn/article/show/587f1974aa2c3f280956e7bb) [13.3.2.3 特征抽取:Word2Vec--spark.ml](http://mocom.xmu.edu.cn/article/show/587f103faa2c3f280956e7b6) [12.3.2.4 标签和索引的转化:StringIndexer- IndexToString-VectorIndexer](http://mocom.xmu.edu.cn/article/show/587f11deaa2c3f280956e7ba) 第12.3.3节 分类与回归 [12.3.3.1 逻辑斯蒂回归的分类器](http://mocom.xmu.edu.cn/article/show/586679ecaa2c3f280956e7af) [12.3.3.2 决策树分类器](http://mocom.xmu.edu.cn/article/show/58667ae3aa2c3f280956e7b0)
[技术] Tue Dec 20 2016 11:54:32 GMT+0800 (中国标准时间)

构建一个机器学习工作流 -- spark.ml

返回 [Spark MLlib入门教程](http://mocom.xmu.edu.cn/article/show/5858ab782b2730e00d70fa08/0/1) --- ## 工作流(ML Pipelines)例子 本节以逻辑斯蒂回归为例,构建一个典型的机器学习过程,来具体介绍一下工作流是如何应用的。我们的目的是查找出所有包含"spark"的句子,即将包含"spark"的句子的标签设为1,没有"spark"的句子的标签设为0。 首先,导入工作流和逻辑斯蒂回归所需要的包。 ```scala import org.apache.spark.SparkConf import org.apache.spark.SparkContext import org.apache.spark.sql.SQLContext import org.apache.spark.ml.{Pipeline, PipelineModel} import org.apache.spark.ml.classification.LogisticRegression import org.apache.spark.ml.feature.{HashingTF, Tokenizer} import org.apache.spark.mllib.linalg.Vector import org.apache.spark.sql.Row ``` 接下来,根据SparkContext来创建一个SQLContext,其中sc是一个已经存在的SparkContext;然后导入sqlContext.implicits._来实现RDD到Dataframe的隐式转换。 ```scala scala> val sqlContext = new SQLContext(sc) sqlContext: org.apache.spark.sql.SQLContext = org.apache.spark.sql.SQLContext@129bfa97 scala> import sqlContext.implicits._ import sqlContext.implicits._ ``` 然后,我们构建训练数据集。 ```scala scala> val training = sqlContext.createDataFrame(Seq( | (0L, "a b c d e spark", 1.0), | (1L, "b d", 0.0), | (2L, "spark f g h", 1.0), | (3L, "hadoop mapreduce", 0.0) | )).toDF("id", "text", "label") training: org.apache.spark.sql.DataFrame = [id: bigint, text: string, label: double] ``` 在这一步中我们要定义 Pipeline 中的各个工作流阶段PipelineStage,包括转换器和评估器,具体的,包含tokenizer, hashingTF和lr三个步骤。 ```scala scala> val tokenizer = new Tokenizer(). | setInputCol("text"). | setOutputCol("words") tokenizer: org.apache.spark.ml.feature.Tokenizer = tok_5151ed4fa43e scala> val hashingTF = new HashingTF(). | setNumFeatures(1000). | setInputCol(tokenizer.getOutputCol). | setOutputCol("features") hashingTF: org.apache.spark.ml.feature.HashingTF = hashingTF_332f74b21ecb scala> val lr = new LogisticRegression(). | setMaxIter(10). | setRegParam(0.01) lr: org.apache.spark.ml.classification.LogisticRegression = logreg_28a670ae952f ``` 有了这些处理特定问题的转换器和评估器,接下来就可以按照具体的处理逻辑有序的组织PipelineStages 并创建一个Pipeline。 ```scala scala> val pipeline = new Pipeline(). | setStages(Array(tokenizer, hashingTF, lr)) pipeline: org.apache.spark.ml.Pipeline = pipeline_4dabd24db001 ``` 现在构建的Pipeline本质上是一个Estimator,在它的fit()方法运行之后,它将产生一个PipelineModel,它是一个Transformer。 ```scala scala> val model = pipeline.fit(training) model: org.apache.spark.ml.PipelineModel = pipeline_4dabd24db001 ``` 我们可以看到,model的类型是一个PipelineModel,这个管道模型将在测试数据的时候使用。所以接下来,我们先构建测试数据。 ```scala scala> val test = sqlContext.createDataFrame(Seq( | (4L, "spark i j k"), | (5L, "l m n"), | (6L, "spark a"), | (7L, "apache hadoop") | )).toDF("id", "text") test: org.apache.spark.sql.DataFrame = [id: bigint, text: string] ``` 然后,我们调用我们训练好的PipelineModel的transform()方法,让测试数据按顺序通过拟合的工作流,生成我们所需要的预测结果。 ```scala scala> model.transform(test). | select("id", "text", "probability", "prediction"). | collect(). | foreach { case Row(id: Long, text: String, prob: Vector, prediction: Double) => | println(s"($id, $text) --> prob=$prob, prediction=$prediction") | } (4, spark i j k) --> prob=[0.5406433544851421,0.45935664551485783], prediction=0.0 (5, l m n) --> prob=[0.9334382627383259,0.06656173726167405], prediction=0.0 (6, spark a) --> prob=[0.15041430048068286,0.8495856995193171], prediction=1.0 (7, apache hadoop) --> prob=[0.9768636139518304,0.023136386048169585], prediction=0.0 ``` 通过上述结果,我们可以看到,第4句和第6句中都包含"spark",其中第六句的预测是1,与我们希望的相符;而第4句虽然预测的依然是0,但是通过概率我们可以看到,第4句有46%的概率预测是1,而第5句、第7句分别只有7%和2%的概率预测为1,这是由于训练数据集较少,如果有更多的测试数据进行学习,预测的准确率将会有显著提升。
[技术] Tue Dec 20 2016 11:48:57 GMT+0800 (中国标准时间)

机器学习工作流(ML Pipelines)—— spark.ml包

返回 [Spark MLlib入门教程](http://mocom.xmu.edu.cn/article/show/5858ab782b2730e00d70fa08/0/1) --- 一个典型的机器学习过程从数据收集开始,要经历多个步骤,才能得到需要的输出。这非常类似于流水线式工作,即通常会包含源数据ETL(抽取、转化、加载),数据预处理,指标提取,模型训练与交叉验证,新数据预测等步骤。 在介绍工作流之前,我们先来了解几个重要概念: - DataFrame:使用Spark SQL中的DataFrame作为数据集,它可以容纳各种数据类型。 较之 RDD,包含了 schema 信息,更类似传统数据库中的二维表格。它被 ML Pipeline 用来存储源数据。例如,DataFrame中的列可以是存储的文本,特征向量,真实标签和预测的标签等。 - Transformer:翻译成转换器,是一种可以将一个DataFrame转换为另一个DataFrame的算法。比如一个模型就是一个 Transformer。它可以把 一个不包含预测标签的测试数据集 DataFrame 打上标签,转化成另一个包含预测标签的 DataFrame。技术上,Transformer实现了一个方法transform(),它通过附加一个或多个列将一个DataFrame转换为另一个DataFrame。 - Estimator:翻译成评估器,它是学习算法或在训练数据上的训练方法的概念抽象。在 Pipeline 里通常是被用来操作 DataFrame 数据并生产一个 Transformer。从技术上讲,Estimator实现了一个方法fit(),它接受一个DataFrame并产生一个转换器。如一个随机森林算法就是一个 Estimator,它可以调用fit(),通过训练特征数据而得到一个随机森林模型。 - Parameter:Parameter 被用来设置 Transformer 或者 Estimator 的参数。现在,所有转换器和估计器可共享用于指定参数的公共API。ParamMap是一组(参数,值)对。 - PipeLine:翻译为工作流或者管道。工作流将多个工作流阶段(转换器和估计器)连接在一起,形成机器学习的工作流,并获得结果输出。 工作流如何工作 要构建一个 Pipeline工作流,首先需要定义 Pipeline 中的各个工作流阶段PipelineStage,(包括转换器和评估器),比如指标提取和转换模型训练等。有了这些处理特定问题的转换器和 评估器,就可以按照具体的处理逻辑有序的组织PipelineStages 并创建一个Pipeline。比如: ```scala val pipeline = new Pipeline().setStages(Array(stage1,stage2,stage3,…)) ``` 然后就可以把训练数据集作为输入参数,调用 Pipeline 实例的 fit 方法来开始以流的方式来处理源训练数据。这个调用会返回一个 PipelineModel 类实例,进而被用来预测测试数据的标签。更具体的说,工作流的各个阶段按顺序运行,输入的DataFrame在它通过每个阶段时被转换。 对于Transformer阶段,在DataFrame上调用transform()方法。 对于估计器阶段,调用fit()方法来生成一个转换器(它成为PipelineModel的一部分或拟合的Pipeline),并且在DataFrame上调用该转换器的transform()方法。 ![ml-pipeline](http://mocom.xmu.edu.cn/blog/5858a5e22b2730e00d70fa05.png "") 上面,顶行表示具有三个阶段的流水线。 前两个(Tokenizer和HashingTF)是Transformers(蓝色),第三个(LogisticRegression)是Estimator(红色)。 底行表示流经管线的数据,其中圆柱表示DataFrames。 在原始DataFrame上调用Pipeline.fit()方法,它具有原始文本文档和标签。 Tokenizer.transform()方法将原始文本文档拆分为单词,向DataFrame添加一个带有单词的新列。 HashingTF.transform()方法将字列转换为特征向量,向这些向量添加一个新列到DataFrame。 现在,由于LogisticRegression是一个Estimator,Pipeline首先调用LogisticRegression.fit()产生一个LogisticRegressionModel。 如果流水线有更多的阶段,则在将DataFrame传递到下一个阶段之前,将在DataFrame上调用LogisticRegressionModel的transform()方法。 值得注意的是,工作流本身也可以看做是一个估计器。在工作流的fit()方法运行之后,它产生一个PipelineModel,它是一个Transformer。 这个管道模型将在测试数据的时候使用。 下图说明了这种用法。 ![ml-pipelinemodel](http://mocom.xmu.edu.cn/blog/5858a5db2b2730e00d70fa04.png "") 在上图中,PipelineModel具有与原始流水线相同的级数,但是原始流水线中的所有估计器都变为变换器。 当在测试数据集上调用PipelineModel的transform()方法时,数据按顺序通过拟合的工作流。 每个阶段的transform()方法更新数据集并将其传递到下一个阶段。工作流和工作流模型有助于确保培训和测试数据通过相同的特征处理步骤。
[技术] Tue Dec 20 2016 11:31:18 GMT+0800 (中国标准时间)

特征抽取: TF-IDF -- spark.ml

返回 [Spark MLlib入门教程](http://mocom.xmu.edu.cn/article/show/5858ab782b2730e00d70fa08/0/1) 这一部分我们主要介绍和特征处理相关的算法,大体分为以下三类: - 特征抽取:从原始数据中抽取特征 - 特征转换:特征的维度、特征的转化、特征的修改 - 特征选取:从大规模特征集中选取一个子集 ## 特征抽取 Feature Extractors ### TF-IDF (HashingTF and IDF) ​ “词频-逆向文件频率”(TF-IDF)是一种在文本挖掘中广泛使用的特征向量化方法,它可以体现一个文档中词语在语料库中的重要程度。 ​ 词语由t表示,文档由d表示,语料库由D表示。词频TF(t,d)是词语t在文档d中出现的次数。文件频率DF(t,D)是包含词语的文档的个数。如果我们只使用词频来衡量重要性,很容易过度强调在文档中经常出现,却没有太多实际信息的词语,比如“a”,“the”以及“of”。如果一个词语经常出现在语料库中,意味着它并不能很好的对文档进行区分。TF-IDF就是在数值化文档信息,衡量词语能提供多少信息以区分文档。其定义如下: $$ IDF(t,D) = log \frac{\left| D \right| + 1}{DF(t,D) + 1} $$ ​ 此处$\left| D \right|$ 是语料库中总的文档数。公式中使用log函数,当词出现在所有文档中时,它的IDF值变为0。加1是为了避免分母为0的情况。TF-IDF 度量值表示如下: $$ TFIDF(t,d,D) = TF(t,d) \cdot IDF(t,D) $$ ​ 在Spark ML库中,TF-IDF被分成两部分:TF (+hashing) 和 IDF。 **TF**: HashingTF 是一个Transformer,在文本处理中,接收词条的集合然后把这些集合转化成固定长度的特征向量。这个算法在哈希的同时会统计各个词条的词频。 **IDF**: IDF是一个Estimator,在一个数据集上应用它的fit()方法,产生一个IDFModel。 该IDFModel 接收特征向量(由HashingTF产生),然后计算每一个词在文档中出现的频次。IDF会减少那些在语料库中出现频率较高的词的权重。 ​ Spark.mllib 中实现词频率统计使用特征hash的方式,原始特征通过hash函数,映射到一个索引值。后面只需要统计这些索引值的频率,就可以知道对应词的频率。这种方式避免设计一个全局1对1的词到索引的映射,这个映射在映射大量语料库时需要花费更长的时间。但需要注意,通过hash的方式可能会映射到同一个值的情况,即不同的原始特征通过Hash映射后是同一个值。为了降低这种情况出现的概率,我们只能对特征向量升维。i.e., 提高hash表的桶数,默认特征维度是 2^20 = 1,048,576. ​ 在下面的代码段中,我们以一组句子开始。首先使用分解器Tokenizer把句子划分为单个词语。对每一个句子(词袋),我们使用HashingTF将句子转换为特征向量,最后使用IDF重新调整特征向量。这种转换通常可以提高使用文本特征的性能。 ​ 首先,导入TFIDF所需要的包: ```scala import org.apache.spark.SparkConf import org.apache.spark.SparkContext import org.apache.spark.sql.SQLContext import org.apache.spark.ml.feature.{HashingTF, IDF, Tokenizer} ``` ​ 接下来,根据SparkContext来创建一个SQLContext,其中sc是一个已经存在的SparkContext;然后导入sqlContext.implicits._来实现RDD到Dataframe的隐式转换。 ```scala scala> val sqlContext = new SQLContext(sc) sqlContext: org.apache.spark.sql.SQLContext = org.apache.spark.sql.SQLContext@225a9fc6 scala> import sqlContext.implicits._ import sqlContext.implicits._ ``` ​ 第三步,创建一个集合,每一个句子代表一个文件。 ```scala scala> val sentenceData = sqlContext.createDataFrame(Seq( | (0, "I heard about Spark and I love Spark"), | (0, "I wish Java could use case classes"), | (1, "Logistic regression models are neat") | )).toDF("label", "sentence") sentenceData: org.apache.spark.sql.DataFrame = [label: int, sentence: string] ``` 第四步,用tokenizer把每个句子分解成单词 ```scala scala> val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words") tokenizer: org.apache.spark.ml.feature.Tokenizer = tok_494411a37f99 scala> val wordsData = tokenizer.transform(sentenceData) wordsData: org.apache.spark.sql.DataFrame = [label: int, sentence: string, words: array] scala> wordsData.foreach {println} [1,Logistic regression models are neat,WrappedArray(logistic, regression, models, are, neat)] [0,I wish Java could use case classes,WrappedArray(i, wish, java, could, use, case, classes)] [0,I heard about Spark and I love Spark,WrappedArray(i, heard, about, spark, and, i, love, spark)] ``` ​ 从打印结果可以看到,tokenizer的transform()方法把每个句子拆分成了一个个单词。 第五步,用HashingTF的transform()方法把句子哈希成特征向量。我们这里设置哈希表的桶数为2000。 ```scala scala> val hashingTF = new HashingTF(). | setInputCol("words").setOutputCol("rawFeatures").setNumFeatures(2000) hashingTF: org.apache.spark.ml.feature.HashingTF = hashingTF_2591ec73cea0 scala> val featurizedData = hashingTF.transform(wordsData) featurizedData: org.apache.spark.sql.DataFrame = [label: int, sentence: string, words: array, rawFeatures: vector] scala> featurizedData.foreach {println} [1,Logistic regression models are neat,WrappedArray(logistic, regression, models, are, neat),(2000,[65,618,852,992,1194],[1.0,1.0,1.0,1.0,1.0])] [0,I wish Java could use case classes,WrappedArray(i, wish, java, could, use, case, classes),(2000,[103,105,192,774,818,1265,1703],[1.0,1.0,1.0,1.0,1.0,1.0,1.0])] [0,I heard about Spark and I love Spark,WrappedArray(i, heard, about, spark, and, i, love, spark),(2000,[105,365,727,1469,1858,1926],[2.0,2.0,1.0,1.0,1.0,1.0])] ``` ​ 我们可以看到每一个单词被哈希成了一个不同的索引值。以"I heard about Spark and I love Spark"为例,输出结果中2000代表哈希表的桶数,“[105,365,727,1469,1858,1926]”分别代表着“i, spark, heard, about, and, love”的哈希值,“[2.0,2.0,1.0,1.0,1.0,1.0]”为对应单词的出现次数。 ​ 第六步,调用IDF方法来重新构造特征向量的规模,生成的idf是一个Estimator,在特征向量上应用它的fit()方法,会产生一个IDFModel。 ```scala scala> val idf = new IDF().setInputCol("rawFeatures").setOutputCol("features") idf: org.apache.spark.ml.feature.IDF = idf_7fcc9063de6f scala> val idfModel = idf.fit(featurizedData) idfModel: org.apache.spark.ml.feature.IDFModel = idf_7fcc9063de6f ``` 同时,调用IDFModel的transform方法,可以得到每一个单词对应的TF-IDF 度量值。 ```scala scala> val rescaledData = idfModel.transform(featurizedData) rescaledData: org.apache.spark.sql.DataFrame = [label: int, sentence: string, words: array, rawFeatures: vector, features: vector] scala> rescaledData.select("features", "label").take(3).foreach(println) [(2000,[105,365,1329,1469,1926],[0.28768207245178085,0.6931471805599453,0.693147 1805599453,0.6931471805599453,0.6931471805599453]),0] [(2000,[103,105,192,774,818,1265,1703],[0.6931471805599453,0.28768207245178085,0 .6931471805599453,0.6931471805599453,0.6931471805599453,0.6931471805599453,0.693 1471805599453]),0] [(2000,[65,618,852,992,1194],[0.6931471805599453,0.6931471805599453,0.6931471805 599453,0.6931471805599453,0.6931471805599453]),1] ``` “[105,365,727,1469,1858,1926]”分别代表着“i, spark, heard, about, and, love”的哈希值。105和365分别代表了“i"和"spark",其TF-IDF值分别是0.2876820724517808和0.6931471805599453。这两个单词都在第一句中出现了两次,而"i"在第二句中还多出现了一次,从而导致"i"的TF-IDF 度量值较低。相对而言,“spark”可以对文档进行更好的区分。 通过TF-IDF得到的特征向量,在接下来可以被应用到相关的机器学习方法中。
[技术] Tue Dec 20 2016 09:45:50 GMT+0800 (中国标准时间)

决策树分类器--spark.mllib

返回 Spark MLlib入门教程


一、方法简介

​ 决策树(decision tree)是一种基本的分类与回归方法,这里主要介绍用于分类的决策树。决策树模式呈树形结构,其中每个内部节点表示一个属性上的测试,每个分支代表一个测试输出,每个叶节点代表一种类别。学习时利用训练数据,根据损失函数最小化的原则建立决策树模型;预测时,对新的数据,利用决策树模型进行分类。

二、基本原理

​ 决策树学习通常包括3个步骤:特征选择、决策树的生成和决策树的剪枝。

(一)特征选择

​ 特征选择在于选取对训练数据具有分类能力的特征,这样可以提高决策树学习的效率。通常特征选择的准则是信息增益(或信息增益比、基尼指数等),每次计算每个特征的信息增益,并比较它们的大小,选择信息增益最大(信息增益比最大、基尼指数最小)的特征。下面我们重点介绍一下特征选择的准则:信息增益。

​ 首先定义信息论中广泛使用的一个度量标准——熵(entropy),它是表示随机变量不确定性的度量。熵越大,随机变量的不确定性就越大。而信息增益(informational entropy)表示得知某一特征后使得信息的不确定性减少的程度。简单的说,一个属性的信息增益就是由于使用这个属性分割样例而导致的期望熵降低。信息增益、信息增益比和基尼指数的具体定义如下:

信息增益:特征A对训练数据集D的信息增益$g(D,A)$,定义为集合D的经验熵$H(D)$与特征A给定条件下D的经验条件熵$H(D|A)$之差,即 $$ g(D,A)=H(D)-H(D|A) $$ ​
信息增益比:特征A对训练数据集D的信息增益比$g_R(D,A)$定义为其信息增益$g(D,A)$与训练数据集D关于特征A的值的熵$H_A(D)$之比,即 $$ g_R(D,A)=\frac{g(D,A)}{H_A(D)}$$

其中,$HA(D) = - \sum_{i=1}^{n} \frac{\left|D_i\right|}{\left|D\right|}log_2\frac{\left|D_i\right|} {\left|D\right|}$,n是特征A取值的个数。

基尼指数:分类问题中,假设有K个类,样本点属于第K类的概率为$p_k$, 则概率分布的基尼指数定义为:

$$Gini(p) = \sum_{k=1}^{K} p_k(1 - p_k) = 1- \sum_{k=1}^{K} p_k^2$$

(二)决策树的生成

​ 从根结点开始,对结点计算所有可能的特征的信息增益,选择信息增益最大的特征作为结点的特征,由该特征的不同取值建立子结点,再对子结点递归地调用以上方法,构建决策树;直到所有特征的信息增均很小或没有特征可以选择为止,最后得到一个决策树。

​ 决策树需要有停止条件来终止其生长的过程。一般来说最低的条件是:当该节点下面的所有记录都属于同一类,或者当所有的记录属性都具有相同的值时。这两种条件是停止决策树的必要条件,也是最低的条件。在实际运用中一般希望决策树提前停止生长,限定叶节点包含的最低数据量,以防止由于过度生长造成的过拟合问题。

(三)决策树的剪枝

​ 决策树生成算法递归地产生决策树,直到不能继续下去为止。这样产生的树往往对训练数据的分类很准确,但对未知的测试数据的分类却没有那么准确,即出现过拟合现象。解决这个问题的办法是考虑决策树的复杂度,对已生成的决策树进行简化,这个过程称为剪枝。

​ 决策树的剪枝往往通过极小化决策树整体的损失函数来实现。一般来说,损失函数可以进行如下的定义: $$ C_a(T)=C(T)+a\left|T\right| $$ ​ 其中,T为任意子树,$C(T)​$ 为对训练数据的预测误差(如基尼指数),$\left|T\right|​$ 为子树的叶结点个数,$a\ge0​$为参数,$C_a(T)​$ 为参数是$a​$时的子树T的整体损失,参数$a​$权衡训练数据的拟合程度与模型的复杂度。对于固定的$a​$,一定存在使损失函数$C_a(T)​$ 最小的子树,将其表示为$T_a​$ 。当$a​$大的时候,最优子树$T_a​$偏小;当$a​$小的时候,最优子树$T_a​$偏大。

三、示例代码

​ 我们以iris数据集(https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data)为例进行分析。iris以鸢尾花的特征作为数据来源,数据集包含150个数据集,分为3类,每类50个数据,每个数据包含4个属性,是在数据挖掘、数据分类中非常常用的测试集、训练集。

1. 导入需要的包:

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
import org.apache.spark.mllib.tree.DecisionTree
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.linalg.{Vectors,Vector}

2. 读取数据:

​ 首先,读取文本文件;然后,通过map将每行的数据用“,”隔开,在我们的数据集中,每行被分成了5部分,前4部分是鸢尾花的4个特征,最后一部分是鸢尾花的分类。把这里我们用LabeledPoint来存储标签列和特征列。LabeledPoint在监督学习中常用来存储标签和特征,其中要求标签的类型是double,特征的类型是Vector。所以,我们把莺尾花的分类进行了一下改变,"Iris-setosa"对应分类0,"Iris-versicolor"对应分类1,其余对应分类2;然后获取莺尾花的4个特征,存储在Vector中。

scala> val data = sc.textFile("G:/spark/iris.data")
data: org.apache.spark.rdd.RDD[String] = G:/spark/iris.data MapPartitionsRDD[1]
at textFile at :26

scala> val parsedData = data.map { line =>
     |     val parts = line.split(',')
     |     LabeledPoint(if(parts(4)=="Iris-setosa") 0.toDouble else if (parts(4)
=="Iris-versicolor") 1.toDouble else
     |       2.toDouble, Vectors.dense(parts(0).toDouble,parts(1).toDouble,parts
(2).toDouble,parts(3).toDouble))
     |     }
parsedData: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPo
int] = MapPartitionsRDD[2] at map at :28

​ 然后,我们把数据打印看一下:

scala> parsedData.foreach { x => println(x) }
(0.0,[5.1,3.5,1.4,0.2])
(1.0,[6.0,2.9,4.5,1.5])
(0.0,[4.9,3.0,1.4,0.2])
(1.0,[5.7,2.6,3.5,1.0])
(0.0,[4.7,3.2,1.3,0.2])
... ...

3. 构建模型

​ 接下来,首先进行数据集的划分,这里划分70%的训练集和30%的测试集:

scala> val splits = parsedData.randomSplit(Array(0.7, 0.3))
splits: Array[org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.Labeled
Point]] = Array(MapPartitionsRDD[3] at randomSplit at :30, MapPartition
sRDD[4] at randomSplit at :30)

scala> val (trainingData, testData) = (splits(0), splits(1))
trainingData: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.Labeled
Point] = MapPartitionsRDD[3] at randomSplit at :30
testData: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoin
t] = MapPartitionsRDD[4] at randomSplit at :30

​ 然后,调用决策树的trainClassifier方法构建决策树模型,设置参数,比如分类数、信息增益的选择、树的最大深度等:

scala> val numClasses = 3
numClasses: Int = 3

scala> val categoricalFeaturesInfo = Map[Int, Int]()
categoricalFeaturesInfo: scala.collection.immutable.Map[Int,Int] = Map()

scala> val impurity = "gini"
impurity: String = gini

scala> val maxDepth = 5
maxDepth: Int = 5

scala> val maxBins = 32
maxBins: Int = 32

scala> val model = DecisionTree.trainClassifier(trainingData, numClasses, categoricalFeaturesInfo, impurity, maxDepth, maxBins)
model: org.apache.spark.mllib.tree.model.DecisionTreeModel = DecisionTreeModel c
lassifier of depth 5 with 15 nodes

​ 接下来我们调用决策树模型的predict方法对测试数据集进行预测,并把模型结构打印出来:

scala> val labelAndPreds = testData.map { point =>
     |       val prediction = model.predict(point.features)
     |       (point.label, prediction)
     |     }
labelAndPreds: org.apache.spark.rdd.RDD[(Double, Double)] = MapPartitionsRDD[28]
 at map at :40

scala> println("Learned classification tree model:\n" + model.toDebugString)
Learned classification tree model:
DecisionTreeModel classifier of depth 5 with 15 nodes
  If (feature 2 <= 1.9)
   Predict: 0.0
  Else (feature 2 > 1.9)
   If (feature 2 <= 4.8)
    If (feature 3 <= 1.6)
     Predict: 1.0
    Else (feature 3 > 1.6)
     If (feature 1 <= 2.8)
      Predict: 2.0
     Else (feature 1 > 2.8)
      Predict: 1.0
   Else (feature 2 > 4.8)
    If (feature 3 <= 1.7)
     If (feature 2 <= 5.1)
      If (feature 3 <= 1.5)
       Predict: 2.0
      Else (feature 3 > 1.5)
       Predict: 1.0
     Else (feature 2 > 5.1)
      Predict: 2.0
    Else (feature 3 > 1.7)
     Predict: 2.0

4. 模型评估

​ 最后,我们把模型预测的准确性打印出来:

scala> val testErr = labelAndPreds.filter(r => r._1 != r._2).count().toDouble / testData.count()
testErr: Double = 0.05128205128205128

scala> println("Precision = " + (1-testErr))
Precision = 0.9487179487179487
[技术] Tue Dec 20 2016 00:19:31 GMT+0800 (中国标准时间)

逻辑斯蒂回归分类器--spark.mllib

返回 [Spark MLlib入门教程](http://mocom.xmu.edu.cn/article/show/5858ab782b2730e00d70fa08/0/1) --- ## 方法简介 ​ 逻辑斯蒂回归(logistic regression)是统计学习中的经典分类方法,属于对数线性模型。logistic回归的因变量可以是二分类的,也可以是多分类的。 ## 基本原理 ### logistic分布 ​ 设X是连续随机变量,X服从logistic分布是指X具有下列分布函数和密度函数: ​ $$F(x)=P(x \le x)=\frac 1 {1+e^{-(x-\mu)/\gamma}}$$ ​ $$f(x)=F^{'}(x)=\frac {e^{-(x-\mu)/\gamma}} {\gamma(1+e^{-(x-\mu)/\gamma})^2}$$ ​ 其中,$\mu$为位置参数,$\gamma$为形状参数。 ​ $f(x)$与$F(x)$图像如下,其中分布函数是以$(\mu, \frac 1 2)$为中心对阵,$\gamma$越小曲线变化越快。 ##### ![](http://mocom.xmu.edu.cn/blog/58578f242b2730e00d70f9fb.jpg) ### 二项logistic回归模型: ​ 二项logistic回归模型如下: ​ $$P(Y=1|x)=\frac {exp(w \cdot x + b)} {1 + exp(w \cdot x + b)}$$ ​ $$P(Y=0|x)=\frac {1} {1 + exp(w \cdot x + b)}$$ ​ 其中,$x \in R^n$是输入,$Y \in {0,1}$是输出,w称为权值向量,b称为偏置,$w \cdot x$为w和x的内积。 ### 参数估计 ​ 假设: ​ $$P(Y=1|x)=\pi (x), \quad P(Y=0|x)=1-\pi (x)$$ ​ 则采用“极大似然法”来估计w和b。似然函数为: ​ $$\prod_{i=1}^N [\pi (x_i)]^{y_i} [1 - \pi(x_i)]^{1-y_i}$$ ​ 为方便求解,对其“对数似然”进行估计: ​ $$L(w) = \sum_{i=1}^N [y_i \log{\pi(x_i)} + (1-y_i) \log{(1 - \pi(x_i)})]$$ ​ 从而对$L(w)$求极大值,得到$w$的估计值。求极值的方法可以是梯度下降法,梯度上升法等。 ### 示例代码 ​ 我们以iris数据集([https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data](https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data))为例进行分析。iris以鸢尾花的特征作为数据来源,数据集包含150个数据集,分为3类,每类50个数据,每个数据包含4个属性,是在数据挖掘、数据分类中非常常用的测试集、训练集。 #### 1. 导入需要的包: ```scala import org.apache.spark.SparkConf import org.apache.spark.SparkContext import org.apache.spark.mllib.classification.{LogisticRegressionWithLBFGS, LogisticRegressionModel} import org.apache.spark.mllib.evaluation.MulticlassMetrics import org.apache.spark.mllib.regression.LabeledPoint import org.apache.spark.mllib.linalg.{Vectors,Vector} ``` #### 2. 读取数据: ​ 首先,读取文本文件;然后,通过map将每行的数据用“,”隔开,在我们的数据集中,每行被分成了5部分,前4部分是鸢尾花的4个特征,最后一部分是鸢尾花的分类。把这里我们用LabeledPoint来存储标签列和特征列。 LabeledPoint在监督学习中常用来存储标签和特征,其中要求标签的类型是double,特征的类型是Vector。这里,先把莺尾花的分类进行变换,"Iris-setosa"对应分类0,"Iris-versicolor"对应分类1,其余对应分类2;然后获取莺尾花的4个特征,存储在Vector中。 ```scala scala> val data = sc.textFile("G:/spark/iris.data") data: org.apache.spark.rdd.RDD[String] = G:/spark/iris.data MapPartitionsRDD[1] at textFile at :28 scala> val parsedData = data.map { line => | val parts = line.split(',') | LabeledPoint(if(parts(4)=="Iris-setosa") 0.toDouble else if (parts(4) =="Iris-versicolor") 1.toDouble else | 2.toDouble, Vectors.dense(parts(0).toDouble,parts(1).toDouble,parts (2).toDouble,parts(3).toDouble)) | } parsedData: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPo int] = MapPartitionsRDD[2] at map at :30 ``` ​ 然后,把数据打印看一下: ```scala scala> parsedData.foreach { x => println(x) } (1.0,[6.0,2.9,4.5,1.5]) (0.0,[5.1,3.5,1.4,0.2]) (1.0,[5.7,2.6,3.5,1.0]) (0.0,[4.9,3.0,1.4,0.2]) (1.0,[5.5,2.4,3.8,1.1]) (0.0,[4.7,3.2,1.3,0.2]) (1.0,[5.5,2.4,3.7,1.0]) (0.0,[4.6,3.1,1.5,0.2]) ... ... ``` #### 3. 构建模型 ​ 接下来,首先进行数据集的划分,这里划分60%的训练集和40%的测试集: ```scala scala> val splits = parsedData.randomSplit(Array(0.6, 0.4), seed = 11L) splits: Array[org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.Labeled Point]] = Array(MapPartitionsRDD[3] at randomSplit at :32, MapPartition sRDD[4] at randomSplit at :32) scala> val training = splits(0).cache() training: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoin t] = MapPartitionsRDD[3] at randomSplit at :32 scala> val test = splits(1) test: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint] = MapPartitionsRDD[4] at randomSplit at :32 ``` ​ 然后,构建逻辑斯蒂模型,用set的方法设置参数,比如说分类的数目,这里可以实现多分类逻辑斯蒂模型: ```scala scala> val model = new LogisticRegressionWithLBFGS(). | setNumClasses(3). | run(training) model: org.apache.spark.mllib.classification.LogisticRegressionModel = org.apach e.spark.mllib.classification.LogisticRegressionModel: intercept = 0.0, numFeatur es = 8, numClasses = 3, threshold = 0.5 ``` ​ 接下来,调用多分类逻辑斯蒂模型用的predict方法对测试数据进行预测,并把结果保存在MulticlassMetrics中。这里的模型全名为LogisticRegressionWithLBFGS,加上了LBFGS,表示**Limited-memory BFGS**。其中,BFGS是求解非线性优化问题(L(w)​求极大值)的方法,是一种秩-2更新,以其发明者Broyden, Fletcher, Goldfarb和Shanno的姓氏首字母命名。 ```scala scala> val predictionAndLabels = test.map { case LabeledPoint(label, features) => | val prediction = model.predict(features) | (prediction, label) | } predictionAndLabels: org.apache.spark.rdd.RDD[(Double, Double)] = MapPartitionsR DD[56] at map at :40 ``` 这里,采用了test部分的数据每一行都分为标签label和特征features,然后利用map方法,对每一行的数据进行model.predict(features)操作,获得预测值。并把预测值和真正的标签放到predictionAndLabels中。我们可以打印出具体的结果数据来看一下: ```scala (0.0,0.0) (1.0,1.0) (0.0,0.0) (2.0,1.0) (0.0,0.0) (2.0,1.0) (0.0,0.0) (1.0,1.0) (0.0,0.0) ... ... ``` 可以看出,大部分的预测是对的。但第4行的预测与实际标签不同。 #### 4. 模型评估 ​ 最后,我们把模型预测的准确性打印出来: ```scala scala> val metrics = new MulticlassMetrics(predictionAndLabels) metrics: org.apache.spark.mllib.evaluation.MulticlassMetrics = org.apache.spark. mllib.evaluation.MulticlassMetrics@2bd9ef8c scala> val precision = metrics.precision precision: Double = 0.9180327868852459 scala> println("Precision = " + precision) Precision = 0.9180327868852459 ```
[技术] Mon Dec 19 2016 15:42:00 GMT+0800 (中国标准时间)

基本的统计工具(2) - spark.mllib

返回 [Spark MLlib入门教程](http://mocom.xmu.edu.cn/article/show/5858ab782b2730e00d70fa08/0/1) --- ## 五、假设检验 Hypothesis testing ​ Spark目前支持皮尔森卡方检测(Pearson’s chi-squared tests),包括“适配度检定”(Goodness of fit)以及“独立性检定”(independence)。 ​ 首先,我们导入必要的包 ```scala import org.apache.spark.SparkContext import org.apache.spark.mllib.linalg._ import org.apache.spark.mllib.regression.LabeledPoint import org.apache.spark.mllib.stat.Statistics._ ``` ​ 接下来,我们从数据集中选择要分析的数据,比如说我们取出iris数据集中的前两条数据v1和v2。不同的输入类型决定了是做拟合度检验还是独立性检验。拟合度检验要求输入为Vector, 独立性检验要求输入是Matrix。 ```scala scala> val v1: Vector = sc.textFile("G:/spark/iris.data").map(_.split(",")).map(p => Vectors.dense(p(0).toDouble, p(1).toDouble, p(2).toDouble, p(3).toDouble)).first v1: org.apache.spark.mllib.linalg.Vector = [5.1,3.5,1.4,0.2] scala> val v2: Vector = sc.textFile("G:/spark/iris.data").map(_.split(",")).map(p => Vectors.dense(p(0).toDouble, p(1).toDouble, p(2).toDouble, p(3).toDouble)).take(2).last v2: org.apache.spark.mllib.linalg.Vector = [4.9,3.0,1.4,0.2] ``` ### (一) 适合度检验 Goodness fo fit Goodness fo fit(适合度检验):验证一组观察值的次数分配是否异于理论上的分配。其 H0假设(虚无假设,null hypothesis)为一个样本中已发生事件的次数分配会服从某个特定的理论分配。实际执行多项式试验而得到的观察次数,与虚无假设的期望次数相比较,检验二者接近的程度,利用样本数据以检验总体分布是否为某一特定分布的统计方法。 通常情况下这个特定的理论分配指的是均匀分配,目前Spark默认的是均匀分配。以下是代码: ```scala scala> val goodnessOfFitTestResult = Statistics.chiSqTest(v1) goodnessOfFitTestResult: org.apache.spark.mllib.stat.test.ChiSqTestResult = Chi squared test summary: method: pearson degrees of freedom = 3 statistic = 5.588235294117647 pValue = 0.1334553914430291 No presumption against null hypothesis: observed follows the same distribution as expected.. ``` 可以看到P值,自由度,检验统计量,所使用的方法,以及零假设等信息。我们先简单介绍下每个输出的意义: method: 方法。这里采用pearson方法。 statistic: 检验统计量。简单来说就是用来决定是否可以拒绝原假设的证据。检验统计量的值是利用样本数据计算得到的,它代表了样本中的信息。检验统计量的绝对值越大,拒绝原假设的理由越充分,反之,不拒绝原假设的理由越充分。 degrees of freedom:自由度。表示可自由变动的样本观测值的数目, pValue:统计学根据显著性检验方法所得到的P 值。一般以P < 0.05 为显著, P<0.01 为非常显著,其含义是样本间的差异由抽样误差所致的概率小于0.05 或0.01。 一般来说,假设检验主要看P值就够了。在本例中pValue =0.133,说明两组的差别无显著意义。通过V1的观测值[5.1, 3.5, 1.4, 0.2],无法拒绝其服从于期望分配(这里默认是均匀分配)的假设。 ### (二)独立性检验 Indenpendence 卡方独立性检验是用来检验两个属性间是否独立。其中一个属性做为行,另外一个做为列,通过貌似相关的关系考察其是否真实存在相关性。比如天气温变化和肺炎发病率。 首先,我们通过v1、v2构造一个举证Matrix,然后进行独立性检验: ```scala scala> val mat: Matrix =Matrices.dense(2,2,Array(v1(0),v1(1),v2(0),v2(1))) mat: org.apache.spark.mllib.linalg.Matrix = 5.1 4.9 3.5 3.0 scala> val a =Statistics.chiSqTest(mat) a: org.apache.spark.mllib.stat.test.ChiSqTestResult = Chi squared test summary: method: pearson degrees of freedom = 1 statistic = 0.012787584067389817 pValue = 0.90996538641943 No presumption against null hypothesis: the occurrence of the outcomes is statistically independent.. ``` ​ 这里所要检验是否独立的两个属性,一个是样本的序号,另一个是样本的数据值。在本例中pValue =0.91,说明无法拒绝“样本序号与数据值无关”的假设。这也符合数据集的实际情况,因为v1和v2是从同一个样本抽取的两条数据,样本的序号与数据的取值应该是没有关系的。 我们也可以把v1作为样本,把v2作为期望值,进行卡方检验: ```scala scala> val c1 = Statistics.chiSqTest(v1, v2) c1: org.apache.spark.mllib.stat.test.ChiSqTestResult = Chi squared test summary: method: pearson degrees of freedom = 3 statistic = 0.03717820461517941 pValue = 0.9981145601231336 No presumption against null hypothesis: observed follows the same distribution as expected.. ``` 本例中pValue =0.998,说明样本v1与期望值等于V2的数据分布并无显著差异。事实上,v1=[5.1,3.5,1.4,0.2]与v2= [4.9,3.0,1.4,0.2]很像,v1可以看做是从期望值为v2的数据分布中抽样出来的的。 同样的,键值对也可以进行独立性检验,这里我们取iris的数据组成键值对: ```scala scala> val data=sc.textFile("G:/spark/iris.data") data: org.apache.spark.rdd.RDD[String] = G:/spark/iris.data MapPartitionsRDD[13] at textFile at :44 scala> val obs = data.map{ line => | val parts = line.split(',') | LabeledPoint(if(parts(4)=="Iris-setosa") 0.toDouble else if (parts(4)=="Iris-versicolor") 1.toDouble else | 2.toDouble, Vectors.dense(parts(0).toDouble,parts(1).toDouble,parts (2).toDouble,parts(3).toDouble))} obs: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint] = MapPartitionsRDD[14] at map at :46 ``` ​ 进行独立性检验,返回一个包含每个特征对于标签的卡方检验的数组: ```scala scala> val featureTestResults= Statistics.chiSqTest(obs) featureTestResults: Array[org.apache.spark.mllib.stat.test.ChiSqTestResult] = Array(Chi squared test summary: method: pearson degrees of freedom = 68 statistic = 156.26666666666665 pValue = 6.6659873176888595E-9 Very strong presumption against null hypothesis: the occurrence of the outcomes is statistically independent.., Chi squared test summary: method: pearson degrees of freedom = 44 statistic = 88.36446886446883 pValue = 8.303947787857702E-5 Very strong presumption against null hypothesis: the occurrence of the outcomes is statistically independent.., Chi squared test summary: method: pearson degrees of freedom = 84 statistic = 271.79999999999995 pValue = 0.0 Very strong presumption against null hypothesis: the occurrence of the outcomes is statistically independent.., Chi... ``` ​ 这里实际上是把特征数据中的每一列都与标签进行独立性检验。可以看出,P值都非常小,说明可以拒绝“某列与标签列无关”的假设。也就是说,可以认为每一列的数据都与最后的标签有相关性。我们用foreach把完整结果打印出来: ```scala scala> var i = 1 i: Int = 1 scala> featureTestResults.foreach { result => | println(s"Column $i:\n$result") | i += 1 | } Column 1: Chi squared test summary: method: pearson degrees of freedom = 68 statistic = 156.26666666666665 pValue = 6.6659873176888595E-9 Very strong presumption against null hypothesis: the occurrence of the outcomes is statistically independent.. Column 2: Chi squared test summary: method: pearson degrees of freedom = 44 statistic = 88.36446886446883 pValue = 8.303947787857702E-5 Very strong presumption against null hypothesis: the occurrence of the outcomes is statistically independent.. Column 3: Chi squared test summary: method: pearson degrees of freedom = 84 statistic = 271.79999999999995 pValue = 0.0 Very strong presumption against null hypothesis: the occurrence of the outcomes is statistically independent.. Column 4: Chi squared test summary: method: pearson degrees of freedom = 42 statistic = 271.75 pValue = 0.0 Very strong presumption against null hypothesis: the occurrence of the outcomes is statistically independent.. ``` ​ spark也支持Kolmogorov-Smirnov 检验,下面将展示具体的步骤: ```scala scala> val test = sc.textFile("G:/spark/iris.data").map(_.split(",")).map(p => p(0).toDouble) test: org.apache.spark.rdd.RDD[Double] = MapPartitionsRDD[22] at map at :44 // run a KS test for the sample versus a standard normal distribution scala> val testResult = Statistics.kolmogorovSmirnovTest(test, "norm", 0, 1) testResult: org.apache.spark.mllib.stat.test.KolmogorovSmirnovTestResult = Kolmogorov-Smirnov test summary: degrees of freedom = 0 statistic = 0.999991460094529 pValue = 0.0 Very strong presumption against null hypothesis: Sample follows theoretical distribution. // perform a KS test using a cumulative distribution function of our making scala> val myCDF: Double => Double = (p=>p*2) myCDF: Double => Double = scala> val testResult2 = Statistics.kolmogorovSmirnovTest(test, myCDF) testResult2: org.apache.spark.mllib.stat.test.KolmogorovSmirnovTestResult = Kolmogorov-Smirnov test summary: degrees of freedom = 0 statistic = 14.806666666666668 pValue = 0.0 Very strong presumption against null hypothesis: Sample follows theoretical distribution. ``` ## 六、随机数生成 Random data generation ​ RandomRDDs 是一个工具集,用来生成含有随机数的RDD,可以按各种给定的分布模式生成数据集,Random RDDs包下现支持正态分布、泊松分布和均匀分布三种分布方式。RandomRDDs提供随机double RDDS或vector RDDS。 ​ 下面的例子中生成一个随机double RDD,其值是标准正态分布N(0,1),然后将其映射到N(1,4)。 ​ 首先,导入必要的包: ```scala import org.apache.spark.SparkContext import org.apache.spark.mllib.random.RandomRDDs._ ``` ​ 生成1000000个服从正态分配N(0,1)的RDD[Double],并且分布在 10 个分区中: ```scala scala> val u = normalRDD(sc, 10000000L, 10) u: org.apache.spark.rdd.RDD[Double] = RandomRDD[35] at RDD at RandomRDD.scala:38 ``` ​ 把生成的随机数转化成N(1,4) 正态分布: ```scala scala> val v = u.map(x => 1.0 + 2.0 * x) v: org.apache.spark.rdd.RDD[Double] = MapPartitionsRDD[36] at map at :50 ``` ## 七、核密度估计 Kernel density estimation ​ Spark ML 提供了一个工具类 KernelDensity 用于核密度估算,核密度估算的意思是根据已知的样本估计未知的密度,属於非参数检验方法之一。核密度估计的原理是。观察某一事物的已知分布,如果某一个数在观察中出现了,可认为这个数的概率密度很大,和这个数比较近的数的概率密度也会比较大,而那些离这个数远的数的概率密度会比较小。Spark1.6.2版本支持高斯核(Gaussian kernel)。 ​ 首先,导入必要的包: ```scala import org.apache.spark.mllib.stat.KernelDensity import org.apache.spark.rdd.RDD ``` ​ 同时留意到已经导入的数据: ```scala scala> val test = sc.textFile("G:/spark/iris.data").map(_.split(",")).map(p => p(0).toDouble) test: org.apache.spark.rdd.RDD[Double] = MapPartitionsRDD[22] at map at :44 ``` 用样本数据构建核函数,这里用假设检验中得到的iris的第一个属性的数据作为样本数据进行估计: ```scala scala> val kd = new KernelDensity().setSample(test).setBandwidth(3.0) kd: org.apache.spark.mllib.stat.KernelDensity = org.apache.spark.mllib.stat.KernelDensity@26216fa3 ``` 其中setBandwidth表示高斯核的宽度,为一个平滑参数,可以看做是高斯核的标准差。 构造了核密度估计kd,就可以对给定数据数据进行核估计: ```scala scala> val densities = kd.estimate(Array(-1.0, 2.0, 5.0, 5.8)) densities: Array[Double] = Array(0.011372003554433524, 0.059925911357198915, 0.12365409462424519, 0.12816280708978114) ``` 这里表示的是,在样本-1.0, 2.0, 5.0, 5.8等样本点上,其估算的概率密度函数值分别是:0.011372003554433524, 0.059925911357198915, 0.12365409462424519, 0.12816280708978114。 ---------------
[技术] Sun Dec 11 2016 17:43:33 GMT+0800 (中国标准时间)

分类与回归 - spark.mllib 

返回 [Spark MLlib入门教程](http://mocom.xmu.edu.cn/article/show/5858ab782b2730e00d70fa08/0/1) --- ## 一、分类算法概述 分类是一种重要的机器学习和数据挖掘技术。分类的目的是根据数据集的特点构造一个分类函数或分类模型(也常常称作分类器),该模型能把未知类别的样本映射到给定类别中的一种技术。 分类的具体规则可描述如下:给定一组训练数据的集合T(Training set),T的每一条记录包含若干条属性(Features)组成一个特征向量,用矢量$x=({x_1,x_2,.., x_n})$表示。$x_i$可以有不同的值域,当一属性的值域为连续域时,该属性为连续属性(Numerical Attribute),否则为离散属性(Discrete Attribute)。用$C=c_1,c_2,.. c_k$表示类别属性,即数据集有k个不同的类别。那么,T就隐含了一个从矢量X到类别属性C的映射函数:$f(X)\mapsto C$。分类的目的就是分析输入数据,通过在训练集中的数据表现出来的特性,为每一个类找到一种准确的描述或者模型,采用该种方法(模型)将隐含函数表示出来。 构造分类模型的过程一般分为训练和测试两个阶段。在构造模型之前,将数据集随机地分为训练数据集和测试数据集。先使用训练数据集来构造分类模型,然后使用测试数据集来评估模型的分类准确率。如果认为模型的准确率可以接受,就可以用该模型对其它数据元组进分类。一般来说,测试阶段的代价远低于训练阶段。 ## 二、spark.mllib分类算法 分类算法基于不同的思想,算法也不尽相同,例如支持向量机SVM、决策树算法、贝叶斯算法、KNN算法等。`spark.mllib`包支持各种分类方法,主要包含 [二分类](http://en.wikipedia.org/wiki/Binary_classification), [多分类](http://en.wikipedia.org/wiki/Multiclass_classification)和 [回归分析](http://en.wikipedia.org/wiki/Regression_analysis)。下表列出了每种类型的问题支持的算法。 | 问题类型 | 支持的方法 | | ---- | ---------------------------------------- | | 二分类 | 线性支持向量机,Logistic回归,决策树,随机森林,梯度上升树,朴素贝叶斯 | | 多类分类 | Logistic回归,决策树,随机森林,朴素贝叶斯 | | 回归 | 线性最小二乘法,Lasso,岭回归,决策树,随机森林,梯度上升树, isotonic regression | 在以下的章节里,我们将介绍一些典型的分类和回归算法。
[技术] Sun Dec 11 2016 17:31:35 GMT+0800 (中国标准时间)

Markdown和编辑器

目前自己写一些东西,都用的是Markdown格式的编辑器。比如Typora和Ulysses,非常轻量级,界面也非常干净整洁。写起文字来,总是有一种如释重负之感,这种感觉,犹如初高中时候新买了一个非常漂亮的日记本。

客户端写的舒服,不免想要在发布博客的时候,与markdown无缝的对接。由于网页编辑器采用的是ckEditor,因此,发现它有一个markdown的插件,这个插件用了好一段时间。但有几次,感觉非常的不好用。主要原因是用户必须先点击“markdown”的按钮,输入markdown文本;然后在点击提交之前再次点击这个按钮,才能把输入的文本转化成html文本。否则,文本都将丢失。而且好几次,在输入代码模块的时候,html和markdown文本之间的转换会丢失信息。

因此,痛下决心,要改造这个网页编辑器。找来找去,找到了以下几个控件:

<script src="/mdeditor/js/markdown.js"></script>
    <script src="/mdeditor/js/to-markdown.js"></script>
    <script src="/mdeditor/js/bootstrap-markdown.js"></script>

实现了markdown编辑器的功能,仅需要一行语句:

 <textarea id="txtContent" name="txtContent" type="text"
                          rows="30" class="form-control"                      
                          data-provide="markdown"                      
                          placeholder="内容*">  </textarea>

在网页渲染的时候,也很容易转化成html文本:

markdown.toHTML($("#textcontent").text()

此外,写技术博客最需要的代码的高亮显示和latex的公式编辑。前者的解决方法是调用highlight库,会自动的对code内的文本进行高亮,且还可自选主题。

<link rel="stylesheet" href="/highlight/styles/googlecode.css">
<script src="/highlight/highlight.pack.js"></script>

而后者,解决方案不少,但相对技术资料较少。方法一是调用某个能提供latex图片显示的网址,从该网址上获取转换后的图片。比如ckeditor就支持eqneditor的插件,可以把数学公司转化成图片,并集成进来。但这种方法的图片总是觉得模糊,且万一提供图片转化的服务器不可用了,则无法显示公式图片了。方法二呢,利用第三方的js库,直接生成非图片的数学公式。公示显示非常清晰,也不担心第三方服务器不可用的问题。找来找去,发现一个非常好用的库 mathjax(http://docs.mathjax.org/en/latest/index.html)。 仅需要导入这个库,即可把latex文本转化成公式。一个例子是

IDF(t,D) = log \frac{\left| D \right| + 1}{DF(t,D) + 1}

将被解析成:

$$ IDF(t,D) = log \frac{\left| D \right| + 1}{DF(t,D) + 1} $$

[技术] Thu Dec 08 2016 22:33:21 GMT+0800 (中国标准时间)

基本的统计工具(1) - spark.mllib

返回 Spark MLlib入门教程


给定一个数据集,数据分析师一般会先观察一下数据集的基本情况,称之为汇总统计或者概要性统计。一般的概要性统计用于概括一系列观测值,包括位置或集中趋势(比如算术平均值、中位数、众数和四分位均值),展型(比如四分位间距、绝对偏差和绝对距离偏差、各阶矩等),统计离差,分布的形状,依赖性等。除此之外,spark.mllib库也提供了一些其他的基本的统计分析工具,包括相关性、分层抽样、假设检验,随机数生成等。在本章,我们将从以下几个方面进行介绍:

  • 概括统计 summary statistics
  • 相关性 correlations
  • 分层抽样 Stratified sampling
  • 假设检验 hypothesis testing
  • 随机数生成 random data generation
  • 核密度估计 Kernel density estimation

一、Iris数据集

Iris数据集也称鸢尾花卉数据集,是一类多重变量分析的数据集,是常用的分类实验数据集,由Fisher于1936收集整理。数据集包含150个数据集,分为3类,每类50个数据,每个数据包含4个属性。可通过花萼长度,花萼宽度,花瓣长度,花瓣宽度4个属性预测鸢尾花卉属于(Setosa,Versicolour,Virginica)三个种类中的哪一类。大家可以到这个链接下载该数据集:https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data。  其基本数据的样子是:

5.1,3.5,1.4,0.2,Iris-setosa
4.9,3.0,1.4,0.2,Iris-setosa
4.7,3.2,1.3,0.2,Iris-setosa
4.6,3.1,1.5,0.2,Iris-setosa
5.0,3.6,1.4,0.2,Iris-setosa
5.4,3.9,1.7,0.4,Iris-setosa
4.6,3.4,1.4,0.3,Iris-setosa
5.0,3.4,1.5,0.2,Iris-setosa
4.4,2.9,1.4,0.2,Iris-setosa
... ...

二、摘要统计 Summary statistics

​ 对于RDD[Vector]类型的变量,Spark MLlib提供了一种叫colStats()的统计方法,调用该方法会返回一个类型为MultivariateStatisticalSummary的实例。通过这个实例看,我们可以获得每一列的最大值,最小值,均值、方差、总数等。具体操作如下所示:

​ 首先,我们导入必要的包:

import org.apache.spark.mllib.linalg.Vector
import org.apache.spark.mllib.stat.{MultivariateStatisticalSummary, Statistics}

​ 接下来读取要分析的数据,把数据转变成RDD[Vector]类型:

scala> val observations=sc.textFile("G:/spark/iris.data").map(_.split(",")).map(p => Vectors.dense(p(0).toDouble, p(1).toDouble, p(2).toDouble, p(3).toDouble))
observations: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.Vector] = MapPartitionsRDD[3] at map at :32

​ 上面我们就把莺尾花的四个属性,即萼片长度,萼片宽度,花瓣长度和花瓣宽度存储在observations中,类型为RDD[Vector]。

​ 然后,我们调用colStats()方法,得到一个MultivariateStatisticalSummary类型的变量:

scala> val summary: MultivariateStatisticalSummary = Statistics.colStats(observations)
summary: org.apache.spark.mllib.stat.MultivariateStatisticalSummary = org.apache.spark.mllib.stat.MultivariateOnlineSummarizer@52137879

​ 最后,依次调用统计方法,得到相应统计结果:

scala> println(summary.count)
150
scala> println(summary.mean)
[5.843333333333332,3.0540000000000003,3.7586666666666666,1.1986666666666668]
scala> println(summary.variance)
[0.685693512304251,0.18800402684563744,3.113179418344516,0.5824143176733783]
scala> println(summary.max)
[7.9,4.4,6.9,2.5]
scala> println(summary.min)
[4.3,2.0,1.0,0.1]
scala> println(summary.normL1)
[876.4999999999998,458.1000000000001,563.8000000000002,179.79999999999995]
scala> println(summary.normL2)
[72.27620631992245,37.77631533117014,50.82322303829225,17.38677658451963]
scala> println(summary.numNonzeros)
[150.0,150.0,150.0,150.0]

​ 其中,主要方法的含义与返回值类型如下:

方法名 方法含义 返回值类型
count 列的大小 long
mean 每列的均值 vector
variance 每列的方差 vector
max 每列的最大值 vector
min 每列的最小值 vector
normL1 每列的L1范数 vector
normL2 每列的L2范数 vector
numNonzeros 每列非零向量的个数 vector

三、相关性Correlations

​ Correlations,相关度量,目前Spark支持两种相关性系数:皮尔逊相关系数(pearson)和斯皮尔曼等级相关系数(spearman)。相关系数是用以反映变量之间相关关系密切程度的统计指标。简单的来说就是相关系数绝对值越大(值越接近1或者-1),当取值为0表示不相关,取值为(0~-1]表示负相关,取值为(0, 1]表示正相关。

​ Pearson相关系数表达的是两个数值变量的线性相关性, 它一般适用于正态分布。其取值范围是[-1, 1], 当取值为0表示不相关,取值为[-1~0)表示负相关,取值为(0, 1]表示正相关。

Pearson

​ Spearman相关系数也用来表达两个变量的相关性,但是它没有Pearson相关系数对变量的分布要求那么严格,另外Spearman相关系数可以更好地用于测度变量的排序关系。其计算公式为:

Spearman

​ 根据输入类型的不同,输出的结果也产生相应的变化。如果输入的是两个RDD[Double],则输出的是一个double类型的结果;如果输入的是一个RDD[Vector],则对应的输出的是一个相关系数矩阵。具体操作如下所示:

​ 首先,我们导入必要的包:

import org.apache.spark.SparkContext
import org.apache.spark.mllib.linalg._
import org.apache.spark.mllib.stat.Statistics

​ 接下来我们先从数据集中获取两个series,这两个series要求必须数量相同,这里我们取莺尾花的前两个属性:

scala> val seriesX = sc.textFile("G:/spark/iris.data").map(_.split(",")).map(p => p(0).toDouble) 
seriesX: org.apache.spark.rdd.RDD[Double] = MapPartitionsRDD[8] at map at :35
scala> val seriesY = sc.textFile("G:/spark/iris.data").map(_.split(",")).map(p => p(1).toDouble) 
seriesY: org.apache.spark.rdd.RDD[Double] = MapPartitionsRDD[12] at map at :35

​ 然后,我们调用Statistics包中的corr()函数来获取相关性,这里用的是"pearson",当然根据不同需要也可以选择"spearman":

scala> val correlation: Double = Statistics.corr(seriesX, seriesY, "pearson")
correlation: Double = -0.10936924995064437
scala> print(correlation)
-0.10936924995064437

​ 说明数据集的前两列,即花萼长度和花萼宽度具有微小的负相关性。

上面介绍了求两个series的相关性,接下来介绍一下如何求相关系数矩阵。方法是类似的,首先还是先从数据集中获取一个RDD[Vector],为了进行对照,我们同样选择前两个属性:

scala> val data = sc.textFile("G:/spark/iris.data").map(_.split(",")).map(p
=> Vectors.dense(p(0).toDouble, p(1).toDouble))
data: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.Vector] = MapPartit
ionsRDD[20] at map at :35

​ 然后,我们调用Statistics包中的corr()函数,这里也同样可以选择"pearson"或者"spearman",得到相关系数矩阵:

scala> val correlMatrix1: Matrix = Statistics.corr(data, "pearson")
correlMatrix1: org.apache.spark.mllib.linalg.Matrix =
1.0                   -0.10936924995064437
-0.10936924995064437  1.0
scala> print(correlMatrix1)
1.0                   -0.10936924995064437
-0.10936924995064437  1.0

​ 相关矩阵也叫相关系数矩阵,是由矩阵各列间的相关系数构成的。也就是说,相关矩阵第i行第j列的元素是原矩阵第i列和第j列的相关系数。可以看到,输入两个RDD[Double]或一个RDD[Vector],求相关性得到结果是一致的。

四、分层抽样 Stratified sampling

​ 分层取样(Stratified sampling)顾名思义,就是将数据根据不同的特征分成不同的组,然后按特定条件从不同的组中获取样本,并重新组成新的数组。分层取样算法是直接集成到键值对类型 RDD[(K, V)] 的 sampleByKey 和 sampleByKeyExact 方法,无需通过额外的 spark.mllib 库来支持。

(一)sampleByKey 方法

sampleByKey 方法需要作用于一个键值对数组,其中 key 用于分类,value可以是任意数。然后通过 fractions 参数来定义分类条件和采样几率。fractions 参数被定义成一个 Map[K, Double] 类型,Key是键值的分层条件,Double 是该满足条件的 Key 条件的采样比例,1.0 代表 100%。

​ 首先,导入必要的包:

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.rdd.PairRDDFunctions

​ 接下来,这里为了方便观察,没有从iris数据集中取数据,而是重新创建了一组数据,分成 “female” 和 “male” 两类:

scala> val data = sc.makeRDD(Array(
     |     ("female","Lily"),
     |     ("female","Lucy"),
     |     ("female","Emily"),
     |     ("female","Kate"),
     |     ("female","Alice"),
     |     ("male","Tom"),
     |     ("male","Roy"),
     |     ("male","David"),
     |     ("male","Frank"),
     |     ("male","Jack")))
data: org.apache.spark.rdd.RDD[(String, String)] = ParallelCollectionRDD[0] at makeRDD at :26

​ 然后,我们通过 fractions 参数来定义分类条件和采样几率:

scala> val fractions : Map[String, Double]= Map("female"->0.6,"male"->0.4)
fractions: Map[String,Double] = Map(female -> 0.6, male -> 0.4)

​ 这里,设置采取60%的female和40%的male,因为数据中female和male各有5个样本,所以理想中的抽样结果应该是有3个female和2个male。接下来用sampleByKey进行抽样:

scala> val approxSample = data.sampleByKey(withReplacement = false, fractions, 1)
approxSample: org.apache.spark.rdd.RDD[(String, String)] = MapPartitionsRDD[1] at sampleByKey at :30
scala>     approxSample.collect().
     |     foreach {
     |         println
     |     }
(female,Lily)
(female,Lucy)
(female,Emily)
(female,Kate)
(female,Alice)
(male,Tom)

​ 从上面可以看到,本应该抽取3个female和2个male,但结果抽取了5个female和1个male,结果并不够准确,不过在样本数量足够大且要求一定效率的情况下,用sampleByKey进行抽样还是可行的。

(二)sampleByKeyExact 方法

sampleByKey 和 sampleByKeyExact 的区别在于 sampleByKey 每次都通过给定的概率以一种类似于掷硬币的方式来决定这个观察值是否被放入样本,因此一遍就可以过滤完所有数据,最后得到一个近似大小的样本,但往往不够准确。而 sampleByKeyExtra 会对全量数据做采样计算。对于每个类别,其都会产生 (fk⋅nk)个样本,其中fk是键为k的样本类别采样的比例;nk是键k所拥有的样本数。 sampleByKeyExtra 采样的结果会更准确,有99.99%的置信度,但耗费的计算资源也更多。

​ 接下来是sampleByKeyExact的例子:

scala> val exactSample = data.sampleByKeyExact(withReplacement = false, fractions, 1)
exactSample: org.apache.spark.rdd.RDD[(String, String)] = MapPartitionsRDD[3] at    sampleByKeyExact at :30
scala> exactSample.collect().
     |     foreach {
     |         println
     |     }
(female,Lily)
(female,Kate)
(female,Alice)
(male,Tom)
(male,Roy)

​ 从实验结果可以看出,所得结果和预想一致,但当样本数量比较大时,可能会耗时较久。其中,sampleByKeyExact抽样方法中所涉及到的参数解释如下:

参数 含义
withReplacement 每次抽样是否有放回
fractions 控制不同key的抽样率
seed 随机数种子
[技术] Wed Dec 07 2016 23:45:15 GMT+0800 (中国标准时间)

MLlib基本数据类型(2)

返回 Spark MLlib入门教程


四、分布式矩阵(Distributed Matrix)

分布式矩阵由长整型的行列索引值和双精度浮点型的元素值组成。它可以分布式地存储在一个或多个RDD上,MLlib提供了三种分布式矩阵的存储方案:行矩阵RowMatrix索引行矩阵IndexedRowMatrix坐标矩阵CoordinateMatrix和分块矩阵Block Matrix。它们都属于org.apache.spark.mllib.linalg.distributed包。

(一)行矩阵(RowMatrix)

行矩阵RowMatrix是最基础的分布式矩阵类型。每行是一个本地向量,行索引无实际意义(即无法直接使用)。数据存储在一个由行组成的RDD中,其中每一行都使用一个本地向量来进行存储。由于行是通过本地向量来实现的,故列数(即行的维度)被限制在普通整型(integer)的范围内。在实际使用时,由于单机处理本地向量的存储和通信代价,行维度更是需要被控制在一个更小的范围之内。RowMatrix可通过一个RDD[Vector]的实例来创建,如下代码所示:

scala> import org.apache.spark.rdd.RDD
import org.apache.spark.rdd.RDD
scala> import org.apache.spark.mllib.linalg.{Vector,Vectors}
import org.apache.spark.mllib.linalg.{Vector,Vectors}
scala> import org.apache.spark.mllib.linalg.distributed.RowMatrix
import org.apache.spark.mllib.linalg.distributed.RowMatrix

// 创建两个本地向量dv1 dv2
scala> val dv1 : Vector = Vectors.dense(1.0,2.0,3.0)
dv1: org.apache.spark.mllib.linalg.Vector = [1.0,2.0,3.0]
scala> val dv2 : Vector = Vectors.dense(2.0,3.0,4.0)
dv2: org.apache.spark.mllib.linalg.Vector = [2.0,3.0,4.0]
// 使用两个本地向量创建一个RDD[Vector]
scala> val rows : RDD[Vector] = sc.parallelize(Array(dv1,dv2))
rows: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.Vector] = ParallelCollectionRDD[13] at parallelize at :38

// 通过RDD[Vector]创建一个行矩阵
scala> val mat : RowMatrix = new RowMatrix(rows)
mat: org.apache.spark.mllib.linalg.distributed.RowMatrix = org.apache.spark.mllib.linalg.distributed.RowMatrix@76fc0fa
//可以使用numRows()和numCols()方法得到行数和列数
scala> mat.numRows()
res0: Long = 2
scala> mat.numCols()
res1: Long = 3
scala> mat.rows.foreach(println)
[1.0,2.0,3.0]
[2.0,3.0,4.0]

在获得RowMatrix的实例后,我们可以通过其自带的computeColumnSummaryStatistics()方法获取该矩阵的一些统计摘要信息,并可以对其进行QR分解SVD分解PCA分解,这一部分内容将在特征降维的章节详细解说,这里不再叙述。 统计摘要信息的获取如下代码段所示(接上代码段):


// 通过computeColumnSummaryStatistics()方法获取统计摘要
scala> val summary = mat.computeColumnSummaryStatistics()
// 可以通过summary实例来获取矩阵的相关统计信息,例如行数
scala> summary.count
res2: Long = 2
// 最大向量
scala> summary.max
res3: org.apache.spark.mllib.linalg.Vector = [2.0,3.0,4.0]
// 方差向量
scala> summary.variance
res4: org.apache.spark.mllib.linalg.Vector = [0.5,0.5,0.5]
// 平均向量
scala> summary.mean
res5: org.apache.spark.mllib.linalg.Vector = [1.5,2.5,3.5]
// L1范数向量
scala> summary.normL1
res6: org.apache.spark.mllib.linalg.Vector = [3.0,5.0,7.0]

(二)索引行矩阵(IndexedRowMatrix)

索引行矩阵IndexedRowMatrixRowMatrix相似,但它的每一行都带有一个有意义的行索引值,这个索引值可以被用来识别不同行,或是进行诸如join之类的操作。其数据存储在一个由IndexedRow组成的RDD里,即每一行都是一个带长整型索引的本地向量。

RowMatrix类似,IndexedRowMatrix的实例可以通过RDD[IndexedRow]实例来创建。如下代码段所示(接上例):

scala>import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix}
import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix}


// 通过本地向量dv1 dv2来创建对应的IndexedRow
// 在创建时可以给定行的索引值,如这里给dv1的向量赋索引值1,dv2赋索引值2
scala> val idxr1 = IndexedRow(1,dv1)
idxr1: org.apache.spark.mllib.linalg.distributed.IndexedRow = IndexedRow(1,[1.0,2.0,3.0])
scala> val idxr2 = IndexedRow(2,dv2)
idxr2: org.apache.spark.mllib.linalg.distributed.IndexedRow = IndexedRow(2,[2.0,3.0,4.0])

// 通过IndexedRow创建RDD[IndexedRow]
scala> val idxrows = sc.parallelize(Array(idxr1,idxr2))
idxrows: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.distributed.IndexedRow] = ParallelCollectionRDD[14] at parallelize at :45

// 通过RDD[IndexedRow]创建一个索引行矩阵
scala> val idxmat: IndexedRowMatrix = new IndexedRowMatrix(idxrows)
idxmat: org.apache.spark.mllib.linalg.distributed.IndexedRowMatrix = org.apache.spark.mllib.linalg.distributed.IndexedRowMatrix@532887bc
//打印
scala> idxmat.rows.foreach(println)
  IndexedRow(1,[1.0,2.0,3.0])
  IndexedRow(2,[2.0,3.0,4.0])

(三)坐标矩阵(Coordinate Matrix)

坐标矩阵CoordinateMatrix是一个基于矩阵项构成的RDD的分布式矩阵。每一个矩阵项MatrixEntry都是一个三元组:(i: Long, j: Long, value: Double),其中i是行索引,j是列索引,value是该位置的值。坐标矩阵一般在矩阵的两个维度都很大,且矩阵非常稀疏的时候使用。

CoordinateMatrix实例可通过RDD[MatrixEntry]实例来创建,其中每一个矩阵项都是一个(rowIndex, colIndex, elem)的三元组:

scala> import org.apache.spark.mllib.linalg.distributed.{CoordinateMatrix, MatrixEntry}

// 创建两个矩阵项ent1和ent2,每一个矩阵项都是由索引和值构成的三元组
scala> val ent1 = new MatrixEntry(0,1,0.5)
ent1: org.apache.spark.mllib.linalg.distributed.MatrixEntry = MatrixEntry(0,1,0.5)
scala> val ent2 = new MatrixEntry(2,2,1.8)
ent2: org.apache.spark.mllib.linalg.distributed.MatrixEntry = MatrixEntry(2,2,1.8)
// 创建RDD[MatrixEntry]
scala> val entries : RDD[MatrixEntry] = sc.parallelize(Array(ent1,ent2))
entries: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.distributed.MatrixEntry] = ParallelCollectionRDD[15] at parallelize at :42
// 通过RDD[MatrixEntry]创建一个坐标矩阵
scala> val coordMat: CoordinateMatrix = new CoordinateMatrix(entries)
coordMat: org.apache.spark.mllib.linalg.distributed.CoordinateMatrix = org.apache.spark.mllib.linalg.distributed.CoordinateMatrix@25b2d465
//打印
scala> coordMat.entries.foreach(println)
MatrixEntry(0,1,0.5)
MatrixEntry(2,2,1.8)

坐标矩阵可以通过transpose()方法对矩阵进行转置操作,并可以通过自带的toIndexedRowMatrix()方法转换成索引行矩阵IndexedRowMatrix。但目前暂不支持CoordinateMatrix的其他计算操作。

// 将coordMat进行转置
scala> val transMat: CoordinateMatrix = coordMat.transpose()
transMat: org.apache.spark.mllib.linalg.distributed.CoordinateMatrix = org.apache.spark.mllib.linalg.distributed.CoordinateMatrix@c1ee50
scala> transMat.entries.foreach(println)
  MatrixEntry(1,0,0.5)
MatrixEntry(2,2,1.8)
// 将坐标矩阵转换成一个索引行矩阵
scala> val indexedRowMatrix = transMat.toIndexedRowMatrix()
indexedRowMatrix: org.apache.spark.mllib.linalg.distributed.IndexedRowMatrix = org.apache.spark.mllib.linalg.distributed.IndexedRowMatrix@7ee7e1bb
scala> indexedRowMatrix.rows.foreach(println)
IndexedRow(1,(3,[0],[0.5]))
IndexedRow(2,(3,[2],[1.8]))

####(四)分块矩阵(Block Matrix)

分块矩阵是基于矩阵块MatrixBlock构成的RDD的分布式矩阵,其中每一个矩阵块MatrixBlock都是一个元组((Int, Int), Matrix),其中(Int, Int)是块的索引,而Matrix则是在对应位置的子矩阵(sub-matrix),其尺寸由rowsPerBlockcolsPerBlock决定,默认值均为1024。分块矩阵支持和另一个分块矩阵进行加法操作和乘法操作,并提供了一个支持方法validate()来确认分块矩阵是否创建成功。

分块矩阵可由索引行矩阵IndexedRowMatrix坐标矩阵CoordinateMatrix调用toBlockMatrix()方法来进行转换,该方法将矩阵划分成尺寸默认为1024x1024的分块,可以在调用toBlockMatrix(rowsPerBlock, colsPerBlock)方法时传入参数来调整分块的尺寸。 下面以矩阵A(如图)为例,先利用矩阵项MatrixEntry将其构造成坐标矩阵,再转化成如图所示的4个分块矩阵,最后对矩阵A与其转置进行乘法运算:

Matrix A

scala> import org.apache.spark.mllib.linalg.distributed.{CoordinateMatrix, MatrixEntry}
import org.apache.spark.mllib.linalg.distributed.{CoordinateMatrix, MatrixEntry}
scala> import org.apache.spark.mllib.linalg.distributed.BlockMatrix
import org.apache.spark.mllib.linalg.distributed.BlockMatrix

// 创建8个矩阵项,每一个矩阵项都是由索引和值构成的三元组
scala> val ent1 = new MatrixEntry(0,0,1)
...
scala> val ent2 = new MatrixEntry(1,1,1)
...
scala> val ent3 = new MatrixEntry(2,0,-1)
...
scala> val ent4 = new MatrixEntry(2,1,2)
...
scala> val ent5 = new MatrixEntry(2,2,1)
...
scala> val ent6 = new MatrixEntry(3,0,1)
...
scala> val ent7 = new MatrixEntry(3,1,1)
...
scala> val ent8 = new MatrixEntry(3,3,1)
...
// 创建RDD[MatrixEntry]
scala> val entries : RDD[MatrixEntry] = sc.parallelize(Array(ent1,ent2,ent3,ent4,ent5,ent6,ent7,ent8))
entries: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.distributed.MatrixEntry] = ParallelCollectionRDD[21] at parallelize at :57

// 通过RDD[MatrixEntry]创建一个坐标矩阵
scala> val coordMat: CoordinateMatrix = new CoordinateMatrix(entries)
coordMat: org.apache.spark.mllib.linalg.distributed.CoordinateMatrix = org.apache.spark.mllib.linalg.distributed.CoordinateMatrix@31c5fb43

  // 将坐标矩阵转换成2x2的分块矩阵并存储,尺寸通过参数传入
val matA: BlockMatrix = coordMat.toBlockMatrix(2,2).cache()
  matA: org.apache.spark.mllib.linalg.distributed.BlockMatrix = org.apache.spark.mllib.linalg.distributed.BlockMatrix@26b1df2c
// 可以用validate()方法判断是否分块成功
matA.validate()

构建成功后,可通过toLocalMatrix转换成本地矩阵,并查看其分块情况:

scala> matA.toLocalMatrix
res31: org.apache.spark.mllib.linalg.Matrix =
1.0   0.0  0.0  0.0
0.0   1.0  0.0  0.0
-1.0  2.0  1.0  0.0
1.0   1.0  0.0  1.0

// 查看其分块情况
scala> matA.numColBlocks
res12: Int = 2
scala> matA.numRowBlocks
res13: Int = 2

// 计算矩阵A和其转置矩阵的积矩阵
scala> val ata = matA.transpose.multiply(matA)
  ata: org.apache.spark.mllib.linalg.distributed.BlockMatrix = org.apache.spark.mllib.linalg.distributed.BlockMatrix@3644e451
scala> ata.toLocalMatrix
res1: org.apache.spark.mllib.linalg.Matrix =
3.0   -1.0  -1.0  1.0
-1.0  6.0   2.0   1.0
-1.0  2.0   1.0   0.0
1.0   1.0   0.0   1.0

分块矩阵BlockMatrix将矩阵分成一系列矩阵块,底层由矩阵块构成的RDD来进行数据存储。值得指出的是,用于生成分布式矩阵的底层RDD必须是已经确定(Deterministic)的,因为矩阵的尺寸将被存储下来,所以使用未确定的RDD将会导致错误。而且,不同类型的分布式矩阵之间的转换需要进行一个全局的shuffle操作,非常耗费资源。所以,根据数据本身的性质和应用需求来选取恰当的分布式矩阵存储类型是非常重要的。

[技术] Wed Dec 07 2016 22:44:39 GMT+0800 (中国标准时间)

MLlib基本数据类型(1)

返回 Spark MLlib入门教程

MLLib提供了一序列基本数据类型以支持底层的机器学习算法。主要的数据内心包括:本地向量、标注点(Labeled Point)、本地矩阵、分布式矩阵等。单机模式存储的本地向量与矩阵,以及基于一个或多个RDD的分布式矩阵。其中本地向量与本地矩阵作为公共接口提供简单数据模型,底层的线性代数操作由Breeze库和jblas库提供。标注点类型用来表示监督学习(Supervised Learning)中的一个训练样本。

在正式学习机器学习算法之前,让我们先了解下这些数据类型的用法。

##一、本地向量(Local Vector)

本地向量存储在单机上,其拥有整型、从0开始的索引值以及浮点型的元素值。MLlib提供了两种类型的本地向量,稠密向量DenseVector稀疏向量SparseVector稠密向量使用一个双精度浮点型数组来表示其中每一维元素,而稀疏向量则是基于一个整型索引数组和一个双精度浮点型的值数组。例如,向量(1.0, 0.0, 3.0)的稠密向量表示形式是[1.0,0.0,3.0],而稀疏向量形式则是(3, [0,2], [1.0, 3.0]),其中,3是向量的长度,[0,2]是向量中非0维度的索引值,表示位置为0、2的两个元素为非零值,而[1.0, 3.0]则是按索引排列的数组元素值。

所有本地向量都以org.apache.spark.mllib.linalg.Vector为基类,DenseVectorSparseVector分别是它的两个实现类,故推荐使用Vectors工具类下定义的工厂方法来创建本地向量,请看如下实例(假设在Spark-shell中运行,下同):

scala>import org.apache.spark.mllib.linalg.{Vector, Vectors}
import org.apache.spark.mllib.linalg.{Vector, Vectors}

// 创建一个稠密本地向量
scala> val dv: Vector = Vectors.dense(2.0, 0.0, 8.0)
dv: org.apache.spark.mllib.linalg.Vector = [2.0,0.0,8.0]
// 创建一个稀疏本地向量
// 方法第二个参数数组指定了非零元素的索引,而第三个参数数组则给定了非零元素值
scala> val sv1: Vector = Vectors.sparse(3, Array(0, 2), Array(2.0, 8.0))
sv1: org.apache.spark.mllib.linalg.Vector = (3,[0,2],[2.0,8.0])
// 另一种创建稀疏本地向量的方法
// 方法的第二个参数是一个序列,其中每个元素都是一个非零值的元组:(index,elem)
scala> val sv2: Vector = Vectors.sparse(3, Seq((0, 2.0), (2, 8.0)))
sv2: org.apache.spark.mllib.linalg.Vector = (3,[0,2],[2.0,8.0])

这里需要注意的是,Scala会默认引入scala.collection.immutable.Vector,我们要显式地引入org.apache.spark.mllib.linalg.Vector来使用MLlib提供的向量类型。

###二、标注点(Labeled Point)

标注点LabeledPoint是一种带有标签(Label/Response)的本地向量,它可以是稠密或者是稀疏的。在MLlib中,标注点在监督学习算法中被使用。由于标签是用双精度浮点型来存储的,故标注点类型在回归(Regression)分类(Classification)问题上均可使用。例如,对于二分类问题,则正样本的标签为1负样本的标签为0,而对于多类别的分类问题来说,标签则应是一个以0开始的索引序列:0, 1, 2 ...

标注点的实现类是org.apache.spark.mllib.regression.LabeledPoint,请注意它与前面介绍的本地向量不同,并不位于linalg包下,标注点的创建如下所示:

scala> import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.regression.LabeledPoint
//创建一个标签为1.0(分类中可视为正样本)的稠密向量标注点
scala> val pos = LabeledPoint(1.0, Vectors.dense(2.0, 0.0, 8.0))
pos: org.apache.spark.mllib.regression.LabeledPoint = (1.0,[2.0,0.0,8.0])
//创建一个标签为0.0(分类中可视为负样本)的稀疏向量标注点
scala> val neg = LabeledPoint(0.0, Vectors.sparse(3, Array(0, 2), Array(2.0, 8.0)))
neg: org.apache.spark.mllib.regression.LabeledPoint = (0.0,(3,[0,2],[2.0,8.0]))

在实际的机器学习问题中,稀疏向量数据是非常常见的,MLlib提供了读取LIBSVM格式数据的支持,该格式被广泛用于LIBSVMLIBLINEAR等机器学习库。在该格式下,每一个带标注的样本点由以下格式表示:

label index1:value1 index2:value2 index3:value3 ...

其中label是该样本点的标签值,一系列index:value对则代表了该样本向量中所有非零元素的索引和元素值。这里需要特别注意的是,index是以1开始并递增的。 MLlib在org.apache.spark.mllib.util.MLUtils工具类中提供了读取LIBSVM格式的方法loadLibSVMFile,其使用非常方便。

scala> import org.apache.spark.mllib.util.MLUtils
import org.apache.spark.mllib.util.MLUtils

// 用loadLibSVMFile方法读入LIBSVM格式数据
// sample_libsvm_data.txt为spark自带的一个示例,在以下地址可以找到:
// $SPARK_HOME$/data/mllib/sample_libsvm_data.txt
scala> val examples = MLUtils.loadLibSVMFile(sc, "/data/mllib/sample_libsvm_data.txt")
//返回的是组织成RDD的一系列LabeledPoint
examples: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint] = MapPartitionsRDD[6] at map at MLUtils.scala:108

这里,sc是Spark-shell自动建立的SparkContext。我们可以查看下加载进来的标注点的值:

scala> examples.collect().head
res7: org.apache.spark.mllib.regression.LabeledPoint = (0.0,(692,[127,128,129,130,131,154,155,156,157,158,159,181,182,183,184,185,186,187,188,189,207,208,209,210,211,212,213,214,215,216,217,235,236,237,238,239,240,241,242,243,244,245,262,263,264,265,266,267,268,269,270,271,272,273,289,290,291,292,293,294,295,296,297,300,301,302,316,317,318,319,320,321,328,329,330,343,344,345,346,347,348,349,356,357,358,371,372,373,374,384,385,386,399,400,401,412,413,414,426,427,428,429,440,441,442,454,455,456,457,466,467,468,469,470,482,483,484,493,494,495,496,497,510,511,512,520,521,522,523,538,539,540,547,548,549,550,566,567,568,569,570,571,572,573,574,575,576,577,578,594,595,596,597,598,599,600,601,602,603,604,622,623,624,625,626,627,628,629,630,651,652,653,654,655,656,657],[51.0,159.0,253.0,159.0,50...

这里,examples.collect()把rdd转换为了向量,并取第一个元素的值。每个标注点共有692个维,其中第127列对应的值是51.0,第128列对应的值是159.0,依此类推。

###三、本地矩阵(Local Matrix)

本地矩阵具有整型的行、列索引值和双精度浮点型的元素值,它存储在单机上。MLlib支持稠密矩阵DenseMatrix稀疏矩阵Sparse Matrix两种本地矩阵,稠密矩阵将所有元素的值存储在一个列优先(Column-major)的双精度型数组中,而稀疏矩阵则将非零元素以列优先的CSC(Compressed Sparse Column)模式进行存储,关于CSC等稀疏矩阵存储方式的具体实现,可以参看Sparse Matrix Compression Formats一文。

本地矩阵的基类是org.apache.spark.mllib.linalg.MatrixDenseMatrixSparseMatrix均是它的实现类,和本地向量类似,MLlib也为本地矩阵提供了相应的工具类Matrices,调用工厂方法即可创建实例:

scala>import org.apache.spark.mllib.linalg.{Matrix, Matrices}
import org.apache.spark.mllib.linalg.{Matrix, Matrices}
// 创建一个3行2列的稠密矩阵[ [1.0,2.0], [3.0,4.0], [5.0,6.0] ]
// 请注意,这里的数组参数是列先序的!
scala> val dm: Matrix = Matrices.dense(3, 2, Array(1.0, 3.0, 5.0, 2.0, 4.0, 6.0))
dm: org.apache.spark.mllib.linalg.Matrix =
1.0  2.0
3.0  4.0
5.0  6.0

这里可以看出列优先的排列方式,即按照列的方式从数组中提取元素。也可以创建稀疏矩阵:


// 创建一个3行2列的稀疏矩阵[ [9.0,0.0], [0.0,8.0], [0.0,6.0]]
// 第一个数组参数表示列指针,即每一列元素的开始索引值
// 第二个数组参数表示行索引,即对应的元素是属于哪一行
// 第三个数组即是按列先序排列的所有非零元素,通过列指针和行索引即可判断每个元素所在的位置
scala> val sm: Matrix = Matrices.sparse(3, 2, Array(0, 1, 3), Array(0, 2, 1), Array(9, 6, 8))
sm: org.apache.spark.mllib.linalg.Matrix =
3 x 2 CSCMatrix
(0,0) 9.0
(2,1) 6.0
(1,1) 8.0

这里,创建一个3行2列的稀疏矩阵[ [9.0,0.0], [0.0,8.0], [0.0,6.0]]。Matrices.sparse的参数中,3表示行数,2表示列数。第1个数组参数表示列指针,即每一列元素的开始索引值, 第二个数组参数表示行索引,即对应的元素是属于哪一行;第三个数组即是按列先序排列的所有非零元素,通过列指针和行索引即可判断每个元素所在的位置。比如取每个数组的第2个元素为2,1,6,表示第2列第1行的元素值是6.0。

[技术] Wed Dec 07 2016 22:37:38 GMT+0800 (中国标准时间)

Spark MLlib介绍

最近,我们小组在学习spark的机器学习库Mllib,将会陆续的推出一序列博文,请大家关注。

Spark MLlib入门教程

博文同时发布在  子雨大数据之Spark入门教程  。


一、什么是机器学习

机器学习可以看做是一门人工智能的科学,该领域的主要研究对象是人工智能。机器学习利用数据或以往的经验,以此优化计算机程序的性能标准。一种经常引用的英文定义是:

A computer program is said to learn from experience E with respect to some class of tasks T and performance measure P, if its performance at tasks in T, as measured by P, improves with experience E。

               

机器学习强调三个关键词:算法、经验、性能,其处理过程如上图所示。在数据的基础上,通过算法构建出模型并对模型进行评估。评估的性能如果达到要求,就用该模型来测试其他的数据;如果达不到要求,就要调整算法来重新建立模型,再次进行评估。如此循环往复,最终获得满意的经验来处理其他的数据。机器学习技术和方法已经被成功应用到多个领域,比如个性推荐系统,金融反欺诈,语音识别,自然语言处理和机器翻译,模式识别,智能控制等。

二、基于大数据的机器学习

传统的机器学习算法,由于技术和单机存储的限制,只能在少量数据上使用。即以前的统计/机器学习依赖于数据抽样。但实际过程中样本往往很难做好随机,导致学习的模型不是很准确,在测试数据上的效果也可能不太好。随着 HDFS(Hadoop Distributed File System) 等分布式文件系统出现,存储海量数据已经成为可能。在全量数据上进行机器学习也成为了可能,这顺便也解决了统计随机性的问题。然而,由于 MapReduce 自身的限制,使得使用 MapReduce 来实现分布式机器学习算法非常耗时和消耗磁盘IO。因为通常情况下机器学习算法参数学习的过程都是迭代计算的,即本次计算的结果要作为下一次迭代的输入,这个过程中,如果使用 MapReduce,我们只能把中间结果存储磁盘,然后在下一次计算的时候从新读取,这对于迭代 频发的算法显然是致命的性能瓶颈。

在大数据上进行机器学习,需要处理全量数据并进行大量的迭代计算,这要求机器学习平台具备强大的处理能力。Spark 立足于内存计算,天然的适应于迭代式计算。即便如此,对于普通开发者来说,实现一个分布式机器学习算法仍然是一件极具挑战的事情。幸运的是,Spark提供了一个基于海量数据的机器学习库,它提供了常用机器学习算法的分布式实现,开发者只需要有 Spark 基础并且了解机器学习算法的原理,以及方法相关参数的含义,就可以轻松的通过调用相应的 API 来实现基于海量数据的机器学习过程。其次,Spark-Shell的即席查询也是一个关键。算法工程师可以边写代码边运行,边看结果。spark提供的各种高效的工具正使得机器学习过程更加直观便捷。比如通过sample函数,可以非常方便的进行抽样。当然,Spark发展到后面,拥有了实时批计算,批处理,算法库,SQL、流计算等模块等,基本可以看做是全平台的系统。把机器学习作为一个模块加入到Spark中,也是大势所趋。

三、Spark 机器学习库MLLib

MLlib是Spark的机器学习(Machine Learning)库,旨在简化机器学习的工程实践工作,并方便扩展到更大规模。MLlib由一些通用的学习算法和工具组成,包括分类、回归、聚类、协同过滤、降维等,同时还包括底层的优化原语和高层的管道API。具体来说,其主要包括以下几方面的内容:

  1. 算法工具:常用的学习算法,如分类、回归、聚类和协同过滤;

  2. 特征化公交:特征提取、转化、降维,和选择公交;

  3. 管道(Pipeline):用于构建、评估和调整机器学习管道的工具;

  4. 持久性:保存和加载算法,模型和管道;

  5. 实用工具:线性代数,统计,数据处理等工具。

Spark 机器学习库从 1.2 版本以后被分为两个包:

  • spark.mllib包含基于RDD的原始算法API。Spark MLlib 历史比较长,在1.0 以前的版本即已经包含了,提供的算法实现都是基于原始的 RDD。

  • spark.ml 则提供了基于DataFrames 高层次的API,可以用来构建机器学习工作流(PipeLine)。ML Pipeline 弥补了原始 MLlib 库的不足,向用户提供了一个基于 DataFrame 的机器学习工作流式 API 套件。

使用 ML Pipeline API可以很方便的把数据处理,特征转换,正则化,以及多个机器学习算法联合起来,构建一个单一完整的机器学习流水线。这种方式给我们提供了更灵活的方法,更符合机器学习过程的特点,也更容易从其他语言迁移。Spark官方推荐使用spark.ml。如果新的算法能够适用于机器学习管道的概念,就应该将其放到spark.ml包中,如:特征提取器和转换器。开发者需要注意的是,从Spark2.0开始,基于RDD的API进入维护模式(即不增加任何新的特性),并预期于3.0版本的时候被移除出MLLib。

Spark在机器学习方面的发展非常快,目前已经支持了主流的统计和机器学习算法。纵观所有基于分布式架构的开源机器学习库,MLlib可以算是计算效率最高的。MLlib目前支持4种常见的机器学习问题: 分类、回归、聚类和协同过滤。下表列出了目前MLlib支持的主要的机器学习算法: 

 

 

在接下来的几个章节里,我们将基于MLLib,从基本的机器学习算法入手来学习基于Spark的机器学习。

 

 

[技术] Wed Dec 07 2016 22:03:27 GMT+0800 (中国标准时间)

竞赛评审有感

目前针对大学生创新创业、软件外包服务、大数据等方面的赛事越来越多。有全国性的、有区域性的,甚至还有市一级的赛事。最近几年我也带过几届学生,也有幸作为评委参加过几个赛事的评审。因而,时常也有学生来问我一些问题:该如何选题?该怎样在比赛中获得好成绩,并脱颖而出?这些问题都很实际,也很重要。因此,我就结合个人的经验谈下看法。看法仅限于个人的感受,是个人观点,有兴趣的同学可以和我联系并一起深入讨论。

首先是选题和组队,这是赛前应该做好的工作。对于非固定题目的赛事,选题可自己结合热点选一个;也可以和老师沟通联系。这里,需要避免的一个误区是,选题不要都只从自己的身边入手。作为学生,大家的生活圈就这么点,容易雷同。比如,前几届,发现类似校园、课堂方面的题材比较多,也就不太容易形成亮点。当然,这个选题雷同的趋势也有所好转。组队方面,定一个参赛的目标,组成一个有战斗力的队伍,非常关键。组队甚至比选什么题目更重要。一个好的团队去找老师,老师可能会给你一个很不错的题目。在科研实践中,老师经常有一些想法,容易做成demo的那种想法。但苦于没有人可以实现,或者没有一个好的团队帮助实施。因此如果有一个好团队,就成功了一半,就可能把事情做好。好的团队,能够自我激励和管理。理论上,辅导老师一般时间有限,不可能进行事无巨细的指导。因此,团队能否基于目标进行自我激励和自我管理,决定了他们能否产生良好的项目预期。 此外,评审过程中虽然每个赛事的侧重点会有不太一致的地方,大家要先熟悉各个赛事的评价标准。但从各个赛事的评分项来看,其实大同小异,最后都落实到评委的综合判断上。在复赛的时候,一般上时间都非常紧,不太可能完全严格的按照评分项来计算分数。因此,一个项目的总体创新性、完整性,总体上的好印象是非常关键的。那么,有哪些地方需要注意的呢?以下我罗列了一些,重要性不分先后。大家可以逐条思考,逐条批判:)

  1. 作品的视频质量、交互性比较重要;视频最好配有有音频、介绍。评审一大部分的印象分是来自于视频;
  2. 写的文档,也进行格式清楚,有逻辑性,对核心的技术描述要清楚,不要藏着掖着;
  3. 创新点和亮点很关键。分清楚自己团队的共享和已有的东西,如果是模式创新,说清楚你和别人不一样的地方;
  4. 演示过程要简洁,突出重点。很多人还在讲解“用户注册”之类的功能,浪费时间;
  5. 完成度非常重要,功能完备、不能太简单;比赛毕竟是在与其他团队竞争。发现有些idea不错,但完成度不行,也得分不高;
  6. 一些模式创新的项目,最好可以说明你要解决的痛点,能够带来的价值在哪里?如果能说清楚与已有产品的区别、与竞争对手的区别会更好;如果项目已经商业化运作,请说清楚商业前景、融资计划、竞争优势。这是加分的点。

另外,在初审后还可以进一步改进的赛事(如intel杯),持续改进是关键。比如,在某次评审中,发现初赛排名前10的队伍居然只有一支队伍进入了决赛。复赛中真正脱引而出的,大部分是10-30名的队伍,因为他们在后期做了更多的改进。同时,复赛时的现场演示,一般用采用的是ppt,有严格的时间限制。PPT是否用心,是否做了大量的前期准备,评委都可以看得到。现场演示时候,最好完全准备好,避免关键时候掉链子。

最后,祝大家比赛顺利,拿到理想的成绩。也欢迎有志于课外实践和比赛的同学和我联系(邮件即可)。

[随笔] Fri Nov 04 2016 22:03:08 GMT+0800 (中国标准时间)

程序员的月饼

这是之前写的一篇论文,月饼门也已经慢慢冷却了,快被遗忘了。但仍旧发出来吧。

最近,有一件事情闹得沸沸扬扬,即阿里的“月饼门”事件。相比其他行业,技术界的新闻一般都弱弱的。这次居然也让大家谈论起来,实在值得稍微研究一下。作为软院的小伙伴,可能后知后觉。但如果不知道此“门”为何物,赶紧baidu并面壁思过去吧。

事情是这样:

9月12日,阿里巴巴在内部搞了一个中秋抢月饼的活动,不过安全部门的4名员工却写了个脚本自动抢月饼,不动声色地刷了124盒月饼。随后阿里巴巴作出决定,开除4名涉事员工。

随后网络新闻和评论劈天盖地。大家关心的问题集中在:阿里作为公司,这样做对不对,解除4名员工合法吗?被解雇的员工是否很冤枉,他们伤害到其他人的利益了吗?抢月饼之前,阿里有规定不能用js刷网页吗?……当然,更有人的问题是这样:阿里月饼长什么样,真有这么好吃吗?

道德、法律的讨论已经太多,大家可以各自评判。而作为技术咖,我今天要讨论的话题是:

程序员,你真的很喜欢吃月饼吗?

哦,说错了,说错了。我要说的是:程序员,你为什么这么喜欢写脚本?

不做道德评价,看到这个新闻的当时,我隐约有点同情这些阿里员工。这也可以理解,他们如果是学计算机或软件专业的,我甚至可以想象他们的样子。我现在班上的某某同学,很可能就是这样的阿里员工。动手快、能力强,很有自己的想法,甚至还有点小聪明。这类同学,如果是做课程设计,往往能给我一些惊喜。有点创意,实现完整,做出的小软件或者网站都是基本可以运行的。而且,他会告诉你,他的程序是如何组织的,哪些代码进行了复用,哪些模块可以扩展,哪些地方可以省去很多手工的步骤。

是的,软件其实一直强调代码重用、可扩展、自动化,从而提高开发的效率。“不要重复发明轮胎”——这句话深深的刻在了每个学软件和计算机的同学的脑海里,挥之不去。因此,凡是学过编程及计算机的同志,估计都有一个习惯:极不情愿做重复的事情;如果一定要做一个动作很多次,一定得让电脑或程序来做。因此,脚本和脚本语言流行的动力源头,是那些逃避重复的程序员。脚本可以用来配置程序、系统,和软件。比如,软件测试用的脚本,可以避免测试人员每次的数据输入、鼠标点击等机械枯燥的事情。因此,当秒杀的时候,发现每次都要输入验证码,而验证码会影响你的秒杀成功率的时候,一个典型的程序员能想到的,自然是如何避免每次的手动输入了。据说,当初热闹了一阵的火车票刷票软件,也是用的这个原理。从这个角度来说,没有想过减少重复操作的程序员,都不是好司机^_^

然而,任何事情,技术之外还会有法律和道德的因素。在互联网时代的今天,我们追求简单,去除中间环节,减低交易的成本。但仍旧有一些东西,我们会人为的设置障碍,故意造成一定的低效率。而其目的,其实是为了达到一种所谓的公平和正义。虽然,社会很难有绝对的公平和正义。这也是为何,有人觉得被开除的员工很冤枉,也有人支持马云的铁腕做法。但作为一个正在从事技术研究,讲授编程技术和思想的教书先生,我支持大家按照技术的逻辑,不断的去尝试和创新;但同时,也建议大家可以从更高的维度来思考问题。

即,在技术之外考虑技术,从而看见我们这个时代的限制,思考这个时代的机会。因为,除了代码,除了月饼,这个世界还有很多其他的事情需要我们去听、去看、去想呢。

[随笔] Fri Oct 14 2016 22:51:24 GMT+0800 (中国标准时间)

Node JS遍历文件夹

最近在添加网站的一些功能,本以为半个小时可以搞定的事情,却花了我好多时间。因此,决定顺便梳理一下。

需求:给定一个文件夹地址,把该文件夹以下的目录和文件递归的以文本的方式显示在网页上。

这个代码,如果用传统的java或者C#语言来做,大概是非常轻松的事情。随手可以从网上可以摘录一个代码。

public   static   void   ListFiles(FileSystemInfo   info, ArrayList array) 
  { 
    if(!info.Exists)   
              return;
      DirectoryInfo   dir   =   info   as   DirectoryInfo; 
      //不是目录 
    if(dir   ==   null)   
              return;
         array.Add(dir.Name);
    FileSystemInfo   []   files   =   dir.GetFileSystemInfos(); 
    for(int   i   =   0;   i   <   files.Length;   i++) 
    { 
      FileInfo   file   =   files[i]   as   FileInfo; 
      //是文件 
      if(file   !=   null) 
        array.Add(file.FullName   +   "\t "   +   file.Length); 
      //对于子目录,进行递归调用 
      else 
        ListFiles(files[i]);
    } 
  } 
}

这里有关键语句FileSystemInfo [] files = dir.GetFileSystemInfos();获取文件夹下的所有目录和文件,随后判断是否是文件夹。如果是,继续递归,否则的话,加入到数字array中。几乎对js的回调非常习惯了,于是随手从百度上找找到了一个与dir.GetFileSystemInfos对应的函数:

fs.readdir(path[, options], callback)

The callback gets two arguments (err, files) where filesis an array of the names of the files in the directory excluding '.' and '..'.

但是,发现这个用这个回调函数,也来个递归,似乎比较难以下手。callback已经是回调了,当对files也进行循环判断,如果遇到的是文件夹,那怎么进行递归呢?(大家先想想)

var fs = require('fs');

/*

递归处理文件,文件夹

path 路径
floor 层数
handleFile 文件,文件夹处理函数

*/

function walk(path, floor, handleFile) {
    handleFile(path, floor);
    floor++;
    fs.readdir(path, function(err, files) {
        if (err) {
            console.log('read dir error');
        } else {
            files.forEach(function(item) {
                var tmpPath = path + '/' + item;
                fs.stat(tmpPath, function(err1, stats) {
                    if (err1) {
                        console.log('stat error');
                    } else {
                        if (stats.isDirectory()) {
                            walk(tmpPath, floor, handleFile);
                        } else {
                            handleFile(tmpPath, floor);
                        }
                    }
                })
            });

        }
    });
}

exports.walk = walk;

这里的关键还是回调,即在递归函数walk里,嵌入一个handleFile的回调函数,遇到文件的时候调用它;遇到文件夹的时候,还用原来的递归函数walk。具体的用法如下:

var dirWalker = require('./dirWalker');
var fs = require('fs');

var array=new Array();
function handleFile(path, floor) {
    var blankStr = '';
    for (var i = 0; i < floor; i++) {
        blankStr += '    ';
    }
    fs.stat(path, function(err1, stats) {
        if (err1) {
            console.log('stat error');
        } else {
            if (stats.isDirectory()) {
                array.push('+' + blankStr + path);
            } else {
                array.push('-' + blankStr + path);
            }
        }
    })
}

dirWalker.walk('/Users/MacBackup', 0, handleFile);

我发现,还有一个看上去稍微复杂点的写法,感兴趣的可以继续研究:

var fs = require('fs');
var path = require('path');


var array=new Array();
function travel(dir, visit, finish) {
    fs.readdir(dir, function (err, files) {
        (
            function next(i) { //定义一个函数next,并同时调用 next(0);
                if (i < files.length) {
                    var pathname = path.join(dir, files[i]);
                    fs.stat(pathname, function (err, stats) {
                        if (stats.isDirectory()) {
                            travel(pathname, visit, function () {
                                next(i + 1);
                            });
                        } else {
                            visit(pathname, function () {
                                next(i + 1); //处理一个文件,才继续调用下一个
                            });
                        }
                    });
                } else { //顶层的文件/子目录全部处理完毕,调用finish
                    finish && finish(array);
                }`
            }(0)
        );
    });
}

travel("/Users/MacBackup",
    function addPathToArray(pathname,callback){
        array.push(pathname);
        callback();
    },
    function finish(){
        for(var i=0;i<array.length;i++){
            console.log(i+": "+array[i]);
        }
    });

这里,travel函数的visit和finish都是现场定义的回调函数。(visit=addPathToArray,finish=finish)。对于fs.readdir返回来的files中的每个文件逐一判断。程序控制得很周全:对于1个具体的文件files[i],如果是文件夹,则继续调用travel函数;如果是文件,则调用visit。无论种类如何,都必须等到处理完file[i]后,才继续处理files[i+1]。这种一环扣一环的执行顺序,依靠的是travel和visit的回调函数next。直到所有的顶层的files文件都列举和访问完了,则才开始调用finish函数。这样的精心回调和程序流控制,的确很费脑^_^

花了九牛二虎之力理解了回调的写法。其实回过头来仔细看nodejs的fs模块,它为所有的文件操作,都提供了非回调的版本1。也为readdir提供了一个非回调的版本:

fs.readdirSync(path[, options])

此时写个与c#一样的函数,轻而易举:

function travelPath(path1, fileArray) {
    var array = fs.readdirSync(path1)   
    for (var i = 0; i < array.length; i++) {
        var s = path1 + "/" + array[i];
        fileArray.push(s);
        var stats = fs.statSync(s);
        if (stats.isDirectory()) {
            travelPath(s, fileArray);
        }
    }
}
[技术] Tue Oct 11 2016 21:57:49 GMT+0800 (中国标准时间)

编程是什么:面朝大海,打字编码

在每年的本科生导师见面会前后,我都要关心下所谓的“被调剂学生数“。这个数字,大体体现了IT和信息行业的热度;而对同学个人而言,则会影响到他们第一学期的学习兴趣。高考时报的是厦大的所谓王牌专业(经济类、管理类、法律等),而学的却是软件和编码,很多人会提不起来兴趣来。那感觉,就如同,你想要的是:

面朝大海,春暖花开1

到了厦大软件学院,发现的却是:

面朝大海,打字编码。

学软件,以后就是以后当”码农“,怎么一个”惨“字可以形容!到了大二,软件学院的学生分为“软件工程”和“数字媒体”两个专业。而,数媒班,则成了很多不喜欢编程、逃避技术的同学的避风港。很不幸 ,我主讲的“人机交互”课程,就需要给一些所谓不爱编程的同学上课2。我发现了,他们的难处很大,恐惧很多,忧虑更多。他们中的一小部分人,对编程和技术有点抵触,甚至到了谈编程色变的地步。

然而,编程真的是如此前景暗淡,枯燥无味吗?下面,我就来说说编程,或者学习编程,可以“是”什么。可以,成为什么。

编程是一种创造

据说古代猿人是因为懂得使用和创造工具,才进化成为了人。其实,除了基本的吃喝玩乐,每个人都想有所创造,都想为这个世界留下点什么。而创造的工具,作家用文字,音乐家用五线谱,而我们程序猿用的就是——编程语言,一种类似于神谕的语言。据路边社统计,除了睡觉我们有80%的时候是和电脑、手机在一起玩耍的;而正是程序猿编写的程序,在驱动着电脑和手机的运转。掌握了编程,就等于拥有了一个创造的工具。你可以通过它构建一些你觉得有趣的、特别想要的东西。当某天,你通过自己的编程,完成了某件事情,拥有属于自己软件或产品,你会觉得它像是你创造的孩子。成就感杠杠的。

编程促进人的成长

编程语言做是一种语言,其实和汉语、英语并无本质不同。自然语言主要目的是交流,通过说话和写作,表达观点,驱动他人;编程语言呢,其主要目的是与机器对话,表达你对机器的期望,驱动的是机器。因此,可以把编程看做一种表达工具。通过它指挥机器工作,指挥与其他机器协作,只会机器完成人没法的事情。人的能力和时间实在有限,但机器正在丰富和扩展我们。目前,人与机器有逐渐融合的趋势:机器慢慢的成为人的外延,成为人能力的延伸。当今有一个观点,说是我们如果要具备有竞争力,就必须“人机合一”。我深信不疑。君不见,无论是经济学,统计学,还是金融,都慢慢的需要从业者有编程的功底。即通过学习编程,学会与机器的交流,最大化挖掘机器的能力,来为提高个人的能力服务。

大学的时候,我在一个周围全部是文科院系的学校学习计算机,因此也认识了一些专业是文科,但仍然自学编程和计算机的朋友。听他们聊我的专业,总有一种从门外看门内的味道,多了一种视野。比如,有一个朋友,学的是经济学,他自学了数据结构,操作系统后,大为折服。我当时看来平淡无奇、枯燥的数据结构,在他看来却觉得是很精妙的安排,甚至快要上升到方法论的地步。对于操作系统,他觉得更是神奇。恨不得把操作系统的容错性、内存的管理方法,统统用于指导其个人的生活。再后来,互联网思维大行其道,估计这个哥们更要有一番言论了。这几年,也听说过一个很有意思的观点:计算机、编程、互联网等等,都是人类极少数的人想出来的,发明出来的。他们的想法,代表了当今人类最新进的思维方法。我们学习编程,学习计算机,就是在向人类中极少数的优秀、聪明的大脑学习。学习他们的思维方式,学习他们的想法。

听起来不错吧。原来,编程还是这样的”高“、”大“、”上“。

编程是一种乐趣

编程和程序员给人的刻板印象根深蒂固:

枯燥、无趣、偏执、不修边幅。

英文也有一个词来形容编程高手——geek。这个词其实无所谓褒贬,但在很多人眼里,它是贬义的。但编码,真的是枯燥的吗?程序员和作家、会计师相比,哪个职业会更有乐趣呢?

你也许会说,乐趣、兴趣的问题很大程度上取决于主观的因素。的确,一件事情是否有乐趣,很大程度上取决于于做这个事情的人。人们对于经常做不好的事情,是会慢慢的失去兴趣和乐趣的。所以,一个人如果觉得数学很难,很多题不会解;则他可能会有挫折感,慢慢的对数学有抵触情绪,进而不上心、不投入,也不做数学题。因此数学更学不好了,恶性循环。

反之,如果一开始对数学不是很有兴趣,但慢慢的发现自己能看得懂公式,能解答习题,则大抵数学的信心就会加强,慢慢的对数学就有好感了。获得了正向的反馈(正能量),因此也觉得数学有点意思了。因此,我们是否能胜任这个事情,能否持续的、正向的、及时的反馈,是觉得该事情是否有乐趣的一个重要因素。而所谓的作家,在互联网时代之前,其实是很苦的行业。因为反馈的周期实在太长。你想,没有成名之前,好不容易码子写了个长篇小说,却可能无人帮你出版,得到处投稿。可一投就好几个月才能获知结果,其实很难获得持续、正向的反馈。

这里,我强调持续正向及时的反馈,也是借鉴了网络上打游戏的观点3。 然而,如果一件事情能完全胜任,人又会觉得无聊。觉得没有挑战了,乐趣也会跟着大大降低。如果有一定的偶然性和挑战性,跳一跳可以够得着;同时发现你够到的东西,还有点意料之外,则就更好了。比如,牌和麻将就具有这样的特质。麻将需要一定的技巧,努力之后可以提高;但仍具有很大随机性及运气因素。这可以解释为何中国大妈大叔们工作之后,觉得最有趣的事情就是打牌打麻将了。而会计这个行业,我不十分了解。但我推测不会是一个很有趣的行业。证据之一是有会计师的朋友,总抱怨说看excel表格,看的头发都白了。想想也是,会计的规则和准则都是固定的,老板应该也不会喜欢有“意外”的会计报表,不喜欢有太有创意的财会人员。更有甚者,会计师偶尔还要被逼着做些自己不愿意做的事情。具体什么事情,你懂的。

因此,如果一件事情是可以获得持续的、正向的、及时的反馈,并且同时具有一定挑战性的,它就具有"有趣"的基本特征了。

很幸运的是,编程恰好有这么几个优秀的基因。首先,编程可以马上看到结果,每改动一个语句,没改动一个变量,机器都可以告诉你对错,告诉你运行的结果如何。反馈持续进行,且总体上往好的方向进行。近年来流行的“测试驱动“的开发方法,则可以在你写对代码的时候,把红色的道道变成绿色的道道4。这可以解释为何一些程序猿、geek可以每天加班检点,就是想做出一个好的东东来。你们看似很苦bi,很有毅力的程序猿们,其实可能正陶醉在自己的有趣世界里,无法自拔呢。

编程是一种生存方式

为何不说是一种”生活方式“呢?当然,这样写估计也没有什么错。为了避免此文成为一个鸡汤味太浓的软文,我觉得应该去除一些文艺气息太重的词,比如,生活方式。我所说的“生存方式”,其实是想告诉同学们:

编程,可以成为一个人养家糊口、报效国家、安身立命的生存方式。

说的直白点就是:编程可以让你赚钱。的确,这个到底很多人相信。但相信的程度实在太低。

以我将近10年的教学生涯和十多年的编程生涯告诉大家,编程的确可以让你以很好的状态生活着。我发现大学时候编程好的同学,大都生活的很滋润。要不在美帝IT企业,工资以美金计,要不就在国内大企业的信息部门逍遥自在。而且,IT行业的入门的门槛不高,完全可以靠本事吃法(大家都很鄙夷靠脸吃饭),平均薪酬也是可以的。中国老百姓最朴素的道理——”一技之长”、“技不压身“——可以很好的用在编程这个工种身上。

虽然大家的收入还是可以的,但顶着“码农”的恶名,我发现大家真正焦虑的事情,其实是:

不是说IT吃的是青春饭吗?这么累,老了怎么办?

首先说下“累”的问题。累,其实是用来吓唬那些向往成功,但意志不是特别坚定的人的。试问这个世界,什么事情是不会累,又可以轻轻松松就获得成功的呢。我们是生在经济高速发展的时代,如果没有一个富爸爸,又想过上好的生活,99%的人出路只有一条:那就是努力再努力,等待好时机(还有1%的人可以买彩票)。同样是高薪的工作,那些在金融、投资行业的朋友,其实加起班来不会比IT行业更少。而且,我相信随着经济的发展和产业的转型,员工生产效率提高了,企业的利润足够了,IT加班很多很累的刻板印象会逐渐淡去。人们会逐渐回到正常上下班、正常工作的状态;就如同今天美国IT界和程序员的生活状态。

其次是“老了”的问题。这个问题其实很好回答。让自己不会老,不就可以了嘛。我说的不会老,不是生理上的,而是心态上的年轻。保持年轻的形态,持续学习,你可以在IT的行业越做越远。据说现在在国外,60多的程序员大有人在。这些老程序员写代码,纯粹是为了个人兴趣。写些小程序玩玩,颇有怀旧的意味。而经验丰富的程序员,会成为架构师、成为资深产品经理、成为技术合伙人、成为公司的CTO。甚至,像很多当今的创业者一样,成为公司的老板。据我说知,我大学时候所在的信息学院,也有好些师兄、同学、师弟都出来创业了,公司成功者不在少数。而在这个”大众创业“的时代,IT是最容易起步和创业的地方。年轻者如正在看此文的同学们,如果想创业,赶紧找个idea,打开电脑,自己就开始动手打代码吧~

最后

说了这么多,颇费口舌。无非在讲编程的好处,提升编程的level。本来,大家喜不喜欢编程,和我实在是没有太大关系。只是每次上课,我都要强调下编程的好,希望以此提高大家的学习热情。从软件”复用“的角度来说,我还是有必要写篇文字统一说明下,这样效率比较高。也欢迎同学们留下你们的意见和建议。

最后我要强调的是,细心呵护同学们对编程的热情,是一个老师的最首要、最重要的责任。理论上,老师只是起到引导、辅助的作用。以我的经验,真正的编程高手,都是自学而来的。如果某一天,你成为了编程高手,依靠编程获得了经济的独立和自由,那么,你的人生,或许意境是这样的:

打字编码,春暖花开,面朝大海。

是的,希望在厦大,在软件学院,同学们都能轻松做到这一点。


1 近年厦大在网络的热度不退,很多人来厦大读书、授课,或许也是受到了海子一句诗的影响。而厦大,则真的有条件实现“面朝大海,穿暖花开”的意境。

2 其实,与专业关系也不是那么大。软工也有不喜欢编程的人。

3 你看游戏多有趣啊。可是为什么有趣呢?除了打斗等场面外,一个重要的原因是,它不断让你看到你的战果,获得持续的心理满足感。

4 用过诸如JUnit的同学,知道我在讲什么。

[课堂] Tue Sep 27 2016 22:05:15 GMT+0800 (中国标准时间)

如何做好一次实验

我所教授的编程类课程(比如c++、c#语言、中间件技术),一般都配置有8次或16次的实验课。如果是8次实验课,则大概会安排5-6次实验;如果是16次,一般会安排8-10次实验。那么今天,我们就来聊一下实验课程设置和实验课的注意事项。

首先,同学们可能会很好奇,实验内容到底是怎么选择的,有什么依据吗?说实话,实验内容一定是依课程和大家所处的水平来定的,当然也会参考教材上的实验设置。比如,c、c++课程设置在大一的上下学期,是基础的编程的入门课程,则实验内容倾向于当场做完。每次课程就是完成1次的实验内容。这个安排有个好处就是可以较为精准的瞄准某个知识点,在大家完成实验的过程,即现场掌握了这个知识点。而对于c#编程,中间件的编程,则已经是编程的提高阶段了,则每次实验课就不局限于“当堂完成”了。在进阶阶段的编程课,应该不是写“一看就会”的代码。相反,我会选择一个相对完整的“任务”,给大家一个相对长的时间周期来提交。因此,基础编程课的实验内容,我会参考教材的知识点,一个一个进行;而其他的进阶课程,则从大家的学习或生活实际,选择适当的实验题材了。比如,c#课程第1个实验,要求是写一个控制台版本的“贪吃蛇”游戏;第2个实验,可能是写一个记事本或者画图板。这些游戏或软件大家都玩过用过,有些同学可能不是马上就能想到思路。如何开始编程?需要哪些知识点?是否有多种实施的方式?有些同学看到实验内容,颇觉得有难度。

其实,有难度是我故意设置的障碍。因为,在进阶阶段,我们要求学习编程的同学要有一定的设计过程、思考过程。针对具体的算法,去网络上可能可以找到现成的答案和代码;但解决问题的思路和过程,则是网络上找不到的,需要大家自己思考。事实上,有过项目开发和实践经验的人都知道。每一个新的需求提出的时候,一些功能往往是不知道如何实施的。这个时候,需要思考,总结以前的开发经验,利用网络检索的知识,或者靠请教懂行的高人去找到解决的思路。有了思路,有时候还不放心,得先做个demo来验证这个思路。所以,每个实验课,我都留了足够长的时间来让大家完成。比如2周布置任务,我会要求在第6周的时候检查和提交;一般的,在第4周也会布置另一个实验,但这个实验会在第8周的时候要求提交。可以看到,实验的提交周期是有重叠的。这样的安排,周期较长,是为了避免给大家造成太大的压力。在此,也提醒那些希望每次实验都“短平快”的同学,我们布置的实验虽然看上去有点难,甚至觉得有些内容无从下手,但千万别一开始就放弃。因为你有6周的时间来思考、解决这个问题。第1-2周想思路,结合课程学习新的知识;第3-4周以后,可以设计和编码了。相信我,如果你发现一个原本不太好解决的问题,随着自己的步步深入而获得了一些思路,最后做出效果,你会超级有成就感的。但关键是,你一开始不要放弃,而是要尽可能的去找资料、看课本,直至最后做成。而这个过程,其实也是我想让大家都经历的过程。这样,同学们在课堂听课的时候更有动力,更有针对性,也更愿意与老师和助教交流,不知不觉中也提高了大家自学的能力。

然而,我发现即使是天才如我厦大的同学,也不是每一个人都可以把每个实验完全从“开始”走到“结束”的。因此,在实验要求上,我会分为“基础”和“提高”部分。“基础”部分是根据上课所讲和我提供的一些材料,所有同学都必须完成的部分。完成基础部分,即可以拿到这个实验的分数。“提高”的部分,则作为额外奖励的分数。完成提高部分,大概需要同学自学一些知识,或者自己找一些资料、找一些人讨论才可以完成的。从以往的而经验看,一些有能力的同学也会自己增加实验的难度和内容,把小实验当做一个小软件来做。甚至有同学在实验过程中获得了一些思路,做出了一款软件并提交到学院的软件设计大赛中去。能这样做的,很多时候是聪明的同学,因为这样做是“一举多得、一石二鸟”。

最后,虽然实验是以个人为单位完成,但我也很期待同学们能自己组合,搭配一个团队,在一学期课程中完成一个软件或网站。之前几届,同学组队做“期末大作业”是作为一个课程任务,也分配了分数比率。但我发现,强扭的瓜不甜,“搭便车”的现象很严重。你会发现很多组虽然有3-4个同学,但真正编程的,其实只有1-2个同学。因此,团队完全出于自愿,才可能有真正的效果出现。在我看来,无论是学习、还是编程,都应该是一种完全自觉的行为。如果只是被逼着去学,去coding,学习的效果同行大打折扣。这也可以验证那个朴素的真理:

真正的编程高手,都是自学而来的;真正喜欢的事情,读它千遍也不厌倦。

[课堂] Wed Sep 21 2016 14:49:57 GMT+0800 (中国标准时间)

从一个bug谈动态语言的隐形成本

这个网站是node+mongodb写的。刚开始入手写的时候很是舒畅:部署简单,代码简洁,数据扔到mongo的文档里,基本不需要考虑任何模式。编码,仿佛卸下了沉重的脚镣,一种解脱感。

可是,慢慢的,发现一些莫名的错误,很是百思不得其解。花了大量的时间来解决细节问题。这个过程中,也发现了几个非常典型的错误。按现在的流行说法,这几个都“坑”。每次学过新东西,都需要从这些坑踩过去。但无疑的,有些语言的坑比较多,有些坑比较少;有些坑前人已经告诫过你了;有些坑,你就是“前人”。

这里,举个简单的问题,是把文章或者blog存入mongodb,然后按时间逆序读取出来。一开始,我定义了一个blog的对象,赋值一个创建时间:

newBlog.time = Date();

代码没有报错,也很顺利的存进了据库的。但取出来显示的时候,发现blog无法按照时间来排序。查看mongodb的后台,显示的是:

"time" : "Sun Apr 10 2016 21:27:36 GMT+0800 (中国标准时间)"

时间似乎是对的,但为何没能进行排序呢?

我百度了(原谅我,也在用baidu)一些资料,觉得是存到monggodb的时间的格式不对。(据以前的开发经验,网页开发除了中文的编码问题,就是时间的格式问题了。) 用python写了个脚本,把现有的日期格式改成ISODate的格式:

"2011-12-20T07:22:50.836Z"

还把新插入的日期代码,改成这样:

newBlog.time = Date().toISOString();

发现可以排序了,貌似也运行无误,终于可以缓一口气了。

可是,诡异的是,过了一段时间,发现新加入的内容竟没有排在最前面!!问题原来还是没有解决!这次决定不用百度了^_^。几经挫折,又浪费了好多时间。

问题的答案,不得不回到mongodb的存储问题了。mongo以文档collection为基本的存储单元,无需定义所谓的数据模式schema,就可以往mongo的任何一个文档collection扔进去任何东西,包括字符串,数字,或者一个json对象。不巧的是,js也是一个对数据类型定义不严格的语言。虽然也有所谓的数据类型,但定义变量的时候,不会出现诸如Date today;这样的代码来标记数据的类型。当js+mongo+json进行开发的时候,真没有怎么想到还有数据类型这个概念,感觉一切都是字符串,一切都是文本。于是,悲剧出现了。你存到数据库里的是文本,那mongdo也按照文本给你排序了。

但其实mongodb虽然没有schema,但存储的时候,mongo还是会保存数据的类型。而这个类型,恰恰从插入的数据推导而来。

newBlog.time = Date();

newBlog.time得到的,居然是一个字符串!也就是说Date()给的字符串(这可能是js语言非常狗血的地方, 必须加一个new才能是一个对象。而这个对象,可以是mongodb中的一个带数据类型的字段。

newBlog.time = new Date();

至此,真相大白。但经过这个过程,你才发现,这个错误隐藏得如此之深,而网上基本无法找到类似的问题,也很难通过搜索检索到答案。假如我用的是c#、java这类的语言,或者我后台用的是传统的关系型数据库(如mysql),这个bug是没有机会出现的。它们严格的类型检查已经把这个问题过滤了。但只有javascript,monggodb,(动态语言+NoSQL数据库)让这些问题持久的存在,几乎到了生成阶段最后才被发现和解决。

原本想通过这个小例子,总结下静态语言和动态语言的优缺点。但网上一搜,发现有人已经写得非常清楚了:

不同的语言有不同的特点,同时也带来不同的优势。如果不能理解Scala的特点,就不可能知道如何运用Scala,以及发挥其最大的优势。一些语言有很显而易见的优势,也很容易理解,比如Python,Python的哲学(Zen of Python PEP 20 -- The Zen of Python**),我很早的时候曾经觉得有道理,尤其是One way to do it(一种方法做一件事情),理由是对任何任务,虽然可以采用很多方法,但总有最好的一种方法,通过在语言或者哲学层面这样定义后,能简化程序员的任务,从而达到提高效率的方法。但经过一段时间的思考后,我突然发现Python其实并不是“一种方法做一件事”的哲学,而是“一种方法做一百万件事情”的哲学:极其有限的数据结构(只有四个: List, Tuple, Dictionary, Sets),以及不能查看时间复杂度的访问方法,比如鼓励人们使用for x in list。

这种处理方式能达到Python最初的打算:发明一种每个人都能使用的简易语言,但是对于追求速度和效率的程序员而言,这几乎是略带噩梦性质的。当然,这不是说Python很慢,通过各种优化(比如NumPy/SciPy中的),以及Cython这样的将Python直接翻译为C/C++语言又重新通过C_Module方式读回Python环境的编译器,性能可以得到不少提升,但是仍旧,Python并不追求快。

再举一个语言的例子:Java。Java的特性或者优势何在?Java的第一个优势在于它是第一个系统提供模块化(module)设计的语言(在此之前有Smalltalk存在,该货是OOP的鼻祖)。在Java之前,炒程序员鱿鱼是很困难的事情,那些C/C++程序员,以及而且尤其是那些Lisp程序员,一旦炒掉他们,新来的人没有十天半个月,甚至半年,是不可能搞懂前任人士的代码的。每个人的代码有自己的逻辑,自己的思路,写上个数万行任谁来看都头疼。这也是为什么Paul Graham保罗·格雷厄姆(写了《黑客与画家》)讲他给雅虎做了一个用Lisp写成的在线商店的案例,在他离开后,雅虎根本没法维护他写的代码,因为数万行Lisp没人能弄得很清楚。

Java的模块化,给企业、大公司带来了第一道曙光,模块化之后,这些公司不再给程序员一整个任务,而是一大块任务的一小块。接口一定义,虚拟类一定义,换谁上都可以,管你是保罗·格雷厄姆这样的明星程序员,还是一个新来的大学生,程序员不听话就直接开除,反正模块化之后,开除程序员的成本大大降低,这也是为什么谷歌、甲骨文(这货最后收购了Java)一类的公司大规模的推崇Java,还一度提出了模块化人事管理的理念(把人当模块化的积木一样随时移进移出)。

过度企业化后,这延展出了Java的第二个特性,束缚手脚。保罗·格雷厄姆在《黑客与画家》中写道,Java属于B&D(捆绑与束缚)类型的语言。为何束缚手脚?因为要让新手和明星程序员写出类似质量的代码,尽可能的抹消人的才华对程序的影响。不同于C/C++,老手和新手写出的Java代码不会有上百倍的耗时差距。但同样也导致了Java的一个弱点——不容易优化。很多优化Java代码的程序员必须要对JVM(虚拟机)进行优化,实际上增大了很多任务难度。

以上是近期看到的帖子。近期在关注spark,也在看scala语言。推荐大家阅读:

Scala 是一门怎样的语言,具有哪些优缺点?

[技术] Sun May 22 2016 22:51:45 GMT+0800 (中国标准时间)

编写有价值的软件代码

    软件技术是二十一世纪的最普遍的生产工具,各个领域、各个行业都在大面积的使用。我们越来越生活在一个被软件包围的世界。前几天,我欣喜的发现,以后出门可以不带钱包了。因为发现无论超市,还是面包店,还是楼下买菜的小店,都可以用支付宝或者微信支付。软件已经渗透到生活的所有角落。

    来到软院,我教授编程语言(C++、C#、Javascript),软件设计的方法和工具(面向对象分析与设计、数据库、人机交互)。课堂也涉及各种框架和中间件,从Spring、Java EE、Hibernate,到Web Service等等。总结下来,的确学的东西特别多,也有些杂。前端、后端都大概了解;算下编程语言,我写过C、C++、Java、Python、C#、Sql、common lisp、Swift、php、html、css、JavaScript、以及当初用于传感器节点的tinyOS 等NesC语言。编程工具更是数不胜数,想想这些过往,发现自己对编程也真是不反感。在老师的岗位上,更多的时间发在了论文上;但对着新的技术框架,往往很有动力去了解它,跟着tutorial跑example。自娱自乐。一方面为了教学,一方面纯粹为了好奇心。

     比较汗颜的是,我写的代码,大都停留在toy玩具的阶段。我越来越觉得,我们应该让代码能够活动起来。构建一些可以实际使用、实际演示的系统或者软件;让这些软件不断的进化、不断的改进,像一棵树一样的生长起来。我曾经在课堂上问同学:软件行业与其他行业的区别有哪些呢? 同学们都很聪明,给出了很多答案。归结起来,软件其实有一个很基本的特性:它在很大程度上是“软”的,是“柔”的(顾名思义)。一方面,软件是现实世界的虚拟,需求变了,软件也的跟着动。客观上,软件需要不断的更新、重新设计和编写;另一方面,它又像是一个泥人,只要你设计得当,是可以很好的更改它、扩展它,进行拷贝和复用,且几乎没什么成本。现在的软件工程思想,大都围绕这个话题寻求降低软件设计、开发和维护的成本,甚至寻求全自动的、智能的软件开发。通过不断的迭代,不断的优化,软件的目标只有一个:不断的变得更加美好、更加有用。如果你用心,你可以看着一个软件,逐渐逐渐的变好,变得可用,变得易用,变得有价值。(当然,如果不用心,你可以可能看到它不断的衰败,甚至马上被废弃)

     所以,我打算以后让一些项目和代码能具有可持续性。能够有学生和我一起,不断的去维护它。后面的同学可以在前面同学工作的基础上,继续开发。辛苦开发出来的东西,不应该马上就被遗忘和废弃;而不被遗忘的前提,是它应该是有用,是有价值的。所以,以后,我们将尽量做些有价值的事情,编写些有价值的代码,把代码看做自己的孩子。有这种态度,你将会看见它的成长和进步,你将感受到你自己内心的成就感。

[随笔] Wed May 11 2016 22:20:13 GMT+0800 (中国标准时间)

MOCOM平台上线记

     厦门大学移动计算小组(MObile COMputing Group)终于上线了。这将成为MOCOM研究小组的宣传网页,以及之后我所教授课程的主页。

     在慕课(MOOC, Massive Open Online Course)大行其道的今天,一个课程如果没有课程主页实在是说不过去的事情。更何况,这是软件类的课程。2009年我来到软件学院,第二年我就在自己办公室的台式机搭建了一个基于Moodle 的课程平台(那个时候办公室的IP是可被公网访问的)。这个平台一直用到2014年。15年的时候,办公室的IP无法外网访问了,于是搬到学校的课程平台(course.xmu.edu.cn)。但总觉得这些平台有些不是很顺畅,各种不便。既然没有合用的工具,为何不自己做一个呢?这学期刚好要教授软件工程专业的《中间件技术》和数字媒体的《人机交互技术》,前后端的知识都会涉及。于是,萌生了一个构建自己的课程平台和研究小组主页的念头。

    一开始,我想找学生来做。所谓学以致用,边学边做。我的如意算盘是把这个网站作为《中间件》课程期末设计的一个选题,希望有同学报名。感兴趣的同学有一些;但他们一打听,发现网站有不少功能,且不能套用现成的模板(要通过案例来学习嘛),很多都纷纷打了退堂鼓。但事情还是要做的,我觉得这个网站的工作量并不是很大,于是便决定自己做。同时,我也在《人机交互技术》课程招募课外实践的同学。居然有一个同学(易同学)报名了。易同学对网页设计是零基础,但也足以帮助我来做些界面设计。于是,断断续续,改改停停;从服务端到前端,大概1个月之后这个网站基本完工了。

      网站后端采用Nodejs+mongoDB的架构,前端基于Boostrap。简约,量身定做;没有采用第三方的模板,完全是我一个字一个字敲进去的代码。完工了,倒有几个感想。

      首先是一点点的成就感。不管做的好坏,但算是完成了一个任务。犹如把一个钝斧磨锋利了,然后用它做了一个可用的凳子。这种小小的成就,和写个论文,写个项目申请的感觉一致,但还更浓烈一些。我一直以来有一个想法,那就是由于种种巧合,我们学到了这个时代最流行、也最尖端的一把斧子——IT技术。那么,不应该浪费这把斧子。考虑用它做点什么事情吧,除了授课,除了项目论文,真应该做点真正有用或者好玩的东东。这个小网站只是其中很小很小的一步。

     其次,更加了解了当今网页技术的开发和实践现状。网站选择技术,浏览了各类的web服务器,前后端框架。这个过程中,也算是把Javascript完全的熟悉了。之前对js只是了解,写过片段的代码,但这次从后端到前端都用到了js。曾经以为它是奇怪而丑陋的语言,但真正深入进去还是发现了很多的乐趣。语法相似,但它和java C++其实是两个路数,感觉是不太一样。怎么个不一样法呢?留到以后再聊。

     第三,前端的编程是琐碎的事情,但实践的确有利于加深已有的知识。虽然大部分的知识原理都是了解的。CSS、JQuery、Bootstrap,主要就用到了这些。有时候,有些地方并未达到所预期的效果,也时常是挑灯夜调,找bug。通常达到了效果,总有些恍然醒悟之感。前端有各种可选的框架,也有些小的技巧,但找若干合用的工具非常重要(比如写js的WebStorm就是个神器;而直到如何用Chrome来调试也至关重要)。习惯于写后端代码的同学,其实应该谦虚点,发点时间学习下前端的编程。这个领域其实更面对用户,有时候可能还更加重要。

      这个网站和课程主页将不断的完善改进,并让内容变得更加丰富。衷心的希望它可以成为连接我和学生们的一个桥梁。

[技术] Tue May 03 2016 22:27:05 GMT+0800 (中国标准时间)