21

模型建立与调参

 4 years ago
source link: https://www.wmathor.com/index.php/archives/1427/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本篇文章将会从简单的线性模型开始,了解如何建立一个模型以及建立完模型之后要分析什么东西,然后学习交叉验证的思想和技术,并且会构建一个线下测试集,之后我们会尝试建立更多的模型去解决这个问题,并对比它们的效果,当把模型选择出来之后,我们还得掌握一些调参的技术发挥模型最大的性能,模型选择出来之后,也调完参数,但是模型真的就没有问题了吗?我们还需要绘制学习率曲线看模型是否存在过拟合或者欠拟合的问题并给出相应的解决方法

大纲如下:

  • 从最简单的模型开始(线性回归 & 交叉验证 & 构建线下测试集)
  • 评估算法模型的框架(这里会给出一个选择模型的框架,适合迁移)
  • 模型的调参技术(贪心调参, GridSearchCV调参和贝叶斯调参)
  • 绘制训练集曲线与验证集曲线(从曲线分析过拟合欠拟合的问题,以及如果发生了这些问题,我们应该怎么去尝试解决)
  • 总结

1. 从简单的线性模型开始

二手车交易价格预测 比赛是一个回归问题,所以需要选择一些回归模型来解决,线性模型就是一个比较简单的回归模型了,所以我们就从这个模型开始,看看针对这个模型,我们会得到什么结果以及这些结果究竟是什么含义

线性回归(Linear Regression)是利用最小平方损失函数对一个或多个自变量和因变量之间关系进行建模的一种回归分析。简单的说,假设预测的二手车价格用$Y$来表示,而我们构造的特征用$x_i$,之后就可以建立如下的等式来描述它们的关系

$$ Y=w_1x_1+w_2x_2+...+w_nx_n+b $$

训练模型其实就是根据训练集的$(x_1,x_2,...,x_n,Y)$样本求出合适权重$(w_1,w_2,...,w_n)$的过程

首先导入特征工程处理完毕后保存的数据

# 导入之前处理好的数据
data = pd.read_csv('./pre_data/pre_data.csv')
data.head()

# 然后训练集和测试集分开
train = data[:train_data.shape[0]]
test = data[train_data.shape[0]:]    # 这个先不用

# 选择那些数值型的数据特征
continue_fea = ['power', 'kilometer', 'v_2', 'v_3', 'v_4', 'v_5', 'v_6', 'v_10', 'v_11', 'v_12', 'v_14',
                'v_std', 'fuelType_price_average', 'gearbox_std', 'bodyType_price_average', 'brand_price_average',
                'used_time', 'estivalue_price_average', 'estivalueprice_std', 'estivalue_price_min']
train_x = train[continue_fea]
train_y = train_data['price']

然后建立线性模型,直接使用sklearn库,非常简单

from sklearn.linear_model import LinearRegression

model = LinearRegression(normalize=True)
model.fit(train_x, train_y)

通过上面两行代码,其实就已经建立并且训练完了一个线性模型,接下来可以查看一下模型的一些参数(w&b)

"""查看训练的线性回归模型的截距(intercept)与权重(coef)"""
print('intercept: ' + str(model.intercept_))
sorted(dict(zip(continue_fea, model.coef_)).items(), key=lambda x: x[1], reverse=True)


## 结果:
intercept: -178881.74591832393
[('v_6', 482008.29891714785),
 ('v_std', 23713.66414841167),
 ('v_10', 7035.056136559963),
 ('v_14', 1418.4037751433352),
 ('used_time', 186.48306334062053),
 ('power', 12.19202369791551),
 ('estivalue_price_average', 0.4082359327905722),
 ('brand_price_average', 0.38196351334425965),
 ('gearbox_std', 0.1716754674248321),
 ('fuelType_price_average', 0.023785798378739224),
 ('estivalueprice_std', -0.016868767797045624),
 ('bodyType_price_average', -0.21364358471329278),
 ('kilometer', -155.11999534761347),
 ('estivalue_price_min', -574.6952072539285),
 ('v_11', -1164.0263997737668),
 ('v_12', -1953.0558048250668),
 ('v_4', -2198.03802357537),
 ('v_3', -3811.7514971187525),
 ('v_2', -5116.825271420712),
 ('v_5', -447495.6394686485)]

