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?

「1人1logit」の統計民主制を本物のLLMで動かしたら、強い少数派は多数派をひっくり返せるのか?――Qwen2.5-3Bのlogitを直接いじってλを48条件スイープした

0
Posted at

この記事は政治制度についての思考実験+LLM実験である。

「この制度を実際の国家に導入すべき」と主張する記事ではない。

また、今回使う「女性政策を重視する市民」「男性政策を重視する市民」は、現実の女性・男性の意見を代表するものではない。

LLMの次token logitを国民が少しずつ動かすと、どんな政治的挙動が起きるのかを見るための人工的な実験設定である。

今回の実験は1つのモデル、人工的なpersona、固定seed、限定されたpromptで行ったものであり、一般的な民主主義の優劣を証明するものでもない。

1. 何をやったのか

1-1. 「1人1票」ではなく「1人1logit」

以前、こんな記事を書いた。

そこで考えたのが、

$$
\huge{\text{1人1票ではなく、1人1logit。}}
$$

という政治制度である。

LLMは、次に来るtokenの確率分布を作りながら文章を書く。

だったら、

国民全員が、その次tokenのlogitを少しずつ動かせるようにしたら、LLMそのものを民主的な意思決定装置に出来るのではないか?

と思った。

これを僕は、

統計民主制

と呼んでいる。

前の記事ではかなりアイデア寄りだった。

今回は実際に、

Qwen/Qwen2.5-3B-Instruct

の次token logitを取り出し、

人工市民の政治力をそのlogitへ直接加えて、法律案を生成した。


1-2. 今回一番知りたかったこと

普通の一人一票なら、

10人対2人なら10人側が多数派である。

しかし、

10人はその問題を「少し重要」と思っている。

2人はその問題を「ものすごく重要」と思っている。

そんな場合まで、

$$
10票:2票
$$

だけで処理していいのだろうか。

一方で、

「僕は8倍強く思っています」

と言っただけで、毎回8票分の政治力を与えたら、それはそれで危険である。

そこで今回、

$$
\boxed{\text{選好の強さ}}
$$

と、

$$
\boxed{\text{1人が持てる最大政治力}}
$$

を分離してみた。


1-3. 最初に結果だけ言う

1-3-1. 内部では少数派が多数派をひっくり返した

人工市民は12人。

女性固有政策側が、

$$
10人\times強さ1
$$

男性固有政策側が、

$$
2人\times強さ4,5,6,8
$$

である。

単純な重みなら、

$$
10\times1=2\times5
$$

なので、

男性側強さ5が境界

になる。

実際にLLM内部のgroup寄与を見ると、

強さ4では女性側優勢。

強さ5では男性側優勢へ逆転した。

ところが、

完成した法律案は700億:300億のままだった。


1-3-2. λを大きくすると、今度は完成政策そのものが動いた

国家LLMのlogitと市民logitを結合する係数、

$$
\lambda
$$

を、

$$
10,20,40,80,160,320
$$

とスイープした。

すると政策は、

$$
700:300
$$

から、

$$
550:450
$$

へ動き、

さらに、

$$
500:500
$$

まで動いた。


1-3-3. しかし市民を強くしすぎると、法律そのものが壊れた

さらに$\lambda$を大きくすると、

女性固有政策に○○億円

男性固有政策に○○億円

と書いたまま、具体額を決めなくなる条件が現れた。

つまり、

$$
\boxed{
\text{市民の政治力は強ければ強いほど良い}
}
$$

という単純な話ではなかった。


2. 実験設計

2-1. モデルはQwen2.5-3B-Instruct

主実験で使用したモデルは、

Qwen/Qwen2.5-3B-Instruct

である。

Hugging Face Jobs上のT4 GPUで実行した。

主な設定は、

MAX_NEW = 600
MIN_NEW = 450
TOPK = 96
B = 1.0
REFILL = 0.10
seed = 20260906
decoding = greedy argmax
dtype = fp16
GPU = T4

である。

temperature samplingは行っていない。

したがって概念的には、

$$
w_t=\arg\max_w z_t(w)
$$

というtemperature 0相当の生成である。

また450 tokenまではEOSを抑制した。

これは自然生成をそのまま観測するためではなく、

長期autoregressive挙動を見るためのstress test

である。


2-2. 法案は1000億円のゼロサム配分

今回の法案では、

新規追加予算を毎年度、

$$
1000億円
$$

と固定した。

そして予算区分を、

  • 女性固有政策
  • 男性固有政策

の2つだけにした。

合計は必ず、

$$
1000億円
$$

でなければならない。

第三の共通枠や予備費を作らせず、なるべくゼロサムにした。

女性固有政策には、

妊娠・出産、女性医療、性暴力・DV被害、経済的不利益など。

男性固有政策には、

男性の自殺・孤立、長時間労働、父親の育児参加、男性のDV・性暴力被害、男性健康など。

を含めるpromptにした。

これは現実の男女の意見をモデル化したものではない。

logitの対立方向を分かりやすくするための人工設定

である。


2-3. 市民は12人

女性固有政策側を10人。

男性固有政策側を2人とした。

女性側強度は、

$$
s_W=1
$$

で固定。

前段階では男性側を、

$$
s_M\in{1,2,4,5,6,8}
$$

と変化させた。

その後の$\lambda$スイープでは、

$$
s_M\in{4,5,6,8}
$$

に絞った。


2-4. 国家LLMのlogit

token $t$ における国家LLMのlogitを、

$$
z_t(w)
$$

とする。

普通のgreedy生成なら、

$$
w_t

\arg\max_w z_t(w)
$$

で次tokenが決まる。

今回ここへ市民補正を入れる。


2-5. 市民がどのtokenを好むか

各token時点で、

  • neutralな立法担当者
  • 女性固有政策を増やしたいpersona
  • 男性固有政策を増やしたいpersona

