ml基于DatafrmaeAPI
spark.mllib
以及错误修复。向量
Local vector本地向量,主要向Spark提供一组可进行操作的数据集合
Labled Point标签向量,让用户能够分类不同的数据集合
通过指定Vectors给定dense或sparse等向量
从mllib.regression.LabeledPoint中获取lablepoint通过该方法给特征进行标签赋值
Spark读取libsvm格式数据
https://www.csie.ntu.edu.tw/~cjlin/libsvm/
鸢尾花-----花瓣的长度和宽度、花萼的长度和宽度
鸢尾花几种类别—三种类别—setosa、versicolor、vernica
1 1:-0.555556 4:-0.916667
1 1:-0.666667 2:-0.166667 3:-0.864407 4:-0.916667
1 1:-0.777778 3:-0.898305 4:-0.916667
1 1:-0.833333 2:-0.0833334 3:-0.830508 4:-0.916667
1 1:-0.611111 2:0.333333 3:-0.864407 4:-0.916667
1 1:-0.388889 2:0.583333 3:-0.762712 4:-0.75
1 1:-0.833333 2:0.166667 3:-0.864407 4:-0.833333
1 1:-0.611111 2:0.166667 3:-0.830508 4:-0.916667
1 1:-0.944444 2:-0.25 3:-0.864407 4:-0.916667
1 1:-0.666667 2:-0.0833334 3:-0.830508 4:-1
1 1:-0.388889 2:0.416667 3:-0.830508 4:-0.916667
1 1:-0.722222 2:0.166667 3:-0.79661 4:-0.916667
1 1:-0.722222 2:-0.166667 3:-0.864407 4:-1
1 1:-1 2:-0.166667 3:-0.966102 4:-1
1 1:-0.166667 2:0.666667 3:-0.932203 4:-0.916667
1 1:-0.222222 2:1 3:-0.830508 4:-0.75
1 1:-0.388889 2:0.583333 3:-0.898305 4:-0.75
libsvm格式非常适合于稀疏性数据(0值元素较多非0元素较少的情况)
SparkMLlib读取LibSvm数据,libsvm数据格式为:Label index1:value1 index2:value2 …
2 1:5 2:8 3:9(索引从1开始,从0开始会报错)
1 1:7 2:6 3:7
1 1:3 2:2 3:1
(2.0列别标签,(9元素个数,[0,1,2]下标,[5.0,8.0,9.0]值))
(1.0,(9,[0,1,2],[7.0,6.0,7.0]))
(1.0,(9,[0,1,2],[3.0,2.0,1.0]))
(2.0,(9,[0,1,2],[5.0,8.0,9.0]))
矩阵
LocalMatrix–本地矩阵
val dm: Matrix = Matrices.dense(3, 2, Array(1.0, 3.0, 5.0, 2.0, 4.0, 6.0))
//dense(行数 , 列数 , 数组())
println(dm(2,0))
// Create a sparse matrix ((9.0, 0.0), (0.0, 8.0), (0.0, 6.0))
val sm: Matrix = Matrices.sparse(3, 2, Array(0, 1, 3), Array(0, 2, 1), Array(9, 6, 8))
println(sm(2,1))//6
println(sm(2,0))//0
//sparse(行数,列数,数组1--列索引(),数组2--行索引(),数组3--数值())
//没有的为0
提取(抽取) : 从"原始"数据中提取要素
转换 : 缩放 , 转换或修改功能
选择 : 从更大的功能集中选择子集
TF-IDF–适用于文本分析----自然语言的文本处理之中 ---- 常用与基于内容的推荐
TF-IDF可以筛选掉不是非常常用的关键词
Word2Vec词向量工具----一个词能够使用不同词向量表示
[[0.03173386193811894,0.009443491697311401,0.024377789348363876]]
建议自学word2vec原理
word2vec缺陷
1是词语数量较大时,向量维度高且稀疏,向量矩阵巨大而难以存储
2是向量并不包含单词的语义内容,只是基于数量统计。
3是当有新的词加入语料库后,整个向量矩阵需要更新
利用skip-gram算法克服上述三个缺陷
隐藏层 : 根据最终需要获取的词向量维数决定隐藏神经元就是多少个
skip-gram算法构造的神经网络神经元太多了,导致权重矩阵非常大 , 导致数据难以训练
负抽样 : 主要解决模型难以训练
使用SGD(最速下降法)训练神经网络的过程就是抽取一条样本数据 , 然后据此去调整神经网络的所有权重
==>所有权重导致数据难以训练
负抽样解决方法就是使得对每一条样本的每一次训练 , 只更新很小的一部分的权重, 而不是全部更新
让我们用一个例子来具体感受一下。假设我们负抽样的维度数为5,我们的词表中有10000个单词,词向量的维度为300,也即是隐藏层有300个神经元。
那么,在输出层,权重矩阵的大小将是300*10000。现在我们抽取了5个负的维度(输出应当为0的维度),加上输出为1的维度,只更新这6个维度所对应的神经元。那么需要更新的权重系数是300*6=1800个。这只占输出层中所有权重系数的0.06%!!
为什么是300*6?????
如何抽样?
* P(w_i)就是w_i这个单词被负抽样抽中的概率。f(w_i)即是w_i在语料库中出现的次数。
* 至于为什么要取一次3/4次方,据说是基于经验,这样效果会更好。
CountVector–处理文本中的词频,按照词频进行原文本的排序(按照原文本顺序?次数排序?)
CountVectorizer
并CountVectorizerModel
旨在帮助将一组文本文档转换为令牌计数的向量。当apriori字典不可用时,CountVectorizer
可以用作Estimator
提取词汇表,并生成一个CountVectorizerModel
。该模型为词汇表上的文档生成稀疏表示,然后可以将其传递给其他算法,如LDA。CountVectorizer
将选择vocabSize
按语料库中的术语频率排序的顶部单词。可选参数minDF
还通过指定术语必须出现在文档中的最小数量(或<1.0)来影响拟合过程。另一个可选的二进制切换参数控制输出向量。如果设置为true,则所有非零计数都设置为1.这对于模拟二进制而非整数计数的离散概率模型特别有用。n-gram中的n就是选取出现频率较多的n个词来进行匹配
二值化
import org.apache.spark.ml.feature.Binarizer
import org.apache.spark.sql.SparkSession
object Binaziner_3 {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.appName("SparkMlilb")
.master("local[2]")
.getOrCreate()
spark.sparkContext.setLogLevel("WARN")
val data = Array((0, 0.1), (1, 0.8), (2, 0.2))
val dataFrame = spark.createDataFrame(data).toDF("label", "feature")
val binarizer: Binarizer = new Binarizer()
.setInputCol("feature")
.setOutputCol("binarized_feature")
.setThreshold(0.5)
//小于0.5为0 , 大于0.5为1
val binarizedDataFrame = binarizer.transform(dataFrame)
val binarizedFeatures = binarizedDataFrame.select("binarized_feature")
binarizedFeatures.collect().foreach(println)
}
}
//输出
//[0.0]
//[1.0]
//[0.0]
stringtoindexer : 将标签的字符串列编码为标签索引项 . 索引[0, numLabels)
按标签频率排序,因此最常用的标签获得索引0
。
import org.apache.spark.ml.feature.StringIndexer
import org.apache.spark.sql.SparkSession
object StringIndex {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.appName("SparkMlilb")
.master("local[2]")
.getOrCreate()
spark.sparkContext.setLogLevel("WARN")
val df = spark.createDataFrame(
Seq((0, "a"), (1, "b"), (2, "c"), (3, "a"), (4, "a"), (5, "c"))
).toDF("id", "category")
val indexer = new StringIndexer()
.setInputCol("category")
.setOutputCol("categoryIndex")
val indexed = indexer.fit(df).transform(df)
indexed.show()
//输出
// +---+--------+-------------+
// | id|category|categoryIndex|
// +---+--------+-------------+
// | 0| a| 0.0|
// | 1| b| 2.0|
// | 2| c| 1.0|
// | 3| a| 0.0|
// | 4| a| 0.0|
// | 5| c| 1.0|
// +---+--------+-------------+
}
}
indextoString : 一列标签索引映射回包含原始标签作为字符串的列。一个常见的用例是从标签生成索引StringIndexer
,使用这些索引训练模型,并从预测索引列中检索原始标签IndexToString
。但是,可以自由提供自己的标签。
StandardScaler : StandardScaler
转换Vector
行的数据集,将每个要素标准化以具有单位标准差和/或零均值。
需要的参数 :
withStd
:默认为True。将数据缩放到单位标准偏差。withMean
:默认为False。在缩放之前使用均值将数据居中。它将构建一个密集的输出,因此这不适用于稀疏输入并将引发异常。import org.apache.spark.sql.SparkSession
object StandScater {
def main(args: Array[String]): Unit = {
import org.apache.spark.ml.feature.StandardScaler
val spark: SparkSession = SparkSession.builder()
.appName("SparkMlilb")
.master("local[2]")
.getOrCreate()
spark.sparkContext.setLogLevel("WARN")
val dataFrame = spark.read.format("libsvm").load("I:\\ideaworkspace\\Saprk_Test_ML\\src\\main\\scala\\cn\\apple\\BigDataMachineLearningPro\\sample_libsvm_data.txt")
val scaler = new StandardScaler()
.setInputCol("features")
.setOutputCol("scaledFeatures")
.setWithStd(true)
.setWithMean(false)
// Compute summary statistics by fitting the StandardScaler.
val scalerModel = scaler.fit(dataFrame)
// Normalize each feature to have unit standard deviation.
val scaledData = scalerModel.transform(dataFrame)
scaledData.show()
// +-----+--------------------+--------------------+
// |label| features| scaledFeatures|
// +-----+--------------------+--------------------+
// | 0.0|(692,[127,128,129...|(692,[127,128,129...|
// | 1.0|(692,[158,159,160...|(692,[158,159,160...|
// | 1.0|(692,[124,125,126...|(692,[124,125,126...|
// | 1.0|(692,[152,153,154...|(692,[152,153,154...|
// | 1.0|(692,[151,152,153...|(692,[151,152,153...|
// | 0.0|(692,[129,130,131...|(692,[129,130,131...|
// | 1.0|(692,[158,159,160...|(692,[158,159,160...|
// | 1.0|(692,[99,100,101,...|(692,[99,100,101,...|
// | 0.0|(692,[154,155,156...|(692,[154,155,156...|
// | 0.0|(692,[127,128,129...|(692,[127,128,129...|
// | 1.0|(692,[154,155,156...|(692,[154,155,156...|
// | 0.0|(692,[153,154,155...|(692,[153,154,155...|
// | 0.0|(692,[151,152,153...|(692,[151,152,153...|
// | 1.0|(692,[129,130,131...|(692,[129,130,131...|
// | 0.0|(692,[154,155,156...|(692,[154,155,156...|
// | 1.0|(692,[150,151,152...|(692,[150,151,152...|
// | 0.0|(692,[124,125,126...|(692,[124,125,126...|
// | 0.0|(692,[152,153,154...|(692,[152,153,154...|
// | 1.0|(692,[97,98,99,12...|(692,[97,98,99,12...|
// | 1.0|(692,[124,125,126...|(692,[124,125,126...|
// +-----+--------------------+--------------------+
// only showing top 20 rows
}
}
Bucketizer 分箱 , 分桶 : Bucketizer
将一列连续特征转换为一列要素存储区,其中存储区由用户指定。它需要一个参数:
splits
:用于将连续要素映射到存储桶的参数。对于n +1个分裂,有n个桶。由splits x,y定义的存储区包含除最后一个存储区之外的[x,y]范围内的值,该存储区还包括y。
import org.apache.spark.sql.SparkSession
object Bucketizer {
def main(args: Array[String]): Unit = {
import org.apache.spark.ml.feature.Bucketizer
val spark: SparkSession = SparkSession.builder()
.appName("SparkMlilb")
.master("local[2]")
.getOrCreate()
spark.sparkContext.setLogLevel("WARN")
val splits = Array(Double.NegativeInfinity, -0.5, 0.0, 0.5, Double.PositiveInfinity)
val data = Array(-0.5, -0.3, 0.0, 0.2)
val dataFrame = spark.createDataFrame(data.map(Tuple1.apply)).toDF("features")
val bucketizer = new Bucketizer()
.setInputCol("features")
.setOutputCol("bucketedFeatures")
.setSplits(splits)
// Transform original data into its bucket index.
val bucketedData = bucketizer.transform(dataFrame)
bucketedData.show()
// +--------+----------------+
// |features|bucketedFeatures|
// +--------+----------------+
// | -0.5| 1.0|
// | -0.3| 1.0|
// | 0.0| 2.0|
// | 0.2| 2.0|
// +--------+----------------+
}
}
//结果解释 : -0.5和-0.3都在1号桶 , 0和0.2在2号桶
//桶的编号从0开始???
SQLTransformer
import org.apache.spark.ml.feature.SQLTransformer
import org.apache.spark.sql.SparkSession
object SqlTransformer {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.appName("SparkMlilb")
.master("local[2]")
.getOrCreate()
spark.sparkContext.setLogLevel("WARN")
val df = spark.createDataFrame(
Seq((0, 1.0, 3.0), (2, 2.0, 5.0))).toDF("id", "v1", "v2")
df.show()
// +---+---+---+
// | id| v1| v2|
// +---+---+---+
// | 0|1.0|3.0|
// | 2|2.0|5.0|
// +---+---+---+
val sqlTrans = new SQLTransformer().setStatement(
"SELECT *, (v1 + v2) AS v3, (v1 * v2) AS v4 FROM __THIS__")
sqlTrans.transform(df).show()
// +---+---+---+---+----+
// | id| v1| v2| v3| v4|
// +---+---+---+---+----+
// | 0|1.0|3.0|4.0| 3.0|
// | 2|2.0|5.0|7.0|10.0|
// +---+---+---+---+----+
}
}
//结果解读 : 将低维的数据进行增加维度 , 是对原来维度的整合 , 特征融合
QuantileDiscretizer : 连续属性离散化 —分位数
QuantileDiscretizer
采用具有连续特征的列,并输出具有分箱分类特征的列。箱数由numBuckets
参数设定。
import org.apache.spark.sql.SparkSession
object QuantileDiscreator {
def main(args: Array[String]): Unit = {
import org.apache.spark.ml.feature.QuantileDiscretizer
val spark: SparkSession = SparkSession.builder()
.appName("SparkMlilb")
.master("local[2]")
.getOrCreate()
spark.sparkContext.setLogLevel("WARN")
val data = Array((0, 18.0), (1, 19.0), (2, 8.0), (3, 5.0), (4, 2.2))
var df = spark.createDataFrame(data).toDF("id", "hour")
val discretizer = new QuantileDiscretizer()
.setInputCol("hour")
.setOutputCol("result")
.setNumBuckets(3)
val result = discretizer.fit(df).transform(df)
result.show()
// +---+----+------+
// | id|hour|result|
// +---+----+------+
// | 0|18.0| 2.0|
// | 1|19.0| 2.0|
// | 2| 8.0| 1.0|
// | 3| 5.0| 1.0|
// | 4| 2.2| 0.0|
// +---+----+------+
}
}
特征选择VectorSlicer是一个变换器 , 它采用一个特征向量 ,并输出一个带有原始特征子阵列的新特征向量。
import org.apache.spark.ml.attribute.{Attribute, AttributeGroup, NumericAttribute}
import org.apache.spark.ml.feature.VectorSlicer
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.sql.types.StructType
object VectorSlicer_4 {
def main(args: Array[String]): Unit = {
import java.util.Arrays
val spark: SparkSession = SparkSession.builder()
.appName("SparkMlilb")
.master("local[2]")
.getOrCreate()
spark.sparkContext.setLogLevel("WARN")
val data = Arrays.asList(Row(Vectors.dense(-2.0, 2.3, 0.0)))
val defaultAttr = NumericAttribute.defaultAttr
val attrs = Array("f1", "f2", "f3").map(defaultAttr.withName)
val attrGroup = new AttributeGroup("userFeatures", attrs.asInstanceOf[Array[Attribute]])
val dataset = spark.createDataFrame(data, StructType(Array(attrGroup.toStructField())))
val slicer = new VectorSlicer().setInputCol("userFeatures").setOutputCol("features")
slicer.setIndices(Array(1)).setNames(Array("f3"))
//or slicer.setIndices(Array(1, 2)), or slicer.setNames(Array("f2", "f3"))
val output = slicer.transform(dataset)
println(output.select("userFeatures", "features").first())
//[[-2.0,2.3,0.0],[2.3,0.0]]
}
}
RFormula根据R公式
RFormula选择由R模型公式指定的列。目前,我们支持R运算符的有限子集,包括'〜','。',':','+'和' - '。基本的运营商是:
• ~ 单独的目标和条款
• + concat术语,“+ 0”表示删除拦截
• - 删除一个术语,“ - 1”表示删除拦截
• : 交互(数值乘法或二进制分类值)
• . 除目标之外的所有列
import org.apache.spark.ml.feature.RFormula
import org.apache.spark.sql.SparkSession
object RSelectElement {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.appName("SparkMlilb")
.master("local[2]")
.getOrCreate()
spark.sparkContext.setLogLevel("WARN")
val dataset = spark.createDataFrame(Seq(
(7, "US", 18, 1.0),
(8, "CA", 12, 0.0),
(9, "NZ", 15, 0.0)
)).toDF("id", "country", "hour", "clicked")
val formula = new RFormula()
.setFormula("clicked ~ country + hour")
.setFeaturesCol("features")
.setLabelCol("label")
val output = formula.fit(dataset).transform(dataset)
output.select("features", "label").show()
// +--------------+-----+
// | features|label|
// +--------------+-----+
// |[0.0,0.0,18.0]| 1.0|
// |[1.0,0.0,12.0]| 0.0|
// |[0.0,1.0,15.0]| 0.0|
// +--------------+-----+
}
}
卡方验证Chisquare
ChiSqSelector
代表Chi-Squared特征选择。它使用具有分类特征的标记数据进行操作。ChiSqSelector根据 类的独立性Chi-Squared测试来命令特征 ,然后过滤(选择)类标签最依赖的顶级特征。这类似于产生具有最强预测能力的特征。
import org.apache.spark.ml.feature.ChiSqSelector
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.sql.SparkSession
object chiSquare {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.appName("SparkMlilb")
.master("local[2]")
.getOrCreate()
spark.sparkContext.setLogLevel("WARN")
val data = Seq(
(7, Vectors.dense(0.0, 0.0, 18.0, 1.0), 1.0),
(8, Vectors.dense(0.0, 1.0, 12.0, 0.0), 0.0),
(9, Vectors.dense(1.0, 0.0, 15.0, 0.1), 0.0)
)
val df = spark.createDataset(data).toDF("id", "features", "clicked")
val selector = new ChiSqSelector()
.setNumTopFeatures(1)
.setFeaturesCol("features")
.setLabelCol("clicked")
.setOutputCol("selectedFeatures")
val result = selector.fit(df).transform(df)
result.show()
}
}
@Since("0.8.0")
case class Rating @Since("0.8.0") (
@Since("0.8.0") user: Int,
@Since("0.8.0") product: Int,
@Since("0.8.0") rating: Double)
package com.itck.als
import org.apache.spark.metrics.source
import org.apache.spark.{SparkConf, SparkContext}
/**
* 余弦相似度
*/
object ConsineSimilar {
val conf = new SparkConf()
.setAppName("ConsineSimilar")
.setMaster("local")
val sc = new SparkContext(conf)
//实例化环境
val users = sc.parallelize(Array("aaa", "bbb", "ccc", "ddd", "eee"))
//设置电影名
val films = sc.parallelize(Array("smzdm", "yixb", "znh", "nhsc", "fcwr"))
//使用一个 source 嵌套 map 作为姓名电影名和分值的存储
var source = Map[String, Map[String, Int]]()
//设置一个用以存放电影分的 map
val filmSource = Map[String, Int]()
/**
* 设置电影评分
*
* @return
*/
def getSource(): Map[String, Map[String, Int]] = {
val user1FilmSource = Map("smzdm" -> 2, "yixb" -> 3, "znh" -> 1, "nhsc" -> 0, "fcwr" -> 1)
val user2FilmSource = Map("smzdm" -> 1, "yixb" -> 2, "znh" -> 2, "nhsc" -> 1, "fcwr" -> 4)
val user3FilmSource = Map("smzdm" -> 2, "yixb" -> 1, "znh" -> 0, "nhsc" -> 1, "fcwr" -> 4)
val user4FilmSource = Map("smzdm" -> 3, "yixb" -> 2, "znh" -> 0, "nhsc" -> 5, "fcwr" -> 3)
val user5FilmSource = Map("smzdm" -> 5, "yixb" -> 3, "znh" -> 1, "nhsc" -> 1, "fcwr" -> 2)
source += ("aaa" -> user1FilmSource) //对人名进行存储
source += ("bbb" -> user2FilmSource) //对人名进行存储
source += ("ccc" -> user3FilmSource) //对人名进行存储
source += ("ddd" -> user4FilmSource) //对人名进行存储
source += ("eee" -> user5FilmSource) //对人名进行存储
source //返回 map
}
/**
* 计算余弦相似性
*
* @param user1
* @param user2
* @return
*/
def getCollaborateSource(user1: String, user2: String): Double = {
//获得第一个用户的评分
val user1FilmSource = source.get(user1).get.values.toVector
//获得第二个用户的评分
val user2FileSource = source.get(user2).get.values.toVector
//对欧几里得公式分子部分进行计算
val member =
user1FilmSource.zip(user2FileSource).map(num => num._1 * num._2).reduce(_ + _).toDouble
//求出分母第一个变量的值
val temp1 = math.sqrt(user1FilmSource.map(num => {
math.pow(num, 2)
}).reduce(_ + _)).toDouble
//求出分母第二个变量的值
val temp2 = math.sqrt(user2FileSource.map(num => {
math.pow(num, 2)
}).reduce(_ + _)).toDouble
//求出分母
val denominator = temp1 * temp2
//返回结果
member / denominator
}
def main(args: Array[String]): Unit = {
//初始化分数
getSource()
//设定目标对象
val name = "bbb"
//迭代进行计算
users.foreach(user => {
println(name + " 相对于 " + user + "的相似性分数为: " + getCollaborateSource(name, user))
})
val frist = users.sortBy((user => getCollaborateSource(name, user)), false, 1).first()
println("-----------------------------------------------------------")
println("相似度最高的用户为:" + frist)
/**
* 计算结果如下:
* bbb 相对于 aaa 的相似性分数为: 0.7089175569585667
* bbb 相对于 bbb 的相似性分数为: 1.0000000000000002
* bbb 相对于 ccc 的相似性分数为: 0.8780541105074453
* bbb 相对于 ddd 的相似性分数为: 0.6865554812287477
* bbb 相对于 eee 的相似性分数为: 0.6821910402406466
*/
}
}
import org.apache.spark.mllib.recommendation.{ALS, Rating}
import org.apache.spark.{SparkConf, SparkContext}
object CollaborativeFilter {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("CollaborativeFilter")
.setMaster("local")
val sc = new SparkContext(conf)
//设置数据集
val data = sc.textFile("..\\d.txt")
//处理数据
val ratings = data.map(_.split(" ") match {
case Array(user, item, rate) => //转化数据集
Rating(user.toInt, item.toInt, rate.toDouble) //将数据集转化为专用的 Rating
})
val rank = 2 //设置隐藏因子
val numIterations = 2 //设置迭代次数
val model = ALS.train(ratings, rank, numIterations, 0.01) //进行模型训练
val rs = model.recommendProducts(2, 1) //为用户 2 推荐一个商品
rs.foreach(println) //打印推荐结果
val result: Double = rs(0).rating //预测的评分结果
val realilty = data.map(_.split(" ") match {
case Array(user, item, rate) =>
Rating(user.toInt, item.toInt, rate.toDouble)
}).map(num => {
if (num.user == 2 && num.product == 15)
num.rating //返回实际评分结果
else
0
}).foreach(num => {
if (num != 0)
println("对 15 号商品预测的准确率为:" + (1 - (math.abs(result - num) / 1)))
})
}
}
交替最小二乘法是对最小二乘法处理多个变量时的扩展
了解ALS算法源码部分
ALS算法并行化
https://issues.apache.org/jira/browse/SPARK-3066
http://www.csdn.net/article/2015-05-07/2824641
https://blog.csdn.net/buptfanrq/article/details/73299116
SparkMLlib&ALS