上面的这些就是等式中每个$x_1$前面的系数$w_i$, intercept代表$b$

如果已经有了一系列$(x_1,x_2,...,x_n)$的样本,要预测$y$,只需要下面一句话

y_pred = model.predict(x_test)

虽然线性模型非常简单,但是关于线性模型还有些重要的东西我们得了解一下,比如从这些权重中如何看出哪个特征对线性模型来说更加重要些?这个其实我们看的是权重的绝对值,因为正相关和负相关都是相关,越大的说明那个特征对线性模型影响就越大

其次,我们还可以看一下线性回归的训练效果,绘制一下v_6这个特征和标签的散点图:

subsample_index = np.random.randint(low=0, high=len(train_y), size=50)

plt.scatter(train_x['v_6'][subsample_index], train_y[subsample_index], color='black')
plt.scatter(train_x['v_6'][subsample_index], model.predict(train_x.loc[subsample_index]), color='blue')
plt.xlabel('v_6')
plt.ylabel('price')
plt.legend(['True Price','Predicted Price'],loc='upper right')
print('The predicted price is obvious different from true price')
plt.show()

结果如下:

ayUB3mI.png!web

从上图中我们可以发现发现模型的预测结果(蓝色点)与真实标签(黑色点)的分布差异较大,且部分预测值出现了小于0的情况,说明我们的模型存在一些问题。 这个还是需要会看的,从这里我们也可以看出或许price这个需要处理一下

price的分布图如下:

Nb6RfyR.png!web

通过这张图我们发现price呈长尾分布,不利于我们的建模预测。原因是很多模型都假设数据误差项符合正态分布,而长尾分布的数据违背了这一假设。参考博客: https://blog.csdn.net/Noob_daniel/article/details/76087829

所以我们可以先尝试取个对数

train_y_ln = np.log1p(train_y)
print('The transformed price seems like normal distribution')
plt.figure(figsize=(15,5))
plt.subplot(1,2,1)
sns.distplot(train_y_ln)
plt.subplot(1,2,2)
sns.distplot(train_y_ln[train_y_ln < np.quantile(train_y_ln, 0.9)])

QZVJZvN.png!web

这样效果就好多了,然后重新训练一下

model = model.fit(train_x, train_y_ln)

print('intercept:'+ str(model.intercept_))
sorted(dict(zip(continue_fea, model.coef_)).items(), key=lambda x:x[1], reverse=True)

这个权重结果就不显示了,只画出v_6和price的散点图看一下:

yyANVv7.png!web

到此,线性模型就结束了。虽然我们的主要模型并不是线性模型,而且线性模型很少用到,但是通过这个过程,我们可以学到一些东西:训练和预测模型的步骤和线性模型基本上是一致的,依然是 .fit(X,Y).predict(X_test) 方法。所以在这里先体会一下如何建立一个模型,并且对它进行训练和预测

1.1 交叉验证

在使用数据集对参数进行训练的时候,经常会发现人们通常会将整个训练集分为三个部分:训练集、验证集和测试集。这其实是为了保证训练效果而特意设置的。测试集很好理解,就是完全不参与训练的过程,仅仅用来观测测试效果的数据。而训练集和验证集则牵涉到下面的知识

因为在实际的训练中,训练的结果对于训练集的拟合程度通常还是挺好的(初始条件敏感),但是对于训练集之外的数据的拟合程度通常就不那么令人满意了。因此我们通常并不会把所有的数据集都拿来训练,而是分出一部分来(这一部分不参加训练)对训练集生成的模型进行测试,相对客观的判断这个模型对训练集之外的数据的符合程度。在验证中,比较常用的就是K折交叉验证了,它可以有效的避免过拟合,最后得到的结果也比较具有说服性