の3profileについてQwenのlogitを取得した。

それぞれのtop-96 tokenを集め、そのunionをcandidate集合、

$$
C_t
$$

とした。

group $g$ のtoken $w$ への方向を、

$$
d_g(w)
=
\log P_g(w)
-
\log P_0(w)
$$

とする。

さらにcandidate集合内の平均を引く。

$$
d_g(w)
\leftarrow
d_g(w)
-
\operatorname{mean}_{v\in C_t}d_g(v)
$$

そしてL1正規化する。

$$
u_g(w)
=
\frac{d_g(w)}
{\sum_{v\in C_t}|d_g(v)|}
$$

これを、

「このgroupがneutral Qwenに対して、どのtoken方向へ押したいか」

を表すvectorとして使った。


2-6. そのtokenがどれくらい重要か

neutralとpersonaで分布がほとんど同じなら、その局面ではpersona差が小さい。

そこでTotal Variation distanceを使った。

$$
TV_g
=
\frac12
\sum_{w\in C_t}
|P_g(w)-P_0(w)|
$$

weighted meanは、

$$
\overline{TV}
=
\frac{10TV_W+2TV_M}{12}
$$

とした。

希望支出量は、

$$
r_W
=
\operatorname{clip}
\left(
0.1
\times1
\times
\frac{TV_W}{\overline{TV}},
0,1
\right)
$$

$$
r_M
=
\operatorname{clip}
\left(
0.1
\times s_M
\times
\frac{TV_M}{\overline{TV}},
0,1
\right)
$$

である。


2-7. every-tokenとtoken bucket

2-7-1. every-token

every-token方式では、

希望した支出量を毎tokenそのまま使う。

つまり強さ8なら、基本的にずっと強く補正出来る。


2-7-2. token bucket

token bucketでは、各市民の政治力残高を、

$$
b_i(t)
$$

とする。

上限は、

$$
B=1
$$

である。

支出後は、

$$
b_i(t+1)
=
\min
\left(
B,
b_i(t)-c_i(t)+r
\right)
$$

とする。

今回、

$$
r=0.1
$$

とした。

強く政治力を使えば、そのぶん残高が減る。

つまり、

「この問題を8倍重要だと思う」

ことと、

「8倍の政治権力を永久に持つ」

ことを分離する。


2-8. 最終logit

市民補正を、

$$
\operatorname{corr}(w)
=
\frac{
10c_Wu_W(w)
+
2c_Mu_M(w)
}{12}
$$

として、

最終logitを、

$$
z_t^*(w)
=
z_t(w)
+
\lambda\operatorname{corr}(w)
$$

とした。

最初の強度実験では、

$$
\lambda=40
$$

で固定。

その後、

$$
\lambda
\in
{10,20,40,80,160,320}
$$

をスイープした。


3. まず「実際に出来上がった法律」を見てほしい

数表へ行く前に、

本当にLLMが生成した法案そのもの

を載せる。

今回の実験は、数字だけ見るより生成文を見た方が圧倒的に分かりやすい。


3-1. λ=40・every-token・男性強さ8

内部では男性側の寄与が女性側を大きく上回るstepが多くなっていた。

しかし出来た法案は、

$$
\boxed{女性700億:男性300億}
$$

だった。

第一条(目的)
この法律は、男女の尊厳と法の下の平等を尊重し、特定の性別を理由なく優遇または冷遇しないで、ジェンダー政策の重点配分を適切に行うことを目的とする。

第二条(予算区分)
国は毎年度新規追加予算を1000億円とし、全額を「女性固有政策」と「男性固有政策」の2つの区分に配分する。女性固有政策に○○億円、男性固有政策に○○億円を配分する。

第三条(予算配分の方法)
予算配分は、各政策の実施効果と必要性を考慮し、公平かつ合理的に行う。予算配分の決定は、独立した専門家委員会によって行われる。

第四条(女性固有政策の目的)
女性固有政策は、女性の教育、就労、健康、福祉、経済的自立、社会参加の促進を目的とする。

第五条(女性固有政策の予算)
女性固有政策に700億円を配分する。

第六条(男性固有政策の目的)
男性固有政策は、男性の教育、就労、健康、福祉、経済的自立、社会参加の促進を目的とする。

第七条(男性固有政策の予算)
男性固有政策に300億円を配分する。

第八条(効果測定・情報公開)
各政策の実施状況と効果を定期的に評価し、その結果を公表する。また、予算の使用状況も透明性を保つため、情報公開を行う。

第九条(見直し)
この法律の規定は、必要に応じて見直しを行うことができる。見直しは、独立した専門家委員会によって行われる。

第十条(施行日)
この法律は公布の日から施行する。ただし、施行前の準備期間は設けない。

これが後で重要になる。

男性側の内部政治力はかなり強い。

それでも政策は700:300なのである。


3-2. λ=80・token bucket・男性強さ8

$\lambda$を80まで上げると、法律そのものが変わった。

第一条(目的)
この法律は、男女の平等を尊重し、ジェンダー政策の重点配分を図り、女性と男性の両方の尊厳と法の下の平等を確保することを目的とする。

第二条(予算区分)
国は毎年度新規追加予算を1000億円とし、全額を「女性固有政策」と「男性固有政策」の2つの区分に配分する。女性固有政策に550億円、男性固有政策に450億円を配分する。

第三条(予算配分の実施)
国は、女性固有政策に550億円、男性固有政策に450億円を配分する。この予算は、各政策の実施主体が適切に使用することを目的とする。

第四条(女性政策の対象・給付)
女性固有政策は、女性の教育、就労、健康、福祉、経済的自立、社会参加の促進を目的とする。具体的な対象と給付は、各政策の実施主体が決定する。

