当前位置: 首页 > 科技观察

用Python从零开始写一棵回归树

时间:2023-03-18 00:07:03 科技观察

为了简单起见,这里将使用递归来创建树节点。递归虽然不是一个完美的实现,但是对于说明原理来说是最直观的。首先导入库importpandasaspdimportnumpyasnpiimportmatplotlib.pyplotasplt首先你需要创建训练数据,我们的数据将有独立变量(x)和一个相关变量(y),并使用numpy来添加高斯噪声到相关值,在这里可以用数学方法表示为噪声。代码如下所示。deff(x):mu,sigma=0,1.5返回-x**2+x+5+np.随机的。正常(亩,西格玛,1)num_points=300np。随机的。种子(1)x=np。random.uniform(-2,5,num_points)y=np.array([f(i)foriinx])plt.scatter(x,y,s=5)通过创建A树创建回归树多个节点来预测数值数据。下图展示了回归树的树形结构示例,其中每个节点都有其划分数据的阈值。给定一组数据,输入值会通过相应的规范到达叶节点。到达节点M的所有输入值都可以用X的子集表示。从数学上讲,让我们用一个函数来表示这种情况,如果给定的输入值到达节点M,则可以给出1,否则为0。找到分割数据的阈值:通过在每一步选择2个连续点并计算它们的平均值来迭代训练数据。计算出的平均值将数据分为两个阈值。让我们首先考虑随机阈值来演示任何给定的情况。阈值=1.5low=np.take(y,np.where(xthreshold))plt.scatter(x,y,s=5,label='Data')plt.plot([threshold]*2,[-16,10],'b--',label='Thresholdline')plt.plot([-2,threshold],[low.mean()]*2,'r--',label='左子预测线')plt.plot([threshold,5],[high.mean()]*2,'r--',label='右子预测线')plt.plot([-2,5],[y.mean()]*2,'g--',label='节点预测线')plt.legend()蓝色竖线表示一个阈值,我们假设它是任意两点的平均值,稍后用于划分数据。我们对这个问题的第一个预测是所有训练数据(y轴)的平均值(绿色水平线)。而两条红线是对要创建的子节点的预测。很明显,这些平均值都不能很好地代表我们的数据,但它们的区别也很明显:主节点预测(绿线)得到所有训练数据的平均值,我们将其分为2个子节点,即2子节点节点有自己的预测(红线)。这2个子节点比绿线更好地代表了它们对应的训练数据。回归树会将数据分成两部分——从每个节点创建2个子节点,直到达到给定的停止值(这是一个节点可以拥有的最小数据量)。它提前停止了树的构建过程,我们称之为预修剪树。为什么会有提前停止机制?如果我们要继续进行分配直到节点只有一个值,这会创建一个过度拟合方案,其中每个训练数据只能预测自己。解释:当模型完成时,它不会使用根节点或任何中间节点来预测任何值;它将使用回归树的叶子(这将是树的最后一个节点)进行预测。为了获得最能代表给定阈值数据的阈值,我们使用残差平方和。它可以在数学上定义,让我们看看这一步是如何工作的。现在已经计算出阈值的SSR值,可以使用SSR值最小的阈值。使用此阈值将训练数据分成两部分(低部分和高部分),其中低部分将用于创建左孩子,高部分将用于创建右孩子。defSSR(r,y):returnnp.sum((r-y)**2)SSRs,thresholds=[],[]foriinrange(len(x)-1):threshold=x[i:i+2].mean()low=np.take(y,np.where(xthreshold))guess_low=low.mean()guess_high=high.mean()SSRs.append(SSR(low,guess_low)+SSR(high,guess_high))thresholds.append(threshold)print('最小残差为:{:.2f}'.format(min(SSRs)))print('对应的阈值是:{:.4f}'.format(thresholds[SSRs.index(min(SSRs))]))在进行下一步之前,我将使用pandas创建一个df,并且创建一个用于寻找最佳阈值的方法。所有这些步骤都可以在没有pandas的情况下完成,这里使用它是因为它更方便。df=pd.DataFrame(zip(x,y.squeeze()),columns=['x','y'])deffind_threshold(df,plot=False):SSRs,thresholds=[],[]fori在范围(len(df)-1):threshold=df.x[i:i+2].mean()low=df[(df.x<=threshold)]high=df[(df.x>threshold)]guess_low=low.y.mean()guess_high=high.y.mean()SSRs.append(SSR(low.y.to_numpy(),guess_low)+SSR(high.y.to_numpy(),guess_high))thresholds.append(threshold)ifplot:plt.scatter(thresholds,SSRs,s=3)plt.show()returnthresholds[SSRs.index(min(SSRs))]将数据分成两部分后创建子节点你可以为低值和高值找到单独的阈值。需要注意的是这里加了一个停止条件;因为对于每个节点,数据集中属于该节点的点会更少,所以我们定义每个节点的最小数据点数。如果不这样做,每个节点将仅使用一个训练值进行预测,从而导致过拟合。节点可以递归创建,我们定义了一个名为TreeNode的类,它将存储节点应该存储的每个值。使用此类,我们首先创建根,同时计算其阈值和预测变量。然后它递归地创建它的孩子,其中每个孩子的类存储在父级的left或right属性中。在下面的create_nodes方法中,给定的df首先被分成两部分。然后检查是否有足够的数据来分别创建左右节点。如果有足够的数据点(对于它们中的任何一个),我们计算阈值并使用它来创建一个子节点,再次调用create_nodes方法并将这个新节点作为树。类TreeNode():def__init__(self,threshold,pred):self.threshold=thresholdself.pred=predself.left=Noneself.right=Nonedefcreate_nodes(tree,df,stop):low=df[df.x<=tree.threshold]high=df[df.x>tree.threshold]iflen(low)>stop:threshold=find_threshold(low)tree.left=TreeNode(threshold,low.y.mean())create_nodes(tree.left,low,stop)iflen(high)>stop:threshold=find_threshold(high)tree.right=TreeNode(threshold,high.y.mean())create_nodes(tree.right,high,stop)threshold=find_threshold(df)tree=TreeNode(threshold,df.y.mean())create_nodes(tree,df,5)此方法在第一棵树上进行了修改,因为它不需要返回任何内容。虽然递归函数通常不会这样写(donotreturn),但是由于不需要返回值,当if语句没有被激活时,它什么都不做。完成后,您可以检查此树结构,看看它是否创建了一些适合数据的节点。这里将手动选择第一个节点及其对根阈值的预测。plt.scatter(x,y,s=0.5,label='Data')plt.plot([tree.threshold]*2,[-16,10],'r--',label='根阈值')plt.plot([tree.right.threshold]*2,[-16,10],'g--',label='右节点阈值')plt.plot([tree.threshold,tree.right.threshold],[tree.right.left.pred]*2,'g',label='右节点预测')plt.plot([tree.left.threshold]*2,[-16,10],'m--',label='左节点阈值')plt.plot([tree.left.threshold,tree.threshold],[tree.left.right.pred]*2,'m',label='左节点预测')plt.plot([tree.left.left.threshold]*2,[-16,10],'k--',label='SecondLeftnodethreshold')plt.legend()在这里看到两个预测:第一个左节点对高值的预测(高于其阈值)第一个右节点对低值的预测(低于其阈值)这里我手动裁剪了预测线的宽度,因为如果给定的x值到达这些节点中的任何一个都会表示为属于该节点的所有x值的平均值,这也意味着没有其他rx-values参与节点的预测(希望有意义)。这个树结构不仅仅是两个节点,所以我们可以通过调用其子节点来检查特定的叶节点,如下所示。tree.left.right.left.left这当然意味着有一个分支向下有4个孩子长,但它可能在树的另一个分支上更深。预测我们可以创建一个预测方法来预测任何给定的值。defpredict(x):curr_node=treeresult=NonewhileTrue:ifx<=curr_node.threshold:ifcurr_node.left:curr_node=curr_node.leftelse:breakelifx>curr_node.threshold:ifcurr_node.right:curr_node=curr_node.rightelse:breakreturncurr_node.pred预测方法所做的是通过将我们的输入与每个叶子的阈值进行比较来沿着树向下走。如果输入值大于阈值,则转到右叶,如果小于阈值,则转到左叶,依此类推,直到到达任何底部叶节点。然后使用节点自己的预测值进行预测,并与它的阈值进行最终比较。用x=3测试(创建数据时可以用上面写的函数计算实际值-3**2+3+5=-1,也就是期望值),我们得到:predict(3)#-1.23741计算误差这里用相对平方误差来验证数据defRSE(y,g):returnsum(np.square(y-g))/sum(np.square(y-1/len(y)*sum(y)))x_val=np.random.uniform(-2,5,50)y_val=np.array([f(i)foriinx_val]).squeeze()tr_preds=np.array([predict(i)foriindf.x])val_preds=np.array([predict(i)foriinx_val])print('训练错误:{:.4f}'.format(RSE(df.y,tr_preds)))print('Validationerror:{:.4f}'.format(RSE(y_val,val_preds)))可以看出误差不大,总结结果如下。更深的模型是更适合回归树模型的数据:因为我们的数据是多项式生成的,所以可以使用多项式回归模型来更好地拟合。让我们更改训练数据并将新函数设置为deff(x):mu,sigma=0,0.5ifx<3:return1+np.random.normal(mu,sigma,1)elifx>=3和x<6:返回9+np.random.normal(mu,sigma,1)elifx>=6:返回5+np.random.normal(mu,sigma,1)np.random.seed(1)x=np.random.uniform(0,10,num_points)y=np.array([f(i)foriinx])plt.scatter(x,y,s=5)在此数据集上运行上述所有相同程序,下面的结果比我们从多项式数据中得到的结果误差更低。最后共享一下上面动画的代码:importpandasaspdimportnumpyasnpimportmatplotlib.pyplotaspltfrommatplotlib.animationimportFuncAnimation#===================================================创建数据定义f(x):mu,sigma=0,1.5return-x**2+x+5+np.random.normal(mu,sigma,1)np.random.seed(1)x=np.random.uniform(-2,5,300)y=np.array([f(i)对于我在x])p=x.argsort()x=x[p]y=y[p]#===================================================计算阈值defSSR(r,y):#sendnumpyarrayreturnnp.sum((r-y)**2)SSRs,阈值=[],[]foriinrange(len(x)-1):threshold=x[i:i+2].mean()low=np.take(y,np.where(xthreshold))guess_low=low.mean()guess_high=high.mean()SSRs.append(SSR(low,guess_low)+SSR(high,guess_high))thresholds.append(threshold)#===================================================动画Plotfig,(ax1,ax2)=plt.subplots(2,1,sharex=True)x_data,y_data=[],[]x_data2,y_data2=[],[]ln,=ax1.plot([],[],'r--')ln2,=ax2.plot(阈值,SSRs,'ro',markersize=2)line=[ln,ln2]definit():ax1.scatter(x,y,s=3)ax1.title.set_text('尝试不同的阈值')ax2.title.set_text('阈值与SSR')ax1.set_ylabel('y值')ax2.set_xlabel('Threshold')ax2.set_ylabel('SSR')返回linedefupdate(frame):x_data=[x[frame:frame+2].mean()]*2y_data=[min(y),max(y)]line[0].set_data(x_data,y_data)x_data2.append(thresholds[frame])y_data2.append(SSRs[frame])line[1].set_data(x_data2,y_data2)returnlineani=FuncAnimation(图,更新,帧=298,init_func=init,blit=True)plt.show()