K折交叉验证是将原始数据分成K组,将每个子集数据分别做一次验证集,其余的K-1组子集数据作为训练集,这样会得到K个模型,用这K个模型最终的验证集分类准确率的平均数,作为此K折交叉验证下分类器的性能指标。以下图为例:

e6nyMzr.png!web

关于K折交叉验证详细的原理这里就不描述了,其实很好理解,就拿这个比赛来说,我们训练集共150000个样本,假设做5折交叉验证,就是把这150000个样本分成5份,每份30000个样本,训练模型的时候,选其中四份作为训练集训练模型,然后在另一份上进行预测得到一个结果。这样,这五份轮流着做一遍测试集正好就是循环了五轮,得到了五个分数,然后取平均即可。这样的好处就是防止模型更加偏向某份数据,也能看出是否模型存在过拟合

交叉验证,sklearn中提供了一个函数,叫做 cross_val_score ,我们就是用这个函数实现交叉验证,函数具体的作用可以去查一下sklearn的官方文档

from sklearn.model_selection import cross_val_score
from sklearn.metrics import mean_absolute_error, make_scorer

def log_transfer(func):
    def wrapper(y, yhat):
        result = func(np.log(y), np.nan_to_num(np.log(yhat)))   # 这个是为了解决不合法的值的
        return result
    return wrapper

# 下面是交叉验证
scores = cross_val_score(model, X=train_x, y=train_y, verbose=1, cv=5, scoring=make_scorer(log_transfer(mean_absolute_error)))

# 使用线性回归模型,对未处理标签的特征数据进行五折交叉验证(Error 1.36)
print('AVG:', np.mean(scores))

# 对处理的标签交叉验证
scores = cross_val_score(model, X=train_x, y=train_y_ln, verbose=1, cv = 5, scoring=make_scorer(mean_absolute_error))
print('AVG:', np.mean(scores))

# 输出五次的验证结果:
scores = pd.DataFrame(scores.reshape(1,-1))
scores.columns = ['cv' + str(x) for x in range(1, 6)]
scores.index = ['MAE']
scores

得到的结果如下:

cv1 cv2 cv3 cv4 cv5 0.194979 0.195399 0.19679 0.19257 0.197563

最后多说一点,k折交叉验证,并不适合处理时间序列数据,因为时间序列是有先后关系的。就拿这次比赛来说,通过2018年的二手车价格预测2017年的二手车价格,显然是不合理的,因此我们还可以采用时间顺序对数据集进行分隔。在本例中,我们选用靠前时间的4/5样本当作训练集,靠后时间的1/5当作验证集,最终结果与五折交叉验证差距不大

split_point = len(train_x) // 5 * 4

# 训练集
xtrain = train_x[:split_point]
ytrain = train_y[:split_point]

# 测试集
xval = train_x[split_point:]
yval = train_y[split_point:]
ytrain_ln = np.log1p(ytrain)
yval_ln = np.log1p(yval)

# 训练
model.fit(xtrain, ytrain_ln)
mean_absolute_error(yval_ln, model.predict(xval))

1.2 构建一个线下测试集

这里是简单的介绍一个小技巧吧,当然这里是针对这个比赛,因为有时候我们发现在本地上训练数据集得到的结果很好,但是放到线上进行测试的时候往往不是那么理想,这就意味着我们线下的训练有些过拟合了,而我们一般并不能发现这种情况,毕竟对于线上的测试,我们没有真实的标签对比不,所以我们可以先构建一个线下的测试集。这个实操起来也很简单,就是我们有150000个样本,可以用100000个样本来做训练集,后面的50000做测试集,因为我们已经知道这50000个样本的真实标签,这样训练出来的模型我们就可以直接先测试一下泛化能力,对于后面的调参或者是模型的评估等感觉还是挺好用的

# 导入数据
data = pd.read_csv('./pre_data/pre_data.csv')

train = data[:train_data.shape[0]]
test = data[train_data.shape[0]:]    # 这个先不用

# 选数据
X = train[:100000]
Y= train_data['price'][:100000]
Y_ln = np.log1p(Y)

