ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

K2算法入门:从数据中学习贝叶斯网络结构

K2算法入门:从数据中学习贝叶斯网络结构

简介:这份资源围绕K2算法从数据中学习贝叶斯网络结构这一主题,面向具备一定概率图模型基础、希望动手实现结构学习的研究生与算法工程师。包内共7个文件,以4个m脚本为主体,配合1个c源码、1个mat数据文件与1个txt说明,压缩包约10KB,体量轻便,便于快速阅读与二次开发。其中m文件承担主流程调度、对象构造与评分函数等职责,c源码对应K2算法的底层实现,mat文件提供可直接加载的样本数据,txt则给出许可与使用说明。已有2168人学习下载,说明该实现具备一定的参考价值。读者可借此理解K2算法在给定节点顺序与最大父节点数约束下,如何逐步搜索并确定各节点的父节点集合,进而得到有向无环图结构,同时可结合评分函数与先验DAG参数,将代码迁移到自己的数据集上验证结构学习效果,适合作为贝叶斯网络结构学习的入门实践素材。

1. 从一堆“谁影响谁”说起:K2算法到底在干什么

假设你是一位刚接手新项目的分析师,手里有一张表格,列着十几个变量:用户年龄、访问时段、点击次数、停留时长、是否下单。你想知道它们之间到底谁在影响谁,可数据里没有现成的因果箭头,只有冷冰冰的数值。这时候,贝叶斯网络结构学习就派上用场了——它要做的,就是从数据里把那张“谁指向谁”的关系图给反推出来。而K2算法,正是其中一种经典、直观且适合入门者理解结构学习思路的搜索方法。搞懂它,你就能明白“从数据里学出一张图”这件事到底是怎么一步步发生的,也能分清它和别的结构学习算法到底差在哪里。

2. 为什么需要K2算法:从“打分”到“找最优图”的朴素逻辑

2.1 结构学习的本质:在无数种可能中挑一个最像的

贝叶斯网络由两部分组成:一个有向无环图,以及每个节点对应的条件概率表。图决定了变量之间的依赖关系,概率表则量化了这种依赖的强弱。结构学习要解决的问题是:给定一份数据,如何找到那个最能解释数据生成过程的图。

这件事的难点在于,可能的图太多了。假设有10个变量,所有可能的有向无环图数量是天文数字,穷举根本不现实。所以所有结构学习算法都在做同一件事:用一种聪明的搜索策略,在巨大的图空间里快速找到一个足够好的解。K2算法就是其中一种搜索策略,它的核心思路非常朴素——从一个空图开始,按照某个顺序逐个把变量加进去,每加一个变量,就给它挑一组“父母节点”,挑的标准是让整个网络的评分变得最高。

打个比方,这就像组建一个项目团队。你手里有一份名单,规定了你只能按顺序一个一个把人拉进来。每拉一个人,你都要决定让他向谁汇报。你的目标是让整个团队的协作效率评分最高。K2算法就是那个按顺序拉人、每次只做局部最优决策的“组长”。

2.2 K2的评分函数:为什么它偏爱“简单”的网络

K2算法使用的评分函数通常基于贝叶斯评分准则,比如CH评分或BDe评分。这些评分有一个共同特点:它们会在“拟合数据”和“模型复杂度”之间做权衡。一个节点如果父母太多,虽然能更好地拟合当前数据,但评分函数会因为它太复杂而扣分。

通俗地说,这就像给论文打分。一篇论文如果堆砌了大量复杂模型,但每个模型都没讲透,评委不会给高分;相反,如果模型简洁、解释力强,反而更受青睐。K2算法在给每个节点选父母时,就是在做这种权衡:多一个父母,拟合度可能上升,但复杂度惩罚也会增加,只有当拟合度的提升超过惩罚时,这个父母才值得加。

这种机制让K2算法天然倾向于学出结构相对简洁的网络。对于变量不多、依赖关系不是特别密集的场景,这个特点反而成了优势——它不容易过拟合,学出来的图也更容易被人读懂。

2.3 为什么还要规定一个“节点顺序”

K2算法有一个非常关键的前提:你必须事先给定一个节点的顺序。这个顺序决定了算法按什么次序把节点加入网络,也决定了每个节点只能从它前面的节点里选父母。换句话说,如果A排在B前面,那么B可以选A当父母,但A不能选B当父母。

这个限制看起来有点霸道,但它换来的是效率的大幅提升。如果没有顺序约束,算法在给每个节点选父母时需要考虑所有其他节点,搜索空间会大很多。有了顺序,每个节点的候选父母集合就被限定在它前面的节点里,计算量直线下降。

