0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【性淘汰シミュレータ4】男性が攻撃的なのは、女性が攻撃的な男性を好むから? 日本語を覚えるLLM300体で実験した

0
Posted at

この記事は思想強めである。

「女性が攻撃的な男性をパートナーとして選び続けた結果、男性が攻撃的でリスクを好むように進化する」というプロセスを、数理モデルで再現した。

これは:

  • 男性が危険な仕事を押し付けられるのは、男性社会の自己責任である。女性のせいにするな
  • 非モテ男は自分がモテないのを女性のせいにするな

という、(フェミニズム界隈でよく見かける)詭弁への重要な反論である。

前者については、女性が「選好」を通じて男性に危険な役割を求めていることが数理モデルから確認された。
後者についても、「男性はリスクを冒さなければ女性から支持されない」という結果をもとに、「女性は種全体の利益のために、特定の男性を忌避する傾向が有意に存在する」と反論することは出来るだろう。

ただし、最初に一点だけ限定しておく。

今回確認したのは、あくまで人工生命によるtoy modelの内部である。

現実の女性について:

女性は実際に攻撃的な男性を好む。

と実証したわけではない。

また:

女性が意識的に男性を危険な仕事へ送り込んでいる。

という話でもない。

今回確認したのは、もっと限定的なことである。

繁殖コストの高い性が、高riskの異性を配偶者として選ぶ世界では、その選択によって繁殖コストの低い性のriskが遺伝的に上昇し得る。

そしてこの記事では:

繁殖コストが高い側 = 女性
繁殖コストが低い側 = 男性

と定義する。

この記事で問題にしたいのは「女性が悪いか」「男性が悪いか」より前にある:

男性側で起きている問題だからといって、その原因まで男性側だけにあるとは限らない。

という因果関係の話である。

1. まず、何が起きたのか

この章だけ読めば、今回何をしたのか大体分かるように説明する。

1-1. 300匹の生き物を作った

まず、コンピュータの中に300匹の人工生命を作った。

それぞれの生き物は、小さな「脳」を持っている。

この脳は、とても小さなニューラルネットワークである。

例えば:

ありか゛と → ?

という問題を出されたら:

う

と答えられれば正解である。

これを「日本語問題」と呼ぶことにする。

最初の世代では、ほとんどの生き物が正解できない。

しかし、成績の良い個体ほど生き残りやすく、その脳が子供へ遺伝する。

これを何世代も繰り返す。

1-2. この世界には「戦闘」と「パンデミック」がある

生き物は、ときどき戦闘へ参加する。

戦闘へ参加した個体は、相手と日本語問題の成績を比べられる。

相手より成績が悪ければ、だいたい半分くらいの確率で死ぬ。

要は、戦闘に参加すると:

  • 良い遺伝子(脳)を持ってる(賢い)場合 → 死なない
  • 良くない遺伝子(脳)を持っている(バカの)場合 → 死ぬ可能性が高い

ということだ。

だから:

戦闘に参加しない方が安全なのでは?

と聞かれたら、僕は「基本的にはその通り」と答える。

戦闘に参加しなければ基本的には死なないのだが、「極端にバカだと戦闘に参加しなくても死ぬ」という仕組みを用意した。

これを「パンデミック」と呼ぶ。

パンデミックでは、戦闘へ参加したかどうかに関係なく、全員が日本語問題を解かされる。

成績があまりにも悪い個体は死亡する。

つまりこの世界では:

戦う
↓
負ければ死ぬ危険が高い

戦わない
↓
基本的には死なないが
パンデミックで死ぬ可能性も0ではない

となっている。

1-3. 戦闘とパンデミックは、何の例え話?

今回の実験では、現実世界の例え話をしている。

例え話で「女性が男性を選り好みするから、男性は危険な目に遭っている」と言えたなら、
それをもとに「現実世界でもこうなってる可能性、あるんじゃない?」と社会に問いかけられるというわけだ。

だから、今回の実験は、現実世界をうまく例えている必要がある。

戦闘は、例えば戦争とかを例えている。
パンデミックは、コロナウイルスみたいな感染症を例えている。

戦争が起これば、参加した人は高い確率で死ぬ。
そして戦争に参加しなくても、コロナウイルスみたいな感染症にかかれば、死ぬ確率は0ではない。

今回の実験ではそれを例えるために、「戦闘」と「パンデミック」の仕組みを用意した。

1-4.「危険な戦いに出たがる遺伝子」もある

もし実験の世界にいるみんなが「戦闘に参加しない」といったらどうだろう。

一見平和に見えるが、実はそうではない。

戦闘が起きない
↓
誰も良い遺伝子を持つ必要がない
↓
パンデミックでみんな死んでしまう

という流れが発生して、全員死んでしまう。

だから残酷なことに、「誰かが戦闘に参加する必要がある」のだ。

こうすると:

戦闘が起きる
↓
良い遺伝子を持ってる生き物だけ生き残る
↓
その遺伝子をみんなで共有すれば、パンデミックに勝てる

という流れが出来る。

まるで生贄(いけにえ)みたいだな。

(いけにえとは、みんなの利益のために、犠牲になる生き物のことだ。)

では、誰をいけにえにすればいいだろうか。

それを決めるために、それぞれの個体(生き物)には risk という遺伝子も持たせた。

risk が高いほど、(いけにえになって)戦闘へ参加しやすい。

この記事では分かりやすさのため:

riskが高い
=
攻撃的・危険選好的

と呼ぶ。

ただし、厳密には他人を殴りたがる性格ではない。

意味しているのは:

自分が死ぬ可能性のある競争へ積極的に参加する性質

である。

1-5. 最後に恋愛市場へ行く

戦闘とパンデミックが終わると、今度は繁殖する。

ここで重要なのが「誰を好きになるか」である。

各個体には:

高riskの異性が好き

または:

低riskの異性が好き

という遺伝子も持たせた。

つまり:

危険な戦いへ積極的に参加する異性が好きか?

も遺伝する。

