qinghuan
← 回到知识探索

AI 直觉 / 可下载小项目

推荐清单只放两条,把所有权重抬高为什么没改变选择概率?

进阶实验:用三张候选卡片和精确分数,拆开清单长度、集合概率与单项入选概率。

展开本文目录
  1. 下载完整练习包
  2. 先把三张卡片写成能手算的小表
  3. 用行列式给整个组合赋权重
  4. 清单长度自由时,八个集合一起竞争
  5. 固定两条后,同一个倍率会抵消
  6. 矩阵的对角线还不是最终入选概率
  7. 先验完整输入,不能只看当前要选几条
  8. 运行完整精确分数实验
  9. 把数学上的不重复和推荐效果分开
  10. 记住这些要点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了

你有三条候选建议,清单只能放两条。为了让候选更容易被选中,你把所有权重一起提高,却发现每个二元素组合的选择概率完全没变。问题不在随机数,而在你把同一倍率同时乘到了分子和分母。

如果清单可以自由选择长短,同样的调整却可能让它更常选出较长清单。下面从三张卡片的八个可能子集出发,逐项算清楚。本文标为进阶:需要条件概率和小矩阵运算;它是离线数学练习,没有训练推荐模型。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本;2026-10-18 实测 Windows / Python 3.14.0,3.10 仅做语法检查。仅用标准库,无第三方依赖。仅使用标准库。实际运行日期、环境和范围见随包核验记录。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • dpp_scale.py:教学源码
  • dpp_scale.observed.stdout.txt:说明或核验记录
  • README.md:说明或核验记录
  • verification.json:说明或核验记录
  • SHA256SUMS.txt:说明或核验记录

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

python --version
python -B dpp_scale.py

解压到独立练习文件夹,在包含源码的目录打开终端,按下方顺序运行。示例运行记录不代表独立练习已完成;修改代码后需重新验证。

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

先把三张卡片写成能手算的小表

把候选叫 A、B、C,用两列人为特征表示:A=(2,0),B=(1,1),C=(0,1)。这些数字只服务于本例,不能解释为真实用户偏好或测得的内容质量。

让表格第 i 行第 j 列等于两个特征向量的点积,就得到 L=[[4,2,0],[2,2,1],[0,1,1]]。例如 A 与 B 的点积为2,A与自身的点积为4。这样的表叫 Gram 矩阵。

接下来不是按某一列从高到低排序,而是给整个候选集合赋权重。集合里只有 A 时取 L 的 A 行与 A 列;集合是 AB 时同时留下 A、B 两行两列。留下的小方阵记作 L_S,其中 S 表示所选集合。

用行列式给整个组合赋权重

本例规定集合 S 的权重为 det(L_S)。二阶行列式的规则是左上乘右下,再减去右上乘左下,所以 AB 的权重是4×2−2×2=4;AC是4×1−0×0=4;BC是2×1−1×1=1。

三个单项 A、B、C 的权重分别是4、2、1。ABC 的权重为0,因为三个向量都放在二维空间中,无法张成三个独立方向。空集的行列式约定为1,这一项不能漏掉。

行列式点过程,简称 DPP,用这种集合权重构造概率。这里采用似然矩阵 L 的写法,要求 L 半正定,以保证每个主子式都非负。向量内积构造能满足这个条件;随手填一个“相似度表”则未必满足。

清单长度自由时,八个集合一起竞争

可变长度的概率是 P(Y=S)=det(L_S)/det(I+L)。Y 是随机选出的集合,I 是单位矩阵。在小例子里,可以直接加上全部集合的权重,核对分母确实等于 det(I+L)。

八个集合分为:空集权重1,三个单项权重和7,三个双项权重和9,三项集合权重0。因此总权重 Z=1+7+9=17。这里有时会选空集,有时选一条或两条,不能把它描述成“总是选两条”。

