PR-SD 公式与算法实现讲解
配合论文初稿第 3 节(方法)与第 4 节(实验)阅读的逐式说明
怎样读这份文档。每一段前面都有一个标签,说明这段话的依据:
论文论文初稿中写出的公式或定义,标出所在小节
实现从代码中核对到的事实,标出文件和行号
讲解本文档为帮助理解而补充的推导、算例或解读;不是论文结论,引用到正文前须作者确认并补证据
注意容易误读或表述时需要守住的边界
文中“小玩具图”“算例”中的数字都是为说明而构造的,只有明确注明来源路径的数字来自冻结结果或论文表格。
0. 全局图景与符号速查
论文 3.1PR-SD 分两个阶段(phase)。第一阶段在不完整图 上按 PageRank 得分选出 个传感器;第二阶段在一次传播发生后,用感染节点集合、已感染传感器的首次感染时间戳与信息发送者、经验跳数频率 和 给候选源打分,取最高分者作为估计源。
实验里还有两件事不属于 PR-SD 本身,但离不开它们:在完整图 上生成真实传播与传感器记录;在 上为每个传感器集合标定 。下图把四件事和它们用到的图分开画出。
pr_sd/runner.py:843–906。符号速查
| 符号 | 含义 | 取值或规则(主实验) |
|---|---|---|
| 完整无向传播图;“完整”指未做实验删边 | 七个真实网络,见论文表 1 | |
| 节点数 | Football 115 … LastFM 7,624 | |
| 可获得的结构不完整图 | ||
| 删边率 | 主比较 0.20;4.5 节 0.10–0.40 | |
| 传感器比例与传感器数量 | , | |
| 传感器集合;PageRank 前 名 | ||
| 节点 的转发概率 | ,每个 组合内固定 | |
| 目标传播规模 | 观测 10%、 标定 40%、MC 部署 30% | |
| 停止时曾被感染的累计节点集合 | ||
| 已感染传感器 | ||
| 传感器 的首次感染时间戳与信息发送者 | ||
| 最早感染的传感器及其记录的发送者 | 时间戳并列取最小节点 ID | |
| PageRank 向量、行随机转移矩阵、阻尼因子 | ||
| 已感染传感器的最小非零传播跳数等于 的模拟占比 | 800 次标定,评分只用 | |
| 观测与模拟传感器时间向量的欧氏距离 | 未感染位置记 | |
| 真实源、估计源 | ||
| 每个条件纳入比较的试验数 | 部署对照 300;完整方法比较取前 100 次 | |
| MC 贪心每轮的模拟次数 | 800 |
术语与符号以 writing_memos/03_术语符号与规范表述库.md 为准。该术语表 5.4 节把时间距离写作 ,当前双语稿已统一写作 ,本文档跟随双语稿。
1. 图、传播与观测(论文 3.1)
1.1 完整图与不完整图
论文 3.1可获得的结构不完整图为
逐项含义。 保留全部 个节点,只少了边。 是向下取整,所以删去的边数是整数 。删边以后可能出现零度节点(孤立点),它们仍在 中,这正是算法 1 要为零度节点单独规定转移概率的原因。
讲解算例:Jazz 有 条边, 时 ,删去 548 条,剩 2,194 条。冻结结果 results/active/pr_sd_protocol_v4/v4_exp02_supporting_structural_controls/results/jazz__pdnd__delta0p20__seed111__rho0p05__pr_sd/result.json 中 incomplete_edge_count 恰为 2194。
两种删边策略
论文 4.1RDS 对边均匀抽样;PDND 以完整图中的端点度和为抽样权重:
两者都无放回,正好删去 条。
实现noise/edge_removal.py:14(RDS)与 :43(PDND)。删边数用 int(noise_rate * |E|),对正数等价于向下取整。抽样用 np.random.choice(..., replace=False),PDND 额外传入按 归一化的概率 p。权重用的是 M.degree,即删边前完整图的度,与论文“删边前完整图中的端点度和”一致。随机数种子在 pr_sd/runner.py:320–321 由图随机种子设定。
讲解为什么论文说 RDS 移除任一节点度数的比例约为 。RDS 下每条边被删的概率都是 ,其中 。节点 有 条关联边,被删条数的期望为
与 的度大小无关。PDND 则让关联高度节点的边权重更大,于是高度节点被删得更多(论文 4.5 节实测为 RDS 的 1.22–1.55 倍)。
讲解np.random.choice 带权无放回抽样是逐次抽取、每次在剩余边上重新归一化的过程,因此某条边最终被删的概率并不严格正比于 ;“以 为抽样权重”的说法准确,但不宜写成“删除概率正比于 ”。
1.2 传感器数量与预算
论文 3.1, 为向上取整。
| 网络 | ||||
|---|---|---|---|---|
| Football | 115 | |||
| Jazz | 198 | |||
| LastFM | 7,624 |
讲解这些数可以和正文对上:4.4 节“5% 预算下六个传感器”(Football)、图 4“每种方法部署 10 个传感器”(Jazz, )。
实现pr_sd/runner.py:1207–1211:math.ceil(node_count * ratio),并取三个预算中的最大值 sensor_count_max 只部署一次(见 2.5 节前缀)。
1.3 异质 SI 传播
论文 3.1、4.1易感—感染(SI)模型中节点感染后保持感染。感染节点 以其转发概率 向邻居传播; 独立抽自 ,在每个 组合内跨方法、预算和试验固定。
实现真实传播在 core/parallel.py:285–323。离散时间的规则可以写成:
- 源 在时刻 0 被感染;活跃列表 ,计数 1。
- for 步 do
- 按 的顺序取每个感染节点 ,对每个邻居 :若 未感染且均匀随机数 ,则感染 ,记 ,发送者 ,跳数 ,并放入新感染列表。
- 一旦累计感染数达到 ,立即跳出全部循环。
- 本步结束后把新感染节点并入 (SI:老节点保留,下一步继续尝试)。
- end for
三个记录量的区别。 是时间步; 是使 首次感染的那个邻居; 是 在本次传播树中的深度(从源数起经过几条感染边)。,因为传播树里的路径不一定是最短路。
讲解一条边上的传播延迟服从几何分布。设 在时刻 被感染,邻居 只有 一个感染邻居。 从下一步起每步独立地以 成功,所以
若 有多个感染邻居, 取各条边“先到者”。 越小,延迟越长、越分散。这有助于理解两件事:其一,时间戳携带了到源距离的信息,但带有较大随机性;其二,MLE-P、DISGE 假设所有边的延迟服从同一均值和方差(6.3 节),与“每个节点 不同”的传播设置不完全吻合。后一点只是理解模型假设差异的角度,论文没有据此作结论,也没有做检验。
IC 与 SIR(论文 4.7)
实现core/parallel.py:42–61 的 _advance_frontier 决定下一步谁仍在传播:
- SI:所有感染节点保留在 中,每步都重试;
- IC:只有本步新感染的节点进入下一步,每个节点对每个邻居只尝试一次,成功概率 ;
- SIR:每个仍在传播的节点在本步尝试后以 0.05 的概率恢复,恢复后停止转发,但仍算在 中。
注意在 IC、SIR 实验中,真实传播、 标定与 MC 部署模拟换成对应模型,但第二阶段的逐候选模拟始终是 SI(core/parallel.py:389, 431 直接 q_cur.extend(q_nxt))。论文 4.7 节已写明“逐候选源模拟保持 SI”。
1.4 停止规则与感染集合
论文 3.1传播在累计感染节点数达到 或达到时间步上限时停止, 为截至停止时曾被感染的累计节点集合。
实现core/config.py:46–59:
return min(node_count, max(1, math.ceil(infection_ratio * node_count)))
代码注释说明:原作者代码的条件是“感染数 比例 ”,由于感染数是整数,二者等价于向上取整。Football 在 时目标为 个节点;LastFM 为 个。
讲解因为达到目标就在步内立即停止,最后一步只有一部分节点有机会尝试传播。停止时 ,除非先撞到 10,000 步上限(例如源所在连通分量太小)。
1.5 传感器记录、最早传感器与输出
论文 3.1已感染传感器为 。每个 记录首次感染时间戳 与发送者 。最早感染的传感器记为 ,时间戳并列时选择整数节点标识最小者。 时返回 ; 时不产生源估计。
实现core/parallel.py:331–339 按升序遍历传感器 ID,只在 infect_time < min_time(严格小于)时更新,所以同一时刻最先遇到的就是最小 ID。
讲解两个由定义直接推出的事实,后面会反复用到。
(a) 一定是已感染的非传感器节点。理由:真实源不是传感器,所以 ,它有发送者;发送者的感染时间严格早于 ;如果发送者也是传感器,它就比“最早感染的传感器”更早,矛盾。
(b) 当且仅当 。深度为 1 就是被源直接感染,发送者即源;反之亦然。所以论文表 4 的“一跳率”恰好等于“真实源就是最早感染传感器所记录的发送者”的试验比例。4.5 节会看到,这个候选在评分中拿到更大的权重 。
2. 第一阶段:PageRank 引导的传感器部署(论文 3.2)
2.1 PageRank 方程
论文 3.2令 为 PageRank 向量, 为 上的行随机转移矩阵, 为阻尼因子, 为全 1 列向量。零度节点对应行设为均匀分布。
转移矩阵 是什么
设 为 的邻接矩阵, 为节点 在 中的度。则
每一行加起来都是 1,所以叫“行随机”。第 行描述随机游走者站在 时下一步走到各节点的概率:有邻居就等概率挑一个邻居;没有邻居就等概率跳到任意节点。
按分量写开
第一项:每个邻居 把自己的得分平均分给 个邻居, 拿其中一份。邻居越重要、邻居的邻居越少, 分到越多。第二项:孤立节点的得分均匀撒给所有节点。第三项:均匀重启,保证每个节点至少有 。论文所说“将邻居得分按其度数归一化后递归传递”就是第一项。
讲解为什么 会自动保持。对上式两边求和,由 行和为 1 得 ,于是
从均匀向量 出发,每次迭代后仍是概率分布,所以算法 1 不需要再归一化。
解存在且唯一
讲解利用 ,可以把方程写成 ,其中 每个元素都为正。由 Perron–Frobenius 定理,正的随机矩阵有唯一的平稳分布,因此 PageRank 向量存在且唯一。等价地, 可逆,有闭式解
2.2 PageRank 的三种读法
读法一:带重启的随机游走。论文 3.2 是这样一个游走者的长期停留比例:每步以 沿边走,以 跳回一个均匀随机节点。
读法二:不同长度游走的加权和。讲解上面的级数里, 是“从均匀随机节点出发走 步后所在位置”的分布,权重 是几何分布。所以 PageRank 是“随机长度游走的终点分布”,平均游走长度为
这就是论文所说 PageRank“能够利用直接邻居之外的连接关系”的具体含义: 时平均看大约 5–6 步远的结构。
读法三:两个极端。讲解
- 很小:保留到一阶,,而 。排序主要由“邻居度数倒数之和”决定,不是由 自己的度决定:一个节点如果有很多“只连着它”的低度邻居,得分高。
- ,图连通:无向图上随机游走的平稳分布是 。验证:。所以 PageRank 排序趋近于度排序。
论文 4.8 节引用 Gleich 的文献(引用键 gleich2015pagerank)写“ 时 PageRank 趋向与度成比例的平稳分布”,并实测 PR-SD 与 Degree-SD 传感器集合的平均 Jaccard 相似度从 的 0.507 升到 的 0.673,与读法三的方向一致。
注意这三种读法解释的是 PageRank 排的是什么,不能推出 PageRank 选点对源检测最优;论文也没有这样写。实验只说明在所测条件下的准确率与部署时间。
2.3 算法 1 逐行对照
- ;
- 由 构造行随机矩阵 ;零度节点对应行设为
- for do
- if then ;break
- if 未收敛 then 抛出收敛错误
- 按 降序、节点 ID 升序排列; 前 个
实现PR-SD 调用 nx.pagerank(graph, alpha=0.85)(pr_sd/runner.py:337–342)。本机 exp_env 中 NetworkX 为 3.2.1,实际执行的是 _pagerank_scipy,核心循环为:
A = Q @ A # Q = diag(1/d_i),A 变为行随机(零度行仍为 0)
x = np.repeat(1.0 / N, N) # 算法 1 第 3 行
p = np.repeat(1.0 / N, N) # 均匀重启向量
dangling_weights = p # 零度节点的质量也均匀分配
for _ in range(max_iter): # max_iter = 100 → T_max
xlast = x
x = alpha * (x @ A + sum(x[is_dangling]) * dangling_weights) + (1 - alpha) * p
err = np.absolute(x - xlast).sum() # L1 范数
if err < N * tol: # tol = 1e-6 → ε
return dict(zip(nodelist, map(float, x)))
raise nx.PowerIterationFailedConvergence(max_iter)
来源:D:\Anaconda\envs\exp_env 中 networkx/algorithms/link_analysis/pagerank_alg.py 的 _pagerank_scipy(本次用 inspect.getsource 读出)。
| 算法 1 | NetworkX 3.2.1 | 说明 |
|---|---|---|
| 第 2 行 | Q @ A 加 sum(x[is_dangling]) * dangling_weights | 代码不显式填满零度行,而是在每次迭代时把零度节点的质量均匀加回,数学上与“零度行设为均匀”相同 |
| 第 5 行 | x @ A | 行向量右乘行随机矩阵 列向量左乘其转置 |
| 第 6 行 | N * tol,tol=1e-6 | 等价于“平均每个节点的变化量小于 ” |
max_iter=100 | 默认值,项目未改 | |
| 第 12 行排序 | sorted(scores, key=lambda v: (-scores[v], v)) | 在 runner.py:341,不是 NetworkX 的一部分 |
实现sensor/strategies.py:52 也有一个 deploy_pagerank,逻辑相同,但正式实验走的是 pr_sd/runner.py 的 _pagerank_sequence。
2.4 收敛与迭代次数
讲解设 是精确解,。两式相减,重启项抵消:
每列和为 1、元素非负,对任意向量 有 。所以每迭代一次误差至少缩小到 倍。相邻两次迭代之差同理:
(两个概率向量之差的 范数不超过 2。)令 ,得到一个保守的迭代次数上界
| 情形 | 上界() | 上界() | |
|---|---|---|---|
| Football, | 约 61 次 | 约 191 次 | |
| LastFM, | 约 35 次 | 约 109 次 |
这只是最坏情况上界,真实迭代通常少得多。它说明了两点: 时 100 次上限很宽裕; 越接近 1 收敛越慢,这是 的计算代价。按代码逻辑,若某次运行 100 次仍未收敛会抛出异常并中止,论文 4.8 节已报告 的结果,说明那些运行没有触发该异常。
2.5 排序、并列与前缀嵌套
论文 3.2节点按 降序排列,分数完全相同时按整数节点标识升序;。固定 、参数与并列规则后,。
嵌套性的证明只有一句:两个集合都是同一个排序的前缀,短前缀必然包含在长前缀里。
实现
runner.py:553–564:每个 组合只算一次 PageRank,保存前sensor_count_max名的顺序和一次计时full_ranking_seconds。runner.py:638–646:_deployment_prefix对每个预算取前 个;PageRank 类方法各预算都报同一个计时,MC 方法报“选到第 个时的累计耗时”。- Degree-SD、Eigenvector-SD、Betweenness-SD 用同样的
(-score, id)排序;Random-SD 用由组合名哈希出种子的固定随机排列(runner.py:376–386)。
注意前缀嵌套不是 PR-SD 独有:MC 贪心的选点顺序同样可以截取前缀(论文 4.2 节 SDSA 就是“按选点顺序截取传感器”)。复用排序只省掉重复排名;不同传感器集合仍要各自标定 、各自做源检测试验。
2.6 小玩具图算例
讲解七个节点:两个三角形 、,由路径 连起来。下表是用 nx.pagerank(G, alpha=0.85) 实际算出的数值(构造示例,非实验数据)。
| 节点 | 度 | PageRank() | 介数(归一化) | |
|---|---|---|---|---|
| 0, 4 | 3 | 1.500 | 0.1828 | 0.533 |
| 1, 2, 5, 6 | 2 | 0.833 | 0.1273 | 0 |
| 3 | 2 | 0.667 | 0.1250 | 0.600 |
- PageRank 顺序为 。1、2、5、6 分数相同,按 ID 升序。取 ,,。
- 节点 3 与 1、2、5、6 的度都是 2,但它的两个邻居度都是 3,各自只分给它 1/3,所以 PageRank 最低。这就是 2.2 节“邻居度数倒数之和”的一阶效应。
- 介数中心性却把 3 排第一。PageRank 与介数关注的结构不同,这有助于理解论文表 3、4 中二者准确率接近而传感器集合并不相同。
- 改变 : 时 0 号为 0.1700、3 号为 0.1281; 时 0 号为 0.1872、3 号为 0.1250,逐渐接近度比例 与 ,与 2.2 节的极限一致。
2.7 计算量:PageRank 与 MC 贪心
讲解(推导与 writing_memos/01_方法与实现核对.md 第 6 节一致)
- PageRank 部署:每次迭代处理全部节点和边,;迭代 次;再对 个节点排序 。合计 ,与预算 基本无关。
- MC 贪心部署:选 个传感器要 轮,每轮 次传播模拟,每次感染 个节点。模拟总次数为 。
| 网络(15% 预算) | MC 贪心的传播模拟次数 | 每次感染节点数 | |
|---|---|---|---|
| Football | 18 | 14,400 | 35 |
| LastFM | 1,144 | 915,200 | 2,288 |
论文表 2 中 SDSA 在 LastFM 上部署需 1,480 秒,PR-SD 为 0.0401 秒;数量级差异主要来自上表的模拟次数随 线性增长,而 PageRank 只做一次。
注意“无需传播模拟”只描述第一阶段。第二阶段的逐候选评分、实验中的 标定都要做传播模拟(论文表 7 中这两部分耗时与 SDSA 接近)。部署时间倍率不能写成端到端加速。
3. 对照:蒙特卡洛贪心部署(SDSA-MC)
这一节帮助理解论文中被比较的 SDSA 部署,以及为什么它慢。
实现core/parallel.py:484–649(部署对照用)与 baselines_ext/sdsa_author.py:82–123(完整方法比较中调用作者代码)。两者做的是同一件事。第 轮(已选 ):
其中第 次模拟的源从 均匀抽取,在 上以 传播到 30% 规模, 是 在该次传播树中的深度。代码参数 hops=1,所以 数的是“ 被源直接感染”的次数; 是这一事件概率的蒙特卡洛估计。并列时 np.argmax 取最小 ID。
实现一个容易忽略的细节:计数本身不扣除“已被已选传感器覆盖”的模拟;各轮之间的差别只来自两处,一是已选传感器不再作为源被抽到,二是每轮用新的随机流(core/parallel.py:564–606,作者代码 observer 也是如此)。
SDSA 原文在 Sec. IV-A 用 、 等符号表述期望检测概率目标;按术语表 4.3 节的规定,这些原符号只在介绍 SDSA 来源时使用,不移植到 PR-SD。
4. 第二阶段:源检测(论文 3.3)
4.1 贝叶斯公式与最大后验估计
论文 3.3令 。对候选 :
为什么可以去掉分母。分母 是对所有候选求和的结果,对每个候选都一样;比较大小时乘以同一个正数不改变谁最大。
两项分别是什么。先验 :还没看到这次传播的记录时, 有多大可能是源。似然 :如果源真是 ,看到眼前这组感染集合和传感器记录的可能性有多大。
讲解PR-SD 的评分怎样对应这两项。论文写的是“借鉴这一估计思路,采用经验评分规则”。对照关系是:
| MAP 中的项 | PR-SD 评分中的对应 | 区别 |
|---|---|---|
| 候选限定在 | 相同 | — |
| 先验 | 跳数权重 或 | 只分两档: 取 ,其他候选取 ; 可小于 1,不是归一化概率 |
| 似然 | 时间匹配分 | 用一次模拟的时间向量距离代替似然,不是概率密度 |
所以只能说“借鉴 MAP 思路的经验评分”,不能说 PR-SD 计算了后验概率。
4.2 经验跳数频率
论文 3.3为每个传感器集合在完整图 上离线标定 。源从 均匀抽取,按指定传播规模和停止规则独立模拟。传播跳数指传感器在模拟传播树中的深度。 表示已感染传感器的最小非零传播跳数等于 的模拟占比;没有传感器被感染的模拟仍保留在分母中,因此 可能小于 1。
写成公式(本文档的展开写法,论文用文字定义):
若第 次模拟没有感染任何传感器,括号里的最小值不存在,该次不计入任何 的分子,但仍在分母 中。
实现core/parallel.py:191–250(_worker_hops_test_batch)与 :709–763:
best_hop = N + 999
for obs_id in obs_indices: # 所有传感器
hop = total_hop_arr[obs_id]
if hop != 0 and hop < best_hop: # 只看已感染者(深度非零)
best_hop = hop
if best_hop < N:
detect_hops[best_hop] += 1.0
...
detect_hops /= nr # 分母为全部 800 次
调用处 pr_sd/runner.py:872–882 传入的是 complete_graph,确认标定在 上进行。
实现一个真实例子。上文 1.1 节那个 Jazz 结果(PDND,,,10 个传感器)记录的 hop_prior_distribution 前几项为
这里 ,说明 800 次 40% 规模的模拟每次都至少感染了一个传感器。网络大、预算小、 小时和会小于 1。
注意三个“跳数量”不要混:(1) 中的 是标定模拟中所有已感染传感器的最小传播树深度,不一定是时间最早那个传感器的深度;(2)表 4 的 是真实传播中最早感染传感器的传播树深度;(3) 是完整图上真实源与估计源之间的最短路跳数。三者在代码中分别是 detect_hops、first_infected_sensor_hops、hop_error。
4.3 检测流程逐步对照
core/parallel.py:325–444- 收集 。若为空:不产生估计,本次试验记为错误。(:331–357)
- 复制一份 ,对每个 加入边 。(:326, :337–339)
- 确定 与 ;观测向量 按传感器 ID 升序排列。(:359–361)
- 候选 :在补边后的图上从它出发模拟一次 SI,得到 ,评分用 。(:364–397)
- for 每个其他候选 ,按 ID 升序 do (:400–442)
- 若 与 之间无边,临时加入 ;
- 从 模拟一次 SI(同样的 、、10,000 步上限),得到 ,评分用 ;
- 移除临时边。
- 得分最高者;并列取最小 ID。(:444,
np.argmax返回第一个最大值)
讲解补边与临时边各起什么作用。
- 补发送者边:传感器知道“谁把信息传给了我”,这条边在真实传播中一定存在。若它恰好在删边时被删掉,补回来能修复一部分缺边。补边只限已感染传感器的发送者边,不会暴露其他缺失边。每次试验用独立副本,避免上次补的边留给下次(
baselines_ext/sdsa_author.py的说明指出,作者原脚本对所有试验复用同一张图,补过的边会累积)。 - 临时边 :可以理解为对“”这一两跳假设的检验,即假定 是源、经 两跳到达最早传感器。这与其他候选使用 (两跳)权重相呼应。它只在评估该候选时存在,不代表断言这条边属于 。这一做法沿用自 SDSA 公开代码(
SDSA-main/main_source_detection_RDS_code_in_paper.py的find_source)。
4.4 时间戳距离与评分公式
论文 3.3按相同传感器顺序,观测向量为 ,候选 的模拟向量为 ,未在模拟中感染的位置记为 。
实现代码写的是另一种形式,二者恒等:
prob[suspect] = prior1 * (1.0 - euc_dis / (euc_dis + 1.0)) # core/parallel.py:397
prob[cand] = prior2 * (1.0 - euc_dis / (euc_dis + 1.0)) # core/parallel.py:439
论文采用右边更简洁的写法。prior1、prior2 就是 hop_prior[1]、hop_prior[2],即 。
约定的效果。讲解某传感器在观测中于时刻 被感染、在候选模拟中却没被感染时,该分量的差是 ,平方后为 。观测时间越晚,惩罚越重。由于模拟和观测都在 处停止,“模拟中没感染到”通常意味着候选离该传感器太远,或在 上到不了它。
4.5 评分规则的性质
讲解以下都由公式直接推出,便于回答审稿问题。
- 取值范围。,所以 ;(时间完全吻合)时取 1,距离增大单调下降。于是 ,其他候选 ( 为 0 时得分为 0)。
- 其他候选战胜 的条件。
当 时,其他候选的时间吻合程度要比 好出 倍以上才能胜出。
- 必然胜出的充分条件。其他候选得分最高为 ,所以只要
就一定是估计源。用上面 Jazz 例子,,即 时其他候选无论如何都赢不了。
- 与“一跳率”的关系。结合 1.5 节事实(b):真实源被 加权当且仅当 。一跳率高,意味着真实源更常落在被加权的那个候选上。Jazz 例子中该条件的一跳率为 0.798,可观测条件下的准确率为 0.760,二者接近。这为论文表 4 中“一跳率与准确率同为 PR-SD 最优”提供了一个机制层面的理解;论文没有把它写成因果结论,正文若要使用需另行分析。
- 随机性。每个候选只模拟一次,所以评分本身是随机的;同一候选重算可能得分不同。代码中候选模拟与真实传播共用同一个试验随机流(
rng = np.random.RandomState(seed),core/parallel.py:283),因此给定试验种子可完全重放。
4.6 完整算例
讲解构造的数字,只为演示计算。设 (已按 ID 升序),观测时间 ;,(借用 Jazz 实例的值)。
| 候选 | 模拟时间 | 差的平方 | 权重 | 得分 | |
|---|---|---|---|---|---|
| (2, 3, 5) | 1 + 1 + 1 | 1.732 | 0.7788 / 2.732 = 0.2851 | ||
| (3, 5, 6) | 0 + 1 + 0 | 1.000 | 0.2025 / 2.000 = 0.1013 | ||
| (2, 4, −1) | 1 + 0 + 49 | 7.071 | 0.2025 / 8.071 = 0.0251 |
。注意 的时间吻合比 更好,但权重相差 3.85 倍,仍然落后;而 的 ,满足 4.5 节第 3 条,结果在算 之前就已确定。 的第三个传感器在模拟中未感染,单这一项就贡献了 49。
5. 实验中的评价公式(论文第 4 节)
5.1 源检测准确率 Acc
论文 4.1
未检测到传播或没有返回源估计的试验均计为错误。
实现core/parallel.py:830:accuracy = correct / max(1, attempted),分母是全部尝试的试验,与公式一致。结果文件字段为 metrics.acc。
讲解由定义可分解为
Jazz 例子:,与 acc 一致。observable_acc 只在结果文件里,论文没有报告。这个分解说明部署可以从两条路影响准确率:让传播更常被看到(DetRate),或让看到时更容易判对。
5.2 传播检测率 DetRate
论文 4.3.
实现runner.py:971:observed_trials / attempted_trials,字段 metrics.det_rate。
5.3 首个感染传感器的传播跳数、一跳率与估计误差
论文 4.3平均 、一跳率( 的比例)、平均 都只在至少一个传感器被感染的试验上计算。
实现runner.py:817–840(_detection_distance_metrics)。 取真实传播树中 的深度(parallel.py:472); 用 nx.shortest_path 在完整图上计算(:453–459),二者不连通时记为 None 并不计入均值。
讲解由于源不是传感器,,所以“”就是“”。由 1.5 节事实(b),一跳率也就是“真实源 ”的比例。
5.4 真实源抽样范围
论文 4.3。
实现runner.py:1245–1269:先求所有比较方法传感器集合的并集,再用 RandomState(source_seed).choice(source_pool, size=300, replace=True) 有放回抽 300 个真实源,所有方法共用这一序列。
讲解为什么要排除并集:如果真实源恰好是某方法的传感器,该方法就无法在 中找到它,比较就不公平。代价是预算越大、方法越多,可抽源范围越小,且被排除的往往是高中心性节点(论文 4.6 节:10% 起度排名前十分位节点全部被排除)。所以准确率只在同一张表内可比。
论文 4.2完整方法比较中,再剔除源属于 SDSA 或 GRSL 自身传感器集合的试验: 次中保留 次,比例 。
5.5 汇总方式:先组合内平均,再组合间等权
论文 4.2、4.3记 为预算集合, 为完整图—不完整图组合集合:
每个组合权重相同,与网络大小无关。论文的“总体 62.40%”就是对 54 个组合这样算的。(上式是本文档对论文文字的形式化。)
5.6 部署时间的几何均值与倍率
论文 4.1部署耗时及耗时比采用图组合等权的几何均值汇总。
实现scripts/analyze_ext_e1.py:146–148:np.exp(np.mean(np.log(values)))。
讲解为什么用几何均值:各网络耗时跨越多个数量级(0.0009 秒到 1,480 秒),算术均值会被最大的网络主导;几何均值等价于在对数尺度上取平均。它还有一个好性质:在同一组组合上,
所以“几何均值之比”就是“逐组合倍率的几何均值”。核对:表 2 中 ,与正文一致;,正文写 27.5,差异来自用未四舍五入的值计算。
5.7 Wilcoxon 符号秩检验(论文 4.5,表 6)
论文 4.5双侧配对 Wilcoxon 检验在各策略 27 个匹配组合上、对三个预算取平均后进行, 值未经多重比较校正。
讲解对每个组合 求差 ,去掉 的组合,把其余 从小到大排秩 ,统计量为
若两方法无差异,正负差应大致对称, 接近其期望 ( 为非零差个数);偏离越大 越小。它只用差的符号和相对大小,不要求差服从正态分布,适合 27 个组合这样的小样本。
实现scripts/analyze_pr_sd_deletion_strategy_split.py:121:scipy.stats.wilcoxon(ours, theirs, zero_method="wilcox"),默认双侧。
注意相对 Betweenness-SD 的 、 只说明“未检测到统计显著差异”,不能写成两者相同或等价。
5.8 Spearman 相关、Jaccard 相似度与社区覆盖率
论文 4.4、4.8
- Spearman 秩相关:把两组数分别换成秩,再算 Pearson 相关。无并列时有 , 为第 对的秩差。它只看单调关系。论文用 表示传感器比例,所以相关系数应写 以免冲突(术语表 4.5 节)。实现:
scripts/analyze_pr_sd_community_coverage.py:168,scipy.stats.spearmanr。 - Jaccard 相似度:,两个传感器集合完全相同为 1、无交集为 0。实现:
runner.py:733–738。 - 社区覆盖率:在不完整图 上用 Louvain 得到社区划分 ,
实现:
networkx.community.louvain_communities(graph, seed=20260918),graph为不完整图(analyze_pr_sd_community_coverage.py:62–67, 109–110)。
5.9 累计运行时间(论文 4.9,表 7)
论文 4.9累计时间对 170 个条件的部署、 估计与“传播生成与源推断”记录求和。
讲解表 7 有两个不同的倍率:部署一项是 ;合计是 。合计倍率小得多,是因为 标定与源检测两部分两者几乎相同(倍率 1.02)。对应字段在 runner.py:988–995 的 runtime_seconds。
6. 完整方法比较中的基线公式(论文 4.2)
这些方法的原始公式以各自论文为准。下面描述的是本项目代码实际计算的内容,便于理解表 2 的差异。凡涉及原文式号之处,引用前应回到原始 PDF 核对(TODO-VERIFY)。
6.1 SDSA(作者代码)
实现baselines_ext/sdsa_author.py:把作者脚本 SDSA-main/main_source_detection_RDS_code_in_paper.py 中的 SI、observer、hops_test、find_source 原样执行,只替换输入的图、转发概率与传播。
- 部署:第 3 节的 MC 贪心,每轮 800 次模拟分到 16 个工作进程;计时从工作进程启动后开始(与论文 4.2 节“部署计时”一致)。
- 检测:评分形式与 PR-SD 第二阶段相同(
find_source第 344、359 行:hops_test_reserve[1]/[2]乘1 - euc_dis/(euc_dis+1))。 - 差别:作者代码的候选条件
... and snapshot[i].node_I and ...对未感染状态 也为真,所以候选是全部非传感器节点,不只是感染节点。论文 4.2 节已写明“检测候选为全部非传感器节点”。每次试验使用独立的不完整图副本。
6.2 GRSL:贪心部署(GS)与推断策略(IS)
实现GS:experiments/generate_gs_sensors.py 调用作者 Java 代码 SI.initNet_SIR_Greedy_lowComplexity(固定提交 a7f6a0f)在 上选点。该例程每轮加入一串传感器,可能超出预算,超出部分用由条件名派生的种子随机去掉,使数量等于 。Java 使用 Math.random() 无法设种子,所以每个条件只存一次结果。
IS:baselines_ext/grsl.py,主比较用 variant="code",不补发送者边,候选为全部非传感器节点(与 Agent.md 第 04 节“GRSL 主配置不补发送者边、不限定感染节点候选”一致)。计算步骤:
- 取最早感染的 4 个传感器 (并列按 ID)。
- 时间平移:若这 4 个时间的最小值 ,则 ; 则 ; 则 ;否则不变。
- 对每个节点 与 :(到不了记为 ),以及
- 得分(越小越好):
- , 为未被感染的传感器;若 是某个相邻已用传感器记录的发送者,再乘 0.9 或 0.95。
讲解直观含义:第一项偏好离这些传感器的发送者都近的节点;第二项 在“时间约等于距离”(每步传一跳)时取最小值 1,用来惩罚时间与距离不匹配; 惩罚邻居中有未被感染传感器的节点(如果源在那里,这些传感器本应被感染)。它不做传播模拟,所以推断很快。
6.3 MLE-P(Pinto 等的高斯估计)
实现baselines_ext/mle_p.py。为避免与本文的传播规模 冲突,下面把边延迟的均值、方差写成 (Pinto 原文记号为 )。
设定。已感染传感器按到达时间排序为 (并列按 ID),以 为参照。对候选 ,取 (补入发送者边)上以 为根的 BFS 树 :
是树中两点间路径, 为边数。
讲解估计式从哪里来。假设每条边延迟独立、均值 、方差 ,到达时间是路径上延迟之和,近似为高斯,。对数似然为
展开平方项:。把 视作与候选无关时,第一项和常数项可以丢掉,剩下 ,就是上面的估计式。代码对每个候选按其 BFS 树计算 ,但仍使用该估计式。
讲解协方差为什么是共享路径长度。 与 相关,是因为二者从 出发走过了公共的一段边,公共边的延迟同时出现在两者中。树上的公共长度可以由距离算出:
代码 _score_candidate(第 119–139 行)正是这样算的:先由最近公共祖先深度得树上距离,再取 0.5*(first[:,None]+first[None,:]-tree_dist)。
实现其余细节:
- 延迟标定(
estimate_delay_moments,第 39–81 行):在完整图上做 200 次、40% 规模的 SI 传播,源在全部节点中均匀抽取;每次感染事件记录“接收者时间 − 发送者时间”,取均值和样本方差(ddof=1)。 - 至少需要两个已感染传感器,否则只有一个到达时间、无法比较,不返回估计(Acc 中记错)。
- 候选为已感染的非传感器节点;协方差加 防止奇异;有传感器在 中到达不了的候选不参与比较。
6.4 DISGE(方向诱导树)
实现baselines_ext/mle_p.py:142–155。与 MLE-P 用同样的高斯评分,只是把 BFS 树换成“方向诱导”的有向搜索:进入一个已感染传感器 的边,只保留来自其记录发送者 的那条;其他边双向可走。于是从候选出发的搜索树中,每个已感染传感器都必须经由它真实的发送者到达。若某候选在这一约束下到不了全部已感染传感器,就不对它评分(主配置 disge 不回退到普通 BFS)。
讲解方向信息让树的形状更贴近真实传播路径,从而 更接近真实传播距离。论文表 2 中,DISGE 两种部署配置的总体平均准确率在三个预算下均高于对应的 MLE-P 配置;论文没有把这一差异归因于方向信息,这里的解释只是一种理解角度。
7. 随机种子与可复现性
实现pr_sd/runner.py:75–78, 123–125 与 core/parallel.py:78–89。
| 用途 | 种子 | 说明 |
|---|---|---|
| 与删边 | 图随机种子 | random.seed(g)、np.random.seed(g) 后依次抽 和删边 |
| 预算种子 | 使不同预算的随机流不同 | |
| MC 部署 | 每轮再按轮次派生 | |
| 真实源序列 | 所有方法共用 | |
| 标定 | ||
| 源检测试验 | 每次试验一个子种子 |
逻辑分片。800 次模拟被固定分成 16 个“逻辑分片”,每片的种子由 np.random.SeedSequence([基础种子, 命名空间, 轮次]) 生成(命名空间 1=MC 部署,2=,3=试验)。分片数固定为 16,与机器实际进程数无关,所以换一台核数不同的电脑结果仍一样;MC 部署中断后也能从任意轮精确续跑。
8. 公式—代码对照总表
| 论文位置 | 公式或步骤 | 代码位置 | 结果文件字段 |
|---|---|---|---|
| 3.1 | noise/edge_removal.py:14, 43 | parameters.incomplete_edge_count | |
| 3.1 | pr_sd/runner.py:1207 | parameters.sensor_count | |
| 3.1、4.1 | core/graph_loader.py:40–49 | 图状态 node_transmission_probabilities | |
| 3.1 | 停止 | core/config.py:46 | — |
| 3.1 | SI 传播、 | core/parallel.py:285–323 | — |
| 3.2 | PageRank、算法 1 | runner.py:337–342;NetworkX 3.2.1 | parameters.pagerank_alpha |
| 3.2 | 前缀 | runner.py:638–646 | sensor_nodes |
| 3.3 | core/parallel.py:191–250, 709–763 | hop_prior_distribution(前 10 项) | |
| 3.3 | 候选、补边、、score | core/parallel.py:325–444 | trial_records[].estimated_source |
| 4.1 | Acc | core/parallel.py:830 | metrics.acc |
| 4.3 | DetRate | runner.py:971 | metrics.det_rate |
| 4.3 | 平均 、一跳率 | runner.py:817–840 | metrics.mean_detect_dist、one_hop_rate |
| 4.3 | 平均 | core/parallel.py:452–461 | metrics.mean_hop_error |
| 4.3 | runner.py:1245–1269 | shared_inputs/*.json | |
| 4.1、4.2 | 几何均值 | scripts/analyze_ext_e1.py:146 | — |
| 4.5 | Wilcoxon | scripts/analyze_pr_sd_deletion_strategy_split.py:121 | — |
| 4.4 | 社区覆盖、Spearman | scripts/analyze_pr_sd_community_coverage.py | — |
| 4.9 | 累计时间 | runner.py:988–995 | runtime_seconds |
9. 核对结论与需作者留意之处
9.1 本次逐项核对后一致的内容
- 算法 1 的初始化、迭代式、零度节点处理、 收敛判据 、迭代上限与异常,均与 NetworkX 3.2.1
_pagerank_scipy一致;排序并列规则与runner.py:341一致。 - 评分式 与代码 恒等; 取自
hop_prior[1]、[2];未感染位置记 与代码sim_time初始值一致。 - 的定义(最小非零深度、分母含未检测模拟、在 上标定、源取自 )与代码一致。
- 候选为 、发送者边补入逐试验独立副本、临时边仅对非 候选加入并在评估后移除、最高分并列取最小 ID,均与代码一致。
- Acc 分母为全部尝试试验、DetRate、条件化的跳数指标、 的构造与代码一致。
- 删边数取整、传感器数取整、停止阈值取整与代码一致。
9.2 不是错误,但值得作者知道的细节
- PDND 的带权无放回抽样是逐次重新归一化,单条边最终被删的概率并不严格正比于权重(1.1 节)。论文写“以……为固定抽样权重”是准确的。
- 术语表
03_术语符号与规范表述库.md5.4 节仍把时间距离写作 ,双语稿已改为 。是否同步术语表由作者决定。 01_方法与实现核对.md第 6 节写“无观测的 hop error 在实现中赋值为 ”,这是旧 schema;v3 及以后为None(core/parallel.py:349–352),与 Agent.md 第 04 节一致。- 4.5 节第 3、4 条( 必胜条件、一跳率与 加权的关系)是由公式推出的解读,论文中没有;如要写入讨论,需先由作者确认并用结果数据检验。
10. 自测问题
以下问题用于检验自己能否不看稿件讲清公式。请先自己写答案,再回到对应小节核对。
- Jazz 网络在 、 时, 和 各是多少?(1.1、1.2 节)
- 为什么 PageRank 迭代不需要每步重新归一化?用一行式子说明。(2.1 节)
- 从 0.85 调到 0.5,PageRank 平均“看”多远的结构?这和论文 4.8 节 Jaccard 结果的变化方向是否一致?(2.2 节)
- 审稿人问:“PR-SD 声称无需传播模拟,但表 7 中 PR-SD 也有 3,280 秒的检测耗时,怎么解释?”(2.7 节、5.9 节)
- 的三个关键词:在哪个图上标定?“最小”是对什么取最小?为什么和可以小于 1?(4.2 节)
- 为什么其他候选要临时连到 ,而 自己不需要?(4.3 节)
- 若 , 的 小于多少时,结果一定是它?(4.5 节)
- 为什么表 4 的一跳率等于“真实源恰为最早感染传感器记录的发送者”的比例?(1.5 节、5.3 节)
- 为什么部署时间用几何均值而不用算术均值?(5.6 节)
- Betweenness-SD 对比的 应怎样表述?(5.7 节)
文档源文件:formula_guide/src/guide_source.html;生成脚本:formula_guide/src/build_guide.mjs(KaTeX 预渲染并内嵌字体,再用本机 Edge 导出 PDF)。本文档不属于论文正文,不随稿件自动更新。