miutti's picture
Source: kernel, desktop app, tools (snapshot of the GitHub repo)
df41178 verified
Raw History Blame Contribute Delete
9.93 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
council.py -- 何人かで考えて、多数決で決める(=考える時間で賢くする)
────────────────────────────────────────────────
「掛け算を増やさずに賢くする」いちばん確実な道は、
考える時間を増やすこと。いま世の中で
「test-time compute(答えるときに考える時間)」と呼ばれているもの。
────────────────────────────────────────────────
やることは3つだけ。
① 何人かに、ちがう順番で探させる(視点を変える)
② 出てきた案を、ぜんぶ下見で確かめる(通らない案はここで落ちる)
③ 残った案を多数決で選ぶ
ここで大事なこと。
③の多数決は「たし算」です。掛け算ではありません。
ですが、探せる範囲は 人数 × 深さ で増えます。
5人 × 深さ4 = 5⁴ = 625通り
掛け算を1回も使わずに、探せる広さだけが掛け算で増える。
多数決には、1万ビットの「たばねる」をそのまま使う。
手順を1本のビット列にして、みんなの分をたばね、
たばねたものにいちばん近い手順を選ぶ。
(票を数えるのと同じことだが、
「ほとんど同じで1手だけ違う案」もちゃんと近いと分かる)
"""
import os, sys, time
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import hdv
# ------------------------------------------------------------------
# 視点 -- 部品の並べ方を変える係
# 同じ問題でも、どの手から試すかで、たどりつく案が変わる
# ------------------------------------------------------------------
def _v_normal(names, slots, path):
"""ふつう。実績の順(policy が決めた順)のまま"""
return names
def _v_narrow_first(names, slots, path):
"""条件を先に使い切る人。「しぼる」を早めに置く"""
return sorted(names, key=lambda n: 0 if n.startswith("しぼる") else 1)
def _v_short_first(names, slots, path):
"""短く済ませたい人。終わりの部品を早めに置く"""
import kernel
return sorted(names, key=lambda n: 0 if kernel.PARTS[n].get("terminal") else 1)
def _v_reverse(names, slots, path):
"""へそ曲がり。実績の逆から試す。
実績どおりだと、いつも同じ案しか出てこないので、わざと逆を見る"""
return list(reversed(names))
def _v_alpha(names, slots, path):
"""名前順。実績にまったく引きずられない、まっさらな視点"""
return sorted(names)
# 視点 = (名前, 部品の並べ方, 探し方)
#
# 【はじめ失敗したやり方と、その理由】
# 最初は5人ぜんぶ「幅(横に広く)」で、部品の順番だけ変えていた。
# 測ったら、ひとりで探すのと まったく同じ数しか解けなかった。
#
# 考える量 ひとり 5人で合議
# 8通り 2/10 2/10
# 20通り 3/10 3/10
# 40通り 6/10 6/10
#
# 理由は単純で、探し方が同じだと、順番を変えても
# 同じ場所を同じ順に見に行くから。人数だけ増やしても意味がない。
#
# 【直したやり方】
# 半分を「深(一本の道を先まで掘る)」にした。
# 広く見る人と、深く掘る人がいて、はじめて見る場所が変わる。
VIEWS = [
("ふつうの人", _v_normal, "幅"),
("しぼり優先の人", _v_narrow_first, "幅"),
("深く掘る人", _v_normal, "深"),
("深く掘るへそ曲がり", _v_reverse, "深"),
("深く掘る名前順", _v_alpha, "深"),
("短く済ます人", _v_short_first, "幅"),
]
# ------------------------------------------------------------------
# 手順を1万ビットにする
# ------------------------------------------------------------------
def plan_vec(plan):
"""手順を1本のビット列にする。
「1番目がさがす」「2番目がしぼる」…を、それぞれ
ずらし(permute) で順番を表してから、たばねる。
順番が違えば違うビット列になる。
"""
if not plan:
return 0
return hdv.bundle_bits([hdv.permute(hdv.atom("手:" + n), i + 1)
for i, n in enumerate(plan)])
# ------------------------------------------------------------------
# 合議
# ------------------------------------------------------------------
def deliberate(slots, allow=None, per_view=3, budget=400, max_depth=6,
verbose=True):
"""何人かで考えて、多数決で1つ選ぶ。
戻り値: (手順, 結果, 記録) 見つからなければ (None, None, 記録)
"""
import kernel
t0 = time.time()
log = {"視点": [], "案": [], "採用": None}
cands = [] # [(手順, 結果, 出した視点のならび)]
# --- ① 何人かに、ちがう順番で探させる ---
for who, bias, strategy in VIEWS:
try:
got = kernel.solve(slots, verbose=False, allow=allow,
bias=bias, budget=budget, strategy=strategy,
max_depth=max_depth, collect=per_view)
except Exception as e:
log["視点"].append((who, f"できなかった({e})"))
continue
got = got if isinstance(got, list) else ([got] if got and got[0] else [])
log["視点"].append((who, f"{len(got)} 案"))
for plan, st in got:
for c in cands:
if c[0] == plan:
c[2].append(who)
break
else:
cands.append([list(plan), st, [who]])
if not cands:
return None, None, log
# --- ② ぜんぶ下見で確かめる。通らない案はここで落とす ---
ok = []
for plan, st, voters in cands:
try:
chk, bad = kernel.try_plan(plan, slots)
except Exception as e:
bad, chk = str(e), {}
good = (not bad) and bool(chk.get("files")) \
and kernel.goal_reached(chk, slots, plan)
log["案"].append({"手順": plan, "票": list(voters),
"通った": good, "理由": bad or ("" if good else "条件不足")})
if good:
ok.append((plan, st, voters))
if not ok:
return None, None, log
# --- ③ 多数決 ---
#
# 【はじめ失敗したやり方と、その理由】
# 最初は「1万ビットでたばねて、真ん中にいちばん近い案」を選んだ。
# 結果はこうなった:
# ✓ さがす → かさなり 票5
# ✓ さがす → つくる → かさなり 票5
# ★ さがす → つくる → ふるいじゅん → かさなり 票5 ← 選ばれた
# いちばん長い、むだな手が2つ入った案が選ばれてしまった。
# 理由は単純で、長い案ほど材料が多く、
# みんなをたばねた真ん中に「なんとなく近く」なるから。
# つまり真ん中との近さは、案の良し悪しではなく長さを測っていた。
#
# 【直したやり方】
# ① 票の多さ … 何人がその案にたどりついたか
# ② 手数の少なさ … 同じことができるなら短いほうがよい
# ③ 真ん中との近さ … ①②が同点のときの、最後の決め手だけに使う
#
# 1万ビットのたばねは、ここでは「同点をほどく係」に格下げした。
# 本当は主役にしたかったが、測ったら主役には向いていなかった。
vecs, ws = [], []
for plan, _st, voters in ok:
vecs.append(plan_vec(plan))
ws.append(len(voters))
center = hdv.bundle_bits(vecs, ws)
scored = []
for (plan, st, voters), v in zip(ok, vecs):
scored.append((len(voters), -len(plan), hdv.near(center, v),
plan, st, voters))
scored.sort(key=lambda x: (x[0], x[1], x[2]), reverse=True)
nvote, _neg, near, plan, st, voters = scored[0]
log["採用"] = {"手順": plan, "票": voters, "近さ": round(near, 3),
"案の数": len(cands), "通った案": len(ok),
"ミリ秒": round((time.time() - t0) * 1000, 1)}
if verbose:
show(log)
return plan, st, log
def show(log):
print(" ── 合議 ──")
for who, r in log["視点"]:
print(f" {who:<12} {r}")
for a in log["案"]:
mark = "✓" if a["通った"] else "✗"
print(f" {mark} {' → '.join(a['手順'])}"
f" 票{len(a['票'])}"
+ ("" if a["通った"] else f" ({a['理由']})"))
d = log.get("採用")
if d:
print(f" ★ 採用: {' → '.join(d['手順'])}"
f" 票{len(d['票'])}/{d['通った案']} "
f"みんなの真ん中との近さ {d['近さ']} {d['ミリ秒']}ミリ秒")
if __name__ == "__main__":
import kernel
kernel.make_demo()
for slots in [
{"動作": "移動", "場所": "Desktop", "種類": "画像", "時期": "去年"},
{"動作": "数える", "場所": "Downloads", "種類": "PDF"},
{"動作": "重複", "場所": "Desktop"},
]:
print(f"\n■ {slots}")
plan, st, log = deliberate(slots)
print(f" → {' → '.join(plan) if plan else '見つからず'}")