qinghuan
← 回到知识探索

AI 直觉 / 动手实验

每个特征都拿零分,为什么放在一起就能知道答案?

四种异或输入足以说明:单列互信息只看单列,不能替你判断两列一起提供的信息。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 先遮住一列,再把两列一起展开
  4. 把“少了多少不确定性”写成一个数
  5. 加一点独立翻转,仍能区分单列与联合
  6. 运行完整小例子
  7. 用单列分数删特征时,究竟删掉了什么?
  8. 记住这三点
  9. 别把例子用过头
  10. 自己试一小步
  11. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 2 个解释
单列互信息
只告诉你某一列的值,平均能减少多少对标签的不确定性。
条件熵
某些信息已经知道之后,平均还剩多少不确定性。
打开完整术语小抄 ↗

有两列 0/1 特征 X₁、X₂,各自像公平硬币,而且相互独立。标签规则是:两列不同就记 1,相同就记 0。四种输入各占四分之一。

如果一次只给一列打分,两列对标签的互信息都等于 0。但同时给出两列,标签完全确定。这不是计算矛盾,而是“单独知道一列”和“同时知道两列”回答了不同的问题。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • demo.py:完整可运行源码
  • README.md:运行步骤与练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和运行验证记录
  • SHA256SUMS.txt:文件摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B demo.py

解压后,在含 demo.py 的目录打开终端。命令分别回车执行。代码枚举八个“输入对与最终标签”的可能结果,按概率聚合熵,并用独立的二元熵公式核对 51 组翻转概率;无随机样本、没有拟合模型,也没有运行 scikit-learn 估计器。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

每列单独得零分,两列合起来能确定标签吗?

正在准备实验控件…

先在无噪声的四种输入中遮住第二列:每个 X₁ 值下面都同时出现两种标签。再同时观察两列。

只看 X₁:互信息
0.000000 bit
只看 X₂:互信息
0.000000 bit
同时看两列:互信息
1.000000 bit
看完两列后的剩余熵
0.000000 bit

单列互信息为 0,只说明这一列单独不能减少标签的不确定性。两列合起来减少 1.000000 bit;先知道 X₁ 后再知道 X₂,也增加 1.000000 bit。

每行是一个结果的概率,全部行合计 1
X₁X₂最终标签 Y联合概率
0000.250000
0010.000000
0100.000000
0110.250000
1000.000000
1010.250000
1100.250000
1110.000000

X₁、X₂ 是相互独立的公平二进制变量;先做异或,再独立以 ε 概率翻转标签。直接枚举完整概率表,互信息用 log₂,单位 bit。没有抽样、训练模型或调用 scikit-learn;其 API 输出单位为 nat。

先遮住一列,再把两列一起展开

无噪声的四种情况是 (0,0)→0、(0,1)→1、(1,0)→1、(1,1)→0,每种概率都是 1/4。这条规则就是异或 XOR。

只告诉你 X₁=0 时,标签仍可能为 0 或 1,而且各占一半。X₁=1 时也一样。所以知道 X₁ 后,标签分布完全没变。对称地,只知道 X₂ 也没有帮助。

同时知道 X₁、X₂ 就不同了:四个输入对各对应唯一标签。此时不仅可以预测,而且在这张完整概率表里没有剩余不确定性。这里还没训练任何模型。

把“少了多少不确定性”写成一个数

用二为底的熵 H(Y)=−Σp(y)log₂p(y) 来记不确定性,约定 0log₂0=0。公平的二元标签,熵为 1 bit;已确定的标签,熵为 0。

互信息 I(X;Y)=H(Y)−H(Y|X),表示知道 X 之后平均减少多少标签不确定性。只看第一列时 H(Y|X₁)=1,所以 I(X₁;Y)=0;两列一起看时 H(Y|X₁,X₂)=0,所以 I((X₁,X₂);Y)=1。

不能直接把 I(X₁;Y) 与 I(X₂;Y) 相加来替代联合信息。分步计算应是:先获得第一列的信息,再加上“已经知道第一列后,第二列额外提供的信息”。在本例里,后者为 1 bit。

加一点独立翻转,仍能区分单列与联合

现在先算异或,再独立以 ε 的概率把标签翻转。限制 0≤ε≤0.5。两个输入变量仍相互独立、各自公平,翻转也与输入独立。标签的总体分布依旧一半为 0、一半为 1。

单列仍与标签独立,互信息保持 0。知道两列后,剩余不确定性只来自这次翻转,等于二元熵 h₂(ε)=−εlog₂ε−(1−ε)log₂(1−ε)。所以联合互信息是 1−h₂(ε)。

ε=0 时得到 1 bit;ε=0.25 时约为 0.188722 bit;ε=0.5 时为 0。互动表里显示的是精确给定分布的概率,不是反复抽样得到的频数。

运行完整小例子

代码枚举八个“输入对与最终标签”的可能结果,按概率聚合熵,并用独立的二元熵公式核对 51 组翻转概率;无随机样本、没有拟合模型,也没有运行 scikit-learn 估计器。

