diff --git a/CLAUDE.md b/CLAUDE.md index c25469b..9d3b8a2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -372,7 +372,7 @@ python -m unittest discover -s tests # 必须全绿 **WGMM 纯算法层(`wgmm_monitor/wgmm/`)** - `scheduler.decide_next_frequency()`: WGMM 调频决策主函数 - `scheduler.scan_future_peak()`: 未来 15 天峰值扫描 -- `scoring.calculate_point_score()` / `batch_calculate_scores()`: 单点 / 批量发布概率得分 +- `scoring.calculate_point_score()` / `batch_calculate_scores()`: 单点 / 批量发布概率得分(周期得分 × 条件间隔得分几何均值 − 负向惩罚) - `features.vectorized_time_features_numpy()`: 周期性时间特征提取 - `features.get_raw_time_components()`: 离散时间维度(用于权重学习) - `learning.aggregate_publish_events()`: 把视频粒度时间戳折成 UP 主行为粒度(正向数据用,默认 600 秒阈值) @@ -386,7 +386,7 @@ python -m unittest discover -s tests # 必须全绿 **业务服务层(`wgmm_monitor/services/`)** - `MonitorService.run_monitor()`: 主监控循环(三层检测 + 通知 + 调频) -- `BilibiliService.check_potential_new_parts()`: 第一层 - 分片预检查 +- `BilibiliService.check_potential_new_parts(memory_urls, known_urls)`: 第一层 - 分片预检查;合并 Gist 状态与本地状态确定最高已知分片,避免 Gist 滞后导致重复探测 - `BilibiliService.quick_precheck()`: 第二层 - 快速ID检查 - `BilibiliService.fetch_video_list()` + `get_all_videos_parallel()`: 第三层 - 完整深度检查 - `FrequencyService.adjust_check_frequency()`: 加载历史 → 过滤异常 → 剪枝 → 调用 scheduler @@ -409,7 +409,7 @@ python -m unittest discover -s tests # 必须全绿 ``` MonitorService.run_monitor() ├── sync_urls_from_gist() # 从 GitHub Gist 同步已备份 URL -├── BilibiliService.check_potential_new_parts() # 第一层:分片预检查 +├── BilibiliService.check_potential_new_parts(memory_urls, known_urls) # 第一层:分片预检查(合并本地与Gist状态) ├── BilibiliService.quick_precheck() # 第二层:快速 ID 检查 │ └── 任一层有变化 → 触发完整检查 ├── BilibiliService.fetch_video_list() # 第三层:完整深度扫描 @@ -434,7 +434,7 @@ FrequencyService.adjust_check_frequency() ├── learning.sync_discovered_periods() # 稳定 custom_N 索引 ├── learning.learn_dimension_weights() # 学习维度权重(含 custom_N) ├── learning.learn_adaptive_sigmas() # 学习时间容忍度(含 custom_N) - ├── scoring.calculate_point_score() # 当前时刻发布概率 + ├── scoring.calculate_point_score() # 当前时刻发布概率(周期得分与条件间隔得分几何均值) ├── scheduler.scan_future_peak() # 扫描未来 15 天找峰值 └── 映射得分 → 检查间隔 → FrequencyDecision ``` diff --git a/docs/code-reference.md b/docs/code-reference.md index 4506605..de10560 100644 --- a/docs/code-reference.md +++ b/docs/code-reference.md @@ -175,7 +175,7 @@ python monitor.py --wgmm-core-only `BilibiliService` 封装 B站相关业务: -- `check_potential_new_parts()`:第一层,多分片预检查。 +- `check_potential_new_parts(memory_urls, known_urls)`:第一层,多分片预检查。合并 `memory_urls`(Gist 状态)与 `known_urls`(本地状态)确定当前最高已知分片编号,再探测下一分片,避免 Gist 滞后时重复探测本地已知分片。 - `quick_precheck()`:第二层,获取最新视频 ID,并同时检查 `memory_urls` 与 `known_urls`。 - `fetch_video_list()`:第三层完整扫描入口。 - `get_video_parts()`:获取单个视频的所有分片 URL。 @@ -253,10 +253,10 @@ python monitor.py --wgmm-core-only ### `wgmm_monitor/wgmm/scoring.py` -- `calculate_point_score()`:计算单个时间点得分。 -- `batch_calculate_scores()`:批量计算扫描窗口得分。 - -得分由正向事件贡献减去负向事件惩罚,并裁剪到 `[0, 1]`。 +- `calculate_point_score()`:计算单个时间点得分;先求周期得分与条件间隔得分的几何均值,再减去负向惩罚,裁剪到 `[0, 1]`。 +- `batch_calculate_scores()`:批量计算扫描窗口得分,逻辑与单点版本相同,用 NumPy 广播实现。 +- `_conditional_interval_scores()`(私有):以加权核密度估计(KDE)对 log 间隔建模,权重 = 历史起始状态与最近发布的周期相似度 × 时间衰减;数据不足时返回全 1。 +- `_cyclic_similarity_to_reference()`(私有):计算历史事件与参考时间点的周期维度相似度,供条件间隔得分加权使用。 ### `wgmm_monitor/wgmm/scheduler.py` diff --git a/docs/wgmm-algorithm.md b/docs/wgmm-algorithm.md index bc290cb..6859a97 100644 --- a/docs/wgmm-algorithm.md +++ b/docs/wgmm-algorithm.md @@ -169,7 +169,9 @@ sigma 越小,匹配越严格;sigma 越大,匹配越宽松。 ## 得分计算 -`calculate_point_score()` 计算一个时间点的得分。 +`calculate_point_score()` 计算一个时间点的得分,最终得分由**周期得分**与**条件间隔得分**共同决定。 + +### 周期得分(pos_score / neg_score) 对每个事件: @@ -181,13 +183,27 @@ dimension_similarity = exp(-distance_sq / (2 * sigma^2)) combined = sum(dimension_weight * dimension_similarity) ``` -正向得分提高检查概率,负向得分抑制检查概率: +### 条件间隔得分(interval_score) + +`_conditional_interval_scores()` 预测「距上次发布已过去多久」的概率。 + +1. 计算历史相邻发布事件的间隔序列。 +2. 用 `_cyclic_similarity_to_reference()` 计算每段间隔的起始状态与最近一次发布的周期相似度。 +3. 权重 = 状态相似度 × 时间衰减(exp(-lambda × 年龄(小时)))。 +4. 以 Silverman 规则自适应带宽,对 log 间隔做加权核密度估计(KDE)。 +5. 将目标时间点距上次发布的 elapsed 代入 KDE,归一化到 `[0, 1]`。 + +### 最终得分合成 ```text -score = clip(pos_score - resistance_coefficient * neg_score, 0, 1) +interval_score = conditional_interval_score(target, pos_events, ...) +combined_pos = sqrt(pos_score * interval_score) # 几何均值,两者均高才得高分 +score = clip(combined_pos - resistance_coefficient * neg_score, 0, 1) ``` -`batch_calculate_scores()` 用 NumPy 广播一次性计算未来扫描窗口的多个时间点。 +几何均值确保「发布时段匹配」和「间隔时机匹配」两个条件必须同时满足,任一为 0 则最终为 0。历史数据不足(< 4 条正向事件)时 `interval_score` 回退为 1.0,退化为纯周期得分。 + +`batch_calculate_scores()` 用 NumPy 广播一次性计算未来扫描窗口的多个时间点,逻辑与单点版本相同。 ## 未来峰值扫描