剪枝是在模型训练过程中使用的一种策略,其主要目的是降低模型的过拟合和提高模型的泛化能力,也可以用来优化模型的复杂度和算法的效率。剪枝是一种常用的优化技术,适用于各种机器学习算法中。
本文将从剪枝的概念、类型和实现方式等方面对剪枝进行详细的介绍和解释,并探讨剪枝在机器学习中的应用。
概念
剪枝是在模型训练中对决策树、神经网络或其他机器学习算法进行修剪的过程。其主要思想是通过去掉一些无用的节点或分支,从而降低模型过拟合的风险,提高模型的泛化能力。
类型
1. 预剪枝
在模型构建的过程中,在完全生成决策树之前就开始剪去一些分支,从而避免决策树对训练数据过度拟合的风险。虽然这种方法可以减少拟合的风险,但是结果可能会比较不稳定,因为预剪枝需要在数据集分离之前自己决定哪些节点是无用的,这是有一定难度的。
2. 后剪枝
后剪枝是在完全生成决策树之后,对决策树进行剪枝,从而去掉一些无用的节点和分支,降低模型过拟合的风险。后剪枝相对于预剪枝来说更加稳定可靠,因为在完全生成决策树之后可以使用验证数据集来判断是否需要进行剪枝。
3. 其他类型
除了预剪枝和后剪枝之外,还有一些其他类型的剪枝方法。例如,标准剪枝和代价复杂度剪枝等,这些方法的目的都是要削减实际成本和模型复杂性之间的关系,提高模型的泛化能力和运行效率。
实现方式
1. 标准剪枝
标准剪枝是一种简单直接的方法,通过对树进行遍历,找到拥有最小预测误差的节点,并将其替换为叶子节点。标准剪枝的过程中,需要使用验证数据集进行实时的误差计算,通过比较剪枝前后的误差大小来确定是否进行剪枝。
2. 代价复杂度剪枝
代价复杂度剪枝也称为基于复杂度的剪枝。该方法通过对树的节点进行分析和比较,选取一些代价较低的部分,然后去掉它们,从而减少树的总体复杂度。代价复杂度剪枝方法的核心是从模型复杂度和训练误差之间的权衡出发,根据“代价-效果”的思想寻找最优的剪枝点。
应用
剪枝在机器学习中应用广泛,尤其是在决策树、神经网络、支持向量机和KNN等算法中常常遇到。剪枝的应用可以提高机器学习算法的泛化能力、降低机器学习算法的复杂度和提高机器学习算法的性能等方面。下面列出了剪枝在机器学习中的几个常见应用。
1. 在决策树中的应用
决策树算法是一种机器学习算法,剪枝是决策树算法中非常重要和常见的一种方法。决策树算法在应用中,容易出现过拟合现象,剪枝可以有效减少决策树的规模,降低过拟合的风险,提高决策树的泛化能力。
2. 在神经网络中的应用
神经网络在很多应用中都表现出非常好的效果,但是随着网络规模的增大,很容易出现过拟合现象。剪枝可以有效的去掉神经网络中无用的连接和节点,减少网络的规模和模型的复杂度,从而降低过拟合的风险,提高网络的泛化能力。
3. 在支持向量机中的应用
支持向量机算法在分类问题上有很好的表现,但是训练支持向量机算法会非常耗时,并且很容易出现过拟合现象。剪枝可以去掉一些无用的点和分支,降低模型的复杂度,提高模型的泛化能力和效率。
4. 在K-近邻中的应用
K-近邻算法在分类问题中表现出了很好的效果,但是如果数据集比较大的话,会出现算法效率低下的问题。通过剪枝可以降低数据集的规模,提高算法的效率,同时还可以降低模型的过拟合风险,从而提高算法的泛化能力。
剪枝策略的寻找的方法
1)微观方法:从问题本身出发,发现剪枝条件
2)宏观方法:从整体出发,发现剪枝条件。
3)注意提高效率,这是关键,最重要的。
总之,剪枝策略,属于算法优化范畴;通常应用在DFS 和 BFS 搜索算法中;剪枝策略就是寻找过滤条件,提前减少不必要的搜索路径。
二:剪枝算法(算法优化)
1、简介
在搜索算法中优化中,剪枝,就是通过某种判断,避免一些不必要的遍历过程,形象的说,就是剪去了搜索树中的某些“枝条”,故称剪枝。应用剪枝优化的核心问题是设计剪枝判断方法,即确定哪些枝条应当舍弃,哪些枝条应当保留的方法。
2、剪枝优化三原则: 正确、准确、高效.原则
搜索算法,绝大部分需要用到剪枝.然而,不是所有的枝条都可以剪掉,这就需要通过设计出合理的判断方法,以决定某一分支的取舍. 在设计判断方法的时候,需要遵循一定的原则.
剪枝的原则
1) 正确性
正如上文所述,枝条不是爱剪就能剪的. 如果随便剪枝,把带有最优解的那一分支也剪掉了的话,剪枝也就失去了意义. 所以,剪枝的前提是一定要保证不丢失正确的结果.
2)准确性
在保证了正确性的基础上,我们应该根据具体问题具体分析,采用合适的判断手段,使不包含最优解的枝条尽可能多的被剪去,以达到程序“最优化”的目的. 可以说,剪枝的准确性,是衡量一个优化算法好坏的标准.
3)高效性
设计优化程序的根本目的,是要减少搜索的次数,使程序运行的时间减少. 但为了使搜索次数尽可能的减少,我们又必须花工夫设计出一个准确性较高的优化算法,而当算法的准确性升高,其判断的次数必定增多,从而又导致耗时的增多,这便引出了矛盾. 因此,如何在优化与效率之间寻找一个平衡点,使得程序的时间复杂度尽可能降低,同样是非常重要的. 倘若一个剪枝的判断效果非常好,但是它却需要耗费大量的时间来判断、比较,结果整个程序运行起来也跟没有优化过的没什么区别,这样就太得不偿失了.
3、分类
剪枝算法按照其判断思路可大致分成两类:可行性剪枝及最优性剪枝.
3.1 可行性剪枝 —— 该方法判断继续搜索能否得出答案,如果不能直接回溯。
3.2 最优性剪枝
最优性剪枝,又称为上下界剪枝,是一种重要的搜索剪枝策略。它记录当前得到的最优值,如果当前结点已经无法产生比当前最优解更优的解时,可以提前回溯。
示例分析
题目来源于poj 3900 The Robbery (类似于背包问题,但是不能够用背包求解)
1 分析:W,C值很大,数组开不下(所以,不能用背包处理),但是发现N值很小,(1+15)*15/2=120,所以可以考虑dfs+剪枝。
首先利用贪心的思想我们对箱子进行排序,关键字为性价比(参考了poj里的discuss)。也就是单位重量的价值最高的排第一,搜索的时候枚举顺序注意一定要从满到空,这样才能最快的找到一个可行解然后利用它进行接下来的剪枝。
剪枝1. 之后所有的钻石价值+目前已经得到的价值<=ans 则剪枝。
剪枝2. 剩下的重量全部装目前最高性价比的钻石+目前已经得到的价值<=ans 则剪枝(非常重要的剪枝)。
2 程序代码
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
#define MY_MAX(a,b) (a)>(b)?(a):(b)
const int maxN = 20;
struct NOTE
{
long long weight;
long long value;
int num;
}box[maxN];
int n;// 个数小于20
long long m,ans;// m 总重量,ans最优解
long long sum[maxN]; //保存一个后缀和
bool cmp(const struct NOTE &a, const struct NOTE &b)
{//按性价比排序,从大到小排列(注意若有取地址符号,则需有const)
return a.value*1.0/a.weight > b.value*1.0/b.weight;
}
inline bool cut (int pos,long long now_value,long long last_weight)
{
if(pos == n+1) return true;//边界返回条件
if(now_value+sum[pos] < ans) return true;如果后面所有的钻石加起来都<=ans,剪掉
double best = (box[pos].value*1.0/box[pos].weight);//当前最大的性价比
if(now_value+(long long)ceil(best*last_weight) < ans) return true;//以这个性价比取剩下的所有重量,如果<=ans,剪掉
return false;
}
void dfs(int pos,long long now_value,long long last_weight) //pos 当前数组的下标位置,now_value 目前的重量和,last_weight当前背包剩余容量
{
ans = MY_MAX(ans,now_value);
if(cut(pos,now_value,last_weight)) return;//剪枝函数
for(int i=box[pos].num;i>=0;--i)//(暴力搜索)枚举顺序从满到空枚举,这样才能最快找到ans,然后利用ans剪枝
{
if(last_weight<box[pos].weight*i) continue;
dfs(pos+1,now_value+box[pos].value*i,last_weight-box[pos].weight*i);
}
}
int main()
{
int cas;
long long sumv,sumw;// 价值和重量的和;仅仅用到了一次(特殊情况才用到,能够一次全带走)
scanf("%d",&cas);
while(cas--)
{
ans=0;
sumv=sumw=0;
scanf("%d%lld",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%lld",&box[i].weight);
sumw+=box[i].weight*i;
}
for(int i=1;i<=n;i++)
{
scanf("%lld",&box[i].value);
box[i].num=i;
sumv+=box[i].value*i;
}
// 以上是数据的输入,下面才是刚刚开始的
// 如果sumv开始就比m总重量还小,直接输出
if(sumw<=m)
{
printf("%lld\n",sumv);
continue;
}
sort(box+1,box+1+n,cmp);// 从1开始计数的
sum[n+1]=0; // 倒着开始的
for(int i=n;i>=1;i--)
{
//计算后缀和
sum[i]=sum[i+1]+box[i].value*box[i].num;
}
dfs(1,0,m);
printf("%lld\n",ans);
}
return 0;
}