解决的问题是预测两个城市某街道上的几处公共自行车停车桩。我们希望根据时间、天气等信息,预测出该街区在一小时内的被借取的公共自行车的数量。数据下载地址:
http://sofasofa.io/competition.php?id=1
比赛已经提供了3个直接可以执行的不同算法代码,直接可以看出不同模型准度差异。其中提供了Xgboost的默认使用方法。本文档建立在公开提供的算法上进行的二次优化。
xgboost的介绍:https://blog.csdn.net/v_JULY_v/article/details/81410574
优化步骤大体可以分为以下3步:
数据处理
特征工程
调参,调参,还是调参
1.数据处理
1.1先看看数据信息info
【介绍】以下是数据变量:y是要预测的,其他和和y相关的内容。
图1-1包含变量
一般优化第一步是要对数据进行一定处理。可以结合一定的工具或者图表观察一下数据特点。
【代码】
train = pd.read_csv("train.csv")
test = pd.read_csv("test.csv")
submit = pd.read_csv("sample_submit.csv")
print(train.info())
info()函数说明:
功能:用于给出样本数据的相关信息概览 :行数,列数,列索引,列非空值个数,列类型,内存占用
使用格式:data.info()
详细了解:https://blog.csdn.net/Dreamer_rx/article/details/100804378
【分析】运行效果如下:有10000个观测值,没有缺失值
图1-2运行效果
类似的还有describe()函数,功能:直接给出样本数据的一些基本的统计量,包括均值,标准差,最大值,最小值,分位数等。使用格式:data.describe()
详细了解:https://blog.csdn.net/Dreamer_rx/article/details/100804378
我们和数据没有缺少,那有没有重复的呢?
d=0
for i in train.duplicated():
if i !=False:
d+=1
print("d:",d)
打印d=14,有14个重复的结果。删除之。然后我们测试一下效果。
#train.drop_duplicates(subset=['city'], keep='first')
train=train.drop_duplicates( keep='first')#删除重复值
#训练和测试
def train_d(X_train, y_train, X_test):
#训练
model=xgb.XGBRegressor()
model.fit(X_train,y_train)
#测试
y_pred=model.predict(X_test)
return y_pred
#定义误差函数
def rmse(y_pre,y_test):
e=[]
for i in range(len(y_test)):
e.append(y_pre[i]-y_test[i])
squaredError=[]
for h in e:
squaredError.append(h * h) # target-prediction之差平方
RMSE =sqrt(sum(squaredError) / len(squaredError)) #均方根误差RMSE
return RMSE
未经处理的模型对应的损失值为:
图1-3 默认xgboost结果
去除重复值后的损失值为:
图1-4 数据处理提高了准度
为了避免篇幅过长,就不每次优化一次,跑一次RMSE。但是实际操作中,每一步设计都是反复测试的。
1.2离群点分析
【介绍】
现实数据中总是存在各式各样地“脏数据”,也成为“离群点”,于是为了不因这些少数的离群数据导致整体特征的偏移,将这些离群点单独汇出,而盒图中的胡须的两级修改成最小观测值与最大观测值。这里有个经验,就是最大(最小)观测值设置为与四分位数值间距离为1.5个IQR(中间四分位数极差)。即IQR = Q3-Q1,即上四分位数与下四分位数之间的差,也就是盒子的长度。
通过画出特征的箱线图来分析是否存在离群点。在本例中用y减去y的评分,将差值大于3倍y的标准差的点作为离群点删去,小于的留下来
【代码】
# 读取数据
train = pd.read_csv("train.csv")
test = pd.read_csv("test.csv")
submit = pd.read_csv("sample_submit.csv")
#画出箱线图
fig, axes = plt.subplots(nrows=3,ncols=2) #画出 3*2=6 个子图
fig.set_size_inches(12, 10) #设置图的大小
sn.boxplot(data=train, y="y", orient="v", ax=axes[0][0]) #axes[0][0]表示子图的坐标或者位置
sn.boxplot(data=train, y="y", x="city", orient="v", ax=axes[0][1])
sn.boxplot(data=train, y="y", x="hour", orient="v", ax=axes[1][0])
sn.boxplot(data=train, y="y", x="is_workday", orient="v", ax=axes[1][1])
sn.boxplot(data=train, y="y", x="weather", orient="v", ax=axes[2][0])
sn.boxplot(data=train, y="y", x="wind", orient="v", ax=axes[2][1])
axes[0][0].set(ylabel='bike',title="Box Plot On bike")
axes[0][1].set(xlabel='city', ylabel='bike',title="Box Plot On bike Across city")
axes[1][0].set(xlabel='Hour Of The Day', ylabel='bike',title="Box Plot On bike Across Hour Of The Day")
axes[1][1].set(xlabel='is_workday', ylabel='bike',title="Box Plot On bike Across is_workday")
axes[2][0].set(xlabel='weather', ylabel='bike',title="Box Plot On bike Across Hour Of weather")
axes[2][1].set(xlabel='wind', ylabel='bike',title="Box Plot On bike Across wind")
离群点分析(不要被图吓到,其实操作很简单)
图 1-5 myplot.png
(weathher:1为晴朗,2为多云、阴天,3为轻度降水天气,4为强降水天气)
【分析】结合我们的数据,具体来说:
(1)"weather"的箱线图:从中位数线可以看出,当weather=3,4的时候(3为轻度降水天气,4为强降水天气),频数较低,这和我们的常识相符吧,下雨骑单车的人会较少
(2)"Hour Of The Day"的箱线图:中位数在早7点-8点,晚5点-6点较高。这两个时间段正值上学放学、上班下班高峰期。
(3)"is_workday"的箱线图**:大多数离群点来自1,也就是"Working Day"而非"Non Working Day".
那么根据用法,我们需要按照链接给的经验规则删除离群点(噪点)之所以这样界定离群点,应该跟正态分布类似,因为差值大于3倍标准差的数据的出现概率很小。
#删去离群点
train_without_outliers = train[np.abs(train["y"]-train["y"].mean())<=(3*train["y"].std())] # np.mean():求均值 np.std():求标准差
# 查看删除离群点前 后 共有多少条数据
print("Shape Of The Before Ouliers: ",train.shape) # np.shape():功能是查看矩阵或者数组的维数。c.shape[1] 为第一维的长度(即多少列),c.shape[0] 为第二维的长度(即多少行)。
print("Shape Of The After Ouliers: ",train_without_outliers.shape)
结果如下:
图1-6 结果
2.特征工程
2.1整体相关性分析
【介绍】之前数据图1-1这么多feature哪个更重要呢?万一要给权重,谁给更高呢?分析变量之间的关系,特别是因变量公共自行车的使用数量受哪些特征影响,剔除一些相关性比较微弱的特征。为此,我们作y关于["city","hour","is_workday","weather","temp_1","temp_2","wind"]的关系热力图,从图中我们可以直接看出变量之间相关性的强弱
【代码】
#绘制关系热力图
corrMatt = train[["city","hour","is_workday","weather","temp_1","temp_2","wind","y"]].corr()
mask = np.array(corrMatt)
mask[np.tril_indices_from(mask)] = False
fig,ax= plt.subplots()
fig.set_size_inches(20,10)
sn.heatmap(corrMatt, mask=mask,vmax=0.8, square=True,annot=True)
plt.show()
图2-1相关性分析
【分析】公共自行车使用数量y与是否是工作日is_workday的相关系数只有0.029的时候,猜想把离群点删去,得到的相关系数会有改变,但实际将数据删去离群点再运行了一次,发现结果是一样的,可能是离群点相较训练集而言,所占比重比较小,对变量之间的相关性影响不大。
由结果可以得到以下结论:
(1)hour, temp_1, temp_2 与y 存在弱的正相关关系,wind与y存在较弱的正相关关系,city, weather与y存在较弱的负相关关系。
(2)is_workday 与 y几乎没有相关性,可以考虑删去这个特征。
(3)“temp_1” and "temp_2"具有强烈的相关性。这和我们的常识相符,天气气温和我们感受到的温度接近,这两个预测变量存在强共线性,应该考虑删去一个。只删除了temp_2和相关性非常微弱(只有0.029)的is_wokday。
#删掉一些相关性比较弱的特征
dropFeatures = ['temp_2',"is_workday"]
train = train.drop(dropFeatures,axis=1)
test = test.drop(dropFeatures,axis=1)
2.2单独相关性分析
【介绍】我们了解了哪个特征重要,但是我们解决的问题是“要投放多少辆自行车适合”?那我们就要知道某一个特征中的数据变化规律,如一天中,什么时间段借车最多?什么温度时借车最少?
【代码】所以可以单独查看不同时间段对应借车的数量:
sn.barplot(data=train,x='hour',y='y',ci=None)
【分析】一目了然了解不同时间段的自行车借阅情况。从图中我们可以看出,借车的高峰值分布在早上7-9点和下午17-19点,这反映出在上下班时间段的自行车的使用量是一天之中的高峰,在之后使用量呈下降的趋势,而在0-4点的自行车使用量更是寥寥无几。从中可以得到的信息是在一天之中不同时间的投放数量的一个参考指标。
图2-2 仅时段数据特征
除此时间特征之外,我们可以查看温度特征;可以查看温度特征;可以查看风速特征。在此省略了这些图,但在实践操作中,我们可以打印查看到在以下这样天气情况和自行车使用情况,特别留意天气“4”的时候,编号为“0”的城市只借了19辆车,而编号为“1”的城市一辆都没有借。这个,我们就可以根据结果安排结果,如果没有这些数据工具,那就只能靠经验和猜了。
图2-3 特别情况
2.3 异常数据和预测结果处理
异常数据处理:此处说的异常数据是指,借车数量大于3倍标准差的样本,进行处理。其中处理的方法有两种,第一就是将数据样本进行剔除,第二将数据样本进行取平均值。
预测结果处理:通过对预测结果的观察,我们发现预测结果中出现了一些负数,但是自行车的投放数量肯定不能为负,所以将所有预测结果为负的数据都改为0。
其次预测结果出现了小数,由于自行车的投放数量为整数,所以我们将采用四舍五入的方法对数据进行取整。
3. 调参
调参就是一门玄学,但是先从科学的方法入手,了解有哪些参数可以调整。
(1) learning rate :每一步迭代的步长(或权重),该值太大,运行准确率不高,该值太小,运行速度慢。通过减少每一步的权重,可以提高模型的鲁棒性。 典型值为0.01-0.2。
(2)min_child_weight : 决定最小叶子节点样本权重和,这个参数用于避免过拟合,当它的值较大时,可以避免模型学习到局部的特殊样本。 但是如果这个值过高,会导致欠拟合。
(3)max_depth:树的最大深度,这个值也是用来避免过拟合的。参数范围常为:3-10
(4)gamma:在节点分裂时,只有分裂后损失函数的值下降了,才会分裂这个节点。Gamma指定了节点分裂所需的最小损失函数下降值。 这个参数的值越大,算法越保守。
(5)subsample:这个参数控制对于每棵树随机采样的比例。 减小这个参数的值,算法会更加保守,避免过拟合。但如果这个值设置得过小,可能会导致欠拟合。参数范围常为:0.5-1
(6)colsample_bytree : 用来控制每棵随机采样的列数的占比(每一列是一个特征)。参数范围常为:0.5-1
(7)reg_alpha:权重的L1正则化项。 可以应用在很高维度的情况下,使得算法的速度更快。
(8)reg_lambda:权重的L2正则化项。 这个参数是用来控制XGBoost的正则化部分的。
3.1 用gridSearchCV调参
【介绍】主要是讲思路,不注重具体调参的细节。
详细了解:gridSearchCV(网格搜索)的参数、方法及示例https://blog.csdn.net/weixin_34342578/article/details/92665252?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase
【代码】主要是看最后一行代码打印调参分数
# 取出训练集的y
param_test1 = {
'reg_alpha': [0, 0.001, 0.005, 0.01, 0.05, 0.1],
'reg_lambda': [0.05, 0.1, 0.5, 1, 1.5, 2, 2.5, 3]
}
gsearch1 = GridSearchCV(estimator=XGBRegressor(learning_rate=0.1, n_estimators=200, max_depth=6, min_child_weight=5,
gamma=0.5, subsample=0.8, colsample_bytree=0.8,
nthread=4, scale_pos_weight=1, seed=27),
param_grid=param_test1, iid=False, cv=5)
gsearch1.fit(train, y_train)
print(gsearch1.best_params_, gsearch1.best_score_)
运行结果:
{'reg_alpha': 0.1, 'reg_lambda': 3} 0.9048829604868451
RMSE:15.042
这已经调了几次的结果了,然后就开始分别调整其他参数,如gamma等:
3-1最好效果.png
一般来说,机器学习程序员都说自己是调参程序员,都需要记录一张自己的调参记录表:
3-2结果汇总.png
在调参一定会有这个问题:因为是一个一个或者一组一组地调参,那怎么能够保证这些调得最优参数的值组合起来也是最优的呢?XGBoost参数调优完全指南(附Python代码) 的评论区回答:这样虽然不是最优,但可以达到局部最优,节约了很多时间。前提是对参数的分组要做好,尽可能使组间参数相互独立。 或者换个角度想,我们是不可能做到遍历所有的参数组合的,我们要做的是在有限的时间内找到尽可能最优的参数组合。在同样的时间内,分组调参的参数范围可以比单个组合大得多。效果未必会比单个组合差。
3.1 根据原理调参
【介绍】xgboost根本上来讲是决策树。可以固定树的不同深度,对其他参数调整,返回损失值最小的组合,作为最优参数组合。
调参的大体思路就是针对四个参数进行调参,分别为学习率、迭代次数、树的深度、权重值。采用数据处理后的xgboost模型。
【完整代码】
import pandas as pd
import matplotlib.pyplot as plt
import xgboost as xgb
import numpy as np
from xgboost import plot_importance
from sklearn.impute import SimpleImputer
from sklearn.feature_selection import f_regression
from sklearn.model_selection import train_test_split
from math import sqrt
# 数据清洗
def data_clean(train, test, sample):
imputer = SimpleImputer(missing_values='NaN', strategy='mean') # 空值用平均数填充
imputer.fit(train.loc[1:, :])
train = imputer.transform(train.loc[1:, :])
return train, test, sample
# 训练和测试
def train_d(X_train, y_train, X_test,weight,le,estimators):
# 训练
model = xgb.XGBRegressor(max_depth=5, learning_rate=le, n_estimators=estimators,min_child_weight=weight,
gamma=0.2,colsample_bytree= 1, subsample=0.7,reg_alpha=0.02)
model.fit(X_train, y_train)
# 测试
y_pred = model.predict(X_test)
y_pred = list(map(lambda x: x if x >= 0 else 1, y_pred))
y_pred=list(map(lambda x: round(x) ,y_pred))
return y_pred
# 定义误差函数
def rmse(y_pre, y_test):
e = []
for i in range(len(y_test)):
e.append(y_pre[i] - y_test[i])
squaredError = []
for h in e:
squaredError.append(h * h) # target-prediction之差平方
RMSE = sqrt(sum(squaredError) / len(squaredError)) # 均方根误差RMSE
return RMSE
# 定义主函数
if __name__ == '__main__':
# 读取数据
train = pd.read_csv("train.csv")
# 删除id
train.drop('id', axis=1, inplace=True)
train = train.drop_duplicates(keep='first') # 删除重复值
train.loc[train['weather'] == 4, 'y'] = 5
train = train[np.abs(train['y'] - train['y'].mean()) <= (3 * train['y'].std())]
Y = train.pop('y')
# 划分数据集
seed = 7
test_size = 0.33
X_train, X_test, y_train, y_test = train_test_split(train, Y, test_size=test_size, random_state=seed)
c=0
i = [0.03,0.05, 0.06, 0.07, 0.08, 0.09, 0.1]#学习率的取值范围
dd=[3,4,5,6,7,8,9,10,11]#树的深度取值范围
A = []
a=0
for le in i :
w=[]
A.append(le)
aa=A[a]
a+=1
df1=pd.DataFrame(aa,index=['学习率'],columns=['le'])
for weight in range(5,20,1):#权重取值范围
e=[]
for estimators in range(200, 650, 50):#迭代次数取值范围
y_pre = train_d(X_train, y_train, X_test,weight,le,estimators)
y_test = [i for i in y_test]
#print("学习率为:",le,"权重为:",weight,"下损失为:",rmse(y_pre, y_test))
e.append(rmse(y_pre, y_test))
w.append(e)
df=pd.DataFrame(w,index=range(5,20,1),columns=range(200, 650, 50))
df1.append(df)
print(df1.append(df).min())
【分析】通过逐层遍历四个维度的参数,然后去计算出一个四个维度不同参数组合下的所有损失值,我们在将这些损失值进行筛选,筛选出损失值最小的参数,然后返回出这个最小损失值对应的四个参数组合,即为我们的最优参数组合。
操作:我们通过以上的代码进行运行,由于参数组合过多,数据运算量过大,我们先固定一个参数(树的深度),运行模型,然后计算在相同树的深度——不同学习率——不同权重——不同迭代次数下不同的损失值,返回这些损失值中最小值,最后产生最小损失值的参数组合即为最优参数组合,这个参数组合为max_depth=6,learning_rate=0.05,n_estimators=250,min_child_weight=7
网友评论