平均长度是把每个集合的条数乘以它的概率再相加,所以 E[|Y|]=(1×7+2×9)/17=25/17。这个平均数不必是整数;它表示许多次抽取的长度平均,而不是一张清单可以装下半条建议。

固定两条后,同一个倍率会抵消

现在把整个矩阵换成3L。单项行列式乘3,双项乘9,三项乘27。可变长度的总权重变成1+21+81=103,平均长度变成(21+2×81)/103=183/103,比原来的25/17大。

若要求恰好选两条,就只在 AB、AC、BC 三个集合内重新归一化。概率分别是4/9、4/9、1/9。换成3L后,三项权重成为36、36、9,归一化后仍是原来的三个分数。

这种固定数量的条件分布叫 k-DPP。一般地,det(cL_S)=c^|S|det(L_S)。固定 |S|=k 后,所有分子与分母都带 c^k,因此任意正数 c 都会抵消。这个一般结论来自行列式的缩放规则,程序枚举只是核验小输入。

这里严格限定 c>0。c=0 会把核变成零矩阵,可变长度分布只剩空集,固定 k>0 的条件事件概率则为0,不能归一化。负倍率也不属于这个结论。本文的“抬高权重”指整体正比例缩放 L,不是给任意模型分数加常数。

矩阵的对角线还不是最终入选概率

另看一个更小的核 L=[[1,1/2],[1/2,1]]。四个集合的未归一化权重分别为:空集1、只有A为1、只有B为1、AB为3/4。总和15/4,因此 P(空集)=4/15,P(只有A)=P(只有B)=4/15,P(AB)=1/5。

A最终入选包括“只有A”和“AB”两种互斥结果,所以包含概率为4/15+1/5=7/15。它与 L_AA=1不同。不要把候选自身的核值直接读成入选百分比。

包含概率使用另一个矩阵 K=L(I+L)^−1。本例 K=[[7/15,2/15],[2/15,7/15]],K 的对角线才是单项包含概率。更一般地,P(T包含于Y)=det(K_T)。这些 K 公式针对这里的普通可变长度 DPP,不能不加推导就照搬给条件固定 k 的分布。

先验完整输入,不能只看当前要选几条

原型的概率入口只接受至多5项的正方形、对称矩阵;每个值必须是整数或 Fraction 精确分数,布尔值和普通浮点数被拒绝。程序检查所有阶数的主子式是否非负,然后才计算请求的 k。这个穷举校验适合极小练习,不适合大规模生产输入。

反例是 diag(−1,−1):二阶行列式等于1,如果只检查要抽取的k=2,就会误放过它;但两个一阶主子式都是−1,根本不是有效的半正定似然核。程序还拒绝不对称、非正方形以及对角线虽正但存在负主子式的矩阵。

从特征构造 Gram 矩阵时,所有向量必须具有相同且非零的维度。[(1,2),(3,)]会直接报错,不能让 zip 静默丢掉多出的坐标。空候选列表单独允许:k=0 时只有空集,概率为1。

零核也只会选空集。请求零核的k>0,或让k超过可实现的秩,都会因条件事件概率为0而拒绝。原型不是“任意表格都能抽样”的接口,调用者应先检查自己表示的是特征、核值还是概率。

运行完整精确分数实验

下方源码与实际输出一同冻结保存。它遍历三个二维向量、每个坐标取−1/0/1的729个向量矩阵;不同向量矩阵可能给出相同 Gram 核,因此不能声称测试了729个互异核。

主路径通过消元计算行列式,独立参考通过排列展开计算;包含概率再与所有满足包含条件的子集概率之和比较。程序还核对归一化、平均长度、成对关系和正缩放条件分布。

本次实际通过20,958项检查,包含16类非法输入或不可实现请求的逐项拒绝核验,以及5项空集、零核边界检查。所有数字都用精确分数计算,没有运行随机 DPP 采样器或 DPPy。

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

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