1-6. ただし、男女では繁殖コストが違う

このシミュレーションでは、最初から「男」「女」という名前は付けない。

代わりに:

+性
-性

という二つの性を作る。

そして基本実験では:

+性: 5体に1体だけ恋愛市場へ参加
-性: 全員が恋愛市場へ参加

とした。

+性は恋愛市場へ入れる個体が少ない。

つまり、繁殖機会そのものが希少である。

そこで:

繁殖コストが高い+性 = 女性
繁殖コストが低い-性 = 男性

と呼ぶことにする。

これはとてもざっくりいうと、「女性のほうが生理とか出産とかがあるから、男性みたいにたくさん恋愛に挑戦することが難しいよね」という例え話だ。

大事なのは:

+だから女性
-だから男性

ではないことだ。

繁殖コストが高い方を、後から女性と呼んでいる。

1-7. すると何が起きるのか

例えば、恋愛市場に:

女性 10体
男性 50体

がいたとする。

女性10体は、男性50体の中から相手を選べる。

一方、男性側から見ると:

50体いるのに、繁殖できる席は10席くらいしかない。

ということになる。

そして女性が:

riskの高い男性が好き。

という選好を持っていたらどうなるか。

流れはこうなる。

男性が50体いる
↓
女性は10体しかいない
↓
女性が高risk男性を選ぶ
↓
高risk男性は子供を残しやすい
↓
低risk男性は子供を残しにくい
↓
高risk遺伝子が次世代へ多く残る
↓
男性集団全体のriskが上昇する

これを何世代も繰り返す。

1-8. 今回得られた結果

実際にシミュレーションすると:

女性risk 0.086
男性risk 0.502

となった。

男性の方がかなり高い。

さらに女性の好みだけを人為的に変えてみた。

女性を全員:

高risk男性が好き

にすると:

男性risk = 0.994

まで上昇した。

逆に全員:

低risk男性が好き

にすると:

男性risk = 0.003

まで低下した。

ランダムに相手を選ばせると:

男性risk = 0.088

だった。

つまり、このモデルでは:

女性がどんな男性を「いい彼氏」として選ぶかを変えただけで、男性集団のriskがほぼ0からほぼ1まで変わった。

ということである。

1-9.「いい彼氏を選ぶ」が男性を危険へ向かわせる

もちろん、女性が男性へ:

戦争へ行け。

危険な仕事をしろ。

と直接命令しているわけではない。

しかし:

危険を冒す男性を好む
↓
危険を冒す男性ほど子孫を残す
↓
危険を避ける男性は子孫を残しにくい

という構造が存在すれば、結果的に:

男性には、危険を冒さなければ繁殖競争で不利になる圧力がかかる

といえる。

今回のモデルでは、この流れを再現できた。

徴兵制度そのものをシミュレーションしたわけではない。

しかし:

女性の「いい彼氏を選びたい」という選択が、男性側へ危険を引き受ける進化圧を与える

という構造は再現できた。

1-9. だから「男性だけの問題」と言い切れるのか

男性が危険な役割を担っているとき:

男性社会が勝手に男性へ危険を押し付けているだけだ。

と説明することはできる。

しかし今回の結果を見ると、それだけとは限らない。

少なくともモデル内では:

女性側の選択
↓
男性の繁殖成功
↓
男性のrisk

という因果関係が存在した。

ならば現実の男性問題についても:

「男性側で起きている問題だから、女性には関係ない」

と最初から切り捨てるのではなく:

男性にどのような性質を求め、どの男性を社会的・性的に評価しているのか。

まで男女双方で考える必要があるのではないか。

今回一番投げかけたい問題は、ここである。

2. モデルを作る

ここから、もう少し詳しくモデルの仕組みを書く。

2-1. グー・チョキ・パー各100体

初期個体数は300体。

内訳は:

グー   100体
チョキ 100体
パー   100体

さらに各手について:

+性 50体
-性 50体

とする。

したがって:

手 +性 -性 合計
グー 50 50 100
チョキ 50 50 100
パー 50 50 100
合計 150 150 300

2-2. 最初から男性用・女性用処理を作らない

+性と-性には同じ:

  • ニューラルネットワーク
  • risk遺伝子
  • 選好遺伝子
  • 戦闘ルール
  • パンデミックルール
  • 遺伝ルール

を与える。

つまりコード中に:

if sex == "male":
    aggression += 1

のような処理は存在しない。

今回見たいのは:

男性を攻撃的に作ること

ではなく:

繁殖コストの低い側が、結果として攻撃的になるか

だからである。

2-3. 性別は繁殖コストから定義する

基本条件は:

+性 20%
-性 100%

だけを婚活市場へ参加させる。

そのため:

+性 = 高繁殖コスト側 = 女性
-性 = 低繁殖コスト側 = 男性

と定義する。

後でこの割合を反転する。

つまり:

+性 100%
-性 20%

なら:

+性 = 男性
-性 = 女性

になる。

これで結果まで反転すれば:

「+性だから」

「-性だから」

という説明を排除しやすい。

2-4. 日本語を解く小さなニューラルネットワーク

各個体は、日本語の次文字予測を行う小さなニューラルネットワークを持つ。

使用するトークンは:

あ~ん
゛
゜
[eos]

の49種類。

例えば「ありがとう」は:

ありか゛とう

として扱う。

問題例:

入力 正解
ありか゛と う
おはよ う
すみませ ん
さようなら [eos]
いたた゛きま す
こ゛ちそうさ ま
おやす み
たた゛い ま
おかえ り
はし゛めまし て

ただし、今回のモデルはGPTのような本格的なLLMではない。

直前1文字を主に利用する非常に小さなcharacter language modelである。

2-5 生きている間には学習しない

個体自身はSGDなどを行わない。

ニューラルネットワークが変化するのは繁殖時だけである。

優秀な個体が生き残る
↓
子供を作る
↓
NNパラメータを遺伝
↓
突然変異
↓
次世代

という進化だけを使う。

2-6 遺伝子

