miutti's picture
Source: kernel, desktop app, tools (snapshot of the GitHub repo)
df41178 verified
Raw History Blame Contribute Delete
13.8 kB
# -*- coding: utf-8 -*-
"""chokkan ── 直感役。頼み文を見て「どの用件か/道具は要らないか」を **確率で** 数ミリ秒で決める(2026-09-18)。
Jev(TypeSafe AI)の「System One モデル」の本物の真似: 文章を書かせず、学習した小さなモデルが
確率つきで決める。9/18 に「素の頭脳に 1トークンで答えさせる」形(kimeru.py)を測ったら 確率が校正されておらず
元の形に負けた(22/36 vs 28/36)。校正された確率には **学習** が要る → ここで自前の教材(chokkan_kyouzai.py)で
文字 n-gram のロジスティック回帰を学習し、温度で校正する。標準ライブラリだけ・重みは JSON(chokkan_omomi.json)。
位置づけ: 頭脳(LLM)の前に置く近道。決まった言い方の表(machine.match)に当たらなかった文を、
ここで「用件 X・自信 p」にする。自信が線(SEN)以上なら道具へ、材料は machine.zairyou か 頭脳の言い方直しで取る。
線より低ければ今まで通り 頭脳が返事を書く(=取りこぼしはしても 誤発動はしない側に倒す)。
使い方: chokkan.kimeru("ちょっと5分はかって") → {"用件": "タイマー", "自信": 0.93, "上位": [...], "ms": 1}
python3 chokkan.py --train … 教材から学習して chokkan_omomi.json を作る(物差しは dougu/hakaru_chokkan.py)
"""
from __future__ import annotations
import json
import math
import os
import random
import re
import time
import unicodedata
HERE = os.path.dirname(os.path.abspath(__file__))
OMOMI = os.path.join(HERE, "chokkan_omomi.json")
ZATSUDAN = "雑談"
SEN = float(os.environ.get("KERNEL_CHOKKAN_SEN", "0.5")) # 線の既定。学習時に校正用で決めた値(重みの sen)があればそちら
_Q = re.compile(r"[「『\"“](.*?)[」』\"”]")
_NUM = re.compile(r"[0-9]+(?:[.,][0-9]+)?")
def _seiri(text: str) -> str:
"""比べるための形にする: NFKC・小文字・空白除去・「…」の中身は Q・数字は 0"""
s = unicodedata.normalize("NFKC", text or "").lower()
s = _Q.sub("「q」", s)
s = _NUM.sub("0", s)
return re.sub(r"\s+", "", s)
def tokuchou(text: str) -> dict:
"""特徴 → 値。文字 1〜3-gram(端に ^ $)と、いくつかの合図"""
s = _seiri(text)
f = {}
t = "^" + s + "$"
for n in (1, 2, 3):
for i in range(len(t) - n + 1):
g = t[i:i + n]
f[g] = f.get(g, 0.0) + 1.0
raw = text or ""
if _NUM.search(raw):
f["#数"] = 1.0
if "「" in raw or '"' in raw:
f["#引用"] = 1.0
if raw.rstrip().endswith(("?", "?")):
f["#問"] = 1.0
n = len(s)
f["#長" + ("短" if n <= 6 else "中" if n <= 16 else "長")] = 1.0
# 出現回数は 1 に丸めてから 長さで割る(長い文で特徴が膨れないように)
norm = 1.0 / math.sqrt(max(1, len(f)))
return {k: norm for k in f}
class Model:
def __init__(self, classes=None, w=None, b=None, T=1.0, sen=None):
self.classes = list(classes or [])
self.w = w or {} # 特徴 → [重み × クラス数]
self.b = b or [0.0] * len(self.classes)
self.T = T
self.sen = sen # 校正用で決めた線(None なら SEN)
# ── 推論 ──
def logits(self, f: dict) -> list:
z = list(self.b)
for k, v in f.items():
row = self.w.get(k)
if row is None:
continue
for c in range(len(z)):
z[c] += row[c] * v
return z
def kakuritsu(self, f: dict, T: float = None) -> list:
z = self.logits(f)
T = self.T if T is None else T
m = max(z)
e = [math.exp((x - m) / T) for x in z]
s = sum(e)
return [x / s for x in e]
# ── 学習(ソフトマックス回帰・SGD・L2)──
def train(self, rei: list, epochs: int = 40, lr: float = 0.5, l2: float = 1e-5, seed: int = 0, quiet: bool = False):
rnd = random.Random(seed)
idx = {c: i for i, c in enumerate(self.classes)}
feats = [(tokuchou(t), idx[c]) for t, c in rei]
C = len(self.classes)
# クラスの重み: 雑談の例が用件の 10倍あるので、そのままだと「分からなければ雑談」に寄る。1/√件数 でならす
kazu = [0] * C
for _, y in feats:
kazu[y] += 1
omosa = [1.0 / math.sqrt(max(1, k)) for k in kazu]
mean = sum(omosa[y] for _, y in feats) / len(feats)
omosa = [o / mean for o in omosa]
for ep in range(epochs):
rnd.shuffle(feats)
eta = lr / (1.0 + ep * 0.15)
loss = 0.0
for f, y in feats:
p = self.kakuritsu(f, T=1.0)
loss -= math.log(max(p[y], 1e-12))
w_y = omosa[y]
for c in range(C):
g = (p[c] - (1.0 if c == y else 0.0)) * w_y
if abs(g) < 1e-4:
continue
self.b[c] -= eta * g
for k, v in f.items():
row = self.w.get(k)
if row is None:
row = self.w[k] = [0.0] * C
row[c] -= eta * (g * v + l2 * row[c])
if not quiet and (ep % 5 == 0 or ep == epochs - 1):
print(" epoch %2d loss %.3f features %d" % (ep, loss / len(feats), len(self.w)))
return self
def ondo(self, rei: list) -> float:
"""温度の校正: 検証用の例で 負の対数尤度が最小の T を探す(Jev の RLCD に当たる「確率を信用できるようにする」段)"""
idx = {c: i for i, c in enumerate(self.classes)}
feats = [(tokuchou(t), idx[c]) for t, c in rei]
best = (float("inf"), 1.0)
for T in [0.25, 0.3, 0.35, 0.4, 0.5, 0.6, 0.7, 0.85, 1.0, 1.2, 1.5, 1.8, 2.2, 2.7, 3.3]:
nll = 0.0
for f, y in feats:
nll -= math.log(max(self.kakuritsu(f, T)[y], 1e-12))
if nll < best[0]:
best = (nll, T)
self.T = best[1]
return self.T
# ── 保存・読み込み ──
def save(self, path: str = OMOMI):
"""原子的に書く(途中で落ちても前の重みが残る)。丸めは 6桁(4桁だと測った値と読み直した値がずれる・Codex の審査)"""
tmp = path + ".tmp"
json.dump({"classes": self.classes, "w": {k: [round(x, 6) for x in v] for k, v in self.w.items()},
"b": [round(x, 6) for x in self.b], "T": self.T, "sen": self.sen, "made": time.strftime("%Y-%m-%d %H:%M")},
open(tmp, "w", encoding="utf-8"), ensure_ascii=False)
os.replace(tmp, path)
@classmethod
def load(cls, path: str = OMOMI):
d = json.load(open(path, encoding="utf-8"))
return cls(d["classes"], d["w"], d["b"], d.get("T", 1.0), d.get("sen"))
_MODEL = None
def model() -> Model:
global _MODEL
if _MODEL is None:
_MODEL = Model.load()
return _MODEL
def kimeru(text: str, sen: float = None) -> dict:
"""頼み文 → {"用件": 名前 or None(雑談), "自信": p, "上位": [(名前, p)×3], "ms": ms}。自信 < 線 なら 用件 None"""
t0 = time.monotonic()
m = model()
p = m.kakuritsu(tokuchou(text))
order = sorted(range(len(p)), key=lambda i: -p[i])
top = [(m.classes[i], round(p[i], 3)) for i in order[:3]]
na, pa = m.classes[order[0]], p[order[0]] # ★ 線の判定は丸める前の値で(Codex の審査 9/18)
sen = (m.sen if m.sen is not None else SEN) if sen is None else sen
out = {"用件": None, "自信": round(pa, 3), "上位": top, "ms": int((time.monotonic() - t0) * 1000)}
if na != ZATSUDAN and pa >= sen:
out["用件"] = na
return out
# ── 教材の用意(物差しの文は落とす)──
def _ngram2(s: str) -> set:
s = _seiri(s)
return {s[i:i + 2] for i in range(len(s) - 1)} if len(s) > 1 else {s}
def monosashi_no_bun() -> list:
"""物差し(dougu/hakaru_erabu.py の DOUGU・BETSU・ZATSUDAN)の文。見つからなければ空。
★ Codex の審査(9/18): --train が物差しを除かずに学習できてしまっていた → いつも除く"""
for d in (os.path.join(HERE, "..", "koukai", "dougu"), os.path.join(HERE, "..", "dougu"), os.path.expanduser("~/LocalAI_mirror/koukai/dougu")):
f = os.path.join(d, "hakaru_erabu.py")
if os.path.isfile(f):
import importlib.util
spec = importlib.util.spec_from_file_location("hakaru_erabu", f)
mod = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(mod)
except Exception:
continue
return [t for t, _ in mod.DOUGU] + [t for t, _ in mod.BETSU] + list(mod.ZATSUDAN)
return []
def kyouzai(nozoku: list = None, quiet: bool = False) -> list:
"""教材 → [(文, 用件)]。物差しの文(自動で集める+nozoku)と 2-gram の重なりが 0.5 以上の文は落とす(丸暗記を測らないため)"""
import chokkan_kyouzai as K
import machine
nozoku = list(nozoku or []) + monosashi_no_bun()
rei = []
for na, bun in K.KYOUZAI.items():
if na not in machine.OPS:
continue
for b in bun:
rei.append((b, na))
for b in K.ZATSUDAN:
rei.append((b, ZATSUDAN))
# 頭脳が作った言い方(dougu/tsukuru_iikata.py)。組=学習 だけ入れる(組=物差し は独立した物差しに使う)
# 雲の先生が作った言い方(dougu/tsukuru_iikata_kumo.py)も 組=学習 だけ(9/19 測って差なし 27→28・誤発動 2→3 → 既定は入れない。KERNEL_KUMO=1 で入れる)
# 言いよどみ・省略の話し言葉(2026-09-22。独立した物差し2 で拾い率 52% だったので足した)。
# 既定で入れる/入れないは 実測で決める: KERNEL_IIYODOMI=0 で外せる。
files = ["chokkan_kyouzai_nou.jsonl"]
if os.environ.get("KERNEL_IIYODOMI", "1") == "1":
files.append("chokkan_kyouzai_iiyodomi.jsonl")
if os.environ.get("KERNEL_KUMO", "0") == "1":
files.append("chokkan_kyouzai_kumo.jsonl")
for fn in files:
nou = os.path.join(HERE, fn)
if not os.path.exists(nou):
continue
for l in open(nou, encoding="utf-8"):
d = json.loads(l)
if d.get("組") == "学習" and (d["用件"] == ZATSUDAN or d["用件"] in machine.OPS):
rei.append((d["文"], d["用件"]))
if nozoku:
ng = [_ngram2(x) for x in nozoku]
mae = len(rei)
def chikai(b):
a = _ngram2(b)
return any(len(a & g) / max(1, len(a | g)) >= 0.5 for g in ng)
rei = [(b, na) for b, na in rei if not chikai(b)]
if not quiet and mae != len(rei):
print(" 物差しに近い文を %d 落とした" % (mae - len(rei)))
return rei
def gakushuu(nozoku: list = None, seed: int = 0, quiet: bool = False) -> Model:
"""学習: 用件ごとに層化して 6/7 で学習 → 残り 1/7(校正用)で温度と線(SEN)を決める → **その模型を保存**。
★ 前は校正のあと全部で学習し直していた(校正が保存した模型のものでない・Codex の審査 9/18)。
線は 校正用で「雑談の誤発動 0 のまま 道具を最も拾う値」。物差し(held-out)は線を決めるのに使わない。"""
rei = kyouzai(nozoku, quiet=quiet)
classes = sorted({c for _, c in rei})
rnd = random.Random(seed)
by = {}
for t, c in rei:
by.setdefault(c, []).append(t)
gaku, kensho = [], []
for c, ts in by.items():
ts = list(ts)
rnd.shuffle(ts)
k = max(1, len(ts) // 7) if len(ts) >= 3 else 0
kensho += [(t, c) for t in ts[:k]]
gaku += [(t, c) for t in ts[k:]]
m = Model(classes).train(gaku, quiet=quiet)
T = m.ondo(kensho)
# 線: 校正用で 雑談を道具と間違えない範囲で いちばん道具を拾う値
best = (0.5, -1)
for sen in (0.3, 0.4, 0.5, 0.6, 0.7, 0.8):
ok = gobatsu = 0
for t, c in kensho:
p = m.kakuritsu(tokuchou(t))
i = max(range(len(classes)), key=lambda j: p[j])
na, pa = classes[i], p[i]
if c == ZATSUDAN:
gobatsu += (na != ZATSUDAN and pa >= sen)
else:
ok += (na == c and pa >= sen)
if gobatsu == 0 and ok > best[1]:
best = (sen, ok)
m.sen = best[0]
ok = sum(1 for t, c in kensho if classes[max(range(len(classes)), key=lambda j: m.kakuritsu(tokuchou(t))[j])] == c)
if not quiet:
print(" 校正用 %d文(用件ごとに 1/7): 当たり %d/%d・温度 T=%.2f・線 SEN=%.1f" % (len(kensho), ok, len(kensho), T, m.sen))
m.save()
if not quiet:
print(" 教材 %d 文(学習 %d)・用件 %d・特徴 %d → %s" % (len(rei), len(gaku), len(classes), len(m.w), os.path.relpath(OMOMI, HERE)))
global _MODEL
_MODEL = Model.load() # 読み直した模型を使う(保存した物と測る物を同じにする)
return _MODEL
if __name__ == "__main__":
import sys
if "--train" in sys.argv:
gakushuu()
else:
for t in sys.argv[1:] or ["ちょっと5分はかって", "今日は疲れた"]:
print(t, "→", kimeru(t))