那这个顺序从哪来?通常来自领域知识。比如你知道“年龄”不可能被“是否下单”影响,但“是否下单”可能受“年龄”影响,那你就可以把年龄排在前面。如果完全没有先验知识,也可以随机指定一个顺序,但学出来的网络质量可能会打折扣。这一点是K2算法最常被诟病的地方,也是它和那些不需要指定顺序的算法(比如基于爬山法的结构学习)最明显的区别。

3. K2算法的执行过程:一步步把图“长”出来

3.1 初始化:从空图开始

K2算法的起点是一个没有任何边的空图。所有节点都孤立着,没有任何依赖关系。然后算法按照你给定的节点顺序,依次处理每一个节点。

处理第一个节点时,它前面没有节点,所以它的父母集合只能是空集。算法会计算这个节点在没有父母时的评分,然后把它加入网络。此时网络里只有一个节点,没有任何边。

处理第二个节点时,它前面有一个节点。算法会尝试两种方案:让第二个节点的父母为空,或者让第一个节点当它的父母。分别计算两种方案的评分,选评分更高的那个。如果选后者,网络里就多了一条从第一个节点指向第二个节点的边。

这个过程一直持续到所有节点都被处理完。每处理一个节点,算法都会在“当前节点前面所有节点”这个候选池里,搜索一个最优的父母集合。搜索的方式通常是贪心的:先看空集评分,然后尝试加入一个父母,如果评分上升就保留,再看能不能再加一个,直到加入任何新父母都会导致评分下降为止。

3.2 贪心搜索父母集合的具体步骤

假设现在处理到节点X,它前面有A、B、C三个节点。算法会这样做:

第一步,计算X没有父母时的评分,记为S0。 第二步,分别计算X以A为父母、以B为父母、以C为父母时的评分,取其中最高的那个。假设是A,且评分高于S0,那就把A加入X的父母集合。 第三步,在已有A的基础上,再尝试加入B或C。分别计算X以{A,B}为父母和以{A,C}为父母的评分。如果{A,B}的评分更高且高于只有A时的评分,就把B也加进去。 第四步,继续尝试加入C,看评分是否还能上升。如果不能,停止。

最终得到的父母集合,就是K2算法为节点X选定的局部最优解。这个过程对每个节点独立进行,所以K2算法的计算复杂度大致是节点数乘以每个节点的候选父母组合数,对于中等规模的网络是可以接受的。

下面用一段伪代码展示这个贪心过程的核心逻辑:

# 假设 nodes 是按顺序排列的节点列表 # score(node, parents) 返回该节点在给定父母集合下的评分 # max_parents 是允许的最大父母数,防止过拟合 for i in range(len(nodes)): node = nodes[i] candidates = nodes[:i] # 只能从前面的节点里选父母 parents = [] current_score = score(node, parents) while len(parents) < max_parents: best_gain = 0 best_candidate = None for c in candidates: if c not in parents: new_score = score(node, parents + [c]) gain = new_score - current_score if gain > best_gain: best_gain = gain best_candidate = c if best_candidate is None: break # 加入任何新父母都无法提升评分 parents.append(best_candidate) current_score += best_gain # 将 node 及其父母集合加入网络 add_to_network(node, parents)

这段代码的关键参数有两个:一个是max_parents,它限制了每个节点最多能有多少个父母,防止学出过于复杂的结构;另一个是score函数,它决定了“什么样的父母集合算好”。不同的评分函数会导致不同的学习结果,但贪心搜索的框架是一样的。

3.3 评分函数怎么选:CH评分与BDe评分的区别

K2算法最初使用的是CH评分,后来更常用的是BDe评分。两者的核心区别在于对参数先验的处理方式。

CH评分假设所有参数是均匀分布的,计算相对简单,但在数据量较少时容易偏向于选择边数较少的网络。BDe评分则引入了一个等价样本大小的概念,允许你控制先验对结果的影响程度。等价样本大小越大,先验越强,学出来的网络越保守;越小,越依赖数据本身。

对于初学者来说,如果只是想做一次快速的结构探索,CH评分足够用。如果数据量不大,又希望结果稳定一些,可以试试BDe评分,把等价样本大小设为一个较小的值,比如1到5之间。这个参数没有绝对的最优值,需要根据具体数据反复试几次。

4. 常见误区与易混概念:K2不是万能钥匙

4.1 误区一:K2算法能自己发现因果方向