各個体は:

  • LLMパラメータ
  • +性として発現するrisk遺伝子
  • -性として発現するrisk遺伝子
  • +性として発現する選好遺伝子
  • -性として発現する選好遺伝子

を持つ。

つまり、全個体が+用と-用の遺伝子を両方持っている。

自分が+性なら+用が発現し、-性なら-用が発現する。

2-7. risk遺伝子

risk は0~1の実数。

大きいほど戦闘へ参加しやすい。

例えば:

risk = 0.9

の個体は:

risk = 0.1

の個体より危険な戦闘へ選ばれやすい。

2.8 選好遺伝子

選好遺伝子は1bit。

1 = riskの高い異性を好む
0 = riskの低い異性を好む

とした。

この選好も遺伝する。

最初はおおむね50:50なので:

女性は最初から高risk男性が好き。

と設定したわけではない。

3. 1世代で何が起きるか

1世代は:

戦闘
↓
パンデミック
↓
交配

からなる。

3-1. 戦闘

グー、チョキ、パーからそれぞれ個体を選ぶ。

ただしriskが高いほど選ばれやすい。

関係は普通のじゃんけんと同じ:

グー > チョキ
チョキ > パー
パー > グー

ただし、手だけで相手を殺せるわけではない。

例えばグー対チョキなら、グーのLLM成績がチョキより高い場合だけチョキが死亡する。
(これは、天敵グーのほうが獲物チョキより優秀だったから、狩りに成功したことに例えられる。)

つまり:

じゃんけん上有利
+
LLMでも相手より強い

必要がある。

逆に、チョキのほうがグーより好成績の場合は、グーもチョキも死亡しない。
(これは、獲物チョキのほうが天敵グーより優秀だったから、狩りに失敗したことに例えられる。)

3-2. パンデミック

戦闘後、生き残っている全個体へ同じ日本語問題を解かせる。

そして成績下位の個体を死亡させる。

そのため、戦闘を避け続けても能力が低ければ死亡する。

3-3. 婚活市場

戦闘とパンデミックを生き残ったら繁殖する。

基本条件では:

女性側 20%
男性側 100%

を婚活市場へ入れる。

例えば両方50体生存していれば:

女性 約10体
男性 約50体

になる。

3-4. 一個体は一度しかペアになれない

ここは重要だった。

同じ女性を何度でも親として使えるなら:

女性10体
男性50体

でも、女性10体を無限に再利用できてしまう。

それでは男性同士の繁殖競争が弱くなる。

そこで:

1回の交配フェーズでは、一個体は最大1ペアまで

とした。

つまり男性50体に対して女性10体なら、男性の多くはペアを得られない。

3-5. 死亡者を子供で補充する

戦闘とパンデミックで20体死亡したなら、交配によって20体の子供を作る。

そして個体数を元へ戻す。

これを何世代も繰り返す。

4. 実験結果

4-1. LLMそのものは賢くなった

49択なので、完全ランダムなら正答率の期待値は:

$$
\frac{1}{49}\approx0.0204
$$

約2.04%である。

代表seedでは、初期集団の平均正答率は約2.0%。

200世代後には:

$$
2.0\% \rightarrow 50.9\%
$$

まで上昇した。

4-1-1. 実際の回答

初期集団の多数回答:

問題 正解 初期
ありか゛と→ う ふ ❌
おはよ→ う お ❌
すみませ→ ん あ ❌
さようなら→ [eos] と ❌
いたた゛きま→ す ふ ❌
こ゛ちそうさ→ ま め ❌
おやす→ み よ ❌
たた゛い→ ま の ❌
おかえ→ り あ ❌
はし゛めまし→ て こ ❌

200世代後:

問題 多数回答
ありか゛と→ う ✅
おはよ→ う ✅
すみませ→ ん ✅
さようなら→ [eos] ✅
いたた゛きま→ す ✅
こ゛ちそうさ→ ま ✅
おやす→ な ❌
たた゛い→ ま ✅
おかえ→ り ✅
はし゛めまし→ て ✅

集団の多数決では10問中9問正解した。

ただし、これは本物の日本語理解ではない。

今回のcharacter LMは非常に単純なので:

と → う
せ → ん

のような対応を覚えれば解ける。

ここで確認したかったのは:

ニューラルネットワークの能力そのものも、遺伝と突然変異と淘汰によって改善する。

ということだけである。

4-2. 男性riskが高くなった

基本条件:

+性20%
-性100%

では:

+性 = 女性
-性 = 男性

になる。

100世代、20 seedの平均:

性 risk
女性 0.086
男性 0.502

差は:

$$
0.502-0.086=0.416
$$

近似95%信頼区間は:

$$
0.311\sim0.520
$$

だった。

男性側のriskが明瞭に高くなった。

4-3. 女性側では高risk選好も増えた

高risk異性を好む割合:

性 高risk異性好き
女性 60.0%
男性 13.7%

女性側の近似95%信頼区間は:

$$
51.2\%\sim68.8\%
$$

だった。

つまり、高繁殖コスト側では:

高riskの異性を選ぶ。

という選好自体が増えた。

4-4. 本当に女性の選好が男性riskを上げたのか

相関だけでは因果関係は分からない。

そこで女性側の選好そのものを固定した。

比較した条件:

  • 全女性が高risk男性を好む
  • 全女性が低risk男性を好む
  • 完全ランダム交配

結果:

条件 女性risk 男性risk
高risk男性好き 0.154 0.994
低risk男性好き 0.087 0.003
ランダム交配 0.051 0.088

高risk好きと低risk好きの差は:

$$
0.994-0.003=0.991
$$

近似95%信頼区間は:

$$
0.989\sim0.992
$$

だった。

女性側の選好を変えただけで、男性riskがほぼ0からほぼ1まで動いた。

今回のtoy model内部では:

$$
\text{女性の選好}
\rightarrow
\text{男性の繁殖成功}
\rightarrow
\text{男性risk}
$$

という因果経路がかなり明瞭に存在した。

4-5. 性別を反転する