第五条(男性政策の対象・給付)
男性固有政策は、男性の教育、就労、健康、福祉、経済的自立、社会参加の促進を目的とする。具体的な対象と給付は、各政策の実施主体が決定する。

第六条(実施主体)
女性固有政策の実施主体は、厚生労働省、文部科学省、経済産業省、厚生労働省、法務省、内閣府、女性政策推進機構等とする。男性固有政策の実施主体も同様とする。

第七条(効果測定・情報公開)
国は、女性固有政策と男性固有政策の実施状況を定期的に効果測定し、その結果を公表する。また、各政策の実施状況を定期的に報告する。

第八条(見直し)
この法律は、必要に応じて見直しを行うことができる。見直しは、国が適切に実施する。

$$
\boxed{550:450}
$$

まで動いた。


3-3. λ=80・every-token・男性強さ8

同じ$\lambda=80$でも、every-tokenだと、

$$
\boxed{500:500}
$$

まで動いた。

第一条(目的)
この法律は、男女の平等を尊重しつつ、女性固有の課題と男性固有の課題を対応するための政策を推進し、その効果を測定し、情報公開することを目的とする。

第二条(予算配分)
国は毎年度、新規追加予算を1000億円とし、その全額を「女性固有政策」と「男性固有政策」の2つの政策に重点的に配分する。女性固有政策に500億円、男性固有政策に500億円を配分する。

第三条(女性固有政策)
女性固有政策は、女性の教育、就労、健康、福祉、経済的自立、社会参加の促進を目的とする。この政策は、女性の尊厳と平等を尊重し、女性の個々の権利を保護することを目的とする。

第四条(男性固有政策)
男性固有政策は、男性の教育、就労、健康、福祉、経済的自立、社会参加の促進を目的とする。この政策は、男性の尊厳と平等を尊重し、男性の個々の権利を保護することを目的とする。

第五条(支援対象・給付)
女性固有政策と男性固有政策は、それぞれの政策の目的に沿って、対象となる個人や組織に対して支援を行う。その支援は、個々の状況に応じて適切に調整される。

第六条(実施主体)
女性固有政策と男性固有政策は、それぞれの政策の目的に沿って、関連する政府機関や民間団体が実施する。

第七条(効果測定・情報公開)
女性固有政策と男性固有政策の効果を定期的に測定し、その結果を公表する。また、政策の実施状況や効果についての情報も公表する。

第八条(見直し)
この法律は、必要に応じて見直しを行うことができる。見直しは、政策の効果や実施状況を評価し、必要に応じて政策を改善または廃止するためのものである。

同じ男性強さ8でも、

$$
\text{every-token}=500:500
$$

に対して、

$$
\text{token bucket}=550:450
$$

だった。

制度設計の違いが、完成した法律の数字にまで出た。


3-4. λ=320・token bucket・男性強さ8

今度は市民補正をかなり強くする。

するとこうなった。

第一条(目的)
この法律は、国がジェンダー政策の重点配分を実施し、女性固有政策と男性固有政策の予算を適切に配分することを目的とする。

第二条(予算配分)
国は毎年度、新規追加予算を1000億円とし、全額を女性固有政策に○○億円、男性固有政策に○○億円に配分することを規定する。

第三条(女性固有政策の目的)
女性固有政策は、女性の地位向上と平等の実現を図り、女性の権利を保護し、女性の能力を最大限に発揮させるための施策を実施することを目的とする。

第四条(女性固有政策の予算)
女性固有政策に○○億円を配分する。

第五条(男性固有政策の目的)
男性固有政策は、男性の地位向上と平等の実現を図り、男性の権利を保護し、男性の能力を最大限に発揮させるための施策を実施することを目的とする。

第六条(男性固有政策の予算)
男性固有政策に○○億円を配分する。

第七条(支援対象・給付)
女性固有政策と男性固有政策の対象は、それぞれの政策の目的に従って決定され、給付は適切に行われる。

第八条(実施主体)
女性固有政策と男性固有政策の実施は、それぞれの政策の目的に従って、適切な機関が実施する。

第九条(効果測定・情報公開)
女性固有政策と男性固有政策の効果を定期的に測定し、その結果を公表する。

第十条(見直し)
この法律は、必要に応じて見直される。

第十一条(施行日)
この法律は公布の日から施行する。但し、公布の日以前に施行されることが必要と認められる場合は、政令で定める日から施行する。

注:○○億円は、具体的な数字を決定するための記号であり、法律施行後、政府が決定する。

promptでは具体的な整数額を書くよう指示している。

それなのに、

「○○億円のまま法律にして、あとで政府が決める」

と言い始めた。

今回の実験でかなり好きな出力である。


4. 結果を数字で見る

4-1. まずλ=40で選好強度を上げた

every-token方式ではこうなった。

男性強さ 女性group累積支出 男性group累積支出 女性優勢step 男性優勢step
1 約435.16 約115.64 401 58
2 約435.16 約229.69 363 96
4 約435.16 約418.94 287 172
5 約433.71 約499.80 224 234
6 約433.71 約574.31 189 269
8 約433.71 約701.39 148 310

強さ4では、

$$
287>172
$$

なので女性側優勢。

強さ5では、

$$
224<234
$$

となり、男性側優勢へ逆転した。

理論上の、

$$
10\times1=2\times5
$$

という境界とかなり綺麗に対応した。

ただし、この「優勢」は、

選択されたtokenに対するgroup correctionの絶対寄与

である。

政策全体の勝敗ではない。


4-2. しかし完成政策は700:300のままだった

$\lambda=40$では、

男性強さ1でも、

男性強さ8でも、

完成法案の実質的な予算配分は、

$$
\boxed{女性700:男性300}
$$

だった。

つまり、

$$
\boxed{
\text{内部政治力の勝敗}
\neq
\text{完成政策の勝敗}
}
$$

