【数据分析实践】森林覆盖类型预测
Kaggle 竞赛 Forest Cover Type Prediction 森林覆盖类型预测
Description
在这个比赛中,你被要求从严格的地图变量(相对于遥感数据)中预测森林覆盖类型(主要的树木覆盖种类)。一个给定的30×30米单元的实际森林覆盖类型是由美国森林服务局(USFS)第二区域资源信息系统数据确定的。然后从美国地质调查局和美国林业局获得的数据中得出独立变量。数据为原始形式(未按比例),包含定性自变量的二进制列数据,如荒野地区和土壤类型。
本研究区包括位于科罗拉多州北部罗斯福国家森林的四个荒野地区。这些地区代表了人为干扰最小的森林,因此,现有的森林覆盖类型更多的是生态过程的结果,而不是森林管理实践。
Requirement
1 | numpy==1.23.3 |
项目的流程图如下
1 | graph LR |
我们将从这五个部分介绍本赛题。
一 赛题理解
1 背景
本次竞赛使用了美国森林服务局(US Forest Service)提供的30x30m分辨率的森林覆盖类型区域,包括一个训练集和一个测试集。要求采用训练集的数据对测试集的森林覆盖类型进行预测。
2 数据
该数据集一共有十三个属性,包括高程、坡度、坡向、到水文地物的垂直距离、水平距离、到道路的水平距离等,主要包括整形的定量数据和one-hot编码过的定名数据
3 评价指标
赛题中并未给出,由于模型是一个多分类问题,我们选择采用多分类交叉熵和R2系数来进行评价。
交叉熵
R2系数
R2系数又称决定系数,反映因变量的全部变异能通过回归关系被自变量解释的比例
表示均方误差,表示方差,值得注意的是,均方误差代表与真实值的偏差,而方差则是与平均值的偏差。
本项目的主要流程如下:
二 数据探索性分析
在本阶段,主要工作是对数据有一个简单的认识,以及对数据进行一定的预处理。
1 前期准备与数据读取
1.1 模块导入
本次使用的模块如下:
直接先导进去吧
1 | from sklearn.model_selection import train_test_split |
1.2 绘图模块
由于需要不停地调用可视化模块接口,这里我搞了些函数方便调用,不装pyecharts的话也没关系,完全可以用matplotlib代替的,这里只是为了美观些🌟
1 | def DrawBox(x,y_data): |
2 数据分布情况
好了结束了前期准备后,我们就可以对数据进行探索啦!这里简单举个栗子,本项目主要学习的是方法啦。数据可以在这里下载:这里
读取数据并进行初步的统计量探索
1 | path=r"YourWorkSpace" |
查看数据表的话,用df.head(n)就行
1 | train.head() |
接着是统计量:
1 | train.describe() |
数据的维度:
1 | train.shape |
这个数据从第十一维特征Wilderness_Area1开始就全是名义数据(定名数据)了,因而我们在分析其分布情况时,需要分开进行。
别忘了看看数据有没有缺失值!
1 | for i in train.columns: |
上面这种方式可以将带有缺失值的列获取出来,根据列特征的不同数据格式选择不同的方法进行处理。然后接下来要对样本标签的分布进行查看,如果样本分布不均衡,很可能会影响模型的训练结果。一般来说,针对分布不均衡情况,可以进行的操作有:
- 降采样
- 上采样
- 生成式模型(自动编码机、受限玻尔兹曼机、对抗生成网络、扩散模型等)
1 | d=train.groupby("Cover_Type")["Id"].count() |
这个数据集已经做过采样了,所以我们不需要在样本分布上做文章。
2.1 数值数据分布情况
本数据集中,数值型数据刚好是前十列。
1 | numeric_col=train.columns[:10] |
利用seaborn的histplot(data,kde=True)可以在绘制数据分箱直方图的同时,绘制密度分布图,帮助我们更好的分析数据的情况。
1 | plt.figure(figsize=(16,12),dpi=200) |
可以发现每种数据的分布都不相同,理论上为了方便我们的计算工作,需要将其转化为正态分布。
♨️ 数据相关性分析
这部分主要探索数字数据之间的相关性,反映出数据分布之间的潜在联系
1 | num=train[numeric_col] |
可以看到的是,坡度与山体阴影呈负相关关系,像元到水文特征的垂直距离和水平距离之间具有较强的相关性。
2.2 名义数据分布情况
1 | def DrawCategoricalData(x): |
1 | categorical_col=train.columns[10:] |
1 | i+=1 |
通过柱状堆叠图,我们可以发现,Wilderness_Area这个字段数据也是十分均衡,而且有些字段取1时,对结果取值有较大的影响,该数据的信息熵较大。
3 数据清洗
此阶段主要针对三类情况:
- 缺失值
- 噪声
- 数据不一致问题
我们在第二阶段已经探查过了,该数据不存在缺失值,数据不一致问题实际上也不存在,那么剩下的就是噪声数据的处理
下面这段代码是调用上面写的绘图函数的,用来绘制箱线图,可以直接跳过,这边做个简单展示
1 | i=0 |
1 | DrawBox(numeric_col[:3],num_std[:3]).render_notebook() |
我们可以直接用pandas的绘图接口:
1 | plt.figure(figsize=(16,9),dpi=300) |
可以看到,有很多类都有一些异常值。这些异常值是通过百分位数极差计算的:
然后我们现在就想把这些异常点去掉,有一个比较简单的方式,就是利用法则
1 | def Outlier(x): |
可以看到,总共有1130个数据属于异常值。
当然,是否删除离群点还需要考虑实际情况,譬如这个离群点是由什么原因造成的?是否是采集过程中造成的,还是数据本身的特性导致的?
如果是后者,其实不建议进行处理。适当的保留噪声会提高模型的泛化能力
至此,我们已经简单完成了EDA阶段的工作。
下一个阶段是特征工程(Feature Engineering)
三 特征工程
在机器学习中,特征工程阶段的好坏往往决定了最终结果的好坏,而深度学习实际上没有这种烦恼,我们将尝试传统机器学习树模型和深度学习模型,分析并比较不同模型的结果,当然,这里会选择一份做了特征工程的数据和一份不做特征工程的数据进行比对,但由于树模型和深度学习模型都带有不确定性,最终的评价不一定准确,考虑到服务器性能,也不做大数据统计了,仅仅做一些简单的工作
特征工程的主要任务包括数据预处理、特征变换、特征提取和特征选择,在之前的工作中,我们已经做过部分预处理了,包括:
- 剔除异常值
- 删除不需要的列
- 分析数据的分布
1 特征变换
可以通过幂律变换sklearn.preprogressing.power_transform对数据进行转换,当然要求数据不能为非正数。本数据存在负数,因而我们的考量是:
- 先做归一化去除负数影响
- 添加极小项避免零值影响
1 | # 数据转为正态分布 |
可以看到,此时之前的长尾分布基本上都被转化为近似正态了。
当然,这组数据做不做正态影响不大,起码对于信息熵没有增益,那么我们就不做了,省的数据区间出错,那么现在就轮到特征提取或者说特征构造阶段了
2 特征提取
主要构造的特征有:
- 高程坡向比
- 高程坡度比
- 坡度坡向比
- 水文特征切比雪夫距离
- 水文特征闵可夫斯基距离(p=1,p=2)
- 高程水文欧氏距离比
- 高程公路欧氏距离比
- 坡度公路比
- 水文公路比
- 山体阴影几何平均值
- 山体阴影平均值
- 山体阴影标准差
1 | def distance(x,y,p=2): |
OK现在我们就有一些奇奇怪怪的特征了。
啊实际上有些特征不能用,因为发生了除零错误。
3 特征选择
特征选择的方式有很多,我们现在介绍一些降维的工具:
- 通过流形学习的非线性降维方式
T-SNE对数据进行降维,避免数据之间冗余度过高以及可能带来的维灾害(虽然特征工程就没构建几个特征) - PCA
- LDA
- LLE
这边的话,由于特征较少,时间有限,就没有做降维了,简单给个栗子。
1 | from sklearn.decomposition import PCA |
四 模型训练
1 准备工作
1.1 数据拆分
拆分喽
1 | x_train,x_val,y_train,y_val=train_test_split(train,label,test_size=0.3) |
1.2 标准化去除量纲影响
实际上我们之前做过一次,但是不碍事,再做一次体验一下
1 | x_train_scale=Standarder.fit_transform(x_train) |
2 机器学习模型
2.1 树模型
随机森林🌳
1 | rf=RandomForestClassifier(max_features='auto',oob_score=True,random_state=2023,n_jobs=-1) |
XGboost🌲
1 | # 算法参数 |
1 | xgb.plot_importance(model,height=0.8,title='Influence Ranking', ylabel='Feature',max_num_features=20) |
查看特征影响得分:
LightGBM🌴
1 | params={'num_leaves':54,'objective': 'multi:softmax','max_depth':18,'learning_rate':0.01,'boosting':'gbdt'} |
2.2 支持向量机
1 | from sklearn import svm |
2.3 优化
讲到调优,机器学习的两大工作一个是特征工程,一个就是调参了,努力成为一名优秀的调参侠吧(笑)
我们通过GridSearchCV模块进行调参,以决策树和XGboost为例:
1 | param_grid={ |
通过格网搜索与交叉验证,寻找模型的最优参数。
1 | pipe = Pipeline( |
这个过程十分吃服务器性能,而且可能费时不太好,需要一定的心理准备。
最终的结果如下(具有一定的优化空间,我这边基本上没怎么做调参)
3 深度学习模型
3.1 构建数据集
首先我们要有一个数据集,或者一个可迭代的容器存放我们的数据
1 | class MyDataSet(Dataset): |
然后是将数据转化为tensor格式
1 | x_t_tensor=torch.from_numpy(x_train_scale).float() |
再者是将这些放到数据容器中:
1 | train_DataLoader=DataLoader( |
3.2 模型选择
目前,我们已经获得了七个简单的网络。值得注意的是,在简单的一维数据中,多层感知机理论上可以拟合任意的函数,但如果加大层数,用一些高维数据的tricks,反而不太能得到好的结果。这是因为复杂的网络其容量也会增大,想要在函数域中找到最优函数变得更加复杂,可能陷入局部最优解
再有,一维卷积神经网络的性能实际上在小样本上会略低于全连接层,也不太适合做Attention
网络的性能除却跟网络结构本身有关系外,主要还是与数据挂钩。吴恩达在CS229中说过,模型只是去逼近数据潜在的上限,目前,我们拿出来做深度学习的数据仅仅做过几个简单的处理,实际上后续可以考虑将做过特征工程的数据进行训练
1 | class Net(nn.Module): |
3.3 训练阶段
1 | Net_train_loss=[] |
1 | net1=Net() |
训练结束后,我们就可以载入模型的参数和结果啦。
1 | # 加载最优参数 |
我们用训练好的模型去计算在验证集上的结果:
1 | pre=[] |
1 | # 查看验证集和训练集精度变化情况 |
可以看到,在验证集上,模型的性能有所下降,这是由于原本的模型对于训练集发生了过拟合现象。Net1实际上是最简单的二层全连接层,尽管它的训练速度最快,参数最少,在训练集上表现良好,但其泛化能力远不如其他模型。而Net2则对验证集的表现良好。
总体来说,模型在验证集上的平均精度略微下降。
五 模型融合
模型融合通过提升特征多样性、样本多样性和模态多样性,可以进一步增加模型的泛化能力和鲁棒性。
根据方法的不同,可以分为过程融合和结果融合。
过程融合🍉
过程融合最基本的有Bagging和Boosting,Bagging就是多个弱分类器堆叠在一起,Boosting就是基于弱分类器分错的结果输入后一个分类器中,从而实现弱模型媲美强模型的过程。
结果融合🍌
主要有以下几种方法
- 加权法
- stacking
- Blending
我们这边使用结果融合,通过尝试直接叠加法、软投票法和Stacking方式,分析并给出一个相对较好的结果
1 线性融合
1.1 投票法
1 | z_1=accuracy(torch.max(pre,1)[1],y_v_tensor.argmax(1)).item() # 直接投票 |
2.2 加权投票
1 | # 结果加权投票 |
2.3 软投票
1 | # 基于Softmax投票 |
2 Stacking
将多个分类器的概率结果作为一个简单分类器的输入,经过该分类器进行输出,我们这边执行两种方法:
- 数值叠加
- 特征叠加
简单分类器选择一个简单的NN
1 | class NN(nn.Module): |
2.1 数值叠加
1 | # 数值叠加 |
值得注意的是,这里通过tolist()来回转换变换内存地址,否则torch在反向传播执行计算图时会把上面那段代码也算进去,导致报错。
1 | # 数据集 |
2.2 特征叠加
1 | # 特征叠加 |
1 | stackingData=DataLoader(MyDataSet(input_data.float(),y_t_tensor),batch_size=64,shuffle=True,drop_last=True) |
3 结果分析
1 | b=Line() |
之前单模在验证集上最高的0.888,平均是0.86,特征叠加的结果会优于之前单模最好结果。
值得一提的是,特征叠加的方法在训练集上最高性能达到了0.93,这个结果远超之前单模的最好结果0.894。当然,用深度学习网络作为输出分类器,高收益的同时也带来了高风险,根据输出分类器的性能,最终结果也会上下浮动。为了避免这种情况,我们其实可以使用Ridge回归、Lasso回归、简单随机森林等模型作为输出模型。
六 结果分析
以下是各模型的分类性能
1 | b=Bar() |
在小样本跟少量特征的情况下,机器学习模型的性能与深度学习模型的性能差距不大,当然这里的深度学习模型只是一些非常简单的模型。
以下是性能最强的两个深度学习模型与两个最强的机器学习模型进行融合的结果
1 | # Net3 Net6 |
emm,这个性能略好于Net6,大于机器学习模型,小于Net3。
值得注意的是,在后处理过程中,模型投票产生的结果可能是:
- 四票全中
- 三票A一票B
- 两票A两票B
- 两票A一票B一票C
- ABCD各一票
由于这里是把结果拿出来了而不是概率(概率请参考深度学习融合),所以我们规定:三票四票的直接就是输出标签,两票A两票B选择创建分支,两票A一票B一票C选择A,ABCD各一票则随机选。
举个简单的后处理栗子:
1 | ans=[] |
这段代码是我在半夜三点神志不清的时候写的,难免像坨屎,应该有很大的优化空间。但不管怎么说,这种后处理方式确实拿到了更好的结果:
1 | accuracy(ans,y_val) # 0.8833774250440917 |
这个结果超过了之前单模最好结果Net3,算是有提升吧。
实际上,我们对深度学习模型的融合也需要进行后处理,这样也能提高些分数。
就不做Stacking了,大概就是这样
以下是特征工程对于机器学习模型的提升
这个图不太直观啊,我们逐个来看看是否有提升:
这里面仅仅构建了几个简单的特征,剔除了异常值,结果得到了明显的提升。但是相应的,异常值会带来泛化能力的提高,模型鲁棒性增强,需要根据具体情况决定是否对异常值进行剔除。





