决策树(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个属性,是在数据挖掘、数据分类中非常常用的测试集、训练集。
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}
首先,读取文本文件;然后,通过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])
... ...
接下来,首先进行数据集的划分,这里划分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
最后,我们把模型预测的准确性打印出来:
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
目前自己写一些东西,都用的是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} $$
给定一个数据集,数据分析师一般会先观察一下数据集的基本情况,称之为汇总统计或者概要性统计。一般的概要性统计用于概括一系列观测值,包括位置或集中趋势(比如算术平均值、中位数、众数和四分位均值),展型(比如四分位间距、绝对偏差和绝对距离偏差、各阶矩等),统计离差,分布的形状,依赖性等。除此之外,spark.mllib库也提供了一些其他的基本的统计分析工具,包括相关性、分层抽样、假设检验,随机数生成等。在本章,我们将从以下几个方面进行介绍:
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
... ...
对于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,相关度量,目前Spark支持两种相关性系数:皮尔逊相关系数(pearson)和斯皮尔曼等级相关系数(spearman)。相关系数是用以反映变量之间相关关系密切程度的统计指标。简单的来说就是相关系数绝对值越大(值越接近1或者-1),当取值为0表示不相关,取值为(0~-1]表示负相关,取值为(0, 1]表示正相关。
Pearson相关系数表达的是两个数值变量的线性相关性, 它一般适用于正态分布。其取值范围是[-1, 1], 当取值为0表示不相关,取值为[-1~0)表示负相关,取值为(0, 1]表示正相关。
Spearman相关系数也用来表达两个变量的相关性,但是它没有Pearson相关系数对变量的分布要求那么严格,另外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)顾名思义,就是将数据根据不同的特征分成不同的组,然后按特定条件从不同的组中获取样本,并重新组成新的数组。分层取样算法是直接集成到键值对类型 RDD[(K, V)] 的 sampleByKey 和 sampleByKeyExact 方法,无需通过额外的 spark.mllib 库来支持。
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进行抽样还是可行的。
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 | 随机数种子 |
四、分布式矩阵(Distributed Matrix)
分布式矩阵由长整型的行列索引值和双精度浮点型的元素值组成。它可以分布式地存储在一个或多个RDD上,MLlib提供了三种分布式矩阵的存储方案:行矩阵RowMatrix
,索引行矩阵IndexedRowMatrix
、坐标矩阵CoordinateMatrix
和分块矩阵Block Matrix
。它们都属于org.apache.spark.mllib.linalg.distributed
包。
行矩阵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
与RowMatrix
相似,但它的每一行都带有一个有意义的行索引值,这个索引值可以被用来识别不同行,或是进行诸如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])
坐标矩阵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),其尺寸由rowsPerBlock
和colsPerBlock
决定,默认值均为1024。分块矩阵支持和另一个分块矩阵进行加法操作和乘法操作,并提供了一个支持方法validate()
来确认分块矩阵是否创建成功。
分块矩阵可由索引行矩阵IndexedRowMatrix
或坐标矩阵CoordinateMatrix
调用toBlockMatrix()
方法来进行转换,该方法将矩阵划分成尺寸默认为1024x1024的分块,可以在调用toBlockMatrix(rowsPerBlock, colsPerBlock)
方法时传入参数来调整分块的尺寸。 下面以矩阵A(如图)为例,先利用矩阵项MatrixEntry
将其构造成坐标矩阵,再转化成如图所示的4个分块矩阵,最后对矩阵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操作,非常耗费资源。所以,根据数据本身的性质和应用需求来选取恰当的分布式矩阵存储类型是非常重要的。
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
为基类,DenseVector
和SparseVector
分别是它的两个实现类,故推荐使用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格式数据的支持,该格式被广泛用于LIBSVM、LIBLINEAR等机器学习库。在该格式下,每一个带标注的样本点由以下格式表示:
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.Matrix
,DenseMatrix
和SparseMatrix
均是它的实现类,和本地向量类似,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。
最近,我们小组在学习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中,也是大势所趋。
MLlib是Spark的机器学习(Machine Learning)库,旨在简化机器学习的工程实践工作,并方便扩展到更大规模。MLlib由一些通用的学习算法和工具组成,包括分类、回归、聚类、协同过滤、降维等,同时还包括底层的优化原语和高层的管道API。具体来说,其主要包括以下几方面的内容:
算法工具:常用的学习算法,如分类、回归、聚类和协同过滤;
特征化公交:特征提取、转化、降维,和选择公交;
管道(Pipeline):用于构建、评估和调整机器学习管道的工具;
持久性:保存和加载算法,模型和管道;
实用工具:线性代数,统计,数据处理等工具。
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的机器学习。
目前针对大学生创新创业、软件外包服务、大数据等方面的赛事越来越多。有全国性的、有区域性的,甚至还有市一级的赛事。最近几年我也带过几届学生,也有幸作为评委参加过几个赛事的评审。因而,时常也有学生来问我一些问题:该如何选题?该怎样在比赛中获得好成绩,并脱颖而出?这些问题都很实际,也很重要。因此,我就结合个人的经验谈下看法。看法仅限于个人的感受,是个人观点,有兴趣的同学可以和我联系并一起深入讨论。
首先是选题和组队,这是赛前应该做好的工作。对于非固定题目的赛事,选题可自己结合热点选一个;也可以和老师沟通联系。这里,需要避免的一个误区是,选题不要都只从自己的身边入手。作为学生,大家的生活圈就这么点,容易雷同。比如,前几届,发现类似校园、课堂方面的题材比较多,也就不太容易形成亮点。当然,这个选题雷同的趋势也有所好转。组队方面,定一个参赛的目标,组成一个有战斗力的队伍,非常关键。组队甚至比选什么题目更重要。一个好的团队去找老师,老师可能会给你一个很不错的题目。在科研实践中,老师经常有一些想法,容易做成demo的那种想法。但苦于没有人可以实现,或者没有一个好的团队帮助实施。因此如果有一个好团队,就成功了一半,就可能把事情做好。好的团队,能够自我激励和管理。理论上,辅导老师一般时间有限,不可能进行事无巨细的指导。因此,团队能否基于目标进行自我激励和自我管理,决定了他们能否产生良好的项目预期。 此外,评审过程中虽然每个赛事的侧重点会有不太一致的地方,大家要先熟悉各个赛事的评价标准。但从各个赛事的评分项来看,其实大同小异,最后都落实到评委的综合判断上。在复赛的时候,一般上时间都非常紧,不太可能完全严格的按照评分项来计算分数。因此,一个项目的总体创新性、完整性,总体上的好印象是非常关键的。那么,有哪些地方需要注意的呢?以下我罗列了一些,重要性不分先后。大家可以逐条思考,逐条批判:)
另外,在初审后还可以进一步改进的赛事(如intel杯),持续改进是关键。比如,在某次评审中,发现初赛排名前10的队伍居然只有一支队伍进入了决赛。复赛中真正脱引而出的,大部分是10-30名的队伍,因为他们在后期做了更多的改进。同时,复赛时的现场演示,一般用采用的是ppt,有严格的时间限制。PPT是否用心,是否做了大量的前期准备,评委都可以看得到。现场演示时候,最好完全准备好,避免关键时候掉链子。
最后,祝大家比赛顺利,拿到理想的成绩。也欢迎有志于课外实践和比赛的同学和我联系(邮件即可)。
这是之前写的一篇论文,月饼门也已经慢慢冷却了,快被遗忘了。但仍旧发出来吧。
最近,有一件事情闹得沸沸扬扬,即阿里的“月饼门”事件。相比其他行业,技术界的新闻一般都弱弱的。这次居然也让大家谈论起来,实在值得稍微研究一下。作为软院的小伙伴,可能后知后觉。但如果不知道此“门”为何物,赶紧baidu并面壁思过去吧。
事情是这样:
9月12日,阿里巴巴在内部搞了一个中秋抢月饼的活动,不过安全部门的4名员工却写了个脚本自动抢月饼,不动声色地刷了124盒月饼。随后阿里巴巴作出决定,开除4名涉事员工。
随后网络新闻和评论劈天盖地。大家关心的问题集中在:阿里作为公司,这样做对不对,解除4名员工合法吗?被解雇的员工是否很冤枉,他们伤害到其他人的利益了吗?抢月饼之前,阿里有规定不能用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);
}
}
}
在每年的本科生导师见面会前后,我都要关心下所谓的“被调剂学生数“。这个数字,大体体现了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的同学,知道我在讲什么。↩
我所教授的编程类课程(比如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,学习的效果同行大打折扣。这也可以验证那个朴素的真理:
真正的编程高手,都是自学而来的;真正喜欢的事情,读它千遍也不厌倦。
这个网站是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语言。推荐大家阅读:
软件技术是二十一世纪的最普遍的生产工具,各个领域、各个行业都在大面积的使用。我们越来越生活在一个被软件包围的世界。前几天,我欣喜的发现,以后出门可以不带钱包了。因为发现无论超市,还是面包店,还是楼下买菜的小店,都可以用支付宝或者微信支付。软件已经渗透到生活的所有角落。
来到软院,我教授编程语言(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玩具的阶段。我越来越觉得,我们应该让代码能够活动起来。构建一些可以实际使用、实际演示的系统或者软件;让这些软件不断的进化、不断的改进,像一棵树一样的生长起来。我曾经在课堂上问同学:软件行业与其他行业的区别有哪些呢? 同学们都很聪明,给出了很多答案。归结起来,软件其实有一个很基本的特性:它在很大程度上是“软”的,是“柔”的(顾名思义)。一方面,软件是现实世界的虚拟,需求变了,软件也的跟着动。客观上,软件需要不断的更新、重新设计和编写;另一方面,它又像是一个泥人,只要你设计得当,是可以很好的更改它、扩展它,进行拷贝和复用,且几乎没什么成本。现在的软件工程思想,大都围绕这个话题寻求降低软件设计、开发和维护的成本,甚至寻求全自动的、智能的软件开发。通过不断的迭代,不断的优化,软件的目标只有一个:不断的变得更加美好、更加有用。如果你用心,你可以看着一个软件,逐渐逐渐的变好,变得可用,变得易用,变得有价值。(当然,如果不用心,你可以可能看到它不断的衰败,甚至马上被废弃)
所以,我打算以后让一些项目和代码能具有可持续性。能够有学生和我一起,不断的去维护它。后面的同学可以在前面同学工作的基础上,继续开发。辛苦开发出来的东西,不应该马上就被遗忘和废弃;而不被遗忘的前提,是它应该是有用,是有价值的。所以,以后,我们将尽量做些有价值的事情,编写些有价值的代码,把代码看做自己的孩子。有这种态度,你将会看见它的成长和进步,你将感受到你自己内心的成就感。
厦门大学移动计算小组(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来调试也至关重要)。习惯于写后端代码的同学,其实应该谦虚点,发点时间学习下前端的编程。这个领域其实更面对用户,有时候可能还更加重要。
这个网站和课程主页将不断的完善改进,并让内容变得更加丰富。衷心的希望它可以成为连接我和学生们的一个桥梁。