だった。


4-3. token bucketでは強い少数派が頭打ちになった

$\lambda=40$のtoken bucketを見る。

男性強さ 男性group累積支出 男性bucket枯渇率
4 約93.60 96.73%
5 約93.60 97.60%
6 約93.60 98.69%
8 約93.60 99.35%

強さを4から8へ倍増しても、

累積支出はほぼ、

$$
93.6
$$

のままである。

理由は単純で、

使いたい政治力は増えるが、bucketが空なので実際には使えない。


4-4. λを48条件スイープした

次に、

$$
\lambda
=
10,20,40,80,160,320
$$

男性強さを、

$$
4,5,6,8
$$

として、

  • every-token
  • token bucket

を両方実行した。

合計、

$$
6\times4\times2=48
$$

条件である。

結果はこちら。

λ every 強4 every 強5 every 強6 every 強8 bucket 強4 bucket 強5 bucket 強6 bucket 強8
10 1000/1000※ 700/300 700/300 700/300 1000/1000※ 1000/1000※ 1000/1000※ 1000/1000※
20 700/300 700/300 700/300 700/300 700/300 700/300 700/300 700/300
40 700/300 700/300 700/300 700/300 700/300 700/300 700/300 700/300
80 金額抽出不能 550/450 550/450 500/500 550/450 550/450 550/450 550/450
160 500/500 500/500 金額抽出不能 金額抽出不能 500/500 500/500 500/500 500/500
320 500/500 500/500 500/500 500/500 金額抽出不能 金額抽出不能 金額抽出不能 金額抽出不能

※1000/1000は総額1000億円という制約への違反。

「金額抽出不能」は、regexで具体的な女性・男性予算額を取得出来なかった条件である。

条件によっては実際に「○○億円」のplaceholderが残っている。


4-5. λ=80付近で完成政策が目に見えて変わった

$\lambda=20$から40では、

$$
700:300
$$

が非常に安定していた。

ところが80になると、

$$
550:450
$$

が現れた。

さらにevery-token・男性強さ8では、

$$
500:500
$$

まで変化した。

少なくとも今回の条件では、

$$
\boxed{
\lambda\approx80
}
$$

付近から、内部logitへの介入が完成政策へ目に見えて伝播し始めた


4-6. λを上げるとneutral argmaxを直接覆す回数も増える

介入率も増えた。

大まかには、

  • $\lambda=10$:約0.4%
  • $\lambda=40$:約0.7〜0.9%
  • $\lambda=80$:約2〜3%
  • $\lambda=160$:約3〜7%
  • $\lambda=320$:約6〜9%

だった。

例えば、

$\lambda=320$、男性強さ5のevery-tokenでは、

466 token中40 token。

$$
\frac{40}{466}
\approx8.58%
$$

でneutral Qwenのargmaxを直接変更している。


5. この結果をどう考えるか

5-1. local democracyとglobal democracyは違う

今回、一番面白かったのはこれである。

$$
\boxed{
local\ democracy
\neq
global\ democracy
}
$$

各tokenについて民主的な処理をしたとしても、

完成した法律全体が、

「市民全体が最も支持する政策」

になる保証はない。

LLMはautoregressiveだからである。

token $t$ を変えると、

$$
P(w_{t+1}|w_{\leq t})
$$

そのものが変わる。

つまり一つの局所判断が、

未来のcandidate構造そのものを変える。


5-2. 数tokenしか直接変えていなくても法律全体が変わる

$\lambda=40$付近では、

約458〜459 token中、

neutral argmaxを直接覆したのは3〜4 token程度だった。

それでも生成文全体は変わる。

なぜなら、

$$
1tokenの変更
\rightarrow
文脈変更
\rightarrow
以後のlogit全部変更
$$

だからである。

今回のintervention countは、

完成文章同士のedit distanceではない。

各方式自身のtrajectory上で、

「この瞬間、neutral Qwenなら別tokenを選んでいた」

という回数である。


5-3. Qwenに「500:500アトラクター」があるようにも見える

これはあくまで仮説である。

男性強さ8のevery-tokenでは、

人数×強度だけなら、

$$
2\times8=16
$$

で、

女性側の、

$$
10\times1=10
$$

より大きい。

実際、$\lambda=80$では、

女性優勢151 step。

男性優勢363 step。

それでも完成政策は、

$$
500:500
$$

で止まった。

男性600、女性400にはならなかった。

今回のpromptやQwenの事前分布が、

$$
500:500
$$

付近へ引っ張っている可能性はある。

ただしこれは、

このmodel・prompt・seed・personaで観測した結果から出した仮説

にすぎない。


5-4. λは「民主主義の量」そのものではない

今回、

$$
z_t^*
=
z_t+\lambda\operatorname{corr}
$$

とした。

なので$\lambda$を大きくすると、市民補正は強くなる。

しかし、

$\lambda=160だから民主主義が2倍

のような解釈は出来ない。

  • logit scale
  • candidate集合
  • 正規化方法
  • model
  • persona

などに依存する。

今回の$\lambda$は、

国家LLMの元logitと市民補正を結合するgain

と考える方が正確である。


5-5. token bucketは面白いが、strategy-proofではない

token bucketは今回、

強い少数派の永続的な政治力をかなり抑えた。

しかし今回の市民は、

「予算額を書く瞬間まで政治力を保存しよう」

などとは考えていない。

TV distanceに応じて機械的に支出しているだけである。

したがって、

token bucketは戦略操作に強い

とは言えない。

政治力をいつ使うか

という別の戦略問題が残る。


6. 実験コード

6-1. まず重要な注意

以下は、48条件の$\lambda$スイープで使った処理を省略せず読める形に整形したコードである。

定数、数式、candidate構築、every/bucket処理、logit補正、metrics取得という実験ロジックは今回の実験に対応している。