XTest = train[100000:]   # 模拟一个线下测试集, 看看模型的泛化能力
Ytrue = train_data['price'][100000:]

2. 评估模型的框架

模型选择的时候,可以根据数据的特征和优化目标先选出很多个模型作为备选,因为我们分析完数据不能立刻得出哪个算法对需要解决的问题更有效

就拿这个比赛来说,我们直观上认为由于问题是预测价格,所以这是一个回归问题,肯定使用回归模型(Regressor系列),但是回归模型太多,但我们又知道部分数据呈线性分布,线性回归和正则化的回归算法可能对解决问题比较有效。而由于数据的离散化,通过决策树算法及相应的集成算法也一般会表现出色,所以我们可以锁定几个模型都尝试一下

我一般习惯建立一个字典,把这些模型放到字典里面,然后分别进行交叉验证,可视化结果来判断哪个模型针对当前问题表现比较好,这样从这里面选出3-4个进行下面的环节,也就是模型的调参工作。这里给出一个我常用的一个评估算法模型的一个框架。首先采用10交叉验证来分离数据,通过绝对值误差来比较算法的准确度,误差值越小,准确度越高

num_folds = 10
seed = 7

# 把所有模型写到一个字典中
models = {}
models['LR'] = LinearRegression()
models['Ridge'] = Ridge()
models['LASSO'] = Lasso()
models['DecisionTree'] = DecisionTreeRegressor()
models['RandomForest'] = RandomForestRegressor()
models['GradientBoosting'] = GradientBoostingRegressor()
models['XGB'] = XGBRegressor(n_estimators = 100, objective='reg:squarederror')
models['LGB'] = LGBMRegressor(n_estimators=100)
#models['SVR'] = SVR()   # 支持向量机运行不出来

results = []
for key in models:
    kfold = KFold(n_splits=num_folds, random_state=seed)
    cv_result = cross_val_score(models[key], X, Y_ln, cv=kfold, scoring=make_scorer(mean_absolute_error))
    results.append(cv_result)
    print('%s: %f (%f)' % (key, cv_result.mean(), cv_result.std()))
    
# 评估算法 --- 箱线图
fig1 = plt.figure(figsize=(15, 10))
fig1.suptitle('Algorithm Comparison')
ax = fig1.add_subplot(111)
plt.boxplot(results)
ax.set_xticklabels(models.keys())
plt.show()


## 结果:
LR: 0.192890 (0.001501)
Ridge: 0.196279 (0.001616)
LASSO: 0.515573 (0.003923)
DecisionTree: 0.190959 (0.002524)
RandomForest: 0.142333 (0.001489)
GradientBoosting: 0.178403 (0.001903)
XGB: 0.178492 (0.001441)
LGB: 0.147875 (0.001397)

看一下箱线图的结果:

eEVney7.png!web

这样,各个模型的效果就一目了然了,从上图可以看出,随机森林和LGB的效果还是好一些的,后面可以基于这两个进行调参,当然xgboost的效果可能由于参数的原因表现不是那么理想,这里也作为了我们调参备选

那么调参究竟有没有影响呢?我这里做了一个实验,可以先看一下:

model2 = LGBMRegressor(n_estimators=100)
model2.fit(X, Y_ln)
pred2 = model2.predict(XTest)
print("mae: ", mean_absolute_error(Ytrue, np.expm1(pred2)))

# 结果:
mae:  713.9408513079144

上面这个是没有调参的LGB,下面再看一下调参的LGB:

def bulid_modl_lgb(x_train, y_train):
    estimator = LGBMRegressor(num_leaves=127, n_estimators=150)
    param_grid = {'learning_rage': [0.01, 0.05, 0.1, 0.2]}
    gbm = GridSearchCV(estimator, param_grid)
    gbm.fit(x_train, y_train)
    return gbm
 
model_lgb = bulid_modl_lgb(X, Y_ln)
val_lgb = model_lgb.predict(XTest)
MAE_lgb = mean_absolute_error(Ytrue, np.expm1(val_lgb))
print(MAE_lgb)