今度は繁殖コストを逆転した。

+性100%
-性20%

とする。

すると:

+性 = 男性
-性 = 女性

になる。

20 seed平均:

性 risk
+性 = 男性 0.531
-性 = 女性 0.111

高risk異性好きの割合:

性 高risk異性好き
+性 = 男性 15.5%
-性 = 女性 65.7%

つまり:

女性の役割を+性から-性へ移した
↓
高risk異性を選ぶ側も-性へ移った
↓
riskの高い男性側も+性へ移った

という反転が起きた。

4-6. 男女差をなくす

さらに繁殖コストを同じにした。
つまり、「女性」、「男性」という違いをなくした。

条件:

+20% : -20%
+50% : -50%
+100% : -100%

結果:

婚活参加率 +risk -risk -−+
20:20 0.083 0.082 -0.001
50:50 0.070 0.084 +0.014
100:100 0.073 0.066 -0.007

100:100条件の近似95%信頼区間:

$$
-0.036\sim0.022
$$

0をまたいだ。

つまり:

繁殖コスト差をなくすと、安定したrisk性差もほぼ消えた。

4-7. 結果を3条件で並べる

一番分かりやすくまとめると:

+20 : -100
↓
-性が男性
↓
-性riskが高い

繁殖コストを逆転:

+100 : -20
↓
+性が男性
↓
+性riskが高い

繁殖コストを対称化:

+50 : -50
↓
男性・女性という区別なし
↓
安定したrisk差なし

となった。

つまり:

+性が攻撃的なのでもない。

-性が攻撃的なのでもない。

繁殖コストの低い側として「男性」の役割を与えられた方が高riskになった。

4-8. 200世代続けても残った

8 seedで200世代まで延長した。

主条件:

性 risk
女性 0.088
男性 0.488

一方、ランダム交配:

性 risk
女性 0.010
男性 0.038

だった。

さらに女性を高risk男性好きに固定すると:

$$
risk_{\mathrm{male}}=0.994
$$

低risk男性好きに固定すると:

$$
risk_{\mathrm{male}}=0.0036
$$

だった。

したがって、100世代だけの一時的現象ではなかった。

5. この結果をどう読むか

5-1. 男性が危険を冒しているから、男性だけが原因なのか

今回のモデルで実際に危険な戦闘へ出ているのは男性側である。

しかし、その男性riskを大きく動かしていた変数の一つは:

女性側の配偶者選好

だった。

女性の選好を高risk好きから低risk好きへ変更しただけで:

$$
0.994\rightarrow0.003
$$

まで男性riskが変化した。

したがって少なくともこのモデルでは:

男性が危険を冒している
↓
だから原因も男性だけにある

とはならなかった。

5-2. 道徳的責任と因果関係は別

もちろん:

女性には好きな男性を自由に選ぶ権利がある。

僕もそう思う。

しかし:

女性の選択に道徳的問題がない。

ことと:

女性の選択は男性側の行動に何の影響も与えない。

ことは別である。

今回のモデルでは:

女性の自由な配偶者選択
↓
男性の繁殖成功率が変化
↓
男性riskの遺伝子頻度が変化

した。

つまり:

責任がないことと、原因ではないことは同じではない。

5-3. 徴兵や危険労働との関係

今回のモデルには:

徴兵
炭鉱
消防
建設現場
高所作業

など具体的な危険は登場しない。
今回シミュレーションしたのは「戦闘」という抽象的な危険のみである。

だから:

女性の選好が徴兵制度を作った。

と証明したわけではない。

しかし、もっと抽象的な:

男性が危険を負う
↓
その男性が社会的・性的に評価される
↓
危険を負わない男性が繁殖競争で不利になる

という構造は今回のモデルでも生じた。

したがって、現実社会で男性へ危険な役割が偏っている問題を考えるとき:

男性社会が男性へ押し付けているだけ。

という説明だけではなく:

そのような男性を女性がどのように評価してきたのか。

まで見る価値はあると思う。

5-4 非モテ問題にも同じことが言える

非モテ男性について:

モテないのは本人の自己責任。

女性のせいにするな。

と言うことはできる。

当然、特定の女性へ交際を強制することはできない。

しかし:

女性側の選択傾向が男性側の繁殖格差の原因ではない。

とまで言えるかは別問題である。

今回のモデルでは、女性の選択基準そのものを変えると男性側の遺伝子分布が大幅に変化した。

つまり「選ぶ」という行為は単なる受動的な結果ではなく:

選ばれる側の競争条件を作る原因でもある。

5-5. 「男性のことは男性だけで解決しろ」は妥当なのか

今回の結果を見て僕が一番考えたいのはここである。

男性の:

  • 危険労働
  • 死亡
  • 競争
  • 非モテ
  • 社会的役割

について:

男性側で起きている問題なのだから、男性が自分たちで解決すればよい。

と女性側が拒絶するのは、本当に妥当なのだろうか。

少なくとも今回のモデルでは:

女性側の選択を変更すると、男性側の性質まで大幅に変わった。

もし現実社会でも似た因果関係が存在するなら、男性側だけを見ても問題は解けない。

見るべきなのは:

  • どんな男性が女性から評価されるのか
  • どんな男性が結婚や恋愛で報われるのか
  • 危険を冒す男性と避ける男性がどう評価されるのか
  • その評価規範を男女双方がどう作っているのか

ではないだろうか。

僕は:

「男性の問題は男性だけでやれ」

と因果関係の検討まで拒絶してしまうのは、不寛容な態度ではないかと思う。

6. 予想と違ったこと・この実験の限界

6-1. パンデミックだけではriskを維持できなかった

最初、僕は:

riskが低い
↓
戦闘を避ける
↓
弱い個体が残る
↓
パンデミックで死亡
↓
risk=0への進化を防げる

と考えていた。

しかし長期間回してみると、これは十分ではなかった。

ランダム交配で200世代回すと:

女性risk 0.010
男性risk 0.038

まで下がった。

つまり:

戦闘+パンデミックだけでは、riskを高く維持できなかった。

むしろ今回のモデルでは、性淘汰がriskを維持する効果が非常に大きかった。

6-2. 一個体を何度でも交配させると性淘汰が弱くなる

実装途中では、同じ個体を何度でも親にできる方式も試した。

しかし:

女性10体
男性50体

でも女性10体を何度でも使えるなら、男性50体の競争が成立しにくい。

そのため:

1交配フェーズにつき1個体1ペア

が重要だった。

繁殖コストとは単なる個体数ではなく:

有限の繁殖枠

として実装する必要があるらしい。

6-3. 現実の女性について証明したわけではない

今回確認したのはtoy model内の因果関係である。

この実験だけでは:

  • 現実の女性が高risk男性を好む
  • 現実の男性の攻撃性の主原因が女性である
  • 現実の徴兵や危険労働を女性の選好が作った
  • 現実の非モテ問題が遺伝子で説明できる
  • 女性が意識的に男性を危険へ追い込んでいる

とは言えない。

特に:

現実の女性が高risk男性を好む傾向がどの程度存在するのか。

は、現実のデータを使って別途検証する必要がある。

6-4. riskは現実の「攻撃性」そのものではない

今回のriskが表しているのは:

死亡リスクのある競争へ参加する傾向

である。

したがって:

risk
=
暴力性
=
犯罪性

ではない。

この記事では読みやすさのため「攻撃的」という言葉も使ったが、より正確には:

競争参加性・危険選好

くらいの概念である。

6-5. 「種全体の利益」は意識的な目的ではない

冒頭では:

「女性は種全体の利益のために、特定の男性を忌避する傾向が有意に存在する」と反論することは出来るだろう。

と書いた。

ただし、このモデルの女性個体が:

種全体を良くしよう。

と考えているわけではない。

モデル内で起きているのは:

ある男性を選ばない
↓
その男性は子孫を残しにくい
↓
集団全体の遺伝子頻度が変化する

という現象である。

つまり「種全体の利益」は女性個体の意識的目的ではなく:

個人的な配偶者選択が、集団レベルでは選別装置として働く。

という意味で読む必要がある。

7. まとめと次にやりたいこと

今回、300体の人工生命へ:

  • 日本語次文字予測NN
  • risk遺伝子
  • 配偶者選好遺伝子
  • グー・チョキ・パー
  • 戦闘
  • パンデミック
  • 性淘汰

を持たせた。

そして:

戦闘
↓
パンデミック
↓
交配

を何世代も繰り返した。

7-1. LLMは進化した

代表seedでは平均正答率が:

$$
2.0%\rightarrow50.9%
$$

まで改善した。

集団の多数回答も:

ありか゛と → う
すみませ   → ん
さようなら → [eos]

のように正解するようになった。

7-2. 男性riskも上昇した

基本条件では:

女性risk 0.086
男性risk 0.502

だった。

さらに女性側の選好だけを変えると:

女性が高risk男性好き
↓
男性risk 0.994

女性が低risk男性好き
↓
男性risk 0.003

ランダム交配
↓
男性risk 0.088

となった。

7-3. 性別を反転すると結果も反転した

繁殖コストを逆転して:

+性 = 男性
-性 = 女性

にすると、今度は+性riskの方が高くなった。

逆に繁殖コストを同じにすると、安定したrisk差はほぼ消えた。

したがって:

+性だから攻撃的なのでも、-性だから攻撃的なのでもない。

今回のモデルでは:

繁殖コストの低い側が、高繁殖コスト側の配偶者選択によって高risk化した。

という結果になった。

7-4. 今回一番言いたいこと

個々の女性が男性へ:

危険を冒せ。

と命令する必要はない。

単に:

「私はこういう男性を彼氏として選びたい」

という選択でも、それが集団全体で何世代も続けば:

選ばれる男性
↓
子を残す男性
↓
次世代に遺伝子を残す男性

を変える。

今回のtoy modelでは:

女性が高risk男性を選ぶ
↓
高risk男性が繁殖する
↓
低risk男性が繁殖できない
↓
男性集団のriskが高くなる

という流れが実際に生じた。

だから:

「男性が危険を冒しているのは男性社会の自業自得であり、女性には関係ない。」

と因果関係まで切ってしまうのは、自明ではない。

同じように:

「非モテ男性が存在するのは本人だけの問題であり、女性側の選択は原因ではない。」

とも言い切れない。

女性が悪いと言いたいわけではない。

しかし:

女性も因果関係の外にはいないのではないか。

今回の実験で一番投げかけたいのは、その問いである。

7-5. 次にやりたいこと

次はモデルをもう少し複雑にしたい。

LLMについては:

直前1文字
↓
4文字程度の文脈
↓
RNN
↓
小型Transformer

と発展させたい。

繁殖コストも:

20 : 100
30 : 100
40 : 100
...
100 : 100

と連続的に変えたい。

これによって:

繁殖コスト差が大きくなるほどrisk性差も大きくなるのか。

を調べられる。

さらに選好についても:

  • riskだけを見る
  • LLM性能だけを見る
  • riskとLLM性能の両方を見る
  • 完全ランダム

を比較したい。

最終的には:

配偶者選択のどの部分が、相手側のどの性質へ、どのくらいの進化圧を与えるのか。

まで分解してみたい。

8. 実験に使用したソースコード

以下が、今回の再現実験で固定して使用したコードである。各節を上から順番に1つのPythonファイルへ貼り付ければ実行できる。100世代実験は seed=0 ~ 19、200世代実験は seed=0 ~ 7 を固定して使用している。CSVはコード自身が出力する。

8-1. 日本語次文字予測課題と基本関数

import argparse
from dataclasses import dataclass, replace
from pathlib import Path
import hashlib
import numpy as np
import pandas as pd