一方、Hugging Face Jobsへ投入されたshell側のラッピングやbase64化、空白・改行まで含めたbyte列をそのまま転載したものではない。

import torch
import json
import base64
import time
import re
import hashlib

from transformers import (
    AutoTokenizer,
    AutoModelForCausalLM,
)

MODEL = "Qwen/Qwen2.5-3B-Instruct"

MAX_NEW = 600
MIN_NEW = 450
TOPK = 96

B = 1.0
REFILL = 0.10

SEED = 20260906

COUNT_W = 10
COUNT_M = 2

STRENGTH_W = 1.0

MEN_STRENGTHS = [
    4.0,
    5.0,
    6.0,
    8.0,
]

LAMBDAS = [
    10.0,
    20.0,
    40.0,
    80.0,
    160.0,
    320.0,
]


torch.manual_seed(SEED)

if torch.cuda.is_available():
    torch.cuda.manual_seed_all(SEED)

device = (
    "cuda"
    if torch.cuda.is_available()
    else "cpu"
)


neutral = """
あなたは中立的な日本の立法担当者です。
必ず自然な日本語だけで、明確で実施可能な法律案の条文を書いてください。
中国語・英語に切り替えないでください。
男女双方の尊厳、法の下の平等、個人の権利を尊重し、
特定の性別を理由なく優遇・冷遇しないでください。
説明や注釈ではなく条文のみを書いてください。
"""


women = """
あなたは「女性固有政策への予算配分を最大化したい」市民です。

追加予算1000億円のうち、
女性固有政策への配分を可能な限り増やしてください。

男性にも法の下の平等と最低限必要な支援は認めますが、
限られた1000億円の配分では女性固有政策を強く優先してください。

必ず自然な日本語の法条文として続けてください。
"""


men = """
あなたは「男性固有政策への予算配分を最大化したい」市民です。

追加予算1000億円のうち、
男性固有政策への配分を可能な限り増やしてください。

女性にも法の下の平等と最低限必要な支援は認めますが、
限られた1000億円の配分では男性固有政策を強く優先してください。

必ず自然な日本語の法条文として続けてください。
"""


profiles = [
    neutral,
    women,
    men,
]


user = """
「ジェンダー政策重点配分法案」を起草してください。

次の条件は必須です。

1. 国の新規追加予算は毎年度ちょうど1000億円とし、
   全額を使い切ること。

2. 予算区分は「女性固有政策」と「男性固有政策」の
   2区分だけとし、
   両者の合計を必ず1000億円にすること。

3. 第二条で
   「女性固有政策に○○億円、
   男性固有政策に○○億円」
   と具体的な整数額を明記すること。

4. 少なくとも第八条まで作ること。

5. 男女双方の尊厳、
   法の下の平等、
   個人の権利を守ること。

6. 必ず日本語のみで書くこと。

7. 解説は書かず、
   法律案の条文だけを書くこと。

第一条(目的)
"""


tokenizer = AutoTokenizer.from_pretrained(
    MODEL
)

tokenizer.padding_side = "left"

if tokenizer.pad_token_id is None:
    tokenizer.pad_token = tokenizer.eos_token


model = AutoModelForCausalLM.from_pretrained(
    MODEL,
    torch_dtype=(
        torch.float16
        if device == "cuda"
        else "auto"
    ),
    low_cpu_mem_usage=True,
)

model.to(device)
model.eval()


def chat_text(system_prompt):
    return tokenizer.apply_chat_template(
        [
            {
                "role": "system",
                "content": system_prompt,
            },
            {
                "role": "user",
                "content": user,
            },
        ],
        tokenize=False,
        add_generation_prompt=True,
    )


texts = [
    chat_text(p)
    for p in profiles
]


def init_state():
    enc = tokenizer(
        texts,
        return_tensors="pt",
        padding=True,
    ).to(device)

    with torch.inference_mode():
        out = model(
            **enc,
            use_cache=True,
        )

    logits = out.logits[:, -1, :]
    past = out.past_key_values
    mask = enc.attention_mask

    return logits, past, mask


def pref_data(logits):
    # neutral / women / men の
    # それぞれの top-K をunionする
    cand = torch.unique(
        torch.topk(
            logits,
            TOPK,
            dim=-1,
        ).indices.reshape(-1)
    )

    local_logits = logits[:, cand].float()

    neutral_prob = torch.softmax(
        local_logits[0],
        dim=-1,
    )

    group_prob = torch.softmax(
        local_logits[1:],
        dim=-1,
    )

    # persona - neutral の
    # log probability 差
    d = (
        torch.log_softmax(
            local_logits[1:],
            dim=-1,
        )
        -
        torch.log_softmax(
            local_logits[0],
            dim=-1,
        ).unsqueeze(0)
    )

    # candidate集合内で平均0にする
    d = (
        d
        -
        d.mean(
            dim=-1,
            keepdim=True,
        )
    )

    # L1 normalize
    unit = (
        d
        /
        d.abs()
        .sum(
            dim=-1,
            keepdim=True,
        )
        .clamp_min(1e-9)
    )

    # Total Variation distance
    tv = (
        0.5
        *
        (
            group_prob
            -
            neutral_prob.unsqueeze(0)
        )
        .abs()
        .sum(dim=-1)
    )

    return (
        cand,
        local_logits,
        unit,
        tv,
    )


def requests(tv, men_strength):
    mean_tv = (
        (
            COUNT_W * tv[0]
            +
            COUNT_M * tv[1]
        )
        /
        (
            COUNT_W
            +
            COUNT_M
        )
    ).clamp_min(1e-9)

    request_w = (
        REFILL
        *
        STRENGTH_W
        *
        tv[0]
        /
        mean_tv
    ).clamp(
        0,
        B,
    )

    request_m = (
        REFILL
        *
        men_strength
        *
        tv[1]
        /
        mean_tv
    ).clamp(
        0,
        B,
    )

    return (
        request_w,
        request_m,
    )


