目次
********
※LINE対応チャットボット版の
「LINEチャットボット屋」
いろんなチャットボットがあります。
ぜひ、ご覧ください!
***************
***************
今回のテスト実験の元の論文
下記の論文に興味をもったので、google colabで簡易的に実験してみました。
“Can We Fix Social Media? Testing Prosocial Interventions using Generative Social Simulation”
https://arxiv.org/pdf/2508.03385
これは2025年8月に preprint(査読前論文)として公開されたもので、アムステルダム大学(University of Amsterdam)の研究チームが、AIエージェントを用いてSNSの根本的な構造を検証したものです。
論文の存在はYoutubeで知りました。
・論文概要
この論文では、
人工的なSNS空間を構築し、LLM(大規模言語モデル)を持ったエージェントに投稿・再投稿・フォローをさせるシミュレーションを作成。
その結果、アルゴリズム等のない最小SNSでも
① エコーチェンバー現象(同じ意見の集団化)
② 少数エリートへの集中(影響力の偏り)
③ 過激な意見の増幅(極端な発言の広がり)
が自然発生することが示されています。
また、既存の分断対策(時系列フィード・多様性重視表示・匿名化など)を実装して検証しても根本的な問題解決にはならなかった、という結論が出ています。

SNSの問題はアルゴリズムではなく構造にあるという研究
近年、SNSが社会の分断を加速させているという議論は世界中で繰り返されている。
その際、多くの場合「おすすめアルゴリズム」が原因として語られる。
しかしアムステルダム大学の研究チームは、問題の根源はもっと深いところ、SNSという仕組みそのものにあると指摘した。
彼らはAIエージェントを大量に用いた仮想社会を構築し、極めて単純な機能しか持たないSNS空間でも、エコーチェンバー(同じ意見の集団化)や影響力の集中が自然発生することを示した。
つまり「アルゴリズムが偏らせている」のではなく、「人が同質な相手と繋がり、拡散できる設計自体」が分断を生む装置になっているという主張である。
この考え方に強い興味を持ち、私はGoogle Colab上で簡易的なSNSシミュレーションを作り、段階的に構造を変化させながら検証を行った。
本記事はその実験の記録である。
投稿・拡散・フォローだけの世界でも分断は起きるのか?
今回のテーマは「人間らしい複雑なSNS」ではなく、最小構造のSNS社会を作ることだった。
エージェントに許した行動はほぼこれだけ。
* 投稿する
* 他人の投稿を見る
* 共感したものに反応する
* 作者に好感を持つ(疑似フォロー)
アルゴリズム的な賢さは一切入れていない。高度な推薦も、広告ターゲティングも無し。
それでも分断や格差が起きるなら、「問題は設計そのものにある」という論文の仮説を裏付けることになる。
Phase1 まず匿名SNSでは格差はほとんど生まれなかった
最初は完全匿名環境で実験した。
投稿には作者情報を持たせず、エージェントは内容(stance)の一致だけでlikeする。
結果
* 投稿数は多いが人気は比較的均等
* Post Gini 約0.33
* Agent格差ほぼ無し
誰が書いたか分からない世界では、評価は「発言単位」にしか蓄積されないため、個人への人気集中が起こらない。これは重要なベースラインになった。

Phase2 投稿者がわかるだけで偏りは始まる
次に作者IDをわかるようにし、likeした相手に親和性を持つ仕組みを追加。
すると
* 少しずつ同じ作者に反応が集まり始める
* ただしまだ大きな勝者は生まれない
* Agent Gini 0.13
人が見えるだけで「好みの人に寄る」挙動が出るが、この段階ではまだSNS特有の爆発的格差には至らなかった。

Phase2.5 露出バイアスを入れた瞬間に勝者総取りが発生
ここで現実SNSの核心要素を追加した。
それはこれ。
「人気投稿ほど表示されやすくする(露出バイアス)」
すると社会は一変した。
* Post Gini 0.55まで上昇
* Agent Gini 0.40
* ロングテール出現
* 一部投稿だけが異常に伸びる
アルゴリズムではなく「人気を参照するだけ」で、勝者総取り構造が自動発生した。