小实验 · python
"""Small exact DPP experiment. Standard library only; not a production sampler."""
from fractions import Fraction as F
from itertools import combinations, permutations, product
import json
from pathlib import Path
import platform


def determinant(matrix):
    a = [list(map(F, row)) for row in matrix]
    n = len(a)
    value = F(1)
    for col in range(n):
        pivot = next((r for r in range(col, n) if a[r][col]), None)
        if pivot is None:
            return F(0)
        if pivot != col:
            a[col], a[pivot] = a[pivot], a[col]
            value = -value
        p = a[col][col]
        value *= p
        for row in range(col + 1, n):
            factor = a[row][col] / p
            for j in range(col + 1, n):
                a[row][j] -= factor * a[col][j]
    return value


def determinant_by_permutations(matrix):
    """Independent Leibniz reference, limited to tiny matrices."""
    n = len(matrix)
    total = F(0)
    for perm in permutations(range(n)):
        sign = (-1) ** sum(perm[i] > perm[j] for i in range(n) for j in range(i + 1, n))
        term = F(sign)
        for i in range(n):
            term *= matrix[i][perm[i]]
        total += term
    return total


def inverse(matrix):
    n = len(matrix)
    a = [list(map(F, row)) + [F(i == j) for j in range(n)] for i, row in enumerate(matrix)]
    for col in range(n):
        pivot = next((r for r in range(col, n) if a[r][col]), None)
        if pivot is None:
            raise ValueError('singular matrix')
        a[col], a[pivot] = a[pivot], a[col]
        p = a[col][col]
        a[col] = [x / p for x in a[col]]
        for row in range(n):
            if row != col:
                factor = a[row][col]
                a[row] = [x - factor * y for x, y in zip(a[row], a[col])]
    return [row[n:] for row in a]


def subsets(n):
    return [s for k in range(n + 1) for s in combinations(range(n), k)]


def minor(matrix, subset):
    return [[matrix[i][j] for j in subset] for i in subset]


def rational(value):
    if type(value) is not int and not isinstance(value, F):
        raise ValueError('only exact int/Fraction values are supported; bool and float are rejected')
    return F(value)


def valid_kernel(matrix):
    """Tiny exact PSD check: symmetry and ALL principal minors, not only size k."""
    if not isinstance(matrix, (list, tuple)) or len(matrix) > 5:
        raise ValueError('a list/tuple kernel of size at most 5 is required')
    n = len(matrix)
    if any(not isinstance(row, (list, tuple)) or len(row) != n for row in matrix):
        raise ValueError('square matrix required')
    a = [[rational(x) for x in row] for row in matrix]
    if any(a[i][j] != a[j][i] for i in range(n) for j in range(n)):
        raise ValueError('symmetric matrix required')
    if any(determinant(minor(a, s)) < 0 for s in subsets(n)):
        raise ValueError('PSD kernel required: a principal minor is negative')
    return a


def probabilities(matrix, k=None):
    matrix = valid_kernel(matrix)
    if k is not None and (type(k) is not int or not 0 <= k <= len(matrix)):
        raise ValueError('invalid fixed size')
    weights = {s: determinant(minor(matrix, s)) for s in subsets(len(matrix)) if k is None or len(s) == k}
    if any(w < 0 for w in weights.values()):
        raise ValueError('negative principal minor')
    z = sum(weights.values())
    if not z:
        raise ValueError('requested cardinality has zero probability')
    return {s: w / z for s, w in weights.items()}


def scale(matrix, c):
    c = rational(c)
    if c <= 0:
        raise ValueError('the scale-invariance experiment requires c > 0')
    matrix = valid_kernel(matrix)
    return [[c * x for x in row] for row in matrix]