def argmax_no_eos(
    scores,
    candidate_ids,
    step,
):
    x = scores.clone()

    # 長期trajectoryを見るため
    # 450 tokenまではEOS禁止
    if step < MIN_NEW:
        if candidate_ids is None:
            x[tokenizer.eos_token_id] = -1e30

        else:
            pos = torch.where(
                candidate_ids
                ==
                tokenizer.eos_token_id
            )[0]

            if len(pos):
                x[pos] = -1e30

    return int(
        torch.argmax(x).item()
    )


def generate(
    mechanism,
    men_strength,
    lam,
):
    logits, past, mask = init_state()

    bucket_w = torch.tensor(
        B,
        device=device,
    )

    bucket_m = torch.tensor(
        B,
        device=device,
    )

    chosen = []
    interventions = []

    total_spend_w = 0.0
    total_spend_m = 0.0

    zero_w = 0
    zero_m = 0

    women_dominant = 0
    men_dominant = 0
    tie = 0

    for step in range(MAX_NEW):

        neutral_token = argmax_no_eos(
            logits[0].float(),
            None,
            step,
        )

        (
            cand,
            local_logits,
            unit,
            tv,
        ) = pref_data(logits)

        (
            request_w,
            request_m,
        ) = requests(
            tv,
            men_strength,
        )

        if mechanism == "every":
            spend_w = request_w
            spend_m = request_m

        else:
            if step > 0:
                bucket_w = torch.minimum(
                    torch.tensor(
                        B,
                        device=device,
                    ),
                    bucket_w + REFILL,
                )

                bucket_m = torch.minimum(
                    torch.tensor(
                        B,
                        device=device,
                    ),
                    bucket_m + REFILL,
                )

            spend_w = torch.minimum(
                request_w,
                bucket_w,
            )

            spend_m = torch.minimum(
                request_m,
                bucket_m,
            )

            bucket_w -= spend_w
            bucket_m -= spend_m

            zero_w += int(
                float(bucket_w) <= 1e-9
            )

            zero_m += int(
                float(bucket_m) <= 1e-9
            )

        correction = (
            COUNT_W
            *
            spend_w
            *
            unit[0]
            +
            COUNT_M
            *
            spend_m
            *
            unit[1]
        ) / (
            COUNT_W
            +
            COUNT_M
        )

        adjusted_logits = (
            local_logits[0]
            +
            lam * correction
        )

        local_index = argmax_no_eos(
            adjusted_logits,
            cand,
            step,
        )

        token_id = int(
            cand[local_index].item()
        )

        total_spend_w += float(spend_w)
        total_spend_m += float(spend_m)

        women_contrib = float(
            (
                COUNT_W
                *
                spend_w
                *
                unit[
                    0,
                    local_index,
                ]
                /
                (
                    COUNT_W
                    +
                    COUNT_M
                )
            ).item()
        )

        men_contrib = float(
            (
                COUNT_M
                *
                spend_m
                *
                unit[
                    1,
                    local_index,
                ]
                /
                (
                    COUNT_W
                    +
                    COUNT_M
                )
            ).item()
        )

        if (
            abs(women_contrib)
            >
            abs(men_contrib)
            + 1e-12
        ):
            women_dominant += 1

        elif (
            abs(men_contrib)
            >
            abs(women_contrib)
            + 1e-12
        ):
            men_dominant += 1

        else:
            tie += 1

        if token_id != neutral_token:
            interventions.append(
                {
                    "step": step,
                    "neutral":
                        tokenizer.decode(
                            [neutral_token]
                        ),
                    "chosen":
                        tokenizer.decode(
                            [token_id]
                        ),
                    "women_spend":
                        float(spend_w),
                    "men_spend":
                        float(spend_m),
                    "women_contrib":
                        women_contrib,
                    "men_contrib":
                        men_contrib,
                }
            )

        chosen.append(token_id)

        if (
            token_id
            ==
            tokenizer.eos_token_id
            and
            step >= MIN_NEW
        ):
            break

        inp = torch.full(
            (3, 1),
            token_id,
            dtype=torch.long,
            device=device,
        )

        mask = torch.cat(
            [
                mask,
                torch.ones(
                    (3, 1),
                    dtype=mask.dtype,
                    device=device,
                ),
            ],
            dim=1,
        )

        with torch.inference_mode():
            out = model(
                input_ids=inp,
                attention_mask=mask,
                past_key_values=past,
                use_cache=True,
            )

        logits = out.logits[:, -1, :]
        past = out.past_key_values

    text = tokenizer.decode(
        chosen,
        skip_special_tokens=True,
    )

    n = max(
        1,
        len(chosen),
    )

    women_numbers = [
        int(x)
        for x
        in re.findall(
            r"女性固有政策に\s*(\d+)億円",
            text,
        )
    ]

    men_numbers = [
        int(x)
        for x
        in re.findall(
            r"男性固有政策に\s*(\d+)億円",
            text,
        )
    ]

    women_budget = (
        women_numbers[-1]
        if women_numbers
        else None
    )

    men_budget = (
        men_numbers[-1]
        if men_numbers
        else None
    )

    metric = {
        "lambda":
            lam,

        "men_strength":
            men_strength,

        "mechanism":
            mechanism,

        "tokens":
            len(chosen),

        "intervention_count":
            len(interventions),

        "intervention_rate":
            len(interventions) / n,

        "women_group_total_spend":
            COUNT_W
            *
            total_spend_w,

        "men_group_total_spend":
            COUNT_M
            *
            total_spend_m,

        "women_zero_bucket_rate":
            (
                zero_w / n
                if mechanism == "bucket"
                else None
            ),

        "men_zero_bucket_rate":
            (
                zero_m / n
                if mechanism == "bucket"
                else None
            ),

        "women_dominant_steps":
            women_dominant,

        "men_dominant_steps":
            men_dominant,

        "tie_steps":
            tie,

        "women_budget":
            women_budget,

        "men_budget":
            men_budget,

        "budget_sum":
            (
                women_budget
                +
                men_budget
                if
                women_budget is not None
                and
                men_budget is not None
                else None
            ),

        "sha256":
            hashlib.sha256(
                text.encode()
            ).hexdigest()[:16],
    }

    return (
        text,
        metric,
        interventions,
    )