## 结果:
591.4221480289154

同样的LGB,调参误差能降到591,不调参713,所以调参还是很重要的。但是在调参之前,先给出一个正态化模板

from sklearn.pipeline import Pipeline

pipelines = {}
pipelines['ScalerLR'] = Pipeline([('Scaler', StandardScaler()), ('LR', LinearRegression())])
pipelines['ScalerRidge'] = Pipeline([('Scaler', StandardScaler()), ('Ridge', Ridge())])
pipelines['ScalerLasso'] = Pipeline([('Scaler', StandardScaler()), ('Lasso', Lasso())])
pipelines['ScalerTree'] = Pipeline([('Scaler', StandardScaler()), ('Tree', DecisionTreeRegressor())])
pipelines['ScalerForest'] = Pipeline([('Scaler', StandardScaler()), ('Forest', RandomForestRegressor())])
pipelines['ScalerGBDT'] = Pipeline([('Scaler', StandardScaler()), ('GBDT', GradientBoostingRegressor())])
pipelines['ScalerXGB'] = Pipeline([('Scaler', StandardScaler()), ('XGB', XGBRegressor(n_estimators = 100, objective='reg:squarederror'))])
pipelines['ScalerLGB'] = Pipeline([('Scaler', StandardScaler()), ('LGB', LGBMRegressor(n_estimators=100))])

results = []
for key in pipelines:
    kfold = KFold(n_splits=num_folds, random_state=seed)
    cv_result = cross_val_score(pipelines[key], X, Y_ln, cv=kfold, scoring=make_scorer(mean_absolute_error))
    results.append(cv_result)
    print('%s: %f (%f)' % (key, cv_result.mean(), cv_result.std()))


# 评估算法 --- 箱线图
fig2 = plt.figure(figsize=(15, 10))
fig2.suptitle('Algorithm Comparison')
ax = fig2.add_subplot(111)
plt.boxplot(results)
ax.set_xticklabels(models.keys())

我这里不用正态化,因为我试验了一下,效果不如之前的好

3. 模型调参

同特征工程一样,模型参数调节也是一项非常繁琐但又非常重要的工作

根据模型复杂程度的不同,需要调节的参数数量也不尽相同。简单如逻辑回归,需要调节的通常只有正则项系数C;复杂如随机森林,需要调节的变量会多出不少,最核心的如树的数量n_estimators,树的深度max_depth等等。参数越多,调参的难度自然也越来越大,因为参数间排列组合的可能性越来越多。在训练样本比较少的情况下,sklearn的GridSearchCV是个不错的选择,可以帮助我们自动寻找指定范围内的最佳参数组合。但实际情况是,GridSearch通常需要的运行时间过长,长到我们不太能够忍受的程度。所以更多的时候需要我们自己手动先排除掉一部分数值,然后使用GridSearch自动调参

模型调参有三种方式:

  • 贪心调参
  • 网格搜索调参
  • 贝叶斯调参

这里给出一个模型可调参数及范围选取的参考:

6jMNryy.png!web

下面我以LGB作为实验,因为其他的模型也都是这个思路,所以为了减少篇幅,只对LGB做实验

objective = ['regression', 'regression_l1', 'mape', 'huber', 'fair']
num_leaves = [10, 55, 70, 100, 200]
max_depth = [ 10, 55, 70, 100, 200]
n_estimators = [200, 400, 800, 1000]
learning_rate =  [0.01, 0.05, 0.1, 0.2]

3.1 贪心调参

拿当前对模型影响最大的参数调优,直到最优化;再拿下一个影响最大的参数调优,如此下去,直到所有的参数调整完毕。这个方法的 缺点就是可能会调到局部最优而不是全局最优,但是省时间省力 ,巨大的优势面前,可以一试

# 先建立一个参数字典
best_obj = dict()

# 调objective
for obj in objective:
    model = LGBMRegressor(objective=obj)
    score = np.mean(cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)))
    best_obj[obj] = score
 