def gram(vectors):
    if not isinstance(vectors, (list, tuple)) or len(vectors) > 5:
        raise ValueError('at most 5 feature vectors are supported')
    if not vectors:
        return []
    if any(not isinstance(row, (list, tuple)) for row in vectors):
        raise ValueError('feature vectors must be lists or tuples')
    dimension = len(vectors[0])
    if dimension == 0 or any(len(row) != dimension for row in vectors):
        raise ValueError('nonempty feature vectors of equal dimension required')
    features = [[rational(x) for x in row] for row in vectors]
    return [[sum(a * b for a, b in zip(x, y)) for y in features] for x in features]


def expected_size(distribution):
    return sum(len(s) * p for s, p in distribution.items())


def main():
    checks = 0
    def check(condition, label):
        nonlocal checks
        checks += 1
        if not condition:
            raise AssertionError(label)

    two = [[F(1), F(1, 2)], [F(1, 2), F(1)]]
    p = probabilities(two)
    include_a = sum(prob for s, prob in p.items() if 0 in s)
    check(p[()] == F(4, 15), 'two-item empty')
    check(p[(0, 1)] == F(1, 5), 'two-item pair')
    check(include_a == F(7, 15) and include_a != two[0][0], 'L diagonal is not inclusion')
    three = gram([(2, 0), (1, 1), (0, 1)])
    base, larger = probabilities(three), probabilities(scale(three, 3))
    fixed = probabilities(three, 2)
    check(expected_size(base) == F(25, 17), 'base expected size')
    check(expected_size(larger) == F(183, 103), 'scaled expected size')
    check(fixed == {(0, 1): F(4, 9), (0, 2): F(4, 9), (1, 2): F(1, 9)}, 'fixed pair probabilities')
    check(fixed == probabilities(scale(three, 3), 2), 'fixed size scale cancellation')

    matrices = 0
    impossible_fixed_sizes = 0
    for entries in product((-1, 0, 1), repeat=6):
        vectors = [entries[i:i + 2] for i in (0, 2, 4)]
        matrix = gram(vectors)
        ss = subsets(3)
        for s in ss:
            check(determinant(minor(matrix, s)) == determinant_by_permutations(minor(matrix, s)), 'independent determinant reference')
        ident_plus = [[matrix[i][j] + F(i == j) for j in range(3)] for i in range(3)]
        check(sum(determinant(minor(matrix, s)) for s in ss) == determinant(ident_plus), 'normalizer identity')
        dist = probabilities(matrix)
        check(sum(dist.values()) == 1, 'normalization')
        inv = inverse(ident_plus)
        kernel = [[F(i == j) - inv[i][j] for j in range(3)] for i in range(3)]
        for s in ss:
            included = sum(v for t, v in dist.items() if set(s).issubset(t))
            check(included == determinant_by_permutations(minor(kernel, s)), 'inclusion identity')
        check(expected_size(dist) == sum(kernel[i][i] for i in range(3)), 'trace expectation')
        for i, j in combinations(range(3), 2):
            joint = sum(v for t, v in dist.items() if i in t and j in t)
            check(joint == kernel[i][i] * kernel[j][j] - kernel[i][j] ** 2, 'pair dependence')
        for k in range(4):
            try:
                fixed = probabilities(matrix, k)
            except ValueError:
                impossible_fixed_sizes += 1
                continue
            for c in (F(1, 2), F(3)):
                check(fixed == probabilities(scale(matrix, c), k), 'conditional scale invariance')
        check(expected_size(probabilities(scale(matrix, F(1, 2)))) <= expected_size(dist)
              <= expected_size(probabilities(scale(matrix, 3))), 'size monotonicity on tested grid')
        matrices += 1

    check(probabilities([], 0) == {(): F(1)}, 'empty ground set k=0')
    check(probabilities(gram([]), 0) == {(): F(1)}, 'empty feature set k=0')
    zero = [[F(0), F(0)], [F(0), F(0)]]
    check(probabilities(zero, 0) == {(): F(1)}, 'zero kernel k=0')
    check(probabilities(zero)[()] == 1, 'zero kernel chooses empty set')
    check(probabilities(three, 0) == {(): F(1)}, 'nonzero kernel k=0')
    rejected = 0
    rejection_cases = [
        ('rank-deficient fixed size', lambda: probabilities([[F(1), F(1)], [F(1), F(1)]], 2)),
        ('k too large', lambda: probabilities(two, 3)),
        ('negative k', lambda: probabilities(two, -1)),
        ('bool k', lambda: probabilities(two, True)),
        ('negative diagonal hidden by positive 2x2 determinant', lambda: probabilities([[-1, 0], [0, -1]], 2)),
        ('non-square matrix', lambda: probabilities([[1, 0], [0]], 0)),
        ('asymmetric matrix', lambda: probabilities([[1, 1], [0, 1]], 1)),
        ('indefinite matrix with positive diagonal', lambda: probabilities([[1, 2], [2, 1]], 1)),
        ('unequal feature dimensions', lambda: gram([(1, 2), (3,)])),
        ('zero kernel k=1', lambda: probabilities(zero, 1)),
        ('zero kernel k=2', lambda: probabilities(zero, 2)),
        ('zero scale', lambda: scale(two, 0)),
        ('negative scale', lambda: scale(two, -1)),
        ('float kernel unsupported', lambda: probabilities([[0.5]], 1)),
        ('empty feature dimensions unsupported', lambda: gram([[], []])),
        ('kernel outside tiny size bound', lambda: probabilities([[0] * 6 for _ in range(6)])),
    ]
    for label, action in rejection_cases:
        failed = False
        try:
            action()
        except ValueError:
            rejected += 1
            failed = True
        check(failed, label)
    check(rejected == len(rejection_cases), 'all invalid inputs rejected')
    report = {
        'status': 'PASS', 'runtime': platform.python_version(), 'checks': checks,
        'grid_vector_matrices': matrices, 'unique_kernel_count_claimed': False,
        'grid': 'Three 2D vectors; every coordinate in {-1,0,1}; 3^6=729 vector matrices, not necessarily distinct kernels.',
        'impossible_fixed_size_cases_skipped': impossible_fixed_sizes,
        'rejected_input_cases': [label for label, action in rejection_cases],
        'input_contract': 'At most 5 items; exact int/Fraction only; square symmetric kernel; every principal minor nonnegative. Gram features must have equal nonzero dimension, except an empty ground set is accepted.',
        'scale_contract': 'c > 0 only. At c=0 the variable-size process collapses to the empty set and fixed k>0 is undefined. Negative scales are outside this experiment.',
        'main_two_item': {'L_diagonal': '1', 'P_empty': str(p[()]), 'P_pair': str(p[(0, 1)]), 'P_A_included': str(include_a)},
        'main_three_item': {'vectors': [[2, 0], [1, 1], [0, 1]], 'E_size_L': str(expected_size(base)),
                            'E_size_3L': str(expected_size(larger)),
                            'fixed_k_2': {str(s): str(v) for s, v in probabilities(three, 2).items()}},
        'boundary': 'Exact finite arithmetic checks only. No DPP library, random sampler, recommender, training, or model-quality evaluation was run.'}
    Path('report.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8')
    print('L_11=1; P(A included)=7/15; P(empty)=4/15; P(A,B)=1/5')
    print('E[size] at L=25/17; at 3L=183/103')
    print('Fixed k=2 at L and 3L: P(A,B)=4/9, P(A,C)=4/9, P(B,C)=1/9')
    print(f'PASS: {checks} checks; {matrices} vector matrices; {rejected} rejected inputs')