「人気→露出→さらに人気」の自己強化ループに拍車
この段階で明確になったのは、SNSは本質的にポジティブフィードバック機械であるという点だ。
1. 共感される
2. 多く表示される
3. さらに共感される
4. 著名人、人気インフルエンサーなどの人気が信頼を強化
このループは自然に格差を固定し、少数の注目される人たちに人気を集中させる。
結果、勝者の総取りが派生する。
Phase3 反対意見を20%混ぜる介入は本当に効くのか?
分断対策としてよく語られる「異なる意見を見せる」施策を実装。
フィードの20%を強制的に反対側から表示させた。
結果 分断は止まらず、投稿格差はむしろ崩壊レベルへ
驚くべき結果になった。
反対意見は評価されないため、結局「同じ少数の投稿」だけにlikeが集中し続け、露出バイアスと組み合わさってバズが暴走した。

SNSを直すのではなく「交流の設計」を変える必要性
ここまでの実験で分かったのは、「炎上を減らす」「反対意見も見せる」「ランキングを隠す」といった小手先の手当てが、根っこの駆動力を止められないという事実だ。
なぜならSNSの分断や勝者総取りは、特定のアルゴリズムが悪さをした結果ではなく、もっと基本の構造、人が繋がり、反応し、拡散できるという設計が生む自然現象だからである。
今回のPhase2.5で最も強烈だったのは、露出バイアス(人気者がより見られる)が入った瞬間に、投稿格差と影響力格差が一気に立ち上がった点だ。
これは「プラットフォームが誰かを意図的に優遇した」というより、人気を参照した瞬間に避けられない自己強化ループが生まれることを意味する。
人気は人気を呼び、露出は露出を呼ぶ。
つまり人が集まり、盛り上がっているものを見せるという当たり前の設計が、格差を自動生成するエンジンになってしまう。
SNSが商業ものである以上、それは仕方のない設計だろう。広告をより多く表示することが、運営側の使命なのだから。
では「露出の偏りを抑えれば良い」のかというと、それも単純ではない。
Phase3の反対意見20%混入は、分断を抑えるつもりの介入が、投稿レベルの集中を壊滅的に悪化させた。
ここから見えるのは、SNSにおける問題は「何を表示するか」だけでなく、「表示されたものに人がどう反応するか」まで含めたループ全体で決まるということだ。
反対意見を見せても、人は必ずしも反対側に歩み寄らない。
むしろ反発・無視・不信につながりやすい。
その結果、誰も評価しない投稿が大量に流れ込み、評価は少数発言者の人気投稿へさらに収束し、露出バイアスがその収束を加速する。
この構造を止めるには、SNS内の「接続(connection)」と「反応(feedback)」の設計そのものを変える必要があるだろう。
しかし、商業上、それは不可能な話だ。なぜなら、SNSはボランティアでの運営ではなく、商取引されている場だからだ。
要するに、「SNSを直す」というのは、壊れた機械のネジを締め直すような単純な話ではない。
構造が自然に生み出す現象を、別の構造に置き換える話である。
接続・反応・拡散・露出。このループをどう設計するかが、SNS社会の空気そのものを決めてしまうだろう。