# 上面调好之后,用上面的参数调num_leaves
best_leaves = dict()
for leaves in num_leaves:
    model = LGBMRegressor(objective=min(best_obj.items(), key=lambda x:x[1])[0], num_leaves=leaves)
    score = np.mean(cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)))
    best_leaves[leaves] = score

# 用上面两个最优参数调max_depth
best_depth = dict()
for depth in max_depth:
    model = LGBMRegressor(objective=min(best_obj.items(), key=lambda x:x[1])[0],
                          num_leaves=min(best_leaves.items(), key=lambda x: x[1])[0],
                          max_depth=depth)
    score = np.mean(cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)))
    best_depth[depth] = score

# 调n_estimators
best_nstimators = dict()
for nstimator in n_estimators:
    model = LGBMRegressor(objective=min(best_obj.items(), key=lambda x:x[1])[0],
                          num_leaves=min(best_leaves.items(), key=lambda x: x[1])[0],
                          max_depth=min(best_depth.items(), key=lambda x:x[1])[0],
                          n_estimators=nstimator)
    
    score = np.mean(cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)))
    best_nstimators[nstimator] = score
 
# 调learning_rate
best_lr = dict()
for lr in learning_rate:
    model = LGBMRegressor(objective=min(best_obj.items(), key=lambda x:x[1])[0],
                          num_leaves=min(best_leaves.items(), key=lambda x: x[1])[0],
                          max_depth=min(best_depth.items(), key=lambda x:x[1])[0],
                          n_estimators=min(best_nstimators.items(), key=lambda x:x[1])[0],
                          learning_rate=lr)
    score = np.mean(cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)))
    best_lr[lr] = score

上面的过程建议放在不同的cell里面运行,之后可视化这个过程的结果:

sns.lineplot(x=['0_initial','1_turning_obj','2_turning_leaves',
               '3_turning_depth','4_turning_estimators', '5_turning_lr'],
            y=[0.143 ,min(best_obj.values()), min(best_leaves.values()), min(best_depth.values()),
              min(best_nstimators.values()), min(best_lr.values())])

yIJnI33.png!web

贪心的调参策略还是不错的,我们还可以打印最后调参的结果:

print("best_obj:", min(best_obj.items(), key=lambda x: x[1]))
print("best_leaves:", min(best_leaves.items(), key=lambda x: x[1]) )
print('best_depth:', min(best_depth.items(), key=lambda x: x[1]))
print('best_nstimators: ', min(best_nstimators.items(), key=lambda x: x[1]))
print('best_lr:', min(best_lr.items(), key=lambda x: x[1]))


## 结果如下:
best_obj: ('regression_l1', 0.1457016215267976)
best_leaves: (100, 0.132929241004274)
best_depth: (20, 0.13275966837758682)
best_nstimators:  (1000, 0.11861541074643345)
best_lr: (0.05, 0.11728267187328578)

3.2 GridSearchCV调参

GridSearchCV,它存在的意义就是自动调参,只要把参数输进去,就能给出最优化的结果和参数。但是这个方法适合于小数据集,一旦数据的量级上去了,很难得出结果。这个在这里面优势不大, 因为数据集很大,不太能跑出结果,但是我也整理一下,有时候还是很好用的

from sklearn.model_selection import GridSearchCV

# 这个我这边电脑运行时间太长,先不跑了
parameters = {'objective':objective, 'num_leaves':num_leaves, 'max_depth':max_depth,
             'n_estimators': n_estimators, 'learning_rate':learning_rate}

model = LGBMRegressor()
clf = GridSearchCV(model, parameters, cv=5)
clf = clf.fit(X, Y_ln)

# 输出最优参数
clf.best_params_

3.3 贝叶斯调参

首先需要安装包 pip install bayesian-optimization

