声明:版权所有,转载请联系作者并注明出处  http://blog.csdn.net/u013719780?viewmode=contents


博主简介:风雪夜归子(英文名:Allen),机器学习算法攻城狮,喜爱钻研Meachine Learning的黑科技,对Deep Learning和Artificial Intelligence充满兴趣,经常关注Kaggle数据挖掘竞赛平台,对数据、Machine Learning和Artificial Intelligence有兴趣的童鞋可以一起探讨哦,个人CSDN博客:http://blog.csdn.net/u013719780?viewmode=contents



分类模型处理表示类别的离散变量,而回归模型则处理可以取任意实数的目标变量。但是二者基本的原则类似,都是通过确定一个模型,将输入特征映射到预测的输出。回归模型和分类模型都是监督学习的一种形式。


回归模型可以用在如下一些场景:

 预测股票收益和其他经济相关的因素;

 预测贷款违约造成的损失(可以和分类模型相结合,分类模型预测违约概率,回归模型预测违约损失);

 推荐系统(博文 spark机器学习笔记:(三)用Spark Python构建推荐系统 中的交替最小二乘分解模型在每次迭代时都使用了线性回归);

 基于用户的行为和消费模式,预测顾客对于零售、移动或者其他商业形态的存在价值。


本文主要内容如下:

 介绍MLlib中的各种回归模型;

 讨论回归模型的特征提取和目标变量的变换;

 使用MLlib训练回归模型;

 介绍如何用训练好的模型做预测;

 使用交叉验证研究设置不同的参数对性能的影响。






1 特征提取



因为回归的基础模型和分类模型一样,所以我们可以使用同样的方法来处理输入的特征。实际中唯一的不同是,回归模型的预测目标是实数变量,而分类模型的预测目标是类别编号。为了满足两种情况,MLlib中的LabeledPoint类已经考虑了这一点,类中的label字段使用Double类型。本文选择bike sharing数据集做实验。这个数据集记录了bike