这是最容易搞混的一点。K2算法学出来的是有向无环图,箭头看起来像是因果关系,但它本质上只是在给定节点顺序的前提下,找到了一组条件独立性关系。箭头方向很大程度上受你指定的节点顺序影响。如果你把顺序换一下,学出来的图可能完全不同。

怎么区分:如果你需要真正的因果推断,光靠K2算法不够,还需要结合干预实验或因果发现方法。K2算法更适合做探索性分析,帮你快速了解变量之间可能存在哪些依赖关系,而不是直接下因果结论。

4.2 误区二:K2算法和爬山法是一回事

两者都是贪心搜索,但搜索策略不同。K2算法是“按顺序加节点,每个节点独立选父母”,爬山法则是“从一个初始图开始,不断尝试加边、删边、反转边,直到评分不再提升”。K2算法的搜索空间受节点顺序限制,爬山法则不受。

怎么区分:如果你有比较可靠的节点顺序先验,K2算法会更快、更稳定。如果你对顺序没把握,爬山法可能更合适,但它也更容易陷入局部最优。两者没有绝对的好坏,取决于你手头有什么信息。

4.3 误区三:父母越多,模型越准

K2算法的评分函数自带复杂度惩罚,所以它不会无限制地给节点加父母。但有些人在使用时会把max_parents设得很大,以为这样能学出更精细的网络。实际上,父母太多会导致参数数量爆炸,每个条件概率表都需要大量数据才能估计准确。数据不够时,学出来的网络反而更不可靠。

怎么区分:看你的数据量。如果样本量只有几百条,每个节点的父母数最好控制在3个以内。如果样本量上万,可以适当放宽,但也要观察学出来的网络是否过于密集、难以解释。

4.4 误区四:K2算法只能处理离散变量

K2算法最初是为离散贝叶斯网络设计的,评分函数也是基于离散变量的计数来计算的。如果数据里有连续变量,直接套用K2算法会出问题。

怎么区分:如果变量是连续的,可以先做离散化,比如把年龄分成几个区间,再跑K2算法。但离散化会损失信息,分箱的粒度需要仔细权衡。另一种做法是改用支持连续变量的评分函数,比如基于高斯分布的BGe评分,但那就不是标准K2算法的范畴了。

4.5 误区五:节点顺序随便定就行

节点顺序是K2算法最关键的输入之一。顺序错了,学出来的网络可能完全偏离真实结构。比如真实情况是A影响B,但你把B排在A前面,K2算法就永远学不出A指向B的边。

怎么区分:尽量利用领域知识来确定顺序。如果实在没有先验,可以尝试多组随机顺序,看学出来的网络是否稳定。如果不同顺序下结果差异很大,说明数据本身的信息不足以支撑可靠的结构学习,需要更多数据或更强的先验。

5. 什么时候该用K2算法,什么时候该换条路

K2算法最适合的场景,是你对变量之间的先后关系有比较清晰的判断,同时变量数量不算太多(比如十几个到几十个),数据量中等,希望快速得到一个可解释的初始网络结构。比如在某个分析项目中,你明确知道用户属性类变量应该排在行为类变量前面,那K2算法就能帮你快速理出一张依赖关系图,作为后续建模的起点。

但如果你面对的是上百个变量、完全没有顺序先验、或者数据里混杂了大量连续变量,K2算法就会显得力不从心。这时候可以考虑基于约束的方法(比如PC算法),它不需要指定顺序,但计算量更大;或者基于评分加随机搜索的方法(比如遗传算法),它能跳出局部最优,但调参更麻烦。

下面这张表可以帮你快速判断:

场景特征适合K2算法建议换其他方法
节点顺序先验有较可靠的顺序完全不知道顺序
变量数量十几个到几十个上百个
变量类型离散为主大量连续变量
数据量中等,能支撑参数估计极少或极多
目标快速探索、可解释追求全局最优

我自己的习惯是,每次拿到一份新数据,先花十分钟想清楚变量之间大概的先后关系,画一个粗略的顺序草图。如果这个草图能画出来,就先用K2算法跑一版,看看学出来的边和我的预期差多少。如果差得太远,再换方法。这个习惯帮我避免了很多次“直接上复杂算法、结果调参调到崩溃”的翻车经历。从那以后我每次做结构学习,都强制自己先走一遍“顺序先验检查”这一步,确认K2算法是不是真的合适。

希望这份关于K2算法和贝叶斯网络结构学习的梳理,能帮你把“从数据里学出一张图”这件事看得更清楚。如果你手头正好有数据想试试,不妨从整理变量顺序开始,一步步把网络“长”出来。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进