机器学习是通过算法对训练数据构建出模型并对模型进行评估,评估的性能如果达到要求就拿这个模型来测试其他的数据,如果达不到要求就要调整算法来重新建立模型,再次进行评估,如此循环往复,最终获得满意的经验来处理其他的数据的过程。
简单点讲,机器学习就是通过一定的模型,让计算机可以从大量的数据中学习到相关的知识,然后利用学习来的知识来预测以后的未知事物。
首先看一下MLlib的构成:
MLlib包涉及数据科学任务的众多方面,其核心功能涉及以下三个方面:
1、数据准备:包括用于特征提取和变换、分类特征的散列和导入预言模型标记语言构建的模型
2、常见算法:包括流行的回归、频繁模式挖掘、分类和聚类算法
3、实用功能:实现了常用的统计方法和模型评估方法
MLlib主要是为RDD和Dstream设计的,我们这里为了便于数据的转换,将数据格式转换成DataFrame格式。在Spark SQL中,我们了解了创建DataFrame的两种方式,这里采用指定数据集schema的方式。
案列:
1.加载数据
数据地址:
链接:https://pan.baidu.com/s/1xgSiJZyWeb9km-GJP7ZVmQ
提取码:pu0f
对于DataFrame的格式,我们先指定Schema格式,
labels变量是指定的数据属性和数据类型组成元组的列表
schema为数据架构,即结构
births为读取的csv文件数据
指定的格式是在对csv文件中的数据进行研读的基础上设计的
header 参数指定为True 表示源文件中有头信息,也就是有属性行,我们用schema 指定数据的正确类型。
from pyspark.sql import SparkSession
from pyspark.sql.types import *
#选择标签
labels = [
('INFANT_ALIVE_AT_REPORT', StringType()),
('BIRTH_YEAR', IntegerType()),
('BIRTH_MONTH', IntegerType()),
('BIRTH_PLACE', StringType()),
('MOTHER_AGE_YEARS', IntegerType()),
('MOTHER_RACE_6CODE', StringType()),
('MOTHER_EDUCATION', StringType()),
('FATHER_COMBINED_AGE', IntegerType()),
('FATHER_EDUCATION', StringType()),
('MONTH_PRECARE_RECODE', StringType()),
('CIG_BEFORE', IntegerType()),
('CIG_1_TRI', IntegerType()),
('CIG_2_TRI', IntegerType()),
('CIG_3_TRI', IntegerType()),
('MOTHER_HEIGHT_IN', IntegerType()),
('MOTHER_BMI_RECODE', IntegerType()),
('MOTHER_PRE_WEIGHT', IntegerType()),
('MOTHER_DELIVERY_WEIGHT', IntegerType()),
('MOTHER_WEIGHT_GAIN', IntegerType()),
('DIABETES_PRE', StringType()),
('DIABETES_GEST', StringType()),
('HYP_TENS_PRE', StringType()),
('HYP_TENS_GEST', StringType()),
('PREV_BIRTH_PRETERM', StringType()),
('NO_RISK', StringType()),
('NO_INFECTIONS_REPORTED', StringType()),
('LABOR_IND', StringType()),
('LABOR_AUGM', StringType()),
('STEROIDS', StringType()),
('ANTIBIOTICS', StringType()),
('ANESTHESIA', StringType()),
('DELIV_METHOD_RECODE_COMB', StringType()),
('ATTENDANT_BIRTH', StringType()),
('APGAR_5', IntegerType()),
('APGAR_5_RECODE', StringType()),
('APGAR_10', IntegerType()),
('APGAR_10_RECODE', StringType()),
('INFANT_SEX', StringType()),
('OBSTETRIC_GESTATION_WEEKS', IntegerType()),
('INFANT_WEIGHT_GRAMS', IntegerType()),
('INFANT_ASSIST_VENTI', StringType()),
('INFANT_ASSIST_VENTI_6HRS', StringType()),
('INFANT_NICU_ADMISSION', StringType()),
('INFANT_SURFACANT', StringType()),
('INFANT_ANTIBIOTICS', StringType()),
('INFANT_SEIZURES', StringType()),
('INFANT_NO_ABNORMALITIES', StringType()),
('INFANT_ANCEPHALY', StringType()),
('INFANT_MENINGOMYELOCELE', StringType()),
('INFANT_LIMB_REDUCTION', StringType()),
('INFANT_DOWN_SYNDROME', StringType()),
('INFANT_SUSPECTED_CHROMOSOMAL_DISORDER', StringType()),
('INFANT_NO_CONGENITAL_ANOMALIES_CHECKED', StringType()),
('INFANT_BREASTFED', StringType())
]
schema = StructType([StructField(e[0],e[1],False) for e in labels])
#读取csv要根据标签对应
births = spark.read.csv("data/births_train.csv",header=True,schema=schema)
由于特征太多,去除不需要的,我们的目标是预测 ‘INFANT_ALIVE_AT_REPORT’ 是 1 or 0,婴儿是否存活。
因此,我们要去除其他与婴儿无关的特征。利用.select()方法提取与预测指标相关的列。
selected_feas = ["INFANT_ALIVE_AT_REPORT",
"BIRTH_PLACE",
"MOTHER_AGE_YEARS",
"FATHER_COMBINED_AGE",
"CIG_BEFORE",
"CIG_1_TRI",
"CIG_2_TRI",
"CIG_3_TRI",
"MOTHER_HEIGHT_IN",
"MOTHER_PRE_WEIGHT",
"MOTHER_DELIVERY_WEIGHT",
"MOTHER_WEIGHT_GAIN",
"DIABETES_PRE",
"DIABETES_GEST",
"HYP_TENS_PRE",
"HYP_TENS_GEST",
"PREV_BIRTH_PRETERM",
]
births_trim = births.select(selected_feas)
此处我们需要做一个特征字典映射:
0意味着母亲在怀孕前或怀孕期间不抽烟;
1-97表示抽烟的实际人数,
98表示98或更多;
而99表示未知,我们将假设未知是0并相应地重新编码。
recode()方法从recode_dictionary中返回key对应的值,
correct_cig方法检查特征feat的值何时不等于99,若不等于99,则返回特征的值;如果这个值等于99,则返回0。
我们不能直接在DataFrame上使用recode函数;它需要转换为Spark理解的UDF。User Define Function, 用户自定义函数,简称UDF,用户可以在Spark SQL 里自定义实际需要的UDF来处理数据。
rec_integer函数:通过传入我们指定的recode函数并指定返回值数据类型,我们可以使用rec_integer做字典映射,传入参数为recode函数并指定返回值数据类型。
import pyspark.sql.functions as func
def recode(col,key):
return recode_dictionary[key][col]
# correct_cig方法检查特征feat的值何时不等于99不等于99,则返回特征的值,等于99,则返回0
def correct_cig(feat):
return func.when(func.col(feat) != 99, func.col(feat)).otherwise(0)
#更正与吸烟相关的特征.withColumn(…) 方法第一个参数是新列名,第二个参数是指定原数据的某列。
births_transformed = births_trim.withColumn("CIG_BEFORE",correct_cig("CIG_BEFORE")) \
.withColumn("CIG_1_TRI",correct_cig("CIG_1_TRI")) \
.withColumn("CIG_2_TRI",correct_cig("CIG_2_TRI")) \
.withColumn("CIG_3_TRI",correct_cig("CIG_3_TRI"))
#阅读数据时会发现数据集中有很多特征是字符串,进行机器学习前需要将其转换为数值形式,经过阅读数据,可以发现数据中的字符串以U/N/Y三种数值存在依次表示unknown,no,yes,因此这里可以采用字典映射的方式。
recode_dictionary = {"YNU":{"Y":1,"N":0,"U":0}}
#transform the function recode into UDF which could be used in spark
rec_integer = func.udf(recode,IntegerType())
#然后找出哪些特征是Yes/No/Unknown :
# 我们先创建一个元素为包含列名和数据类型元祖(cols)的列表
# 遍历这个列表,计算所有字符串列的不同值,如果Y在返回的列表中,将列名追加到YNU_cols列表。
#将字符型UYN改为数值型
cols = [(col.name, col.dataType) for col in births_trim.schema]
YNU_cols = []
for i,s in enumerate(cols):
if s[1] == StringType():
dis = births.select(s[0]).distinct().rdd.map(lambda x:x[0]).collect()
if "Y" in dis:
YNU_cols.append(s[0])
#批量特征转换
exprs_YNU = [rec_integer(x,func.lit("YNU")).alias(x) if x in YNU_cols else x for x in births_transformed.columns]
births_transformed = births_transformed.select(exprs_YNU)
#births_transformed.select(YNU_cols[-5:]).show(5)
#散列技巧将字符串转化为数值类型特征
#create dataset for model predict
#translate dataframe to LabeledPoint RDD
这一步运行时间挺长,大概30分钟
加载数据完成后,再进行数据的探索,首先导入pyspark.mllib包中的stat统计分析相应的模块,
然后指定要选择的特征放在特征列表numeric_cols中
使用select方法选择相应的特征,转化为RDD并进行map处理
最后对选定的特征进行统计分析,包括求均值、方差等等
import pyspark.mllib.feature as ft
import pyspark.mllib.regression as reg
hashing = ft.HashingTF(7)
births_hashed = births_transformed.rdd.map(lambda row:[list(hashing.transform(row[1]).toArray()) if col == "BIRTH_PLACE" else row[i] for i,col in enumerate(features_to_keep)]) \
.map(lambda row:[[e] if type(e) == int else e for e in row]) \
.map(lambda row:[item for sublist in row for item in sublist]) \
.map(lambda row:reg.LabeledPoint(row[0],ln.Vectors.dense(row[1:])))
#summary feas colStats
import pyspark.mllib.stat as st
import numpy as np
# 指定特征放到列表中
numerical_cols = ["MOTHER_AGE_YEARS","FATHER_COMBINED_AGE","CIG_BEFORE","CIG_1_TRI","CIG_2_TRI","CIG_3_TRI","MOTHER_HEIGHT_IN","MOTHER_PRE_WEIGHT","MOTHER_DELIVERY_WEIGHT","MOTHER_WEIGHT_GAIN"]
#选择特征并转化
numeric_rdd = births_transformed.select(numerical_cols).rdd.map(lambda row:[e for e in row])
mllib_stats = st.Statistics.colStats(numeric_rdd)
#求均值,方差等等
for col,m,v in zip(numerical_cols,mllib_stats.mean(),mllib_stats.variance()):
print ("{0}:\t{1:.2f}\t{2:.2f}".format(col,m,np.sqrt(v)))
如图得到数据的初步的描述性统计结果,得到每个特征的均值和方差这些基本数据。
可以看出,与父亲的年龄相比,母亲的年龄更小:母亲的平均年龄是28岁,而父亲的平均年龄是超过44岁;且许多的母亲怀孕后开始戒烟(这是一个好的现象)
接下来我们来探索各个特征间的相关性:
2.数据的探索:特征相关性
相关性可以帮助识别具有共线性数值的特征,也可以针对这些特征进行处理,
我们可以使用corr协方差函数进行相关性分析,通过相关性分析,结果在此不再展示,得出 CIG..特征是高度相关的,所以我们可以选取部分,这里仅保留CIG_1_TRI,删除其他cig特征。重量也是高度相关的,我们这里只保留MOTHER_PRE_WEIGHT,删除其他weight特征
#数据探索,找特征相关性,那肯定是要找不怎么相关的,发现他们的相关性
#calc categorical variables
categorical_cols = [e for e in births_transformed.columns if e not in numerical_cols]
categorical_rdd = births_transformed.select(categorical_cols).rdd.map(lambda row:[e for e in row])
#for i,col in enumerate(categorical_cols):
# agg = categorical_rdd.groupBy(lambda row:row[i]).map(lambda row:(row[0],len(row[1])))
# print (col,sorted(agg.collect(),key=lambda el:el[1],reverse=True))
#numerical feas correlation
#特征间的相关性corr协方差函数
corrs = st.Statistics.corr(numeric_rdd)
print (corrs)
for i,e in enumerate(corrs > 0.5):
correlated = [(numerical_cols[j],corrs[i][j]) for j,e in enumerate(e) if e == 1.0 and j != i]
if len(correlated) > 0:
for e in correlated:
print ("{0}-to-{1}:{2:.2f}".format(numerical_cols[i],e[0],e[1]))
#删除相关性较高的特征
features_to_keep = [
'INFANT_ALIVE_AT_REPORT',
'BIRTH_PLACE',
'MOTHER_AGE_YEARS',
'FATHER_COMBINED_AGE',
'CIG_1_TRI',
'MOTHER_HEIGHT_IN',
'MOTHER_PRE_WEIGHT',
'DIABETES_PRE',
'DIABETES_GEST',
'HYP_TENS_PRE',
'HYP_TENS_GEST',
'PREV_BIRTH_PRETERM'
]
births_transformed = births_transformed.select([e for e in features_to_keep])
经过查看实际数据,发现BIRTH_PlACE特征类型是字符串,这里使用散列技巧将字符串转换成数值类型特征,经过转换,特征全部转换为数值型。至此,数据准备阶段结束,接下来开始经行数据挖掘
3.统计校验
在通过特征变量的相关系数选择特征时,对于一般的分类变量而言,我们无法计算它们之间的相关系数,但是我们可以通过对它们进行卡方校验来检测它们的分布之间是否存在较大的差异。
卡方检验:是用途非常广的一种假设检验方法,它在分类资料统计推断中的应用,包括:两个样本率或两个构成比比较的卡方检验;多个样本率或多个构成比比较的卡方检验以及分类资料的相关分析等。
卡方检验就是统计样本的实际观测值与理论推断值之间的偏离程度,实际观测值与理论推断值之间的偏离程度就决定卡方值的大小,卡方值越大,越不符合;卡方值越小,偏差越小,越趋于符合,若两个值完全相等时,卡方值就为0,表明理论值完全符合。
而在PySpark中你可以用 .chiSqTest() 方法来轻松实现卡方检验。
import pyspark.mllib.linalg as ln
for cat in categorical_cols[1:]:
agg = births_transformed.groupby("INFANT_ALIVE_AT_REPORT") \
.pivot(cat) \
.count()
agg_rdd = agg.rdd.map(lambda row:(row[1:])).flatMap(lambda row:[0 if e == None else e for e in row]).collect()
row_length = len(agg.collect()[0]) - 1
agg = ln.Matrices.dense(row_length,2,agg_rdd)
test = st.Statistics.chiSqTest(agg)
print (cat,round(test.pValue,4))
从结果我们可以看出,所有分类变量对理论值的预测都是有意义的,因此,我们在构建最后的预测模型的时候都要考虑上这些分类型特征变量。
4.创建最后的待训练数据集(可跳过)
经过一轮的数据分析和特征变量筛选之后,最终到了我们最终的建模阶段了。首先我们将筛选出来以DataFrame数据结构模型表达的数据转换成以LabeledPoints形式表示的RDD。
LabeledPoint 是 MLlib 中的一种数据结构,它包含了两个属性值:label(标识),features(特征)一般用作机器学习模型的训练。
其中,label就是我们目标的分类的标识而features就是我们用于分类的特征,
通常是一个Numpy 数组,列表,psyspark.mllib.linalg.SparseVector,pyspark.mllib,linalg.DenseVector或者是scipy.sparse的形式。
5.划分训练集和测试集
形如sklearn.model_selection.train_test_split随机划分训练集和测试集的模块一般,在PySpark中RDDs也有一个便利的**.randomSplit(…)**方法用于随机划分训练集和测试集。
在本例中可以这样使用
births_train,births_test = births_hashed.randomSplit([0.6,0.4])
没错,仅仅需要上面这样一行的代码,我们就可以将我们的待训练数据按照随机60%,40%来划分好我们的训练集和测试集了。
6.开始建模
在一切准备就绪之后,我们就可以开始通过我们上面的训练数据集来建模了。在这里我们来尝试建立两个模型:一个线性的Logistic回归模型,一个非线性的随机森林模型。然后,在初次建模的时候,我们先采用筛选出来的全部特征来建模,然后我们再通过**ChiSqSelector(…)**方法来归纳出最能代表全部整体的四个主成分。
7.Logistic 回归模型
#逻辑回归模型
from pyspark.mllib.classification import LogisticRegressionWithLBFGS as LR
lr_model = LR.train(births_train,iterations=10)#迭代10次
lr_results = (births_test.map(lambda row:row.label) \
.zip(lr_model.predict(births_test.map(lambda row:row.features)))).map(lambda row:(row[0],row[1] * 1.0))
#模型评估
import pyspark.mllib.evaluation as ev
lr_ev = ev.BinaryClassificationMetrics(lr_results)
print ("Area under PR:{}".format(lr_ev.areaUnderPR))
print ("Area under ROC: {}".format(lr_ev.areaUnderROC))
从上面的建模过程可以看出,使用PySpark训练一个模型也是非常简单的。我们只需要调用**.train(…)**方法,并传入之前处理好的LabeledPoints数据即可。不过需要注意的一点是我们要提前指定一个较小训练的迭代次数以免训练时间过长。
同时,在上面的代码中,我们在训练完一个模型之后使用MLlib中为我们提供的评估分类和回归准确度的**.BinaryClassificationMetrics(…)**方法来分析我们最后预测的结果。
最后,结果图示如下:
8.选取出最具代表性的分类特征
通常来说,一个采取更少的特征的简单模型,往往会比一个复杂的模型,在分类问题上更具有代表性和可解释性。而在MLlib中,则可以通过**.Chi-Square selector**来提取出模型中最具代表性的一些分类特征变量来简化我们的模型
#feature selection with chi-square
selector = ft.ChiSqSelector(4).fit(births_train)
topFeatures_train = (
births_train.map(lambda row:row.label) \
.zip(selector.transform(births_train.map(lambda row:row.features)))
).map(lambda row:reg.LabeledPoint(row[0],row[1]))
topFeatures_test = (
births_test.map(lambda row:row.label) \
.zip(selector.transform(births_test.map(lambda row:row.features)))
).map(lambda row:reg.LabeledPoint(row[0],row[1]))
9.随机森林模型
随机森林模型(Random forest 后面简称RF)在训练上总体与Logistic类似,不同的参数是RF在训练前需要指定类别总数:numClasses,树的棵数:numTrees
#random forest model
from pyspark.mllib.tree import RandomForest
rf_model = RandomForest.trainClassifier(data=topFeatures_train,
numClasses=2,
categoricalFeaturesInfo={},
numTrees=6,
featureSubsetStrategy="all",seed=666)
rf_results = (topFeatures_test.map(lambda row:row.label) \
.zip(rf_model.predict(topFeatures_test.map(lambda row:row.features))))
rf_ev = ev.BinaryClassificationMetrics(rf_results)
print ("Area under PR:{}".format(rf_ev.areaUnderPR))
print ("Area under ROC: {}".format(rf_ev.areaUnderROC))
注:在随机森林模型的创建中,我们采用的是上面提取出来的最具代表性的有效特征,这就意味着模型用到的特征是比之前的Logistic要少的。
最后,结果图示如下:
通过结果我们可以看出,随机森林模型,在采用比之前更少的特征下的建模的最终预测效果是优于之前的Logistic回归模型的。
下面我们同样使用代表性特征来重建一次Logistic回归模型
LR_Model_2 = LR.train(topFeatures_train, iterations=10)
LR_results_2 = (
topFeatures_test.map(lambda row: row.label).zip(LR_Model_2.predict(topFeatures_test.map(lambda row: row.features)))).map(lambda row: (row[0], row[1] * 1.0))
LR_evaluation_2 = ev.BinaryClassificationMetrics(LR_results_2)
print('Area under PR: {0:.2f}'.format(LR_evaluation_2.areaUnderPR))
print('Area under ROC: {0:.2f}'.format(LR_evaluation_2.areaUnderROC))
LR_evaluation_2.unpersist()
通过结果,我们可以看出,虽然没有达到RF模型的准确度,但是与采用了全特征的Logistic回归模型处于同一水平。所以,我们在可选的情况下,通常采用更少的特征来构建更为简化和有效的模型。