sharing系统每小时自行车的出租次数。另外还包括日期、时间、天气、季节和节假日等相关信息(bike sharing数据集下载地址:http://archive.ics.uci.edu/ml/datasets/Bike+Sharing+Dataset)。各个字段的含义如下:

- instant: record index

- dteday : date

- season : season (1:springer, 2:summer, 3:fall, 4:winter)

- yr : year (0: 2011, 1:2012)

- mnth : month ( 1 to 12)

- hr : hour (0 to 23)

- holiday : weather day is holiday or not (extracted from http://dchr.dc.gov/page/holiday-schedule)

- weekday : day of the week

- workingday : if day is neither weekend nor holiday is 1, otherwise is 0.

+ weathersit : 

- 1: Clear, Few clouds, Partly cloudy, Partly cloudy

- 2: Mist + Cloudy, Mist + Broken clouds, Mist + Few clouds, Mist

- 3: Light Snow, Light Rain + Thunderstorm + Scattered clouds, Light Rain + Scattered clouds

- 4: Heavy Rain + Ice Pallets + Thunderstorm + Mist, Snow + Fog

- temp : Normalized temperature in Celsius. The values are divided to 41 (max)

- atemp: Normalized feeling temperature in Celsius. The values are divided to 50 (max)

- hum: Normalized humidity. The values are divided to 100 (max)

- windspeed: Normalized wind speed. The values are divided to 67 (max)

- casual: count of casual users

- registered: count of registered users

- cnt: count of total rental bikes including both casual and registered


输入命令 sed 1d hour.csv > hour_noheader.csv,将文件hour.csv的第一行头文件去掉并保存到新的 hour_noheader.csv。


import numpy as np
path = '/Users/youwei.tan/Desktop/Bike-Sharing-Dataset/hour_noheader.csv'
raw_data = sc.textFile(path) 
num_data = raw_data.count() 
records =raw_data.map(lambda x: x.split(',')) 
first = records.first() 
print '数据的第一行:',first 
print '数据样本数:',num_data



输出结果:

数据的第一行: [u'1', u'2011-01-01', u'1', u'0', u'1', u'0', u'0', u'6', u'0', u'1', u'0.24', u'0.2879', u'0.81', u'0', u'3', u'13', u'16']

数据样本数: 17379


结果显示,数据集中共有17 379个小时的记录。接下来的实验,我们会忽略记录中的instant和dteday 。忽略两个记录次数的变量casual 和registered , 只保留cnt ( casual 和

registered的和)。最后就剩下12个变量,其中前8个是类型变量,后4个是归一化后的实数变量。对其中8个类型变量,我们使用之前提到的二元编码,剩下4个实数变量不做处理。


因为变量records下文经常要用到,此处对其进行缓存:


 
         
records.cache()


为了将类型特征表示成二维形式,我们将特征值映射到二元向量中非0的位置。下面定义这样一个映射函数:

 
         

def get_mapping(rdd, idx):
    return rdd.map(lambda fields: fields[idx]).distinct().zipWithIndex().collectAsMap()



上面的函数首先将第idx列的特征值去重,然后对每个值使用zipWithIndex函数映射到一个唯一的索引,这样就组成了一个RDD的键值映射,键是变量,值是索引。上述索引便是特征在二元向量中对应的非0位置,最后我们将这个RDD表示成Python的字典类型。

下面,我们用特征矩阵的第三列(索引2)来测试上面的映射函数:

print '第三个特征的类别编码: %s '%get_mapping(records,2) 





输出结果:

第三个特征的类别编码: {u'1': 0, u'3': 1, u'2': 2, u'4': 3} 


接着,对是类型变量的列(第2~9列)应用该函数:

mappings = [get_mapping(records, i) for i in range(2,10)]   #对类型变量的列(第2~9列)应用映射函数
print '类别特征打编码字典:',mappings
cat_len = sum(map(len,[i for i in mappings]))        #类别特征的个数                 
#cat_len = sum(map(len, mappings))
#print map(len,mappings)

num_len = len(records.first()[11:15])                      #数值特征的个数
total_len = num_len+cat_len                                  #所有特征的个数
print '类别特征的个数: %d'% cat_len
print '数值特征的个数: %d'% num_len
print '所有特征的个数::%d' % total_len

输出结果:

类别特征的编码字典: [{u'1': 0, u'3': 1, u'2': 2, u'4': 3}, {u'1': 0, u'0': 1}, {u'11': 0, u'10': 1, u'12': 2, u'1': 3, u'3': 4, u'2': 5, u'5': 6, u'4': 7, u'7': 8, u'6': 9, u'9': 10, u'8': 11}, {u'20': 0, u'21': 1, u'22': 2, u'23': 3, u'1': 4, u'0': 5, u'3': 6, u'2': 7, u'5': 8, u'4': 9, u'7': 10, u'6': 11, u'9': 12, u'8': 13, u'11': 14, u'10': 15, u'13': 16, u'12': 17, u'15': 18, u'14': 19, u'17': 20, u'16': 21, u'19': 22, u'18': 23}, {u'1': 0, u'0': 1}, {u'1': 0, u'0': 1, u'3': 2, u'2': 3, u'5': 4, u'4': 5, u'6': 6}, {u'1': 0, u'0': 1}, {u'1': 0, u'3': 1, u'2': 2, u'4': 3}]

类别特征的个数: 57

数值特征的个数: 4

所有特征的个数::61




1.1 为线性模型创建特征向量


接下来用上面的映射函数将所有类型特征转换为二元编码的特征。为了方便对每条记录提取特征和标签,我们分别定义两个辅助函数extract_features和extract_label。如下为代码实现,注意需要引入numpy和MLlib的LabeledPoint对特征向量和目标变量进行封装:


from pyspark.mllib.regression import LabeledPoint
import numpy as np

def extract_features(record): 
    cat_vec = np.zeros(cat_len) 
    step = 0
    for i,raw_feature in enumerate(record[2:9]):
        dict_code = mappings[i]
        index = dict_code[raw_feature]
        cat_vec[index+step] = 1
        step = step+len(dict_code)
    num_vec = np.array([float(raw_feature) for raw_feature in record[10:14]])
    return np.concatenate((cat_vec, num_vec))

def extract_label(record):
    return float(record[-1])


在extract_features函数中,我们遍历了数据的每一行每一列,根据已经创建的映射对每个特征进行二元编码。其中step变量用来确保非0特征在整个特征向量中位于正确的位置(另外一种实现方法是将若干较短的二元向量拼接在一起)。数值向量直接对之前已经被转换成浮点数的数据用numpy的array进行封装。最后将二元向量和数值向量拼接起来。定义extract_label函数将数据中的最后一列cnt的数据转换成浮点数。

下面对数据进行特征提取:


data = records.map(lambda point: LabeledPoint(extract_label(point),extract_features(point)))
first_point = data.first()

print '原始特征向量:' +str(first[2:])
print '标签:' + str(first_point.label)
print '对类别特征进行独热编码之后的特征向量: \n' + str(first_point.features)
print '对类别特征进行独热编码之后的特征向量长度:' + str(len(first_point.features))



输出结果:

原始特征向量:[u'1', u'0', u'1', u'0', u'0', u'6', u'0', u'1', u'0.24', u'0.2879', u'0.81', u'0', u'3', u'13', u'16']

标签:16.0

对类别特征进行独热编码之后的特征向量: 

[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.24,0.2879,0.81,0.0]

对类别特征进行独热编码之后的特征向量长度:61





1.2 为决策树创建特征向量


我们已经知道,决策树模型可以直接使用原始数据(不需要将类型数据用二元向量表示)。因此,只需要创建一个分割函数简单地将所有数值转换为浮点数,最后用numpy的array封装:


def extract_features_dt(record):
    return np.array(map(float, record[2:14]))
data_dt = records.map(lambda point: LabeledPoint(extract_label(point), extract_features_dt(point)))
first_point_dt = data_dt.first()
print '决策树特征向量: '+str(first_point_dt.features)
print '决策树特征向量长度: '+str(len(first_point_dt.features))



输出结果:

决策树特征向量: [1.0,0.0,1.0,0.0,0.0,6.0,0.0,1.0,0.24,0.2879,0.81,0.0]

决策树特征向量长度: 12






2 回归模型的训练和应用



使用决策树和线性模型训练回归模型的步骤和使用分类模型相同,都是简单将训练数据封装在LabeledPoint的RDD中,并送到相关的train方法上进行训练。注意在Scala中,如果要自定义不同的模型参数(比如SGD优化的正则化和步长),就需要初始化一个新的模型实例,使用实例的optimizer变量访问和设置参数。

Python提供了方便我们访问所有模型参数的方法,因此只要使用相关方法即可。可以通过引入相关模块,并调用train方法中的help函数查看这些方法的具体细节:

from pyspark.mllib.regression import LinearRegressionWithSGD
from pyspark.mllib.tree import DecisionTree
help(LinearRegressionWithSGD.train)



输出结果:

Help on method train in module pyspark.mllib.regression:


train(cls, data, iterations=100, step=1.0, miniBatchFraction=1.0, initialWeights=None, regParam=0.0, regType=None, intercept=False, validateData=True, convergenceTol=0.001) method of __builtin__.type instance

    Train a linear regression model using Stochastic Gradient

    Descent (SGD).

    This solves the least squares regression formulation

    

        f(weights) = 1/(2n) ||A weights - y||^2,

    

    which is the mean squared error.

    Here the data matrix has n rows, and the input RDD holds the

    set of rows of A, each with its corresponding right hand side

    label y. See also the documentation for the precise formulation.

    

    :param data:              The training data, an RDD of

                              LabeledPoint.

    :param iterations:        The number of iterations

                              (default: 100).

    :param step:              The step parameter used in SGD

                              (default: 1.0).

    :param miniBatchFraction: Fraction of data to be used for each

                              SGD iteration (default: 1.0).

    :param initialWeights:    The initial weights (default: None).

    :param regParam:          The regularizer parameter

                              (default: 0.0).

    :param regType:           The type of regularizer used for

                              training our model.

    

                              :Allowed values:

                                 - "l1" for using L1 regularization (lasso),

                                 - "l2" for using L2 regularization (ridge),

                                 - None for no regularization

    

                                 (default: None)

    

    :param intercept:         Boolean parameter which indicates the

                              use or not of the augmented representation

                              for training data (i.e. whether bias

                              features are activated or not,

                              default: False).

    :param validateData:      Boolean parameter which indicates if

                              the algorithm should validate data

                              before training. (default: True)

    :param convergenceTol:    A condition which decides iteration termination.

                              (default: 0.001)

    

    .. versionadded:: 0.9.0



调用决策树模型的trainRegressor方法查看帮助信息


 
         
help(DecisionTree.trainRegressor)


输出结果:

Help on method trainRegressor in module pyspark.mllib.tree:


trainRegressor(cls, data, categoricalFeaturesInfo, impurity='variance', maxDepth=5, maxBins=32, minInstancesPerNode=1, minInfoGain=0.0) method of __builtin__.type instance

    Train a DecisionTreeModel for regression.

    

    :param data: Training data: RDD of LabeledPoint.

                 Labels are real numbers.

    :param categoricalFeaturesInfo: Map from categorical feature

             index to number of categories.

             Any feature not in this map is treated as continuous.

    :param impurity: Supported values: "variance"

    :param maxDepth: Max depth of tree.

             E.g., depth 0 means 1 leaf node.

             Depth 1 means 1 internal node + 2 leaf nodes.

    :param maxBins: Number of bins used for finding splits at each

             node.

    :param minInstancesPerNode: Min number of instances required at

             child nodes to create the parent split

    :param minInfoGain: Min info gain required to create a split

    :return: DecisionTreeModel

    

    Example usage:

    

    >>> from pyspark.mllib.regression import LabeledPoint

    >>> from pyspark.mllib.tree import DecisionTree

    >>> from pyspark.mllib.linalg import SparseVector

    >>>

    >>> sparse_data = [

    ...     LabeledPoint(0.0, SparseVector(2, {0: 0.0})),

    ...     LabeledPoint(1.0, SparseVector(2, {1: 1.0})),

    ...     LabeledPoint(0.0, SparseVector(2, {0: 0.0})),

    ...     LabeledPoint(1.0, SparseVector(2, {1: 2.0}))

    ... ]

    >>>

    >>> model = DecisionTree.trainRegressor(sc.parallelize(sparse_data), {})

    >>> model.predict(SparseVector(2, {1: 1.0}))

    1.0

    >>> model.predict(SparseVector(2, {1: 0.0}))

    0.0

    >>> rdd = sc.parallelize([[0.0, 1.0], [0.0, 0.0]])

    >>> model.predict(rdd).collect()

    [1.0, 0.0]

    

    .. versionadded:: 1.1.0





在bike sharing 数据上训练回归模型


我们已经从bike sharing数据中提取了用于训练模型的特征,下面进行具体的训练。首先训练线性模型并测试该模型在训练数据上的预测效果:

 
         

linear_model = LinearRegressionWithSGD.train(data, iterations=10, step=0.1, intercept =False)
true_vs_predicted = data.map(lambda point:(point.label,linear_model.predict(point.features)))
print '线性回归模型对前5个样本的预测值: '+ str(true_vs_predicted.take(5))



输出结果:

线性回归模型对前5个样本的预测值: [(16.0, 117.89250386724844), (40.0, 116.22496123192109), (32.0, 116.02369145779232), (13.0, 115.67088016754431), (1.0, 115.56315650834314)]


上述代码中我们没有使用默认的迭代次数和步长,而是使用较小的迭代次数以缩短训练时间,关于步长的设置我们稍后会详细介绍。


接下来,我们在trainRegressor中使用默认参数来训练决策树模型(相当于深度为5的树)。注意,这里训练数据集是从原始特征中提取的,名为data_dt(不同于之前线性模型中使用的二元编码的特征)。另外,我们还需要为categoricalFeaturesInfo传入一个字典参数,这个字典参数将类型特征的索引映射到特征中类型的数目。如果某个特征值不在这个字典中,则将其映射设置为空:

 
         

dt_model = DecisionTree.trainRegressor(data_dt,{})
preds = dt_model.predict(data_dt.map(lambda p: p.features))
actual = data.map(lambda p:p.label)
true_vs_predicted_dt = actual.zip(preds)
print '决策树回归模型对前5个样本的预测值: '+str(true_vs_predicted_dt.take(5)) 
print '决策树模型的深度: ' + str(dt_model.depth()) 
print '决策树模型的叶子节点个数: '+str(dt_model.numNodes())



输出结果:

决策树回归模型对前5个样本的预测值: [(16.0, 54.913223140495866), (40.0, 54.913223140495866), (32.0, 53.171052631578945), (13.0, 14.284023668639053), (1.0, 14.284023668639053)]

决策树模型的深度: 5

决策树模型的叶子节点个数: 63






3 评估回归模型的性能



上一篇博文评估分类模型仅仅关注预测输出的类别和实际类别。特别是对于所有预测的二元结果,某个样本预测的正确与否并不重要,我们更关心预测结果中正确或者错误的总数。对回归模型而言,因为目标变量是任一实数,所以我们的模型不大可能精确预测到目标变量。然而,我们可以计算预测值和实际值的误差,并用某种度量方式进行评估。

一些用于评估回归模型的方法包括:均方误差(MSE,Mean Squared Error)、均方根误差(RMSE,Root Mean Squared Error)、平均绝对误差(MAE,Mean Absolute Error)、R-平方系数(R-squared coefficient)等。





3.1 均方误差和均方根误差


MSE是平方误差的均值,用作最小二乘回归的损失函数,公式如下:

                                                                                            

这个公式计算的是所有样本预测值和实际值平方差之和,最后除以样本总数。而RMSE是MSE的平方根。MSE的公式类似平方损失函数,会进一步放大误差。

为了计算模型预测的平均误差,我们首先预测RDD实例LabeledPoint中每个特征向量,然后计算预测值与实际值的误差并组成一个Double数组的RDD,最后使用mean方法计算所有Double值的平均值。计算平方误差函数实现如下:

def squared_error(actual, pred): 
    return (pred-actual)**2 







3.2 平均绝对误差


MAE是预测值和实际值的差的绝对值的平均值。公式如下:

                                                                                 

MAE和MSE大体类似,区别在于MAE对大的误差没有惩罚。计算MAE的代码如下:

 
         

def abs_error(actual, pred): 
    return np.abs(pred-actual) 






3.3 均方根对数误差


这个度量方法虽然没有MSE和MAE使用得广,但被用于Kaggle中以bike sharing作为数据集的比赛。RMSLE可以认为是对预测值和目标值进行对数变换后的RMSE。这个度量方法适用于目标变量值域很大,并且没有必要对预测值和目标值的误差进行惩罚的情况。另外,它也适用于计算误差的百分率而不是误差的绝对值。计算RMSLE的代码如下:

 
         

def squared_log_error(pred, actual): 
    return (np.log(pred+1)-np.log(actual+1))**2







3.4 R-平方系数


R-平方系数,也称判定系数,用来评估模型拟合数据的好坏,常用于统计学中。R-平方系数具体测量目标变量的变异度(degree of variation),最终结果为0到1的一个值,1表示模型能够完美拟合数据。







3.5 计算不同度量下的性能


根据上面定义的函数,我们在bike sharing数据集上计算不同度量下的性能。




3.5.1 线性模型


我们的方法对RDD的每一条记录应用相关的误差函数,其中线性模型的误差函数为true_vs_predicted,相关代码实现如下:

 
         

mse = true_vs_predicted.map(lambda (t, p): squared_error(t, p)).mean() 
mae = true_vs_predicted.map(lambda (t, p): abs_error(t, p)).mean() 
rmsle = np.sqrt(true_vs_predicted.map(lambda (t, p): squared_log_error(t, p)).mean()) 
print 'Linear Model - Mean Squared Error: %2.4f' % mse 
print 'Linear Model - Mean Absolute Error: %2.4f' % mae 
print 'Linear Model - Root Mean Squared Log Error: %2.4f' % rmsle



输出结果:

Linear Model - Mean Squared Error: 30679.4539

Linear Model - Mean Absolute Error: 130.6429

Linear Model - Root Mean Squared Log Error: 1.4653




3.5.2 决策树




决策树的误差函数为true_vs_predicted_dt,相关代码如下:

 
         

mse_dt = true_vs_predicted_dt.map(lambda (t, p): squared_error(t, p)).mean()
mae_dt = true_vs_predicted_dt.map(lambda (t, p): abs_error(t, p)).mean()
rmsle_dt = np.sqrt(true_vs_predicted_dt.map(lambda (t, p): squared_log_error(t, p)).mean())
print 'Decision Tree - Mean Squared Error: %2.4f' % mse_dt
print 'Decision Tree - Mean Absolute Error: %2.4f' % mae_dt
print 'Decision Tree - Root Mean Squared Log Error: %2.4f' %rmsle_dt


输出结果:

Decision Tree - Mean Squared Error: 11560.7978

Decision Tree - Mean Absolute Error: 71.0969

Decision Tree - Root Mean Squared Log Error: 0.6259




4 改进模型性能和参数调优


在上一篇博文中,我们已经知道特征变换和选择对模型性能有巨大的影响。本文讨论另一种变换方式:对目标变量进行变换。

许多机器学习模型都会假设输入数据和目标变量的分布,比如线性模型的假设为正态分布。但是实际情况中线性回归的这种假设不成立的,比如例子中自行车被租的次数永远不可能为负。这就说明了正态分布的假设不合适。为了更好地理解目标变量的分布,最好的方法是画出目标变量的分布直方图。

 
          

%matplotlib inline
import matplotlib
from matplotlib.pyplot import hist
import numpy as np 
import matplotlib.pyplot as plt 

targets = records.map(lambda r: float(r[-1])).collect() 
hist(targets, bins=40, color='lightblue', normed=True) 
fig = matplotlib.pyplot.gcf() 
fig.set_size_inches(12, 6)
plt.show()


输出结果:




从上图可以看出,目标变量的分布完全不符合正态分布。



解决该问题的一种方法是对目标变量进行变换,比如用目标值的对数代替原始值,通常称为对数变换(这种变换也可以用到特征值上)。下面绘制对目标变量进行对数变换后的分布直方图。

 
          

log_targets = records.map(lambda r : np.log(float(r[-1]))).collect() 
plt.hist(log_targets, bins = 40, color ='lightblue', normed =True) 
fig = plt.gcf()
fig.set_size_inches(12,6)
plt.show()


输出结果:





另一种常用的变换是平方根变换。该变换适用于目标变量不为负数且值域很大的情形。下面绘制对目标变量进行平方根变换后的分布直方图。

 
          

sqrt_targets = records.map(lambda r: np.sqrt(float(r[-1]))).collect() 
plt.hist(sqrt_targets, bins=40, color='lightblue', normed=True) 
fig = matplotlib.pyplot.gcf() 
fig.set_size_inches(12, 6) 



输出结果:





从对数变换和平方根变换后结果来看,变换后的数值比原始数值更均匀。虽然这两个分布也不是正态分布,但是已经比原始数据更加接近正态分布了。






4.1 目标变量变换对模型的影响


接下来测试目标变量在变换之后对模型的性能影响。

 
          

data_log = data.map(lambda lp:LabeledPoint(np.log(lp.label),lp.features)) 
model_log =LinearRegressionWithSGD.train(data_log, iterations=10, step=0.1) 
true_vs_predicted_log = data_log.map(lambda p:(np.exp(p.label),np.exp(model_log.predict(p.features))))

#计算模型的MSE,MAE,RMSLE
mse_log = true_vs_predicted_log.map(lambda(t, p): squared_error(t,p)).mean() 
mae_log = true_vs_predicted_log.map(lambda(t, p): abs_error(t, p)).mean() 
rmsle_log = np.sqrt(true_vs_predicted_log.map(lambda(t, p): squared_log_error(t, p)).mean())

print'Linear Model —— Mean Squared Error:%2.4f'% mse_log 
print'Linear Model —— Mean Absolue Error:%2.4f'% mae_log 
print'Linear Model —— Root Mean Squared Log Error:%2.4f'% rmsle_log 
print'Linear Model —— Non log-transformed predictions:\n'+ str(true_vs_predicted.take(3))
print'Linear Model —— Log-transformed predictions:\n'+ str(true_vs_predicted_log.take(3))


输出结果:

Linear Model —— Mean Squared Error:50685.5559

Linear Model —— Mean Absolue Error:155.2955

Linear Model —— Root Mean Squared Log Error:1.5411

Linear Model —— Non log-transformed predictions:

[(16.0, 117.89250386724845), (40.0, 116.2249612319211), (32.0, 116.02369145779235)]

Linear Model —— Log-transformed predictions:

[(15.999999999999998, 28.080291845456223), (40.0, 26.959480191001774), (32.0, 26.654725629458007)]



将上述结果与原始数据集训练的模型性能进行对比,可以发现,三个评价指标MSE,MAE,RMSLE都没有得到提升,书上说RMSLE的性能得到了提升,也许是spark版本所设置的默认参数不一样导致的。


下面对决策树模型做同样的分析,其代码如下:


 
          

data_dt_log = data_dt.map(lambda lp:LabeledPoint(np.log(lp.label), lp.features)) 
dt_model_log = DecisionTree.trainRegressor(data_dt_log,{}) 
preds_log = dt_model_log.predict(data_dt_log.map(lambda p:p.features)) 
actual_log = data_dt_log.map(lambda p: p.label) 
true_vs_predicted_dt_log = actual_log.zip(preds_log).map(lambda(t,p):(np.exp(t), np.exp(p)))

#计算模型的MSE,MAE,RMSLE
mse_log_dt = true_vs_predicted_dt_log.map(lambda(t, p): squared_error(t, p)).mean() 
mae_log_dt = true_vs_predicted_dt_log.map(lambda(t, p): abs_error(t,p)).mean() 
rmsle_log_dt = np.sqrt(true_vs_predicted_dt_log.map(lambda(t, p): 
squared_log_error(t, p)).mean())

print'Decision Tree —— Mean Squared Error:%2.4f'% mse_log_dt 
print'Decision Tree —— Mean Absolue Error:%2.4f'% mae_log_dt 
print'Decision Tree —— Root Mean Squared Log Error:%2.4f'% rmsle_log_dt 
print'Decision Tree —— Non log-transformed predictions:\n'+ str(true_vs_predicted_dt.take(3))
print'Decision Tree —— Log-transformed predictions:\n'+str(true_vs_predicted_dt_log.take(3))


输出结果:

Decision Tree —— Mean Squared Error:14781.5760

Decision Tree —— Mean Absolue Error:76.4131

Decision Tree —— Root Mean Squared Log Error:0.6406

Decision Tree —— Non log-transformed predictions:

[(16.0, 54.913223140495866), (40.0, 54.913223140495866), (32.0, 53.171052631578945)]

Decision Tree —— Log-transformed predictions:

[(15.999999999999998, 37.530779787154508), (40.0, 37.530779787154508), (32.0, 7.2797070993907287)]








4.2  模型参数调优


到目前为止,本文讨论了同一个数据集上对MLlib中的回归模型进行训练和评估的基本概率。接下来,我们使用交叉验证方法来评估不同参数对模型性能的影响。


首先,我们将原始数据按比率划分为train,test数据集,原书当中pyspark版本还没有randomSplit这个函数,所以用如下的方式处理:

data_with_idx = data.zipWithIndex().map(lambda(k,v):(v,k)) 
test = data_with_idx.sample(False,0.2,42) 
train = data_with_idx.subtractByKey(test)




但是我使用的版本pyspark1.6.1已经有了函数randomSplit,这里直接使用randomSplit:

 
          

train_test_data_split = data.randomSplit([0.8,0.2],123) 
train = train_test_data_split[0] 
test = train_test_data_split[1]
print '测试集的样本数:',test.count()
print '训练集的样本数:',train.count()


输出结果:

测试集的样本数: 3524

训练集的样本数: 13855



同理,dt的数据也做相同的处理

 
          

train_test_data_dt_split = data_dt.randomSplit([0.8,0.2],123) 
train_dt = train_test_data_dt_split[0] 
test_dt = train_test_data_dt_split[1]





参数设置对线性模型的影响


前面已经得到了训练集和测试集,下面研究不同参数设置对模型性能的影响,首先需要为线性模型设置一个评估方法,同时创建一个辅助函数,实现在不同参数设置下评估训练集和测试集上的性能。

本文依然使用Kaggle竞赛中的RMSLE作为评价指标。这样可以和在竞赛排行榜的成绩进行比较。


评估函数定义如下:


 
          

def evaluate(train, test, iterations, step, regParam, regType, intercept):
    model =LinearRegressionWithSGD.train(train, iterations, step, regParam=regParam, 
                                         regType=regType,intercept=intercept)
    testLabel_vs_testPrediction = test.map(lambda point:(point.label, model.predict(point.features)))
    rmsle = np.sqrt(testLabel_vs_testPrediction.map(lambda(t,p):squared_log_error(t,p)).mean())
    return rmsle






迭代次数对模型的影响:

 
          

params = [1, 5, 10, 20, 50, 100]
metrics = [evaluate(train, test, param, 0.01, 0.0, 'l2', False) for param in params]
print params
print metrics


输出结果:

[1, 5, 10, 20, 50, 100]

[2.9352350771042066, 2.0758830668686867, 1.7989931147537059, 1.5883231443924686, 1.4176140084119577, 1.3657070067736425]



绘制迭代次数与RMSLE的关系图:

 
          

plt.plot(params, metrics) 
fig = matplotlib.pyplot.gcf() 
fig.set_size_inches(12, 6) 
plt.xscale('log')
plt.show()





输出结果:






评估step对模型的影响:

 
          

params=[0.01,0.025,0.05,0.1,0.5,1.0] 
metrics =[evaluate(train, test,10,param,0.0,'l2',False)for param in params]
for i in range(len(params)):
    print'the rmsle:%f when step :%f'%(metrics[i],params[i])

#绘制步长与RMSLE的关系图:
plt.plot(params, metrics) 
fig = matplotlib.pyplot.gcf() 
fig.set_size_inches(12, 6) 
plt.xscale('log')
plt.xlabel('step')
plt.ylabel('RMSLE')
plt.show()


输出结果:

the rmsle:1.798993 when step :0.010000

the rmsle:1.417115 when step :0.025000

the rmsle:1.365007 when step :0.050000

the rmsle:1.430426 when step :0.100000

the rmsle:1.397443 when step :0.500000

the rmsle:nan when step :1.000000





从上述结果可以看出为什么不使用默认步长(默认1.0)来训练线性模型,因为其得到的RMSLE结果为nan。这说明SGD模型收敛到了最差的局部最优解。这种情况在步长较大的时候容易出现,原因是算法收敛太快导致不能得到最优解。另外,小的步长与相对较小的迭代次数(比如上面的10次)对应的训练模型性能一般较差,而较小的步长与较大的迭代次数通常可以收敛到较好的结果。



不同正则化系数对模型的影响


我们知道随着正则化的提高,训练集的预测性能会下降,因为模型不能很好的拟合数据。但是我们希望设置合适的正则化参数,能够在测试集上达到最好的性能,最终得到一个泛化能力最优的模型。


先看L2正则化系数对模型的影响

 
          

params=[0.0,0.01,0.1,1.0,5.0,10.0,20.0]
metrics =[evaluate(train, test,10,0.1, param,'l2',False) for param in params]
for i in range(len(params)):
    print'the rmsle:%f when regParam :%f'%(metrics[i],params[i])

#绘制L2正则化系数与RMSLE的关系图:
plt.plot(params, metrics)
fig = matplotlib.pyplot.gcf()
fig.set_size_inches(12, 8)
plt.xscale('log')
plt.xlabel('regParam')
plt.ylabel('RMSLE')
plt.show()


输出结果:

the rmsle:1.430426 when regParam :0.000000

the rmsle:1.429840 when regParam :0.010000

the rmsle:1.424681 when regParam :0.100000

the rmsle:1.383985 when regParam :1.000000

the rmsle:1.382139 when regParam :5.000000

the rmsle:1.537901 when regParam :10.000000

the rmsle:1.854222 when regParam :20.000000








再看L1正则化系数对模型的影响

params=[0.0,0.01,0.1,1.0,10.0,100.0,1000.0]
metrics =[evaluate(train, test,10,0.1, param,'l1',False) for param in params]
for i in range(len(params)):
    print'the rmsle:%f when regParam :%f'%(metrics[i],params[i])

#绘制L2正则化系数与RMSLE的关系图:
plt.plot(params, metrics)
fig = matplotlib.pyplot.gcf()
fig.set_size_inches(12, 8)
plt.xscale('log')
plt.xlabel('regParam')
plt.ylabel('RMSLE')
plt.show()


输出结果:

the rmsle:1.430426 when regParam :0.000000

the rmsle:1.430394 when regParam :0.010000

the rmsle:1.430109 when regParam :0.100000

the rmsle:1.427487 when regParam :1.000000

the rmsle:1.403976 when regParam :10.000000

the rmsle:1.773484 when regParam :100.000000

the rmsle:4.818171 when regParam :1000.000000





从上图可以看到,当使用一个较大的正则化参数时,RMSLE性能急剧下降。


想必大家都知道,使用L1正则化可以得到稀疏的权重向量,我们看看刚刚得到的L1正则化模型是否真是如此呢?

 
          

model_l1 = LinearRegressionWithSGD.train(train,10,0.1,regParam=1.0, regType='l1', intercept=False) 
model_l2 = LinearRegressionWithSGD.train(train,10,0.1,regParam=1.0, regType='l2', intercept=False) 
model_l1_10 = LinearRegressionWithSGD.train(train,10,0.1,regParam=10.0, regType='l1', intercept=False) 
model_l2_10 = LinearRegressionWithSGD.train(train,10,0.1,regParam=10.0, regType='l2', intercept=False) 
model_l1_100 = LinearRegressionWithSGD.train(train,10,0.1,regParam=100.0, regType='l1', intercept=False) 
model_l2_100 = LinearRegressionWithSGD.train(train,10,0.1,regParam=100.0, regType='l2', intercept=False)

print'L1 (1.0) number of zero weights:'+ str(sum(model_l1.weights.array == 0))
print'L2 (1.0) number of zero weights:'+ str(sum(model_l2.weights.array == 0))
print'L1 (10.0) number of zeros weights:'+ str(sum(model_l1_10.weights.array == 0))
print'L2 (10.0) number of zeros weights:'+ str(sum(model_l2_10.weights.array == 0))
print'L1 (100.0) number of zeros weights:'+ str(sum(model_l1_100.weights.array == 0))
print'L2 (100.0) number of zeros weights:'+ str(sum(model_l2_100.weights.array == 0))


输出结果:

L1 (1.0) number of zero weights:4

L2 (1.0) number of zero weights:4

L1 (10.0) number of zeros weights:33

L2 (10.0) number of zeros weights:4

L1 (100.0) number of zeros weights:58

L2 (100.0) number of zeros weights:4



从上面的结果可以看到,与我们预料的基本一致。随着L1正则化参数越来越大,模型的权重向量中0的个数越来越多。




截距对模型的影响

 
          

params=[False, True]
metrics =[evaluate(train, test, 10, 0.1, 1.0,'l2', param) for param in params]
for i in range(len(params)):
    print'the rmsle:%f when intercept:%f'%(metrics[i],params[i])

#绘制L2正则化系数与RMSLE的关系图:
plt.bar(params, metrics, color='r')
fig = matplotlib.pyplot.gcf()
fig.set_size_inches(12, 8)
plt.xlabel('intercept')
plt.ylabel('RMSLE')
plt.show()


输出结果:

the rmsle:1.383985 when intercept:0.000000

the rmsle:1.412280 when intercept:1.000000




同理,我们来对决策树来做一个相同的探讨:

 
          

def evaluate_dt(train, test, maxDepth, maxBins): 
    model =DecisionTree.trainRegressor(train,{},impurity='variance', maxDepth=maxDepth, maxBins=maxBins)
    predictions = model.predict(test.map(lambda point: point.features)) 
    actual = test.map(lambda point: point.label) 
    actual_vs_predictions = actual.zip(predictions) 
    rmsle = np.sqrt(actual_vs_predictions.map(lambda(t, p): squared_log_error(t,p)).mean())
    return rmsle




树的不同最大深度对性能影响:


我们通常希望用更复杂(更深)的决策树提升模型的性能。而较小的树深度类似正则化形式,如线性模型的L2正则化和L1正则化,存在一个最优的树深度能在测试集上获得最优的性能。

 
          

params=[1,2,3,4,5,10,20] 
metrics =[evaluate_dt(train_dt, test_dt, param,32) for param in params]
for i in range(len(params)):    
    print'the rmsle:%f when maxDepth :%d'%(metrics[i],params[i])

#绘制树的最大深度与RMSLE的关系图:
plt.plot(params, metrics)
fig = matplotlib.pyplot.gcf()
fig.set_size_inches(12, 8)
plt.xlabel('maxDepth')
plt.ylabel('RMSLE')
plt.show()


输出结果:

the rmsle:0.986397 when maxDepth :1

the rmsle:0.897647 when maxDepth :2

the rmsle:0.794272 when maxDepth :3

the rmsle:0.724606 when maxDepth :4

the rmsle:0.618654 when maxDepth :5

the rmsle:0.402800 when maxDepth :10

the rmsle:0.433434 when maxDepth :20






最大划分数(每个节点分支时最大bin数)对模型的影响


最后,我们来看看划分数对决策树性能的影响。和树的深度一样,更多的划分数会使模型变得更加复杂,并且有助于提升特征维度较大的模型性能。划分数到一定程度之后,对性能的提升帮助不大。实际上, 由于过拟合的原因会导致测试集的性能变差。

 
          

params=[2,4,8,16,32,64,100] 
metrics =[evaluate_dt(train_dt, test_dt,5, param) for param in params]
for i in range(len(params)):    
    print'the rmsle:%f when maxBins :%d'%(metrics[i],params[i]) 

#绘制树的最大划分数与RMSLE的关系图:
plt.plot(params, metrics)
fig = matplotlib.pyplot.gcf()
fig.set_size_inches(12, 8)
plt.xlabel('maxDepth')
plt.ylabel('RMSLE')
plt.show()


输出结果:

the rmsle:1.236055 when maxBins :2

the rmsle:0.796798 when maxBins :4

the rmsle:0.740663 when maxBins :8

the rmsle:0.716042 when maxBins :16

the rmsle:0.618654 when maxBins :32

the rmsle:0.618750 when maxBins :64

the rmsle:0.618750 when maxBins :100


从上述结果可以看出,最大划分数会影响模型的性能,但是当最大划分数达到30之后,模型性能基本上没有获得提升。最优的最大划分数是在30到35之间。




5 小结



本文讨论了使用MLlib中的线性模型和决策树模型进行回归分析。我们研究了回归问题中类型特征的抽取和对目标变量做变换的影响。最后,我们实现了不同的性能评估指标,并且设计了交叉验证实验,研究线性模型和决策树模型的不同参数对测试集性能的影响。

下一篇博文,将讨论机器学习中的新方法:无监督学中的聚类模型。







Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