Share Notes

chundev

View the Project on GitHub latteouka/share-notes

協調行為偵測中的 base-rate 問題 — power user 現象、文獻回顧與三層解法

日期:2026-04-24 領域: 社群平台協調行為(coordinated inauthentic behavior, CIB)偵測


TL;DR

Count-based 的協調偵測(兩人一起爆推、兩人同 IP、兩人共推同篇)有個不易察覺的結構性缺陷:使用者活躍度服從 power-law 分佈,top 1% 的「閒人 / power user」會跟平台上幾乎所有活躍帳號產生高 co-occurrence,在原始分數上壓過真正的協調群。這是標準的 base-rate fallacy 的具體形態。

解法三層可組合:(A)UI 暴露作者活躍度讓分析者肉眼折算;(B)score 正規化,Jaccard / cosine / association strength 各有理論根據;(C)pre-filter 排掉 top percentile。LLR (G²)、TF-IDF 加權、push-direction z-score 這些已經對 marginal 做校正的方法不需要額外處理;raw count 類的必須。


背景:CIB 偵測的主流方法

Facebook 在 2018 年正式提出 “coordinated inauthentic behavior” (CIB) 一詞,定義為「一群帳號的行為出現異常、可疑、或統計上不預期的相似性」(Rogers & Righetti, 2025)。

文獻上多數 CIB 偵測用的是 co-occurrence-based detection

方法 出處 訊號本質
IP coincidence (CopyCatch) Beutel et al. 2013 (WWW) count of IP co-visits
SynchroTrap Cao et al. 2014 (NSDI) count of synchronized actions in session windows
Hashtag co-occurrence Pacheco et al. 2021 (ICWSM) TF-IDF weighted shared hashtags
Retweet coordination Nizzoli et al. 2021 count of shared retweets
Co-like behavior Giglietto 2023 normalized shared likes

上面這些都有一個共同問題:原始 count 容易被 base rate 主宰,需要不同程度的 normalization。下面說明為什麼。


問題的形式化:為什麼 base rate 會主宰?

假設兩個帳號 A 和 B 在某時間窗內獨立地各自行動:

在獨立假設下,兩人 co-occurrence(同一篇文都留言)的期望值:

\[E[\text{cooc}_{AB}] = N \cdot \frac{k_A}{N} \cdot \frac{k_B}{N} = \frac{k_A \cdot k_B}{N}\]

關鍵洞察:期望 co-occurrence 跟 $k_A \cdot k_B$ 呈線性。如果 A 是閒人($k_A = 400$)、B 是平均使用者($k_B = 20$),在 $N = 1000$ 下獨立情況就該有 $8$ 次 co-occurrence — 不是隨機異常,是預期中

原始分數 $k_{AB}$ 直接拿來排序,必然讓 (閒人, 任何人) 的 pair 排進 top;而真正的協調群 (協調者, 協調者) — 他們各自活躍度可能不高($k = 10$)但異常集中在少數文章 — 反而分數低被淹沒。

這就是 base-rate fallacy 在協調偵測裡的具體形態:忽略基線發生率,誤把「活躍」當「協調」。


為什麼會存在這麼多「閒人」?

不是任何平台特有。線上平台的使用者活躍度服從 power-law 分佈(Muchnik et al. 2013, Scientific Reports):

這個分佈的尾巴(極右端)不是離群值、是系統性存在。他們的行為特徵(高頻、廣泛、隨機)在 co-occurrence 矩陣上看起來就跟協調群類似 — 但本質完全不同。

Muchnik 的論文把這點講得很直白:「heterogeneous human activity distribution 是 power-law degree distribution 的,不是果。」意思是:我們在偵測中看到 degree(閒人) ≈ degree(協調群),不是巧合,是活躍度分佈的直接產物。


哪些方法不受影響?

判斷原則:訊號公式裡有沒有 marginal(邊際)項?

方法 形式 是否隱含校正 備註
Raw co-occurrence count $k_{AB}$ ❌ 無 base-rate 主宰
Jaccard $k_{AB} / (k_A + k_B - k_{AB})$ ✅ 有 set-theoretic
Cosine (Salton) $k_{AB} / \sqrt{k_A \cdot k_B}$ ✅ 有 set-theoretic
Dice $2 k_{AB} / (k_A + k_B)$ ✅ 有 set-theoretic
Association strength $k_{AB} \cdot N / (k_A \cdot k_B)$ ✅ 有 probabilistic
TF-IDF weighted cooc $\Sigma \text{idf}(i) \cdot [A, B \in i]$ ⚠️ 部分 處理物件熱度、不處理作者活躍
LLR / G² (Dunning 1993) 似然比 ✅ 完整 統計檢定,含 expected value
Push-direction z-score $(k - E[k]) / \sigma$ ✅ 完整 顯式 normalize

van Eck & Waltman 2009JASIST)做過系統性比較,結論:association strength 理論上最優(唯一的 probabilistic 度量),但 cosine / Jaccard 作為 set-theoretic 近似在工程上夠用。他們建議 scientometric 用 association strength,bibliographic similarity 用 cosine。


三層解法

A 層 — UI 顯示活躍度,分析者肉眼折算

最便宜、最安全。在 author inspector 展示:

<Badge>發文 {articleCount}</Badge>
<Badge>留言 {commentCount}</Badge>
<Badge>觸及文章 {distinctArticlesCommented}</Badge>
{distinctArticlesCommented >= 100 && (
  <Badge variant="destructive">⚠ 疑似 power user(閒人 loner)</Badge>
)}

優點: 零演算法風險、看得到歷史數據。缺點: 不能驅動自動下游決策(P/R 計算、auto-label)。

B 層 — Score normalization

把 raw count 換成一個 marginal-aware 的相似度度量。實務上四個選項的工程考量:

方法 公式 計算 何時選
Cosine $k_{AB} / \sqrt{k_A \cdot k_B}$ O(1) 偏好幾何直覺;對稱;適合嵌入距離類比
Jaccard $k_{AB} / (k_A + k_B - k_{AB})$ O(1) 偏好集合直覺;union 有 intuition
Dice $2 k_{AB} / (k_A + k_B)$ O(1) 跟 Jaccard 單調等價($D = 2J/(1+J)$),挑一個就好
Association strength $k_{AB} \cdot N / (k_A \cdot k_B)$ 需要 $N$ 最強理論依據;顯示「超出期望幾倍」

工程選 cosine 家族(除以 $\sqrt{k_A \cdot k_B}$):

export function activityNormalizer(
  activities: Map<string, number>,
  a: string,
  b: string,
): number {
  const actA = activities.get(a) ?? 0;
  const actB = activities.get(b) ?? 0;
  if (actA === 0 || actB === 0) return 1;
  return 1 / Math.sqrt(actA * actB);
}

const pairScore = rawCount * activityNormalizer(activities, a, b);

為什麼 cosine 而非 association strength? 後者需要知道 “universe” 大小 $N$(可能是時間窗內的文章總數 / 留言總數,依訊號而定),且在 count 稀疏時方差大。cosine 是較穩健的近似。如果未來要更嚴格的統計意義(例如要設顯著性門檻),再升級到 association strength。

具體效果:

情境 $k_A$ $k_B$ raw count cosine score
小眾協調 10 10 5 5/10 = 0.50
中階閒人巧遇 100 100 5 5/100 = 0.05
骨灰閒人巧遇 400 400 5 5/400 = 0.0125

40 倍差距把協調訊號拉上來,真實情境下效果更顯著(協調群常有 $k_{AB} \gg 5$)。

C 層 — Pre-filter top-percentile

在 pair 計算前就排除 top 1%:

export function findPowerUsers(
  activities: Map<string, number>,
  percentile: number,
): Set<string> {
  if (activities.size === 0) return new Set();
  const values = Array.from(activities.values()).sort((a, b) => a - b);
  const idx = Math.min(
    values.length - 1,
    Math.floor((percentile / 100) * values.length),
  );
  const threshold = values[idx]!;
  const result = new Set<string>();
  for (const [authorId, count] of activities) {
    if (count >= threshold) result.add(authorId);
  }
  return result;
}

理論: power-law 尾巴佔總活動量的比例異常高。排除 top 1% 通常能去除總 pair 候選的 30-50%(視分佈斜率),計算量和噪音同時被壓下。

風險: 協調群內可能混入一個「本來就很活躍」的成員(例如招募自真人而非帳號農場)。這人被排除會讓整個群偵測分數降低。

緩解策略:


組合:三層防禦 (defense in depth)

輸入(comments, IP events, etc.)
  │
  ▼
[C] 排除 top 1% activity 作者(僅 count-based 訊號)
  │
  ▼
pair 計算
  │
  ▼
[B] score ← raw × (1 / √(actA × actB))(僅 count-based 訊號)
  │
  ▼
Fusion + community detection (Louvain / Leiden)
  │
  ▼
[A] UI 顯示每 author activity,標記疑似 power user
  │
  ▼
分析者最終判斷

各層擋的問題層次不同:

單做一層都有漏;三層串起來噪音下降顯著。


延伸思考:這不是新問題

Base-rate 問題在資訊檢索與異常偵測早就有成熟解法:

所以 activity normalization 不是 PTT 特有、不是 CIB 偵測特有;它是任何基於 co-occurrence 的度量都要面對的基礎議題。


怎麼發現這個問題?

實話:坐在書房寫 code 很難想到。活躍度分佈的 power-law 性質是學過的理論,但「它會具體長成一堆 size-50 的大 blob、成員之間看不出關聯」這種肉眼症狀得實際跑幾週才會注意到。

這次的發現路徑有點有趣:讓 LLM 每天讀一次 snapshot 寫分析報告。某天它在「後續觀察」段寫:

temporal-burst 的 pairCount 已達 10000(疑似硬性上限),代表它可能被截斷了。…評估是否要…改用 top-K by score 篩選,避免低品質的 burst pair 被強制納入、稀釋 fusion 品質。

進程式碼一看,確實沒做 per-author activity 正規化。一個老用戶跟 30 個人都有 5+ 共同 burst 是太容易達成的 base case。

這個元方法論:AI 看資料找 anomaly 比人肉 review 便宜很多,尤其在你自己對資料分佈還沒直覺的早期階段。


學到的事


參考資料

基礎文獻(normalization 理論)

CIB 偵測演算法

Power-law / 活躍度分佈

近期方法學