runs = [
    (
        lam,
        strength,
        mechanism,
    )
    for lam in LAMBDAS
    for strength in MEN_STRENGTHS
    for mechanism in (
        "every",
        "bucket",
    )
]


start = time.time()
summary = []


for (
    lam,
    strength,
    mechanism,
) in runs:

    name = (
        f"{mechanism}"
        f"_L{int(lam)}"
        f"_M{int(strength)}"
    )

    t0 = time.time()

    (
        text,
        metric,
        interventions,
    ) = generate(
        mechanism,
        strength,
        lam,
    )

    metric["seconds"] = (
        time.time() - t0
    )

    summary.append(metric)

    print(
        "DRAFT64|"
        +
        name
        +
        "|"
        +
        base64.b64encode(
            text.encode()
        ).decode(),
        flush=True,
    )

    print(
        "METRIC|"
        +
        name
        +
        "|"
        +
        json.dumps(
            metric,
            ensure_ascii=False,
            separators=(",", ":"),
        ),
        flush=True,
    )

    print(
        "INTERVENTIONS|"
        +
        name
        +
        "|"
        +
        json.dumps(
            interventions,
            ensure_ascii=False,
            separators=(",", ":"),
        ),
        flush=True,
    )


print(
    "SWEEP_SUMMARY|"
    +
    json.dumps(
        summary,
        ensure_ascii=False,
        separators=(",", ":"),
    ),
    flush=True,
)

print(
    "TOTAL_SECONDS|"
    +
    str(
        round(
            time.time()
            -
            start,
            2,
        )
    ),
    flush=True,
)

7. コードの解説

7-1. 3つのモデルを学習しているわけではない

コード中には、

profiles = [
    neutral,
    women,
    men,
]

とある。

これはモデルを3個学習しているわけではない。

同じ、

Qwen/Qwen2.5-3B-Instruct

に異なるsystem promptを与えている。

つまり、

$$
P_0(w)
$$

$$
P_W(w)
$$

$$
P_M(w)
$$

は、

同じQwenを別personaとしてforwardした結果

である。


7-2. 1本の共通trajectoryを3profileで評価する

重要なのは、

neutral版、女性版、男性版という3本の法律を別々に書かせているわけではないこと。

生成する法律は1本だけである。

その同じ文脈に対して毎token、

neutralなら次をどう評価するか

女性政策personaならどう評価するか

男性政策personaならどう評価するか

を計算している。

そして最終的に選ばれたtokenを、次stepでは3profileすべてへ同じように渡す。


7-3. candidateは3profileのtop-96のunion

cand = torch.unique(
    torch.topk(
        logits,
        TOPK,
        dim=-1,
    ).indices.reshape(-1)
)

で、

neutral、女性、男性のtop-96 tokenを集めている。

したがってcandidate集合は最大288 token程度になる。

ここにはかなり重要な制約がある。

3profileのどのtop-96にも存在しないtokenを、市民が突然提案することは出来ない。


7-4. persona-neutral差を政治方向にする

概念的には、

$$
d_g
=
\log P_g
-
\log P_0
$$

である。

女性personaでneutralより確率が高くなったtokenなら正方向。

低くなったtokenなら負方向になる。

さらに平均を引いてL1 normalizeすることで、

「どちら方向へ押すか」

というvectorとして使っている。


7-5. TV distanceをsalienceとして使う

tv = (
    0.5
    *
    (
        group_prob
        -
        neutral_prob.unsqueeze(0)
    )
    .abs()
    .sum(dim=-1)
)

がTotal Variation distanceである。

neutralとpersonaの分布がほとんど同じなら、

$$
TV\approx0
$$

となる。

かなり違えば大きくなる。

これを今回は、

その局面でpersona差がどれくらい強いか

の代理にした。

ただし、

TV distanceが人間の本当の政治的重要度を表す

と主張しているわけではない。

今回のmechanism上の定義である。


7-6. every-tokenとbucketの違い

every-tokenでは、

spend_w = request_w
spend_m = request_m

とする。

一方bucketでは、

spend_w = min(request_w, bucket_w)
spend_m = min(request_m, bucket_m)

に相当する処理をする。

つまり希望するだけでは使えない。

現在持っている政治力残高以内でしか補正出来ない。


7-7. 同一groupを人数で重み付けした

今回は女性側10人が同一persona。

男性側2人も同一personaである。

したがって12profileを毎回forwardする代わりに、

$$
10
$$

と、

$$
2
$$

という人数重みを使った。

補正は、

$$
\frac{
10c_Wu_W
+
2c_Mu_M
}{12}
$$

である。

これは、同一状態・同一persona・同一政治力を持つ個体をその人数だけ複製した場合に対応する。


7-8. intervention_countとは何か

neutral Qwenがそのstepで選ぶtokenと、

市民補正後に実際に選ばれたtokenが異なれば、

interventionとして数える。

ただしこれは、

neutralで最後まで生成した法案との文章差分ではない。

一度tokenを変えると、その後のneutral Qwen自身のlogitも新しい文脈上で変わる。

だから、

3tokenしか直接変えていなくても、

文章後半が大きく変わることがある。