贝叶斯优化用于机器学习调参,主要思想是,给定优化的目标函数(广义的函数,只需指定输入和输出即可,无需知道内部结构以及数学性质),通过不断地添加样本点来更新目标函数的后验分布(高斯过程,直到后验分布基本贴合于真实分布。简单的说,就是考虑了上一次参数的信息,从而更好的调整当前的参数

它与常规的网格搜索或者随机搜索的区别是:

  • 贝叶斯调参采用高斯过程,考虑之前的参数信息,不断地更新先验;网格搜索未考虑之前的参数信息
  • 贝叶斯调参迭代次数少,速度快;网格搜索速度慢,参数多时易导致维度爆炸
  • 贝叶斯调参针对非凸问题依然稳健;网格搜索针对非凸问题易得到局部最优

使用方法:

  • 定义优化函数(rf_cv,在里面把优化的参数传入,然后建立模型,返回要优化的分数指标)
  • 定义优化参数
  • 开始优化(最大化分数还是最小化分数等)
  • 得到优化结果
from  bayes_opt import BayesianOptimization

# 定义优化函数
def rf_cv(num_leaves, max_depth, subsample, min_child_samples):
    model = LGBMRegressor(objective='regression_l1', num_leaves=int(num_leaves),
                         max_depth=int(max_depth), subsample=subsample,
                         min_child_samples = int(min_child_samples))
    val = cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)).mean()
    
    return 1-val

# 定义优化参数
rf_bo = BayesianOptimization(
    rf_cv, 
    {
        'num_leaves':(2, 100),
        'max_depth':(2, 100),
        'subsample':(0.1, 1),
        'min_child_samples':(2, 100)
    }
)

#开始优化
num_iter = 25
init_points = 5
rf_bo.maximize(init_points=init_points,n_iter=num_iter)

#显示优化结果
rf_bo.res["max"]


#附近搜索(已经有不错的参数值的时候)
rf_bo.explore(
     {'n_estimators': [10, 100, 200],
      'min_samples_split': [2, 10, 20],
      'max_features': [0.1, 0.5, 0.9],
      'max_depth': [5, 10, 15]
     })

E7VvmiR.png!web

基于上面的思路,我们也可以对随机森林进行调参:

对Random Forest来说,增加“子模型数”(n_estimators)可以明显降低整体模型的方差,且不会对子模型的偏差和方差有任何影响。模型的准确度会随着“子模型数”的增加而提高。由于减少的是整体模型方差公式的第二项,故准确度的提高有一个上限。在不同的场景下,“分裂条件”(criterion)对模型的准确度的影响也不一样,该参数需要在实际运用时灵活调整。调整“最大叶节点数”(max_leaf_nodes)以及“最大树深度”(max_depth)之一,可以粗粒度地调整树的结构:叶节点越多或者树越深,意味着子模型的偏差越低,方差越高;同时,调整“分裂所需最小样本数”(min_samples_split)、“叶节点最小样本数”(min_samples_leaf)及“叶节点最小权重总值”(min_weight_fraction_leaf),可以更细粒度地调整树的结构:分裂所需样本数越少或者叶节点所需样本越少,也意味着子模型越复杂。一般来说,我们总采用bootstrap对样本进行子采样来降低子模型之间的关联度,从而降低整体模型的方差。适当地减少“分裂时考虑的最大特征数”(max_features),给子模型注入了另外的随机性,同样也达到了降低子模型之间关联度的效果。详细的可以参考:

# 定义优化函数
def rf_cv(n_estimators,  max_depth):
    model = RandomForestRegressor(n_estimators=int(n_estimators), 
                         max_depth=int(max_depth))
    val = cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)).mean()
    
    return 1-val

rf_bo = BayesianOptimization(
    rf_cv, 
    {
        'n_estimators':(100, 200),
        'max_depth':(2, 100)
    }
)

rf_bo.maximize()

4. 绘制训练集曲线与验证集曲线

从上面的步骤中,我们通过算法模型的评估框架选择出了合适的几个模型,又通过模型的调参步骤确定了模型的合适参数,这样我们基本上就得到了一个我们认为的比较好的模型了,但是这个模型真的就是好的模型了吗? 我们还不能确定是否存在过拟合或者欠拟合问题,在实际中究竟应该怎么判断? 学习曲线的绘制就是一个非常好的方式,可以帮助我们看一下我们调试好的模型还有没有过拟合或者欠拟合的问题