分断はソフトではなくハード(仕組み)で生まれている
今回のColabでの実験は、元論文が示した結論を、極限まで単純化した形で追体験させてくれた。
重要なのは、ここで起きたことが「誰かが悪意を持って操作した結果」ではなく、ルールがそうさせたという点だ。
匿名での投稿は安定。
非匿名(誰が書いたかわかる)で偏り。
人気投稿の露出で暴走。
反対意見の介入でさらに崩壊。
この4行は、SNSがどこから来るのかを、段階的に切り分けた観察記録になっている。
詳しく見ると、
まず匿名環境では、人気は発言にしか蓄積されず、人に固定されない。
そのためインフルエンサー構造が立ちにくい。
ここでは格差は限定的で、システムは比較的安定する。
次に非匿名(誰が書いたかわかる)を入れると、好きな相手に反応するという自然な行動が生まれ、関係が偏り始める。
ただしこの段階では、まだ爆発的な勝者総取りにはならない。
理由は簡単で、「偏り」があっても増幅装置が無いからだ。
Phase2.5で露出バイアスを入れた瞬間、偏りは増幅に変わる。
人気者の露出が増え、露出増がさらに人気を増やし、格差が自己強化する。
ここで初めて、現実SNSで見慣れたロングテールと、一部投稿の爆発的バズが立ち上がった。
つまり、勝者総取りを生む本体は「露出が人気を参照する」構造である。
アルゴリズムが高度かどうかは副次的で、参照してしまった時点で方向性が決まる。
そしてPhase3の反対意見の介入は、さらに重要な示唆を与えた。
反対意見を混ぜることは、直感的には健全そうに見える。
だがエージェントが賛成側にしか反応しないという条件の下では、反対意見の露出は評価につながらない。
評価されない投稿が大量に流れ込み、評価はさらに少数へ収束し、露出バイアスが収束を極端化する。
その結果、投稿格差(Post Gini)がほぼ最大級に達した。
これは「意見の多様性を増やした」つもりが、「評価の集中」をむしろ強める可能性があるということだ。

今回学んだ教訓
ここから導ける教訓は、SNSの介入は表示をいじれば良いという単純な話ではなく、ループ全体の力学を変えなければならないという点である。
現実社会で分断が拡大しにくいのは、制度・文化・時間・コストといったブレーキが多層的に存在するからだ。
学校や職場、地域のコミュニティでは、いろんな人たちが混在する。
つまり、自然と多種多様な意見の中で生活している。
ところがSNSは、そのブレーキを意図せず取り外した設計になっている。
好意を持った意見の人との交流がメインで、反対意見は表示されにくい。
ではどうすればいいのか
強調したいのは、SNSが???と言いたいわけではないということだ。
SNSは非常に強力な道具で、情報流通や発見の速度を上げ、多くのメリットを生んだ。
問題は、強力な道具には強い反作用がつきものであり、その反作用が構造由来である以上、部分的な調整では根本解決しないという点である。
(まあ、商業商品である以上、解決する必要はないのかも知れない)
もし再設計するのなら、その中心はアルゴリズムの微調整ではない。
接続と反応と拡散が作るフィードバック・ループ。
そのハードをどう組むかで決まってくるのだろう。
今回使ったPythonコード
上からPhase1で全Phase3まであります(Phase2.5含む)。
順番に実行するとわかりやすいです。
*表示のバグで、インテントが崩れてます。
このままコピーして、GPT5先生(おのおのお気に入りAI)に「インテント直して」と言って修正してもらってください。
実行した結果は数値で出ますので、またGPT5先生に聞いてください。
いい感じに解析してくれます。
Phase1
# -*- coding: utf-8 -*-
"""
# Phase1: 完全匿名SNSシミュレーション(最小構成)
このノートブックは、**投稿者が誰かわからない完全匿名SNS**で、
それでも **勝者総取り(反応集中)** が起きるかを検証する最小実験です。
- 非匿名SNSとの比較用ベースライン付き
- フォローなし
- 反応(like)のみ
- エージェントはルールベース
"""
# Cell 1: imports & seed
import random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
# Cell 2: Config
N_AGENTS = 200
T_STEPS = 200
POST_PROB = 0.2
LIKE_PROB_BASE = 0.1
STANCE_NOISE = 0.05
# Cell 3: Agent initialization
agents = []
for i in range(N_AGENTS):
agents.append({
"id": i,
"stance": np.random.normal(0, 0.3)
})
# Cell 4: Simulation (anonymous)
posts = [] # each post: {post_id, stance, likes}
post_id = 0
for t in range(T_STEPS):
# posting
for a in agents:
if random.random() < POST_PROB:
posts.append({
"post_id": post_id,
"stance": a["stance"],
"likes": 0
})
post_id += 1
# reactions (anonymous, content-only)
for p in posts:
for a in agents:
sim = 1 - abs(a["stance"] - p["stance"])
prob = LIKE_PROB_BASE * max(sim, 0)
if random.random() < prob: p["likes"] += 1 # stance update (exposure) for a in agents: if posts: sampled = random.choice(posts) a["stance"] += STANCE_NOISE * np.sign(sampled["stance"] - a["stance"]) # Cell 5: Metrics likes = [p["likes"] for p in posts if p["likes"] > 0]
def gini(x):
if len(x) == 0:
return 0.0
x = np.asarray(x, dtype=np.float64).flatten() # ← ここがポイント(float化)
if np.min(x) < 0:
x = x - np.min(x)
x = x + 1e-9 # floatなのでOK
x = np.sort(x)
n = x.size
index = np.arange(1, n + 1, dtype=np.float64)
return float(np.sum((2 * index - n - 1) * x) / (n * np.sum(x)))
print("Total posts:", len(posts))
print("Gini of likes (post-level):", round(gini(likes), 3))
# Cell 6: Visualization
plt.hist(likes, bins=30)
plt.title("Distribution of Likes per Post (Anonymous SNS)")
plt.xlabel("likes")
plt.ylabel("count")
plt.show()
"""
## 期待される観察結果
- 投稿者を完全匿名にしても、
**一部の投稿に like が集中**
- 投稿単位のジニ係数は高止まり
- 勝者総取りは「人」ではなく「投稿」に宿る
次ステップ:
- 非匿名版との比較
- 擬似匿名(ハンドル固定)
- 内容クラスタ集中の検証
"""
Phase2
# -*- coding: utf-8 -*-
"""
# Phase2: 非匿名SNSシミュレーション(ID効果あり)
"""
import random, numpy as np, matplotlib.pyplot as plt
random.seed(42); np.random.seed(42)
N_AGENTS=200; T_STEPS=200; POST_PROB=0.2; LIKE_PROB_BASE=0.1; STANCE_NOISE=0.05; AFFINITY_GAIN=0.01
agents=[{"id":i,"stance":np.random.normal(0,0.3),"affinity":{}} for i in range(N_AGENTS)]
posts=[]; pid=0
for t in range(T_STEPS):
for a in agents:
if random.random()<POST_PROB:
posts.append({"post_id":pid,"author":a["id"],"stance":a["stance"],"likes":0}); pid+=1
for p in posts:
for a in agents:
sim=1-abs(a["stance"]-p["stance"])
bonus=a["affinity"].get(p["author"],0)
prob=LIKE_PROB_BASE*max(sim+bonus,0)
if random.random()<prob:
p["likes"]+=1
a["affinity"][p["author"]]=a["affinity"].get(p["author"],0)+AFFINITY_GAIN
for a in agents:
if posts:
s=random.choice(posts)
a["stance"]+=STANCE_NOISE*np.sign(s["stance"]-a["stance"])
agent_like={}
for p in posts:
agent_like[p["author"]]=agent_like.get(p["author"],0)+p["likes"]
import numpy as np
def gini(x):
x=np.asarray(x,dtype=float)+1e-9
x=np.sort(x); n=len(x); idx=np.arange(1,n+1)
return (np.sum((2*idx-n-1)*x))/(n*np.sum(x))
print("Total posts:",len(posts))
print("Agent Gini:",round(gini(list(agent_like.values())),3))
plt.hist(list(agent_like.values()),bins=30); plt.title("Likes per Agent"); plt.show()
Phase2.5
# -*- coding: utf-8 -*-
"""
# Phase2.5: 非匿名SNS + 露出バイアス(人気者がさらに見られる)
Phase2(ID + affinity)に **露出バイアス** を追加します。
現実SNSで格差が爆発しやすい「増幅装置」を再現する最小実験です。
## 追加したもの
- 各ステップで各エージェントが見る投稿(FEED_SIZE本)を作る
- 投稿の選ばれやすさ = **投稿者の累積人気(author_total_likes)** に比例
- 人気者の投稿が「より頻繁に露出」→ さらに反応が増える → 人気が加速
## 出力
- Post Gini(投稿単位の反応格差)
- Agent Gini(人単位の反応格差)←ここが跳ね上がるはず
- ヒストグラム(likes per agent)
"""
# Cell 1: Imports & Seed
import random
import numpy as np
import matplotlib.pyplot as plt
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
# Cell 2: Config
N_AGENTS = 200
T_STEPS = 200
POST_PROB = 0.2
# "見る" 本数(現実SNSの「フィード」っぽさ)
FEED_SIZE = 25
# Likeの基本確率
LIKE_PROB_BASE = 0.12
# 見た投稿の影響でstanceが微調整される強さ
STANCE_NOISE = 0.03
# 好感度(擬似フォロー記憶)
AFFINITY_GAIN = 0.01
# 露出バイアスの強さ(0で無効、1で強め)
EXPOSURE_ALPHA = 1.0
# 露出の「最低保証」(人気が0でも露出される)
EXPOSURE_FLOOR = 50.0
# Cell 3: Agent initialization
agents = []
for i in range(N_AGENTS):
agents.append({
"id": i,
"stance": np.random.normal(0, 0.3),
"affinity": {},
"total_likes": 0, # 受け取った累積like(人気)
})
# Cell 4: Helpers
def gini(x):
if len(x) == 0:
return 0.0
x = np.asarray(x, dtype=np.float64).flatten()
if np.min(x) < 0:
x -= np.min(x)
x += 1e-9
x = np.sort(x)
n = x.size
idx = np.arange(1, n + 1, dtype=np.float64)
return float(np.sum((2 * idx - n - 1) * x) / (n * np.sum(x)))
def exposure_weights(posts, agents):
"""投稿がフィードに選ばれる重み(人気者ほど露出↑)"""
if len(posts) == 0:
return None
# 投稿者の累積人気
author_pop = np.array([agents[p['author']]['total_likes'] for p in posts], dtype=np.float64)
# floor + alpha * pop の形(alpha=0で完全均等露出)
w = (EXPOSURE_FLOOR + author_pop) ** EXPOSURE_ALPHA
w = np.maximum(w, 1e-9)
w = w / w.sum()
return w
# Cell 5: Simulation (non-anonymous + exposure bias)
posts = []
post_id = 0
for t in range(T_STEPS):
# 1) posting
for a in agents:
if random.random() < POST_PROB:
posts.append({
"post_id": post_id,
"author": a["id"],
"stance": a["stance"],
"likes": 0,
})
post_id += 1
if len(posts) == 0:
continue
# 2) build exposure distribution (changes as popularity changes)
w = exposure_weights(posts, agents)
# 3) each agent scrolls a feed and may like
# (feed sampled with replacement; this is enough for the effect)
post_indices = np.arange(len(posts))
for a in agents:
# sample what this agent sees
seen_idx = np.random.choice(post_indices, size=FEED_SIZE, replace=True, p=w)
# like decisions
for idx in seen_idx:
p = posts[idx]
# content similarity
sim = 1 - abs(a["stance"] - p["stance"])
sim = max(sim, 0.0)
# identity memory bonus (pseudo-follow)
bonus = a["affinity"].get(p["author"], 0.0)
prob = LIKE_PROB_BASE * max(sim + bonus, 0.0)
if random.random() < prob: p["likes"] += 1 # update popularity of author agents[p["author"]]["total_likes"] += 1 # update affinity memory a["affinity"][p["author"]] = a["affinity"].get(p["author"], 0.0) + AFFINITY_GAIN # stance update from one random seen post (light exposure drift) s = posts[random.choice(seen_idx)] a["stance"] += STANCE_NOISE * np.sign(s["stance"] - a["stance"]) # Cell 6: Metrics post_likes = [p["likes"] for p in posts if p["likes"] > 0]
agent_likes = [a["total_likes"] for a in agents]
print("Total posts:", len(posts))
print("Post Gini:", round(gini(post_likes), 3))
print("Agent Gini:", round(gini(agent_likes), 3))
top1 = np.percentile(agent_likes, 99)
share_top1 = sum([x for x in agent_likes if x >= top1]) / (sum(agent_likes) + 1e-9)
print("Top 1% share (agents):", round(share_top1, 3))
# Cell 7: Visualization
plt.hist(agent_likes, bins=30)
plt.title("Likes per Agent (Non-Anonymous + Exposure Bias)")
plt.xlabel("total likes received")
plt.ylabel("count")
plt.show()
plt.hist(post_likes, bins=30)
plt.title("Likes per Post (Non-Anonymous + Exposure Bias)")
plt.xlabel("likes")
plt.ylabel("count")
plt.show()
"""
## 使い方(おすすめ)
- まずこのまま実行して、Agent Gini が Phase2 より上がるか確認
- 次に Cell 2 の **EXPOSURE_ALPHA** を変えてみる
- 0.0 → 露出バイアス無し(Phase2に近い)
- 0.5 → 弱めの増幅
- 1.0 → 強めの増幅
- FEED_SIZE を増やすと「SNS漬け」になり格差が出やすいです
次:
- 匿名版でも「露出バイアスだけ」で格差がどれだけ出るか(構造の切り分け)
"""
Phase3
# -*- coding: utf-8 -*-
"""
# Phase3: Cross-cut Feed (20% Opposing Views Injection)
Non-anonymous SNS simulation with exposure bias + forced opposing content.
"""
# Cell 1: Config
NUM_AGENTS = 200
STEPS = 400
FEED_SIZE = 20
EXPOSURE_ALPHA = 1.0
OPPOSING_RATIO = 0.2
SEED = 42
# Cell 2: Imports
import numpy as np
import random
from collections import defaultdict
import matplotlib.pyplot as plt
# Cell 3: Gini
def gini(x):
x = np.array(x, dtype=float)
if np.amin(x) < 0:
x -= np.amin(x)
x += 1e-9
x = np.sort(x)
n = len(x)
return (np.sum((2*np.arange(1,n+1)-n-1)*x)) / (n*np.sum(x))
# Cell 4: Init agents
random.seed(SEED); np.random.seed(SEED)
agents = []
for i in range(NUM_AGENTS):
stance = random.choice([-1,1])
agents.append({"id":i,"stance":stance,"followers":set(),"likes":0})
# Cell 5: Simulation
posts = []
for step in range(STEPS):
# Post
for a in agents:
if random.random() < 0.1:
posts.append({"author":a["id"],"stance":a["stance"],"likes":1})
if not posts: continue
# Exposure weights
likes = np.array([p["likes"] for p in posts], dtype=float)
weights = likes**EXPOSURE_ALPHA
weights /= weights.sum()
for a in agents:
feed = []
# opposing injection
opp_posts = [p for p in posts if p["stance"] != a["stance"]]
same_posts = posts
n_opp = int(FEED_SIZE * OPPOSING_RATIO)
if opp_posts:
feed += random.choices(opp_posts, k=n_opp)
feed += random.choices(posts, weights=weights, k=FEED_SIZE-len(feed))
for p in feed:
if p["stance"] == a["stance"]:
p["likes"] += 1
agents[a["id"]]["likes"] += 1
# Cell 6: Metrics
post_likes = [p["likes"] for p in posts]
agent_likes = [a["likes"] for a in agents]
print("Total posts:", len(posts))
print("Post Gini:", round(gini(post_likes),3))
print("Agent Gini:", round(gini(agent_likes),3))
top = int(NUM_AGENTS*0.01)
share = sum(sorted(agent_likes, reverse=True)[:top]) / sum(agent_likes)
print("Top1% share:", round(share,3))
# Cell 7: Plot
plt.figure()
plt.hist(post_likes, bins=40)
plt.title("Likes per Post with Opposing Injection")
plt.xlabel("likes"); plt.ylabel("count")
plt.show()
****************
最近のデジタルアート作品を掲載!
X 旧ツイッターもやってます。
https://x.com/ison1232
インスタグラムはこちら
https://www.instagram.com/nanahati555/
***************