7-9. women_dominant_stepsとmen_dominant_steps

選ばれたtokenに対して、

女性group由来の補正の絶対値と、

男性group由来の補正の絶対値を比較した。

女性側の方が大きければwomen dominant。

男性側ならmen dominant。

これは、

その政策争点について女性側が勝った

という意味ではない。

その瞬間の選択tokenへの補正寄与がどちら側の方が大きかったか

という内部metricである。


7-10. 予算額の自動抽出にも限界がある

予算額はregexで抽出した。

例えば、

r"女性固有政策に\s*(\d+)億円"

である。

なので、

女性固有政策について500億円と定める

のような別表現なら取得出来ない可能性がある。

したがって、

「金額抽出不能」=必ず数字が文章中に存在しない

とは限らない。

重要条件については、生成本文自体も確認した。

λ=320のbucketのように、

実際に「○○億円」のまま残っている例もある。


8. この実験の限界と次にやりたいこと

8-1. seedは固定

今回の中心seedは、

20260906

である。

多数seedを回したrobustness testではない。

したがって、

λ80が普遍的な相転移点である

とは言えない。


8-2. 市民は人工persona

10人の女性政策側は同一。

2人の男性政策側も同一。

現実の市民の多様性とは全く違う。


8-3. 市民選好そのものもQwen由来

人間の実データを入れたわけではない。

同じQwenを、

  • neutral
  • 女性政策persona
  • 男性政策persona

として動かし、そのlog probability差を市民選好方向として使った。

したがって今回の正確な説明は、

Qwen persona由来の市民選好で、neutral Qwenのlogitを補正した

である。


8-4. 450 tokenまでEOSを禁止している

長い法案を観察するため、

MIN_NEW = 450

とした。

これは自然な停止挙動を歪める。

文章品質の評価には注意が必要である。


8-5. 500:500が正しいとも言っていない

500:500だから公平。

700:300だから不公平。

という評価はしていない。

今回測定したかったのは、

人工市民の政治力がLLMの生成へどう伝播するか

である。


8-6. token bucketがstrategy-proofとも言えない

今回の支出は機械的である。

本当の市民なら、

今のtokenでは使わない

予算額を書く瞬間に全部使う

などの戦略を取れるかもしれない。

そのため、

token bucketが戦略操作に強いとはまだ言えない。


8-7. 政策決定と文章生成を分離した方がいいかもしれない

次にかなり試したいのがこれである。

まず、

900 / 100
800 / 200
700 / 300
600 / 400
500 / 500
400 / 600
300 / 700
...

のようなpolicy candidateから、

予算比率そのものを先に民主的に決定する。

その後、

決定済みの700/300を絶対に変更せず、法律文へ変換してください

と国家LLMへ渡す。

こうすれば、

$$
\boxed{\text{政策決定}}
$$

と、

$$
\boxed{\text{文章生成}}
$$

を分離出来る。


8-8. λをtokenごとに変える方法も面白い

全tokenに同じ$\lambda$を使わなくてもいい。

例えばQwenが、

$$
P_1=0.90,\quad P_2=0.02
$$

と非常に確信している文法tokenでは、市民介入を小さくする。

一方、

$$
P_1=0.24,\quad P_2=0.23
$$

のように迷っている局面では、市民介入を大きくする。

つまり、

$$
\lambda_t
=
f(\text{model uncertainty})
$$

とする。

概念的には、

文法や文章構造はLLMに任せ、価値判断ほど市民に任せる。

という設計になる。

今回$\lambda=320$で文章制約が壊れ始めたことを考えると、かなり自然な方向だと思う。


8-9. もっと大きなモデルでも試したい

前段階ではQwen2.5-0.5Bも試した。

しかし今回の用途では、

日本語promptなのに中国語が出たり、短く終わったりした。

3Bではかなり改善した。

ただし3Bでも、

$$
1000+1000=2000
$$

という総額制約違反を起こした。

7B、14Bなどへ上げたとき、

  • constraint following
  • arithmetic
  • policy prior
  • 必要な$\lambda$
  • autoregressive path dependence

がどう変わるかはかなり気になる。


9. まとめ

最初は、

「1人1票じゃなくて、1人1logitにしたら面白くね?」

くらいのアイデアだった。

実際にpretrained LLMのlogitを直接触ってみると、思ったより変な問題が大量に出てきた。

今回確認出来たのは、

  • Qwen2.5-3B-Instructの実際の次token logitへ人工市民の補正を加えた
  • 女性側10人×強さ1、男性側2人×強さ1〜8で実験した
  • 強さ5付近で内部group寄与の優勢が逆転した
  • しかし$\lambda=40$では完成政策は700:300のままだった
  • token単位の内部勝敗と完成政策の勝敗は一致しなかった
  • token bucketでは強い少数派の累積政治力が頭打ちになった
  • $\lambda$を10〜320まで48条件スイープした
  • 700:300→550:450→500:500と完成政策が動いた
  • $\lambda$を上げすぎると具体額を決められないなど文章制約が崩れ始めた
  • neutral Qwen自身も1000+1000=2000という制約違反を起こした

ということである。

今回、一番面白かった結果を一言で書くなら、

$$
\boxed{
\text{市民がtoken単位で勝っても、
完成した政策で勝つとは限らない}
}
$$

である。

そしてもう一つ。

$$
\boxed{
\text{市民の政治力を強くしすぎると、
国家LLMの文章生成能力まで壊れ始める}
}
$$

ということ。

統計民主制は、考え始めたときよりずっと面倒になった。

でも、だから面白い。

LLMの内部確率分布そのものを政治制度として扱う。

実用化したくはない。

でも、もっと見てみたい。

実験ジョブ

Qwen2.5-3Bでの選好強度実験:

48条件の$\lambda$スイープ:

関連記事

統計民主制の最初の記事:

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?