21/8/11 读书笔记 子程序设计 决策树

21/8/11 读书笔记

Code Complete 子程序设计

子程序应该有单一且明确的目的。

创建子程序,不仅是为了减少重复的代码,更在于能够封装复杂度、通过函数名称建立更好的抽象、方便性能优化等

建立子程序并赋予其一个合理的名称,可以让代码更具可读性,甚至达到自我注解的地步。

对于一个会重复利用的简单操作,也应该倾向于将其子程序化。因为简单的操作往往会变复杂,使得后续修改很麻烦。

对于子程序层次,内聚性指子程序内部各种操作间联系的紧密程度。内聚性可以分为多个层次,以下内聚性按从好到差的顺序排布,注意到我们所谈到的操作是指低层次的抽象,即接近具体的执行语句,部分情况下我们只要将这些操作独立为更高层次的子程序就能避免陷入差的内聚性:

  • 功能的内聚性:一个子程序只执行一种操作。比如cos()
  • 顺序上的内聚性:一个子程序包含多个操作,这些操作必须要按特定顺序执行,他们共享数据,且均完成后才能达到目标效果。比如先计算工资,再计算税后工资。
  • 通信上的内聚性:一个子程序包含多个操作,这些操作均使用了同样的一份数据,但是没有顺序要求。比如均基于出生日期,分别计算年龄和养老保险。
  • 临时的内聚性:一个子程序包含多个操作,这些操作知识因为需要同时执行而被放在一起。比如初始化中包含多个子系统进行初始化的操作。
  • 过程上的内聚性:这是不可取的内聚性。一个子程序中包含多个操作,这些操作按特定顺序执行,但是这些操作间本质虽然有关联性,但是并不存在顺序性。比如按顺序先获取一个人的年龄、性别,再获取一个人的姓名、籍贯。这两个部分没有本质上的顺序性,但是却被拆分为两个部分。
  • 逻辑上的内聚性:若干操作被放入一个子程序,由传入的控制参数选择执行其中一个操作。比如一个具有分发功能的函数。为了消除这种内聚性,这里的若干操作可以进一步设计为多个独立的子程序,使得当前子程序的功能专注于分发。
  • 偶然的内聚性:一个子程序中包含多个操作,但是这些操作间没有任何可能的关联。

一个子程序的名称如果模糊不清,很可能是因为子程序本身的执行就是含糊不清的,是缺乏功能的内聚性的表征。

子程序名称应该视是否能够清晰表达功能而定,而没有显式的限制。

函数的名称应该体现它的返回值,过程的名称应该是语气强烈的动词加宾语

书中这里函数指有返回值的子程序,过程指没有返回值的子程序。

如果一个子程序的用途就是返回其名称所指明的返回值,那么应该使用函数,否则使用过程。

对于某些常用的通用行为统一命名规则。比如我们取一个对象的ID,应该统一采取.getID(),而不应该让不同的对象各自采取.id()Get()等各式各异的方法。虽然每个命名都能够清晰准确,但是从统一的角度上看会增加不必要的记忆成本。

与其对子程序的长度加以限制,不然让子程序的内聚性、嵌套的层次、变量的数量以及其他复杂度相关的事项来决定。一般来说,如果子程序超过200行,就应该加以小心。

管理子程序的参数应该遵循:

  • 按照输入-修改-输出的顺序排列参数。
  • 子程序最好使用所有的参数。
  • 把状态或返回异常的参数放在列表最后,其也应该属于输出参数。
  • 通过注释或断言的方式对参数的语义进行说明。
  • 如果语言没有额外的支持,那么建立一定的命名规则来区分,比如i_前缀表示输入参数,m_前缀表示可能被修改的参数,o_前缀表示作为输出的参数。
  • 在需要高安全性的情况下,参数的传递采取具名参数的方式,避免因为传参顺序错误导致的隐藏问题。

除非万不得已,否则不使用宏来代替子程序。宏有许多风险,而且还不易理解。

除非性能有明确要求,否则不使用内联子程序。因为内联子程序会将其实现细节暴露给所有人。


机器学习 决策树

决策树生成的基本流程是一个递归过程:

输入:训练集D,属性集合A
过程:函数TreeGenerate(D,A)
生成结点node;
if D中样本全属于类别C then
	将node标记为C类别叶结点;return;
end if
if A不为空 OR D中样本在A上的取值完全相同 then
	将node标记为叶结点,类别标记为D中占优势的类;return;
end if
根据最优划分属性算法从A中得到一个属性a;
for a上可能的取值ai do
	为node生成一个分支结点t;令Di表示D在a上取值为ai的样本子集;
	if Di为空 then
		将分支结点t标记为叶结点,其类别标记为D中占优势的类;return;
	else
		TreeGenerate(Di,A-{a});
	end if
end for

注意有三种情况导致递归返回,而后两者分别对应利用当前节点的后验分布和利用父结点的分布作先验分布。

基本流程中最重要的就是选择最优划分属性,目标是该划分属性使得结点的“纯度”越高越好。度量方式包括:

  • 信息熵:描述数据集的混乱程度。信息熵越小,数据集纯度越高。
    • 信息增益:划分前后信息熵的减少量。信息增益的归纳偏好取值数目多的属性。
    • 增益率:在信息增益的基础上引入对于取值数目的考量,对取值数目少的属性有所偏好。
  • 基尼指数:描述从数据集中随机抽取两个样本,其类别不同的概率。基尼属性越小,说明数据集纯度越高。

理论研究表明,采取信息增益或基尼指数差值的方法,只在2%的情况下有差异。而其实后来提出的其他新的度量方式也并不会取得更好的泛化性能。书中之后的讨论中以信息增益为主。

剪枝处理是决策树学习算法中对抗过拟合的主要手段,分为预剪枝和后剪枝,需要用到验证集(可以回想之前我们将数据集分为了训练集、验证集、测试集):

  • 预剪枝:在决策树生长过程中,每涉及到结点的划分时,利用验证集测试划分前后的分类性能是否提升,如果没有提升就放弃划分。
    • 有的结点可能暂时导致划分性能下降,但是后续划分可能显著提高性能呢。预剪枝扼杀了这种情况。
  • 后剪枝:在决策树生长完成后,从下往上遍历所有分支结点,利用验证集测试该结点对于性能是否有提升作用,否则剪枝。
    • 后剪枝需要对所有分支结点一一考察,导致计算开销很大。

剪枝处理能显著提高决策树泛化性能。

对于连续属性,最简单的方法就是二分法。我们试图找到一个值,使得利用该值作为划分点形成的离散属性,在作为划分属性时对应的信息增益最高,则采取这种划分方式将连续值转化为离散值。与离散属性不同的是,连续属性还可以继续作为后代结点的划分属性

对于有缺失值的属性,我们为每个样本附加一个值为一的权值,当对于划分属性a,样本b缺失a值时,将样本b按概率平分到各个子结点中。并且同时拓展信息增益的考量方式,将样本的权值纳入计算信息增益的考量中,即各个子结点对应的信息熵在计算时考虑各个样本对应的权值。

基于一个划分属性的决策树学习算法的分类边界是轴平行的,提高了可解释性,但是导致分类边界较为复杂时得到的决策树会变得很臃肿,时间开销很高。因此引入多变量决策树的概念,在最优划分属性的考量时考虑多个属性的线性组合,试图在每个分支结点建立一个线性分类器,这使得分类边界能够成为的。

posted @ 2021-08-11 15:27  neumy  阅读(96)  评论(0)    收藏  举报