kana = list(
    "あいうえお"
    "かきくけこ"
    "さしすせそ"
    "たちつてと"
    "なにぬねの"
    "はひふへほ"
    "まみむめも"
    "やゆよ"
    "らりるれろ"
    "わをん"
)
VOCAB = kana + ["゛", "゜", "[eos]"]
stoi = {c: i for i, c in enumerate(VOCAB)}
V = len(VOCAB)

PROMPT_TASKS = [
    ("ありか゛と", "う"),
    ("おはよ", "う"),
    ("すみませ", "ん"),
    ("さようなら", "[eos]"),
    ("いたた゛きま", "す"),
    ("こ゛ちそうさ", "ま"),
    ("おやす", "み"),
    ("たた゛い", "ま"),
    ("おかえ", "り"),
    ("はし゛めまし", "て"),
]
PREVS = np.array([stoi[p[-1]] for p, _ in PROMPT_TASKS], dtype=np.int16)
TARGETS = np.array([stoi[t] for _, t in PROMPT_TASKS], dtype=np.int16)


def score_W(W, idxs=None):
    if idxs is None:
        idxs = np.arange(len(PREVS))
    p = PREVS[idxs]
    t = TARGETS[idxs]
    logits = W[:, p, :]
    m = logits.max(axis=2, keepdims=True)
    logsumexp = m[:, :, 0] + np.log(np.exp(logits - m).sum(axis=2))
    correct = logits[
        np.arange(W.shape[0])[:, None],
        np.arange(len(idxs))[None, :],
        t[None, :],
    ]
    return (correct - logsumexp).mean(axis=1)


def acc_W(W):
    pred = W[:, PREVS, :].argmax(axis=2)
    return (pred == TARGETS[None, :]).mean(axis=1)

8-2. 設定

@dataclass(frozen=True)
class SimConfig:
    pop_per_hand_sex: int = 50   # each hand has 50 plus and 50 minus
    generations: int = 100
    combat_rounds: int = 20
    pandemic_n: int = 18
    p_plus: float = 0.2
    p_minus: float = 1.0
    phase_batch: int = 10
    w_init_sd: float = 0.08
    row_mut_prob: float = 0.12
    row_mut_sd: float = 0.18
    risk_mut_prob: float = 0.12
    risk_mut_sd: float = 0.06
    pref_mut_prob: float = 0.02
    risk_epsilon: float = 0.02
    mating_mode: str = "preference"   # preference or random
    force_plus_pref: int | None = None # 1 high-risk, 0 low-risk
    force_minus_pref: int | None = None

    @property
    def pop_per_hand(self):
        return self.pop_per_hand_sex * 2

8-3. 初期個体

def init_pop(cfg, rng):
    hands = []
    sexes = []
    for h in range(3):
        hands.extend([h] * cfg.pop_per_hand_sex)
        sexes.extend([0] * cfg.pop_per_hand_sex)
        hands.extend([h] * cfg.pop_per_hand_sex)
        sexes.extend([1] * cfg.pop_per_hand_sex)
    hand = np.asarray(hands, dtype=np.int8)
    sex = np.asarray(sexes, dtype=np.int8)
    N = len(hand)
    W = rng.normal(0, cfg.w_init_sd, size=(N, V, V)).astype(np.float32)
    risk = rng.uniform(0, 1, size=(N, 2)).astype(np.float32)
    pref = rng.integers(0, 2, size=(N, 2), dtype=np.int8)
    return {"hand": hand, "sex": sex, "W": W, "risk": risk, "pref": pref}


def subset(pop, mask):
    return {k: v[mask] for k, v in pop.items()}


def expressed_risk(pop):
    idx = np.arange(len(pop["sex"]))
    return pop["risk"][idx, pop["sex"]]


def expressed_pref(pop, cfg):
    idx = np.arange(len(pop["sex"]))
    out = pop["pref"][idx, pop["sex"]].copy()
    if cfg.force_plus_pref is not None:
        out[pop["sex"] == 0] = cfg.force_plus_pref
    if cfg.force_minus_pref is not None:
        out[pop["sex"] == 1] = cfg.force_minus_pref
    return out

8-4. 戦闘

def combat(pop, cfg, rng):
    N = len(pop["sex"])
    alive = np.ones(N, dtype=bool)
    used = np.zeros(N, dtype=bool)
    risks = expressed_risk(pop)

    for _ in range(cfg.combat_rounds):
        selected = []
        for h in range(3):
            candidates = np.flatnonzero(alive & (~used) & (pop["hand"] == h))
            if len(candidates) == 0:
                return subset(pop, alive)
            weights = risks[candidates] + cfg.risk_epsilon
            weights = weights / weights.sum()
            j = int(rng.choice(candidates, p=weights))
            used[j] = True
            selected.append(j)

        idxs = rng.integers(0, len(PREVS), cfg.phase_batch)
        scores = score_W(pop["W"][selected], idxs)
        rock, scissors, paper = selected
        dead = []
        if scores[0] > scores[1]:
            dead.append(scissors)
        if scores[1] > scores[2]:
            dead.append(paper)
        if scores[2] > scores[0]:
            dead.append(rock)
        for j in dead:
            alive[j] = False

    return subset(pop, alive)

8-5. パンデミック

def pandemic(pop, cfg, rng):
    n = min(cfg.pandemic_n, len(pop["sex"]) - 2)
    if n <= 0:
        return pop
    idxs = rng.integers(0, len(PREVS), cfg.phase_batch)
    scores = score_W(pop["W"], idxs)
    dead = np.argpartition(scores, n - 1)[:n]
    keep = np.ones(len(pop["sex"]), dtype=bool)
    keep[dead] = False
    return subset(pop, keep)

8-6. 婚活市場

def market(indices, frac, rng):
    indices = np.asarray(indices, dtype=np.int64)
    if len(indices) == 0:
        return indices
    if frac >= 1.0:
        return indices.copy()
    k = int(round(len(indices) * frac))
    k = max(1, min(k, len(indices)))
    return np.asarray(rng.choice(indices, k, replace=False), dtype=np.int64)

8-7. ペア形成