if __name__ == '__main__':
    main()

怎样运行:保存为 dpp_scale.py,在独立练习目录执行 python -B dpp_scale.py。2026-10-18 已在 Windows、Python 3.14.0 实际运行此份源码,生成 report.json。Python 3.10 仅做语法兼容检查,未启动该版本解释器。程序只使用标准库,不访问网络。

运行后对照结果
L_11=1; P(A included)=7/15; P(empty)=4/15; P(A,B)=1/5
E[size] at L=25/17; at 3L=183/103
Fixed k=2 at L and 3L: P(A,B)=4/9, P(A,C)=4/9, P(B,C)=1/9
PASS: 20958 checks; 729 vector matrices; 16 rejected inputs

把数学上的不重复和推荐效果分开

行列式会受特征方向与长度影响;在这个表示中,方向重复的组合可能得到较小甚至为0的权重。但特征是否能表达用户在意的差异,是另外一个问题。两条措辞不同但事实重复的建议,未必会被某组特征识别出来。

这个实验能够核对概率账本和输入合同,不能证明内容更有用、更可靠或更公平。若要用在真实推荐里,还需要真实任务的标注与评价;本篇没有做这一步。

继续练习时,先写出你究竟要控制“选多少条”还是“固定数量下选哪些组合”,再选择相应的分布。下方只给改动任务与自检要求,不提前给出新参数的答案。

