Links: CS224W L11 (recommender systems, LightGCN, the L11 frontier on recommenders as edge-rewiring operators),
notebooks/project/retweet_thread.py §L11 (the same recommenders). Atlas: recommender systems; information ecosystems.
Question
A coordinated ring does not need to fool people directly. It is enough to fool the ranking: if the ring's retweets make its target accounts look popular, the recommender shows those accounts to real users, who then retweet them for real. How much extra exposure do the targets get from the ring, which recommenders are more exposed, does a feedback loop compound it, and how much does filtering detected accounts undo?
Hypotheses (written before running)
-
Popularity amplifies. With a popularity recommender, the ring targets' share of recommendation slots is at least 1.2× their share in the counterfactual (the same world with every bot retweet deleted), in every world type.
-
Personalisation dampens. The LightGCN-style propagation recommender amplifies less than popularity (smaller factor), because a bot's retweets reach a human's scores only through paths via the bot.
-
Feedback compounds. Over 5 rounds in which users accept recommendations, the targets' excess exposure (factual minus counterfactual) grows.
-
Filtering works. Dropping the retweets of the accounts a cross-world detector flags (top 3% of scores) removes at least 80% of the excess exposure.
Setup
- Worlds: naive, reciprocal and co-amplification rings, seeds in
config.yaml. Train on days 0–20. -
Recommenders: popularity (retweets received) and LightGCN-style propagation (K = 2, untrained), top 20 authors per human, excluding authors already retweeted.
-
Exposure share: the share of all humans' recommendation slots that go to ring targets. Amplification factor: factual share / counterfactual share. Recall@20 on humans' new retweets in days 20–30.
-
Feedback: each round every human accepts each recommendation with probability
accept_p, which adds a retweet. - Detector: the label-free model of
honest-splits, trained on another world of the same type.
Run: uv run marimo edit experiments/recsys-amplification/recsys_amplification.py and press the run button.
Added after a one-world prototype (not a test): an obscure-targets setting (target_rank = (300, 600)), because on
world 0 the popularity ranking of already-popular targets did not move at all.
Results (summary; full tables in results.md)
Amplification factor = targets' share of recommendation slots with the bots' retweets / without them.
| targets | world | popularity | propagation | excess removed by filtering (popularity) |
|---|---|---|---|---|
| popular | naive | 1.37 | 0.92 | 1.00 |
| popular | reciprocal | 1.40 | 0.97 | 0.73 |
| popular | co-amplify | 1.21 | 1.06 | 0.24 |
| obscure | naive | 2.05 | 0.85 | 1.00 |
| obscure | reciprocal | 2.03 | 0.95 | 1.00 |
| obscure | co-amplify | 3.17 | 1.11 | 0.49 |
-
H1 supported (narrowly for co-amplification, 1.21). The same fake retweets buy two to three times more exposure for an obscure account than for a popular one, whose top-20 slot is often secured with or without the ring.
-
H2 supported. Propagation factors are 0.85–1.11. Below 1, the ring lowers its targets' exposure: LightGCN's D_a^{-1/2} normalisation, built against popularity bias, divides by the number of distinct retweeters, which bots inflate.
-
Recall@20 is unchanged by the bots' retweets (to the third decimal in most cells): the standard offline metric cannot see the manipulation.
-
H3 refuted. Under accept-and-retrain feedback the excess shrinks (popular / reciprocal: +0.022 → +0.002 over 5 rounds). A reading, not yet tested: acceptances add organic volume in both worlds, diluting the ring's fixed contribution.
-
H4 refuted. Filtering the top 3% of a cross-world detector's scores removes all of the excess for naive rings, 73% for reciprocal rings, but only 24% for co-amplification rings, where half the flagged accounts are humans.
Next steps
- Rings whose volume grows with the platform (keep posting each round), the test of the H3 reading.
- A trained LightGCN (BPR loss) instead of the untrained propagation, and the hard-negative variants of L11.
- Exposure diversity (community mixing of what humans are shown), the metric regulators ask about.
Results
Generated by recsys_amplification.py from config.yaml (worlds [0, 1, 2], top 20,
5 feedback rounds, accept_p 0.05). Means over worlds.
| targets | world | recommender | share (factual) | share (no bot retweets) | factor | position-weighted factor | share (filtered) | excess removed | Recall@20 | Recall@20 (no bots) |
|---|---|---|---|---|---|---|---|---|---|---|
| obscure | co-amplify | popularity | 0.0553 | 0.0174 | 3.17 | 3.31 | 0.0368 | 0.49 | 0.063 | 0.064 |
| obscure | co-amplify | propagation | 0.0276 | 0.0250 | 1.11 | 1.19 | 0.0264 | nan | 0.086 | 0.087 |
| obscure | naive | popularity | 0.1242 | 0.0605 | 2.05 | 2.15 | 0.0606 | 1.00 | 0.068 | 0.066 |
| obscure | naive | propagation | 0.0266 | 0.0313 | 0.85 | 0.80 | 0.0314 | nan | 0.092 | 0.093 |
| obscure | reciprocal | popularity | 0.0309 | 0.0152 | 2.03 | 2.12 | 0.0153 | 1.00 | 0.073 | 0.073 |
| obscure | reciprocal | propagation | 0.0126 | 0.0132 | 0.95 | 0.91 | 0.0134 | nan | 0.096 | 0.097 |
| popular | co-amplify | popularity | 0.2289 | 0.1892 | 1.21 | 1.25 | 0.2196 | 0.24 | 0.080 | 0.077 |
| popular | co-amplify | propagation | 0.0721 | 0.0681 | 1.06 | 1.05 | 0.0678 | nan | 0.096 | 0.097 |
| popular | naive | popularity | 0.3029 | 0.2204 | 1.37 | 1.36 | 0.2201 | 1.00 | 0.094 | 0.093 |
| popular | naive | propagation | 0.0794 | 0.0862 | 0.92 | 0.91 | 0.0852 | nan | 0.104 | 0.105 |
| popular | reciprocal | popularity | 0.0750 | 0.0535 | 1.40 | 1.29 | 0.0592 | 0.73 | 0.075 | 0.074 |
| popular | reciprocal | propagation | 0.0288 | 0.0298 | 0.97 | 0.95 | 0.0297 | nan | 0.097 | 0.097 |
Excess share (factual − counterfactual) by feedback round:
| targets | world | recommender | round 0 | round 1 | round 2 | round 3 | round 4 | round 5 |
|---|---|---|---|---|---|---|---|---|
| obscure | co-amplify | popularity | +0.0379 | +0.0417 | +0.0428 | +0.0430 | +0.0414 | +0.0401 |
| obscure | co-amplify | propagation | +0.0027 | +0.0027 | +0.0010 | -0.0004 | -0.0028 | -0.0038 |
| obscure | naive | popularity | +0.0636 | +0.0608 | +0.0585 | +0.0575 | +0.0572 | +0.0560 |
| obscure | naive | propagation | -0.0047 | -0.0055 | -0.0046 | -0.0047 | -0.0037 | -0.0041 |
| obscure | reciprocal | popularity | +0.0156 | +0.0148 | +0.0141 | +0.0131 | +0.0120 | +0.0110 |
| obscure | reciprocal | propagation | -0.0006 | -0.0011 | -0.0009 | -0.0005 | -0.0005 | -0.0010 |
| popular | co-amplify | popularity | +0.0397 | +0.0405 | +0.0394 | +0.0367 | +0.0346 | +0.0302 |
| popular | co-amplify | propagation | +0.0039 | +0.0036 | +0.0027 | +0.0027 | +0.0029 | +0.0024 |
| popular | naive | popularity | +0.0826 | +0.0790 | +0.0694 | +0.0616 | +0.0552 | +0.0492 |
| popular | naive | propagation | -0.0068 | -0.0068 | -0.0066 | -0.0058 | -0.0072 | -0.0079 |
| popular | reciprocal | popularity | +0.0215 | +0.0140 | +0.0083 | +0.0053 | +0.0031 | +0.0017 |
| popular | reciprocal | propagation | -0.0010 | -0.0013 | -0.0017 | -0.0021 | -0.0026 | -0.0034 |
Detector flags (top 3% of scores):
| targets | world | precision | recall |
|---|---|---|---|
| obscure | co-amplify | 0.35 | 0.43 |
| obscure | naive | 0.81 | 1.00 |
| obscure | reciprocal | 0.81 | 1.00 |
| popular | co-amplify | 0.47 | 0.58 |
| popular | naive | 0.81 | 1.00 |
| popular | reciprocal | 0.75 | 0.92 |
Verdicts (popular targets, the pre-registered setting): H1 supported, H2 supported, H3 refuted, H4 refuted.
In the repository: experiments/recsys-amplification/ · README.md config.yaml recsys_amplification.py results.json results.md