展开收起代码 · 小实验python · 54 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Enumerate a finite probability distribution; no sampling or estimator."""
from collections import defaultdict
from math import isclose, isfinite, log2


def distribution(flip):
    if (type(flip) not in (int, float) or not isfinite(flip)
            or not 0 <= flip <= 0.5):
        raise ValueError("flip must be a number between 0 and 0.5")
    return [(x1, x2, y, 0.25 * (1 - flip if y == (x1 ^ x2) else flip))
            for x1 in (0, 1) for x2 in (0, 1) for y in (0, 1)]


def entropy(masses):
    return -sum(p * log2(p) for p in masses if p > 0)


def mutual_information(rows, feature):
    px, py, pxy = defaultdict(float), defaultdict(float), defaultdict(float)
    for x1, x2, y, p in rows:
        x = feature(x1, x2)
        px[x] += p
        py[y] += p
        pxy[x, y] += p
    result = entropy(px.values()) + entropy(py.values()) - entropy(pxy.values())
    return 0.0 if abs(result) < 1e-12 else result


def binary_entropy(p):
    return 0.0 if p in (0, 1) else -p * log2(p) - (1 - p) * log2(1 - p)


def main():
    print("X1 and X2: independent fair bits; information unit: bit")
    for flip in (0.0, 0.25, 0.5):
        rows = distribution(flip)
        one = mutual_information(rows, lambda a, b: a)
        two = mutual_information(rows, lambda a, b: b)
        joint = mutual_information(rows, lambda a, b: (a, b))
        print(f"flip={flip:.2f} I(X1;Y)={one:.6f} I(X2;Y)={two:.6f} I(pair;Y)={joint:.6f}")
    # Independent formula for a fair XOR bit through a binary flip channel.
    for k in range(51):
        p = k / 100
        rows = distribution(p)
        assert isclose(sum(row[3] for row in rows), 1.0, abs_tol=1e-12)
        assert mutual_information(rows, lambda a, b: a) == 0.0
        assert mutual_information(rows, lambda a, b: b) == 0.0
        assert isclose(mutual_information(rows, lambda a, b: (a, b)),
                       1 - binary_entropy(p), abs_tol=1e-12)
    print("checks: 51 probability tables agree with 1 - binary_entropy(flip)")


if __name__ == "__main__":
    main()

怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-26 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。代码枚举八个“输入对与最终标签”的可能结果,按概率聚合熵,并用独立的二元熵公式核对 51 组翻转概率;无随机样本、没有拟合模型,也没有运行 scikit-learn 估计器。

运行后对照结果
X1 and X2: independent fair bits; information unit: bit
flip=0.00 I(X1;Y)=0.000000 I(X2;Y)=0.000000 I(pair;Y)=1.000000
flip=0.25 I(X1;Y)=0.000000 I(X2;Y)=0.000000 I(pair;Y)=0.188722
flip=0.50 I(X1;Y)=0.000000 I(X2;Y)=0.000000 I(pair;Y)=0.000000
checks: 51 probability tables agree with 1 - binary_entropy(flip)

用单列分数删特征时,究竟删掉了什么?

scikit-learn 的单变量特征选择按每列与目标的分数筛选;mutual_info_classif 返回的也是每个特征各自的估计值。互信息能表达单个特征与目标之间的非线性依赖,但这不意味着逐列调用就自动检查了所有特征组合。

在本例里,如果用“单列互信息为零就全部删除”的规则,会把组成异或答案的两列一起删除。增加一个明确的组合特征,例如这两列的异或,能够表达这里的交互;在真实任务里是否值得加入,仍需要按训练与验证边界评估。

注意单位与估计对象:本页用 log₂,单位是 bit;文档中 mutual_info_classif 的输出单位是 nat,转成 bit 要除以 ln(2)。二元离散特征使用该 API 时应明确离散设置,不能因为数据是整数就假定 dense 输入的 auto 一定按离散变量处理。

精确分布中的零与有限样本估计出的零也不同。后者可能受样本、估计方式和截断影响。本文没有证明你的实际数据是 XOR,也没有证明所有低分特征都该保留;它只是给出一个不能单凭单列分数排除交互作用的反例。

记住这三点

  • 单列互信息为零,描述的是这一列单独与目标的独立性,不是它与其他列组合后一定无用。
  • 联合信息不是单列信息简单相加;第二列的价值会随已知条件而改变。
  • 概率表的精确值、样本估计值、不同对数单位,需要分别说明。

别把例子用过头

  • 认为互信息能处理非线性,就等于逐列筛选能自动发现任意高阶交互。
  • 忘记独立、公平与独立翻转这些前提,仍把本例的零分结论套到任意数据。
  • 把 bit 与 nat 直接比较,或把有限样本的估计零当成严格独立的证明。

自己试一小步

换一条标签规则,看看零分来自规则还是来自变量名称。

  1. 把 Y 改成 X₁,先手算两列各自和联合的互信息。
  2. 恢复无噪声 XOR,将第一列固定为已知值,再比较第二列的作用。
  3. 把一枚公平硬币改成偏置硬币,重新写出联合概率表,不要只改结论。

做到这些,就算完成

  • 每张表的概率非负且合计为 1。
  • 分别说明完整分布、单列条件分布和两列条件分布。
  • 写明使用的对数底数,并解释自己的验证是否涉及抽样或模型训练。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。