记住这些要点

  • 普通 DPP 同时给清单长度与内容分配概率;固定 k 后只比较相同长度的集合。
  • 整体正缩放 L 会对不同长度产生不同倍率,但固定 k 的公共因子会抵消。
  • L 的对角线不是最终包含概率;包含事件与恰好选出某集合要分开。
  • 输入校验必须覆盖完整核,不能只检查当前 k 阶子式。

别把例子用过头

  • 漏掉空集权重,或把可变长度分布误称为固定选两条。
  • 把普通 DPP 的 K 包含公式直接套给一般 k-DPP。
  • 只看当前 k 阶行列式非负,就接受一个并非半正定的矩阵。
  • 把有限精确枚举说成推荐效果或高效采样器的验证。

自己试一小步

把三个特征改成 (1,0)、(1,1)、(0,2),固定选一条与两条时,整体缩放分别会怎样?

  1. 手算新的 Gram 矩阵以及各个子集的权重,先写下预测再运行。
  2. 分别比较 L、L/3、4L 下的可变长度分布,以及 k=1、k=2 的条件分布。
  3. 再把三个向量都改成同一直线上的向量,检查哪些固定数量请求不再有定义。

做到这些,就算完成

  • 每个已定义分布的概率和等于1,包含概率的和与平均长度一致。
  • 条件事件权重为0时明确拒绝,没有强行把零权重归一化。
  • 区别记录手算结果、代码核验和未验证的实际推荐效果。

查原始资料

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

  • Determinantal Point Processes for Machine Learning (新标签页)↗Alex Kulesza / Ben Taskar 原作者 · 英文

    集合归一化、L/K区别以及固定基数条件分布。

    2026-10-18 实际阅读范围:第2.2节印刷页134–136的L-ensemble与式(2.13)/(2.15),第5.1节印刷页204–205与式(5.1);实际打开PDF正文并查看印刷页136、205的渲染图。 阅读边界:未通读166页;不借用该书图像检索实验作为本原型效果。

  • Finite DPPs — Definition (新标签页)↗DPPy 项目官方文档 · 英文

    两个概率事件的区别、半正定条件和固定数量的可定义范围。

    2026-10-18 实际阅读范围:已打开并阅读 Inclusion probabilities、Likelihood、Existence、k-DPPs 四段及式(1)–(5),特别核对 k≤rank(L) 与 k-DPP不一般等同普通DPP 的说明。 阅读边界:未执行页面中的 NumPy/SciPy/DPPy 示例。

  • Finite DPPs — Properties (新标签页)↗DPPy 项目官方文档 · 英文

    K=L(I+L)^−1、平均数量trace(K)、Gram几何及二项负相关式。

    2026-10-18 实际阅读范围:已打开并阅读 Relation between correlation and likelihood kernels、Number of points/Expectation、Geometrical insights、Diversity,核对式(6)/(10)及两项包含概率。 阅读边界:只核验所列性质;未执行文档随机采样或声称实际推荐多样性改善。