1 简介
决策树是一个图表,显示了不同的选择及其可能的结果,可帮助轻松做出决策。本文是关于决策树是什么、它们如何工作、它们的优缺点以及它们的应用。
首先看一个问题:
有一栋大楼有256栈灯,由于施工时的失误每个灯的控制,
一共256个开关被安装到了总控室,
每个灯都可以被控制开关正确点亮,
现在我们准备了不限量的贴纸标签,
如何利用这些标签在最短的奔波过程后完成对控制开关和灯的一对一关联?
如下所示

- 了解决策树
决策树是解决问题的不同选项的图形表示,并显示不同因素之间的关系。它有一个分层树结构,从顶部的一个主要问题开始,称为一个节点,该节点进一步分支为不同的可能结果,其中:
根节点: 是表示整个数据集的起点。
分支:这些是连接节点的线。它显示了从一个决策到另一个决策的流程。
内部节点 (Internal Nodes) 是根据输入特征做出决策的点。
叶节点:这些是分支末尾的终端节点,表示最终结果或预测。
- 决策树的分类
根据目标变量的性质,我们主要有两种类型的决策树:分类树和回归树。
分类树:它们旨在预测分类结果,这意味着它们将数据分类为不同的类别。他们可以根据电子邮件的各种特征来确定电子邮件是 "垃圾邮件" 还是 "非垃圾邮件"。
回归树 :当目标变量是连续的时使用这些 它预测数值而不是类别。例如,回归树可以根据房屋的大小、位置和其他特征来估算房屋的价格。
2 问题的分析和解答
详细给出该问题的分析步骤
-
问题简化
有256个灯和256个开关,未知每个开关控制哪个灯。 你可以自由切换开关,但无法在开关室看到灯的状态。 每次可以进入灯区观察哪些灯亮了。
想要通过最少的"开关调整 → 灯观察"的往返次数(奔波次数),找出一一对应关系。
关键思路:用"信息编码"定位和记录灯的编号
核心思想是:每次奔波你可以传递的信息是"哪些灯亮了",所以你可以把这个亮灯的状态看作是一个二进制信息串。
这种思路可以配合标签贴纸,在每次操作时记录哪些开关被操作。
- 使用决策树
全部 256 个灯和开关的精确配对。下面是前 10 个灯的观测路径和识别出的控制开关编号结果:
markdown
灯编号 二进制路径(观测结果) 控制开关编号
0 10101010 0
1 01000011 1
2 10100010 2
3 01100001 3
4 01010111 4
5 10111001 5
6 11000000 6
7 01010001 7
8 00100101 8
9 11001010 9
3 实现代码模拟
如下图,假设默认灯全部是关闭的,
第一步操作,在总控室将其中一半的灯128个打开,那么这128个开关控制都标记为1 B0组,没有打开的控制开关标记为0, A0组.
然后去灯区 亮灯标记为1,熄灭的灯标记为0.
第二步,在总控室将B组打开的灯熄灭一半,同时标注控制开关,熄灭的开关标记为0,现在它们为 01, 亮的开关标记为1,现在它们为 11,也就是 00000011, A 组类似的操作 然后去灯区 亮灯标记为1,熄灭的灯标记为0. 这将出现相同的 00000011 组
依次类推有如下图:

每往返一次,在控制开关处操作并标记当前操作的bit值,亮灯标记1,熄灭标记0,在灯区贴标签亮灯为1,熄灭为0。
go
/*
控制灯 决策树方法
每次随机开关灯,并记录当次二进制位的bit数值。
*/
const N = 256
func main() {
t0 := time.Now().UnixMilli()
rand.Seed(time.Now().UnixNano())
// 初始化开关和灯的控制关系:开关i控制灯i
switchToLamp := make(map[int]int)
for i := 0; i < N; i++ {
switchToLamp[i] = i
}
// 每盏灯的标签路径
lampPath := make([]string, N)
// 初始分组:全体灯和开关
type group struct {
switches map[int]bool
lamps map[int]bool
path string
}
initialSwitches := make(map[int]bool)
initialLamps := make(map[int]bool)
for i := 0; i < N; i++ {
initialSwitches[i] = true
initialLamps[i] = true
}
groups := []group{{initialSwitches, initialLamps, ""}}
for len(groups) > 0 {
newGroups := []group{}
for _, g := range groups {
if len(g.switches) <= 1 {
// 唯一对应,记录路径
for lamp := range g.lamps {
lampPath[lamp] = g.path
}
continue
}
// 随机一分为二
switchList := keys(g.switches)
rand.Shuffle(len(switchList), func(i, j int) {
switchList[i], switchList[j] = switchList[j], switchList[i]
})
mid := len(switchList) / 2
onSwitches := toSet(switchList[:mid])
offSwitches := toSet(switchList[mid:])
// 灯亮的集合
lit := map[int]bool{}
dark := map[int]bool{}
for lamp := range g.lamps {
if onSwitches[lamp] {
lit[lamp] = true
} else {
dark[lamp] = true
}
}
newGroups = append(newGroups, group{onSwitches, lit, g.path + "1"})
newGroups = append(newGroups, group{offSwitches, dark, g.path + "0"})
}
groups = newGroups
}
// 反推开关编号
pathToSwitch := make(map[string]int)
for i, p := range lampPath {
pathToSwitch[p] = i
}
for i := 0; i < 10; i++ {
fmt.Printf("Lamp %3d | Path: %s | Switch: %d\n", i, lampPath[i], pathToSwitch[lampPath[i]])
}
fmt.Printf("total cost:%v path switch:%v \n", time.Now().UnixMilli()-t0, pathToSwitch)
}
func keys(m map[int]bool) []int {
res := []int{}
for k := range m {
res = append(res, k)
}
return res
}
func toSet(arr []int) map[int]bool {
res := map[int]bool{}
for _, v := range arr {
res[v] = true
}
return res
}
4 小结
- 决策树的优点
简单性和可解释性:决策树简单明了且易于理解。您可以像流程图一样将它们可视化,从而轻松查看决策是如何做出的。 多功能性:这意味着它们可用于不同类型的任务,可以很好地用于分类和回归 无需特征扩展:它们不需要您对数据进行规范化或扩展。 处理非线性关系: 它能够捕获特征和目标变量之间的非线性关系。
- 决策树的缺点
过拟合:当决策树捕获训练数据中的噪声和细节,并且对新数据执行不佳时,就会发生过拟合。 不稳定性:不稳定性意味着模型可能不可靠 输入的微小变化可能导致预测的显著差异。 偏向于具有更多级别的特征:决策树可能会偏向于许多类别在决策过程中过于关注它们的特征。这可能会导致模型错过其他重要特征,从而导致预测不准确。
决策树的应用
markdown
银行业务中的贷款审批:
银行需要根据客户资料决定是否批准贷款申请。 输入特征包括收入、信用评分、就业状况和贷款历史。 决策树可预测贷款批准或拒绝,帮助银行做出快速可靠的决策。
markdown
医学诊断:
医疗保健提供者希望根据临床测试结果预测患者是否患有糖尿病。 葡萄糖水平、BMI 和血压等特征用于制作决策树。
Tree 将患者分为糖尿病和非糖尿病,协助医生进行诊断。
markdown
预测教育考试成绩 :
School 想根据学习习惯预测学生会通过还是失败。 数据包括出勤率、学习时间和以前的成绩。
决策树可识别有风险的学生,从而允许教师提供额外的支持。