def form_pairs(pop, h, cfg, rng):
    plus = market(np.flatnonzero((pop["hand"] == h) & (pop["sex"] == 0)), cfg.p_plus, rng)
    minus = market(np.flatnonzero((pop["hand"] == h) & (pop["sex"] == 1)), cfg.p_minus, rng)
    if len(plus) == 0 or len(minus) == 0:
        return []
    k = min(len(plus), len(minus))

    if cfg.mating_mode == "random":
        pa = rng.permutation(plus)[:k]
        pb = rng.permutation(minus)[:k]
        return list(zip(pa.tolist(), pb.tolist()))

    trait = expressed_risk(pop)
    # Standardize globally so preference score scale is stable.
    z = (trait - trait.mean()) / (trait.std() + 1e-12)
    pref = expressed_pref(pop, cfg)

    # total mutual evaluation for every candidate pair
    # plus's evaluation of minus + minus's evaluation of plus
    M = ((2 * pref[plus] - 1)[:, None] * z[minus][None, :])
    M += (z[plus][:, None] * (2 * pref[minus] - 1)[None, :])
    M += rng.normal(0, 1e-10, size=M.shape)

    # Sort all pair candidates by score once; greedily keep disjoint pairs.
    flat_order = np.argsort(M.ravel())[::-1]
    used_p = np.zeros(len(plus), dtype=bool)
    used_m = np.zeros(len(minus), dtype=bool)
    pairs = []
    for q in flat_order:
        i, j = divmod(int(q), len(minus))
        if used_p[i] or used_m[j]:
            continue
        pairs.append((int(plus[i]), int(minus[j])))
        used_p[i] = True
        used_m[j] = True
        if len(pairs) >= k:
            break
    return pairs

8-8. 遺伝と突然変異

def make_child(pop, i, j, hand, sex, cfg, rng):
    choose_rows = rng.random(V) < 0.5
    W = np.where(choose_rows[:, None], pop["W"][i], pop["W"][j]).copy()
    mutate_rows = rng.random(V) < cfg.row_mut_prob
    if mutate_rows.any():
        W[mutate_rows] += rng.normal(
            0, cfg.row_mut_sd, size=(int(mutate_rows.sum()), V)
        ).astype(np.float32)

    risk = np.empty(2, dtype=np.float32)
    pref = np.empty(2, dtype=np.int8)
    for locus in range(2):
        risk[locus] = pop["risk"][i, locus] if rng.random() < 0.5 else pop["risk"][j, locus]
        if rng.random() < cfg.risk_mut_prob:
            risk[locus] = np.float32(np.clip(risk[locus] + rng.normal(0, cfg.risk_mut_sd), 0, 1))
        pref[locus] = pop["pref"][i, locus] if rng.random() < 0.5 else pop["pref"][j, locus]
        if rng.random() < cfg.pref_mut_prob:
            pref[locus] = np.int8(1 - pref[locus])

    return np.int8(hand), np.int8(sex), W, risk, pref

8-9. 死亡者を子供で補充(各手×性の50枠を維持)

def reproduce(pop, cfg, rng):
    children = []
    for h in range(3):
        pairs = form_pairs(pop, h, cfg, rng)
        plus_all = np.flatnonzero((pop["hand"] == h) & (pop["sex"] == 0))
        minus_all = np.flatnonzero((pop["hand"] == h) & (pop["sex"] == 1))
        if len(plus_all) == 0 or len(minus_all) == 0:
            raise RuntimeError("A sex went extinct before reproduction; fixed sex slots require both sexes to survive.")
        if not pairs:
            # Deterministic model rule for the rare case that a sampled market is empty.
            pairs = [(int(rng.choice(plus_all)), int(rng.choice(minus_all)))]

        for sex in (0, 1):
            current = int(np.sum((pop["hand"] == h) & (pop["sex"] == sex)))
            need = cfg.pop_per_hand_sex - current
            if need < 0:
                raise RuntimeError("Sex-slot count exceeded target")
            if need == 0:
                continue
            chosen = rng.integers(0, len(pairs), size=need)
            for q in chosen:
                i, j = pairs[int(q)]
                children.append(make_child(pop, i, j, h, sex, cfg, rng))

    if not children:
        return pop

    return {
        "hand": np.concatenate([pop["hand"], np.array([c[0] for c in children], dtype=np.int8)]),
        "sex": np.concatenate([pop["sex"], np.array([c[1] for c in children], dtype=np.int8)]),
        "W": np.concatenate([pop["W"], np.stack([c[2] for c in children])], axis=0),
        "risk": np.concatenate([pop["risk"], np.stack([c[3] for c in children])], axis=0),
        "pref": np.concatenate([pop["pref"], np.stack([c[4] for c in children])], axis=0),
    }

8-10. 統計量

def metrics(pop, cfg):
    accuracy = acc_W(pop["W"])
    risk = expressed_risk(pop)
    pref = expressed_pref(pop, cfg)
    sex = pop["sex"]
    return {
        "acc": float(accuracy.mean()),
        "acc_plus": float(accuracy[sex == 0].mean()),
        "acc_minus": float(accuracy[sex == 1].mean()),
        "risk_plus": float(risk[sex == 0].mean()),
        "risk_minus": float(risk[sex == 1].mean()),
        "pref_plus": float(pref[sex == 0].mean()),
        "pref_minus": float(pref[sex == 1].mean()),
    }

8-11. 1 seedを実行

def run(seed, cfg, keep_initial=False):
    rng = np.random.default_rng(seed)
    pop = init_pop(cfg, rng)
    initial = {k: v.copy() for k, v in pop.items()} if keep_initial else None
    initial_metrics = metrics(pop, cfg)
    for _ in range(cfg.generations):
        pop = combat(pop, cfg, rng)
        pop = pandemic(pop, cfg, rng)
        pop = reproduce(pop, cfg, rng)
        # Invariant: 50 plus and 50 minus for every hand.
        for h in range(3):
            for s in (0, 1):
                n = int(np.sum((pop["hand"] == h) & (pop["sex"] == s)))
                if n != cfg.pop_per_hand_sex:
                    raise RuntimeError(f"population invariant failed: hand={h}, sex={s}, n={n}")
    return initial_metrics, metrics(pop, cfg), initial, pop