关于学习曲线:

  • 学习曲线是不同训练集大小,模型在训练集和验证集上的得分变化曲线
  • 学习曲线图的横坐标是x_train的数据量,纵坐标是对应的train_score,test_score。随着训练样本的逐渐增加,算法练出的模型的表现能力;

绘制学习曲线非常简单

train_sizes,train_scores,test_score = learning_curve(estimator, X, y, groups=None, train_sizes=array([0.1, 0.33, 0.55, 0.78, 1. ]), cv=’warn’, scoring=None)

主要的参数说明如下:

通过cv设置交叉验证,取几次(组)数据,train_sizes设置每一次取值,在不同训练集大小上计算得分

  • estimator:估计器,用什么模型进行学习;
  • cv:交叉验证生成器,确定交叉验证拆分策略;

画训练集的曲线时,横轴为train_sizes, 纵轴为train_scores_mean; train_scores为二维数组,行代表train_sizes不同时的得分,列表示取cv组数据。

画测试集的曲线时:横轴为train_sizes, 纵轴为test_scores_mean; test_scores为二维数组

learning_curve为什么运行时间那么长:模型要进行train_sizes * cv次运行

那么,我们就基于一个训练好的模型,画一下学习曲线,看看这个学习曲线究竟怎么观察:

from sklearn.model_selection import learning_curve, validation_curve

def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None, n_jobs=1, train_size=np.linspace(.1, 1.0, 5)):
    plt.figure()
    plt.title(title)
    if ylim is not None:
        plt.ylim(*ylim)
    plt.xlabel('Training example')  
    plt.ylabel('score')  
    train_sizes, train_scores, test_scores = learning_curve(estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_size, scoring = make_scorer(mean_absolute_error))  
    train_scores_mean = np.mean(train_scores, axis=1)  
    train_scores_std = np.std(train_scores, axis=1)  
    test_scores_mean = np.mean(test_scores, axis=1)  
    test_scores_std = np.std(test_scores, axis=1)  
    plt.grid()#区域  
    plt.fill_between(train_sizes, train_scores_mean - train_scores_std,  
                     train_scores_mean + train_scores_std, alpha=0.1,  
                     color="r")  
    plt.fill_between(train_sizes, test_scores_mean - test_scores_std,  
                     test_scores_mean + test_scores_std, alpha=0.1,  
                     color="g")  
    plt.plot(train_sizes, train_scores_mean, 'o-', color='r',  
             label="Training score")  
    plt.plot(train_sizes, test_scores_mean,'o-',color="g",  
             label="Cross-validation score")  
    plt.legend(loc="best")  
    return plt  

# 假设已经调好了LGB的参数,我们可以绘制一下曲线看看这个模型有没有什么问题
model = LGBMRegressor(n_estimators=1000, leaves=200, learning_rate=0.05, objective='regression_l1')
model.fit(X, Y_ln)
pred2 = model.predict(XTest)
print("mae: ", mean_absolute_error(Ytrue, np.expm1(pred2)))

# 画出学习曲线
plot_learning_curve(model, 'LGB', X[:10000], Y_ln[:10000], ylim=(0.0, 1), cv=5, n_jobs=1)

NNZ3UzZ.png!web

下面整理一下如何观察学习曲线

learning_curve里面有个scoring参数可以设置你想求的值,分类可以设置 accuracy ,回归问题可以设置 neg_mean_squared_error ,总体来说,值都是越大越好,但是注意如果模型设置的是 mae erro ,那就是越低越好

高偏差和高方差应该怎么看呢?引用一个博客里面的图片

FFfm2aU.png!web

什么情况欠拟合:模型在训练集和验证集上准确率相差不大,却都很差,说明模型对已知数据和未知数据都不能准确预测,属于高偏差。左上角那个图

什么情况过拟合:模型在训练集和验证集上的准确率差距很大,说明模型能够很好的拟合已知数据,但是泛化能力很差,属于高方差。右上角那个图

右下角那个图是比较合适的。所以上面lgb的那个模型效果还是不错的


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK