建模调参
学习内容
线性回归模型:
- 线性回归对于特征的要求;
- 处理长尾分布;
- 理解线性回归模型;
模型性能验证:
- 评价函数与目标函数;
- 交叉验证方法;
- 留一验证方法;
- 针对时间序列问题的验证;
- 绘制学习率曲线;
- 绘制验证曲线;
嵌入式特征选择:
- Lasso回归;
- Ridge回归;
- 决策树;
模型对比:
- 常用线性模型;
- 常用非线性模型;
模型调参:
- 贪心调参方法;
- 网格调参方法;
- 贝叶斯调参方法
模型介绍
线性回归
线性回归模型试图学得一个通过属性的线性组合来进行预测的函数
f(x)=w1X1+w2X2+w3X3+…+wdxd+b
向量形式:
f(x)=wTx+b
离散属性处理:若有序,则连续话,否则,转化为K维向量
这里直接贴个链接
链接: lhttps://zhuanlan.zhihu.com/p/49480391.
长尾分布:
比较有意思的描述是,由于成本和效率因素,当商品存储流通展示的场地和渠道足够宽广,商品生产成本急剧下降,以至于个人都可以进行生产,并且商品的销售成本急剧降低时,几乎任何以前看似需求极地的产品,只要有卖,都会有买,这些需求和销量不高的产品所占据的共同市场份额,可以和主流产品的市场份额相比,甚至更大。
贪心算法
在对问题求解时,总是做出在当前看来最好的选择,也就是说,不从整体最优加以考虑,它所做出的是在某种意义上的局部最优解。选择贪心算法必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
网格调参
穷举调参,在所有候选的参数选择中,通过循环遍历,尝试每一种可能性,表现最好的参数就是最终结果。
贝叶斯调参
基于目标函数的过去评估结果建立替代函数,来找到最小化目标函数的值。贝叶斯方法与随机或网格不同之处在于,它尝试下一组超参数时,会参考之前的评估结果,因此可以省去很多无用功。
*二手车价格预测此题适合线性回顾模型的理解
元数据 | 解释 |
---|---|
name | 汽车编码 |
regDate | 汽车注册时间 |
model | 车型编码 |
brand | 品牌 |
bodyType | 车身类型 |
fuelType | 燃油类型 |
gearbox | 变速箱 |
power | 汽车功率 |
kilometer | 汽车行驶公里 |
notRepairedDamage | 汽车有尚未修复的损坏 |
regionCode | 看车地区编码 |
seller | 销售方 |
offerType | 报价类型 |
creatDate | 广告发布时间 |
price | 汽车价格 |
以及其他v_0至v14特征
在Task3中选择了
元数据 | 解释 |
---|---|
name | 汽车编码 |
regDate | 汽车注册时间 |
model | 车型编码 |
brand | 品牌 |
bodyType | 车身类型 |
fuelType | 燃油类型 |
gearbox | 变速箱 |
power | 汽车功率 |
kilometer | 汽车行驶公里 |
notRepairedDamage | 汽车有尚未修复的损坏 |
regionCode | 看车地区编码 |
creatDate | 广告发布时间 |
等多个元数据最为预测所用特征,用以预测二手车价格。及通过属性组合拟合一个函数,用以预测车辆价格。因此可以说是典型的回归问题。
代码学习
#导入模型和读取数据
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
大佬提供的reduce_mem_usage 函数通过调整数据类型,帮助我们减少数据在内存中占用的空间,建议学习
def reduce_mem_usage(df):
""" iterate through all the columns of a dataframe and modify the data type
to reduce memory usage.
"""
start_mem = df.memory_usage().sum()
print('Memory usage of dataframe is {:.2f} MB'.format(start_mem))
for col in df.columns:
col_type = df[col].dtype
if col_type != object:
c_min = df[col].min()
c_max = df[col].max()
if str(col_type)[:3] == 'int':
if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
df[col] = df[col].astype(np.int8)
elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
df[col] = df[col].astype(np.int16)
elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
df[col] = df[col].astype(np.int32)
elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
df[col] = df[col].astype(np.int64)
else:
if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
df[col] = df[col].astype(np.float16)
elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
df[col] = df[col].astype(np.float32)
else:
df[col] = df[col].astype(np.float64)
else:
df[col] = df[col].astype('category')
end_mem = df.memory_usage().sum()
print('Memory usage after optimization is: {:.2f} MB'.format(end_mem))
print('Decreased by {:.1f}%'.format(100 * (start_mem - end_mem) / start_mem))
return df
把数据转化后赋值给变量sample_feature
sample_feature = reduce_mem_usage(pd.read_csv('data_for_tree.csv'))
线性回归 & 五折交叉验证 & 模拟真实业务情况
continuous_feature_names = [x for x in sample_feature.columns if x not in ['price','brand','model','brand']]
sample_feature = sample_feature.dropna().replace('-', 0).reset_index(drop=True)
sample_feature['notRepairedDamage'] = sample_feature['notRepairedDamage'].astype(np.float32)
train = sample_feature[continuous_feature_names + ['price']]
train_X = train[continuous_feature_names]
train_y = train['price']
简单建模
#导入线性回归模块
from sklearn.linear_model import LinearRegression
model = LinearRegression(normalize=True)
model = model.fit(train_X, train_y)
查看训练的线性回归模型的截距(intercept)与权重(coef)
'intercept:'+ str(model.intercept_)
sorted(dict(zip(continuous_feature_names, model.coef_)).items(), key=lambda x:x[1], reverse=True)
[('v_6', 3509133.3556335964),
('v_8', 739866.7711616001),
('v_9', 173781.00967028155),
('v_7', 41044.805433941016),
('v_12', 30888.95721072973),
('v_5', 27453.39738680756),
('v_3', 23664.188371126955),
('v_11', 15953.34701993885),
('v_13', 13071.48191385408),
('v_10', 7815.353298260309),
('gearbox', 900.8564809596271),
('fuelType', 426.18396873493714),
('bodyType', 190.321639675651),
('city', 44.487589413971016),
('power', 27.430553534775616),
('brand_price_median', 0.5498384842815008),
('brand_price_std', 0.48508518798148187),
('brand_amount', 0.14940527531407094),
('used_time', 0.02179830302065399),
('brand_price_max', 0.003136932045778858),
('SaleID', 2.091664025644962e-05),
('offerType', 3.907829523086548e-06),
('train', -1.862645149230957e-09),
('seller', -1.0849907994270325e-06),
('brand_price_sum', -2.165152734579202e-05),
('name', -0.0003995079681757267),
('brand_price_average', -0.4597090013419026),
('brand_price_min', -2.2063783078698163),
('power_bin', -15.98197690477394),
('v_14', -363.8704254957781),
('kilometer', -388.47363328983346),
('notRepairedDamage', -429.0950578648204),
('v_0', -2092.782811714247),
('v_4', -16184.453743213142),
('v_2', -36879.522402007824),
('v_1', -43460.2165225294)]
from matplotlib import pyplot as plt
绘制特征v_9的值与标签的散点图,图片发现模型的预测结果(蓝色点)与真实标签(黑色点)的分布差异较大,且部分预测值出现了小于0的情况,说明我们的模型存在一些问题
plt.scatter(train_X['v_9'][subsample_index], train_y[subsample_index], color='black')
plt.scatter(train_X['v_9'][subsample_index], model.predict(train_X.loc[subsample_index]), color='blue')
plt.xlabel('v_9')
plt.ylabel('price')
plt.legend(['True Price','Predicted Price'],loc='upper right')
print('The predicted price is obvious different from true price')
plt.show()
针对v_9特征散点图会发现的预测值小于0,这是有问题的,有此步骤,通过对每个特征值进行二维投射,可以发现不和常理的预测值,可以作为一种检查模型是否有效的方法。
我们可以在尝试一下其他特征值的二维投影
例如 v_1 和 power 进行比较
v_1特征
power特征值
进一步验证了模型存在问题,都存在负值
通过作图我们发现数据的标签(price)呈现长尾分布,不利于我们的建模预测。原因是很多模型都假设数据误差项符合正态分布,而长尾分布的数据违背了这一假设。
参考博客
链接: https://blog.csdn.net/Noob_daniel/article/details/76087829.
import seaborn as sns
print('It is clear to see the price shows a typical exponential distribution')
plt.figure(figsize=(15,5))
plt.subplot(1,2,1)
sns.distplot(train_y)
plt.subplot(1,2,2)
sns.distplot(train_y[train_y < np.quantile(train_y, 0.9)])
图二报步距缩小产看细节
在这里我们对标签进行了
l
o
g
(
x
+
1
)
log(x+1)
log(x+1) 变换,使标签贴近于正态分布
train_y_ln = np.log(train_y + 1)
import seaborn as sns
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)])
再次查看截距
model = model.fit(train_X, train_y_ln)
print('intercept:'+ str(model.intercept_))
sorted(dict(zip(continuous_feature_names, model.coef_)).items(), key=lambda x:x[1], reverse=True)
[('v_5', 15.589879511666657),
('v_9', 10.68841881845513),
('v_7', 2.4683441522682092),
('v_12', 2.1967462628373675),
('v_1', 1.6910345512082352),
('v_11', 1.488829560391837),
('v_13', 1.1338756600731228),
('v_3', 1.0418417579979447),
('fuelType', 0.01066089982875368),
('v_0', 0.008665972713696313),
('gearbox', 0.008337601442899207),
('power_bin', 0.00704062597507508),
('bodyType', 0.005513383874082351),
('power', 0.0009932176795698084),
('brand_price_min', 2.7759469212786182e-05),
('brand_amount', 2.9385431871300297e-06),
('brand_price_median', 2.270421352196736e-06),
('brand_price_std', 1.077271110357109e-06),
('brand_price_max', 5.043368462444261e-07),
('brand_price_average', 5.1948032183469904e-08),
('SaleID', 3.3792011024575367e-08),
('offerType', 1.7183765521622263e-10),
('train', 8.355982572538778e-12),
('brand_price_sum', -1.0477245921324486e-10),
('seller', -1.2732392917769175e-10),
('name', -7.677721630024973e-08),
('used_time', -9.782091770012744e-06),
('city', -0.0015566347616109493),
('kilometer', -0.015292955467086639),
('v_14', -0.020504831596895784),
('notRepairedDamage', -0.28967606111829225),
('v_4', -0.9844305913339497),
('v_10', -1.3799257334481814),
('v_2', -1.560834871931928),
('v_8', -36.423618706528316),
('v_6', -241.90868358166924)]
再次进行可视化,发现预测结果与真实值较为接近,且未出现异常状况
plt.scatter(train_X['v_9'][subsample_index], train_y[subsample_index], color='black')
plt.scatter(train_X['v_9'][subsample_index], np.exp(model.predict(train_X.loc[subsample_index])), color='blue')
plt.xlabel('v_9')
plt.ylabel('price')
plt.legend(['True Price','Predicted Price'],loc='upper right')
print('The predicted price seems normal after np.log transforming')
plt.show()
v_9特征基本正常
五折交叉验证
在使用训练集对参数进行训练的时候,经常会发现人们通常会将一整个训练集分为三个部分(比如mnist手写训练集)。一般分为:训练集(train_set),评估集(valid_set),测试集(test_set)这三个部分。这其实是为了保证训练效果而特意设置的。其中测试集很好理解,其实就是完全不参与训练的数据,仅仅用来观测测试效果的数据。而训练集和评估集则牵涉到下面的知识了。
因为在实际的训练中,训练的结果对于训练集的拟合程度通常还是挺好的(初始条件敏感),但是对于训练集之外的数据的拟合程度通常就不那么令人满意了。因此我们通常并不会把所有的数据集都拿来训练,而是分出一部分来(这一部分不参加训练)对训练集生成的参数进行测试,相对客观的判断这些参数对训练集之外的数据的符合程度。这种思想就称为交叉验证(Cross Validation)
#sklearn.model_selection为交叉验证模块
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)))
[Parallel(n_jobs=1)]: Using backend SequentialBackend with 1 concurrent workers.
[Parallel(n_jobs=1)]: Done 5 out of 5 | elapsed: 1.1s finished
使用线性回归模型,对未处理标签的特征数据进行五折交叉验证(Error 1.36)
print('AVG:', np.mean(scores))
AVG: 1.3321972524518693
使用线性回归模型,对处理过标签的特征数据进行五折交叉验证(Error 0.19)
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))
AVG: 0.19382863663604424
scores = pd.DataFrame(scores.reshape(1,-1))
scores.columns = ['cv' + str(x) for x in range(1, 6)]
scores.index = ['MAE']
scores
模拟真实业务情况
但在事实上,由于我们并不具有预知未来的能力,**五折交叉验证在某些与时间相关的数据集上反而反映了不真实的情况。**通过2018年的二手车价格预测2017年的二手车价格,这显然是不合理的,因此我们还可以采用时间顺序对数据集进行分隔。在本例中,我们选用靠前时间的4/5样本当作训练集,靠后时间的1/5当作验证集,最终结果与五折交叉验证差距不大
import datetime
sample_feature = sample_feature.reset_index(drop=True)
DataFrame中set_index()与reset_index()介绍
set_index()
keys | 单个或多个列名 |
---|---|
drop | default Ture,将作为行索引的 列删掉 |
例子
import pandas as pd
import numpy as np
df=pd.DataFrame(np.arange(10).reshape(2,5),index=['time','food'])
df
haha=df.set_index(3)
haha
reset_index()
level | 将df的原index_lable作为新的一列留存,且列名为index,同时自动生成数字index |
---|---|
drop | defaule False,如果是Ture,删除原来索引 |
inplace | 下面几个都是Multiindex下的参数,后续研究 |
常用于在数据清洗过后,对数据重新设置连续行索引
例
split_point = len(sample_feature) // 5 * 4
train = sample_feature.loc[:split_point].dropna()
val = sample_feature.loc[split_point:].dropna()
train_X = train[continuous_feature_names]
train_y_ln = np.log(train['price'] + 1)
val_X = val[continuous_feature_names]
val_y_ln = np.log(val['price'] + 1)
model = model.fit(train_X, train_y_ln)
mean_absolute_error(val_y_ln, model.predict(val_X))
0.19443858353490887
绘制学习率曲线与验证曲线
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
plot_learning_curve(LinearRegression(), 'Liner_model', train_X[:1000], train_y_ln[:1000], ylim=(0.0, 0.5), cv=5, n_jobs=1)
这里可以看到随着训练集数量增加,连个曲线逐渐接近,但是不太明白是什么意思
多种模型对比
train = sample_feature[continuous_feature_names + ['price']].dropna()
train_X = train[continuous_feature_names]
train_y = train['price']
train_y_ln = np.log(train_y + 1)
线性模型 & 嵌入式特征选择
在过滤式和包裹式特征选择方法中,特征选择过程与学习器训练过程有明显的分别。而嵌入式特征选择在学习器训练过程中自动地进行特征选择。嵌入式选择最常用的是L1正则化与L2正则化。在对线性回归模型加入两种正则化方法后,他们分别变成了岭回归与Lasso回归。
用简单易懂的语言描述「过拟合 overfitting」: https://www.zhihu.com/question/32246256/answer/55320482.
模型复杂度与模型的泛化能力: http://yangyingming.com/article/434/.
正则化的直观理解: https://blog.csdn.net/jinping_shi/article/details/52433975.
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Ridge
from sklearn.linear_model import Lasso
models = [LinearRegression(),
Ridge(),
Lasso()]
LinearRegression is finished
Ridge is finished
Lasso is finished
result = dict()
for model in models:
model_name = str(model).split('(')[0]
scores = cross_val_score(model, X=train_X, y=train_y_ln, verbose=0, cv = 5, scoring=make_scorer(mean_absolute_error))
result[model_name] = scores
print(model_name + ' is finished')
对三种方法的效果对比
result = pd.DataFrame(result)
result.index = ['cv' + str(x) for x in range(1, 6)]
result
model = LinearRegression().fit(train_X, train_y_ln)
print('intercept:'+ str(model.intercept_))
sns.barplot(abs(model.coef_), continuous_feature_names)
L2正则化在拟合过程中通常都倾向于让权值尽可能小,最后构造一个所有参数都比较小的模型。因为一般认为参数值小的模型比较简单,能适应不同的数据集,也在一定程度上避免了过拟合现象。可以设想一下对于一个线性回归方程,若参数很大,那么只要数据偏移一点点,就会对结果造成很大的影响;但如果参数足够小,数据偏移得多一点也不会对结果造成什么影响,专业一点的说法是『抗扰动能力强』
model = Ridge().fit(train_X, train_y_ln)
print('intercept:'+ str(model.intercept_))
sns.barplot(abs(model.coef_), continuous_feature_names)
L1正则化有助于生成一个稀疏权值矩阵,进而可以用于特征选择。如下图,我们发现power与userd_time特征非常重要。
model = Lasso().fit(train_X, train_y_ln)
print('intercept:'+ str(model.intercept_))
sns.barplot(abs(model.coef_), continuous_feature_names)
除此之外,决策树通过信息熵或GINI指数选择分裂节点时,优先选择的分裂特征也更加重要,这同样是一种特征选择的方法。XGBoost与LightGBM模型中的model_importance指标正是基于此计算的
非线性模型
除了线性模型以外,还有许多我们常用的非线性模型如下,在此篇幅有限不再一一讲解原理。我们选择了部分常用模型与线性模型进行效果比对。
from sklearn.linear_model import LinearRegression
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.neural_network import MLPRegressor
from xgboost.sklearn import XGBRegressor
from lightgbm.sklearn import LGBMRegressor
models = [LinearRegression(),
DecisionTreeRegressor(),
RandomForestRegressor(),
GradientBoostingRegressor(),
MLPRegressor(solver='lbfgs', max_iter=100),
XGBRegressor(n_estimators = 100, objective='reg:squarederror'),
LGBMRegressor(n_estimators = 100)]
result = dict()
for model in models:
model_name = str(model).split('(')[0]
scores = cross_val_score(model, X=train_X, y=train_y_ln, verbose=0, cv = 5, scoring=make_scorer(mean_absolute_error))
result[model_name] = scores
print(model_name + ' is finished')
result = pd.DataFrame(result)
result.index = ['cv' + str(x) for x in range(1, 6)]
result
可以看到随机森林模型在每一个fold中均取得了更好的效果
模型调参
贪心算法: https://www.jianshu.com/p/ab89df9759c8
网格调参 :https://blog.csdn.net/weixin_43172660/article/details/83032029
贝叶斯调参: https://blog.csdn.net/linxid/article/details/81189154
## LGB的参数集合:
objective = ['regression', 'regression_l1', 'mape', 'huber', 'fair']
num_leaves = [3,5,10,15,20,40, 55]
max_depth = [3,5,10,15,20,40, 55]
bagging_fraction = []
feature_fraction = []
drop_rate = []
贪心调参
best_obj = dict()
for obj in objective:
model = LGBMRegressor(objective=obj)
score = np.mean(cross_val_score(model, X=train_X, y=train_y_ln, verbose=0, cv = 5, scoring=make_scorer(mean_absolute_error)))
best_obj[obj] = score
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=train_X, y=train_y_ln, verbose=0, cv = 5, scoring=make_scorer(mean_absolute_error)))
best_leaves[leaves] = score
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=train_X, y=train_y_ln, verbose=0, cv = 5, scoring=make_scorer(mean_absolute_error)))
best_depth[depth] = score
sns.lineplot(x=['0_initial','1_turning_obj','2_turning_leaves','3_turning_depth'], y=[0.143 ,min(best_obj.values()), min(best_leaves.values()), min(best_depth.values())])
Grid Search 调参
from sklearn.model_selection import GridSearchCV
parameters = {'objective': objective , 'num_leaves': num_leaves, 'max_depth': max_depth}
model = LGBMRegressor()
clf = GridSearchCV(model, parameters, cv=5)
clf = clf.fit(train_X, train_y)
clf.best_params_
{'max_depth': 15, 'num_leaves': 55, 'objective': 'regression'}
model = LGBMRegressor(objective='regression',
num_leaves=55,
max_depth=15)
np.mean(cross_val_score(model, X=train_X, y=train_y_ln, verbose=0, cv = 5, scoring=make_scorer(mean_absolute_error)))
0.13626164479243302
贝叶斯调参
from bayes_opt import BayesianOptimization
def rf_cv(num_leaves, max_depth, subsample, min_child_samples):
val = cross_val_score(
LGBMRegressor(objective = 'regression_l1',
num_leaves=int(num_leaves),
max_depth=int(max_depth),
subsample = subsample,
min_child_samples = int(min_child_samples)
),
X=train_X, y=train_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)
}
)
rf_bo.maximize()
总结
在本章中,我们完成了建模与调参的工作,并对我们的模型进行了验证。此外,我们还采用了一些基本方法来提高预测的精度,提升如下图所示。
plt.figure(figsize=(13,5))
sns.lineplot(x=['0_origin','1_log_transfer','2_L1_&_L2','3_change_model','4_parameter_turning'], y=[1.36 ,0.19, 0.19, 0.14, 0.13])