8-12. 複数seedと95%CI

def run_condition(name, cfg, seeds):
    rows = []
    for seed in seeds:
        initial_m, final_m, _, _ = run(seed, cfg)
        row = {"condition": name, "seed": seed, **{f"initial_{k}": v for k, v in initial_m.items()}, **final_m}
        row["risk_gap_minus_plus"] = row["risk_minus"] - row["risk_plus"]
        rows.append(row)
    return pd.DataFrame(rows)


def ci95(values):
    a = np.asarray(values, dtype=float)
    mean = float(a.mean())
    if len(a) < 2:
        return mean, np.nan, np.nan
    se = float(a.std(ddof=1) / np.sqrt(len(a)))
    return mean, mean - 1.96 * se, mean + 1.96 * se

8-13. 実験条件

def build_conditions(generations=100):
    base = SimConfig(generations=generations)
    return {
        "main": base,
        "random": replace(base, mating_mode="random"),
        "female_high": replace(base, force_plus_pref=1),
        "female_low": replace(base, force_plus_pref=0),
        "reversed": replace(base, p_plus=1.0, p_minus=0.2),
        "equal_20": replace(base, p_plus=0.2, p_minus=0.2),
        "equal_50": replace(base, p_plus=0.5, p_minus=0.5),
        "equal_100": replace(base, p_plus=1.0, p_minus=1.0),
    }

8-14. 100世代実験

def run_100(outdir):
    seeds = list(range(20))
    all_rows = []
    for name, cfg in build_conditions(100).items():
        df = run_condition(name, cfg, seeds)
        df.to_csv(outdir / f"100gen_{name}.csv", index=False)
        all_rows.append(df)
    raw = pd.concat(all_rows, ignore_index=True)
    raw.to_csv(outdir / "100gen_all_conditions.csv", index=False)
    return raw

8-15. 200世代実験

def run_200(outdir):
    seeds = list(range(8))
    conds = build_conditions(200)
    names = ["main", "random", "female_high", "female_low"]
    all_rows = []
    for name in names:
        df = run_condition(name, conds[name], seeds)
        df.to_csv(outdir / f"200gen_{name}.csv", index=False)
        all_rows.append(df)
    raw = pd.concat(all_rows, ignore_index=True)
    raw.to_csv(outdir / "200gen_all_conditions.csv", index=False)
    return raw

8-16. 集計と回答例

def summarize(raw, generation_label):
    rows = []
    for name, g in raw.groupby("condition", sort=False):
        gap_mean, gap_lo, gap_hi = ci95(g["risk_gap_minus_plus"])
        rows.append({
            "generation_set": generation_label,
            "condition": name,
            "n_seed": len(g),
            "acc_mean": g["acc"].mean(),
            "risk_plus_mean": g["risk_plus"].mean(),
            "risk_minus_mean": g["risk_minus"].mean(),
            "risk_gap_minus_plus_mean": gap_mean,
            "risk_gap_ci95_low": gap_lo,
            "risk_gap_ci95_high": gap_hi,
            "pref_plus_mean": g["pref_plus"].mean(),
            "pref_minus_mean": g["pref_minus"].mean(),
        })
    return pd.DataFrame(rows)


def majority_predictions(pop):
    predictions = pop["W"][:, PREVS, :].argmax(axis=2)
    result = []
    for task_i, (prompt, answer) in enumerate(PROMPT_TASKS):
        vals, counts = np.unique(predictions[:, task_i], return_counts=True)
        winner = vals[np.argmax(counts)]
        result.append({
            "prompt": prompt,
            "correct": answer,
            "majority_prediction": VOCAB[int(winner)],
            "majority_share": float(counts.max() / len(predictions)),
        })
    return pd.DataFrame(result)


def make_examples(outdir):
    cfg = build_conditions(200)["main"]
    initial_m, final_m, initial, final = run(0, cfg, keep_initial=True)
    ini = majority_predictions(initial)
    ini.insert(0, "stage", "initial")
    fin = majority_predictions(final)
    fin.insert(0, "stage", "200gen")
    ex = pd.concat([ini, fin], ignore_index=True)
    ex.to_csv(outdir / "prediction_examples_seed0.csv", index=False)
    pd.DataFrame([{"stage":"initial", **initial_m}, {"stage":"200gen", **final_m}]).to_csv(outdir / "prediction_metrics_seed0.csv", index=False)
    return ex

8-17. main: CSV出力と再現確認用ハッシュ

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--outdir", default="sim4_results")
    parser.add_argument("--part", choices=["100", "200", "examples", "all"], default="all")
    args = parser.parse_args()
    outdir = Path(args.outdir)
    outdir.mkdir(parents=True, exist_ok=True)

    summaries = []
    if args.part in ("100", "all"):
        raw100 = run_100(outdir)
        s100 = summarize(raw100, "100")
        s100.to_csv(outdir / "summary_100gen.csv", index=False)
        summaries.append(s100)
    if args.part in ("200", "all"):
        raw200 = run_200(outdir)
        s200 = summarize(raw200, "200")
        s200.to_csv(outdir / "summary_200gen.csv", index=False)
        summaries.append(s200)
    if args.part in ("examples", "all"):
        make_examples(outdir)
    if summaries:
        pd.concat(summaries, ignore_index=True).to_csv(outdir / "summary_all.csv", index=False)

    # hash all CSVs, useful for exact rerun verification
    hashes = []
    for p in sorted(outdir.glob("*.csv")):
        h = hashlib.sha256(p.read_bytes()).hexdigest()
        hashes.append({"file": p.name, "sha256": h})
    pd.DataFrame(hashes).to_csv(outdir / "sha256.csv", index=False)

    print(f"done: {outdir.resolve()}")


if __name__ == "__main__":
    main()

また、結果 csv などは

で公開している。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?