dsh-biocore 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +170 -27
- package/package.json +1 -1
- package/python/biocore_runtime/pipeline.py +42 -0
- package/python/biocore_runtime/rerank.py +201 -0
- package/python/mindbase/__init__.py +18 -0
- package/python/mindbase/__main__.py +3 -0
- package/python/mindbase/actions.py +56 -0
- package/python/mindbase/active_inference.py +54 -0
- package/python/mindbase/addressing.py +69 -0
- package/python/mindbase/affect.py +86 -0
- package/python/mindbase/affective.py +111 -0
- package/python/mindbase/allostasis.py +28 -0
- package/python/mindbase/appraisal.py +101 -0
- package/python/mindbase/attachment.py +49 -0
- package/python/mindbase/attention.py +30 -0
- package/python/mindbase/autonomy.py +81 -0
- package/python/mindbase/autonomy_bench.py +251 -0
- package/python/mindbase/autonomy_evolve.py +342 -0
- package/python/mindbase/autonomy_loop.py +65 -0
- package/python/mindbase/autonomy_meta.py +91 -0
- package/python/mindbase/base_only.py +28 -0
- package/python/mindbase/benchmark.py +61 -0
- package/python/mindbase/bio_adaptation.py +274 -0
- package/python/mindbase/bio_noise.py +39 -0
- package/python/mindbase/bio_semantic.py +138 -0
- package/python/mindbase/bm25.py +46 -0
- package/python/mindbase/bootstrap.py +256 -0
- package/python/mindbase/bootstrap_real.py +231 -0
- package/python/mindbase/brain.py +178 -0
- package/python/mindbase/care.py +78 -0
- package/python/mindbase/chaos_search.py +79 -0
- package/python/mindbase/character_profile.py +33 -0
- package/python/mindbase/circadian.py +38 -0
- package/python/mindbase/circuit.py +235 -0
- package/python/mindbase/clock.py +53 -0
- package/python/mindbase/companion_runtime.py +366 -0
- package/python/mindbase/companion_train.py +126 -0
- package/python/mindbase/config.py +66 -0
- package/python/mindbase/connection_state.py +76 -0
- package/python/mindbase/connectome_prior.py +35 -0
- package/python/mindbase/connectome_world.py +158 -0
- package/python/mindbase/constructs.py +88 -0
- package/python/mindbase/context_builder.py +58 -0
- package/python/mindbase/critical.py +36 -0
- package/python/mindbase/criticality.py +123 -0
- package/python/mindbase/defaults.py +27 -0
- package/python/mindbase/demo.py +163 -0
- package/python/mindbase/diary.py +116 -0
- package/python/mindbase/diary_service.py +40 -0
- package/python/mindbase/digital_prior.py +37 -0
- package/python/mindbase/diversity.py +110 -0
- package/python/mindbase/drives.py +106 -0
- package/python/mindbase/embedding.py +132 -0
- package/python/mindbase/embedding_memory.py +74 -0
- package/python/mindbase/embodiment.py +83 -0
- package/python/mindbase/emotion_calibration.py +39 -0
- package/python/mindbase/emotion_calibrator.py +50 -0
- package/python/mindbase/emotion_core.py +113 -0
- package/python/mindbase/emotion_dynamics.py +104 -0
- package/python/mindbase/emotion_intent.py +164 -0
- package/python/mindbase/emotion_layer.py +200 -0
- package/python/mindbase/emotion_schema.py +92 -0
- package/python/mindbase/engine.py +781 -0
- package/python/mindbase/episode_log.py +90 -0
- package/python/mindbase/evaluation.py +493 -0
- package/python/mindbase/event_appraisal.py +84 -0
- package/python/mindbase/evolution.py +77 -0
- package/python/mindbase/evolve_offline.py +232 -0
- package/python/mindbase/experiments.py +358 -0
- package/python/mindbase/expression.py +54 -0
- package/python/mindbase/external_brain.py +157 -0
- package/python/mindbase/fly_mb.py +159 -0
- package/python/mindbase/fly_tasks.py +45 -0
- package/python/mindbase/forage.py +179 -0
- package/python/mindbase/goal_generator.py +98 -0
- package/python/mindbase/goals.py +153 -0
- package/python/mindbase/grid_world.py +123 -0
- package/python/mindbase/group_autonomy.py +92 -0
- package/python/mindbase/group_context.py +67 -0
- package/python/mindbase/homeostatic.py +71 -0
- package/python/mindbase/homeostatic_action.py +38 -0
- package/python/mindbase/human_standard.py +53 -0
- package/python/mindbase/identity_suite.py +50 -0
- package/python/mindbase/image_context.py +27 -0
- package/python/mindbase/impression.py +119 -0
- package/python/mindbase/initiative_quota.py +145 -0
- package/python/mindbase/insight.py +51 -0
- package/python/mindbase/intelligence_search.py +108 -0
- package/python/mindbase/intention.py +76 -0
- package/python/mindbase/interaction_log.py +30 -0
- package/python/mindbase/interoception.py +67 -0
- package/python/mindbase/invariants.py +53 -0
- package/python/mindbase/iteration.py +29 -0
- package/python/mindbase/layered_memory.py +170 -0
- package/python/mindbase/layers/__init__.py +18 -0
- package/python/mindbase/layers/l0_substrate.py +39 -0
- package/python/mindbase/layers/l1_homeostasis.py +6 -0
- package/python/mindbase/layers/l2_affect.py +6 -0
- package/python/mindbase/layers/l3_attention.py +6 -0
- package/python/mindbase/layers/l4_learning.py +17 -0
- package/python/mindbase/layers/l5_self_social.py +35 -0
- package/python/mindbase/layers/l6_expression.py +23 -0
- package/python/mindbase/layers/l7_evaluation.py +45 -0
- package/python/mindbase/learner.py +196 -0
- package/python/mindbase/lexicon.py +33 -0
- package/python/mindbase/life_state.py +169 -0
- package/python/mindbase/lifecycle.py +87 -0
- package/python/mindbase/liquid_readout.py +48 -0
- package/python/mindbase/llm_adapter.py +233 -0
- package/python/mindbase/llm_cache.py +77 -0
- package/python/mindbase/llm_gate.py +85 -0
- package/python/mindbase/llm_openai.py +130 -0
- package/python/mindbase/llm_setup.py +99 -0
- package/python/mindbase/longrun.py +83 -0
- package/python/mindbase/longterm_memory.py +606 -0
- package/python/mindbase/mb/__init__.py +14 -0
- package/python/mindbase/mb/apl.py +22 -0
- package/python/mindbase/mb/dan.py +14 -0
- package/python/mindbase/mb/habituation.py +27 -0
- package/python/mindbase/mb/kc.py +28 -0
- package/python/mindbase/mb/mbon.py +23 -0
- package/python/mindbase/mb/mushroom_body.py +116 -0
- package/python/mindbase/memory.py +201 -0
- package/python/mindbase/memory_framework.py +158 -0
- package/python/mindbase/memory_retriever.py +110 -0
- package/python/mindbase/memory_summarizer.py +177 -0
- package/python/mindbase/memory_task.py +32 -0
- package/python/mindbase/memory_trace.py +46 -0
- package/python/mindbase/meta.py +32 -0
- package/python/mindbase/motivation.py +31 -0
- package/python/mindbase/multi_session.py +19 -0
- package/python/mindbase/narrative.py +87 -0
- package/python/mindbase/needs.py +77 -0
- package/python/mindbase/neural.py +45 -0
- package/python/mindbase/neuron_models.py +134 -0
- package/python/mindbase/noise.py +309 -0
- package/python/mindbase/one_on_one.py +60 -0
- package/python/mindbase/online_iteration.py +63 -0
- package/python/mindbase/ostracism.py +35 -0
- package/python/mindbase/parallel_eval.py +60 -0
- package/python/mindbase/parallel_qd.py +64 -0
- package/python/mindbase/persona.py +221 -0
- package/python/mindbase/persona_ab.py +64 -0
- package/python/mindbase/persona_bridge.py +129 -0
- package/python/mindbase/persona_compile.py +34 -0
- package/python/mindbase/persona_dsh.py +120 -0
- package/python/mindbase/persona_learner.py +157 -0
- package/python/mindbase/physiology.py +197 -0
- package/python/mindbase/population_lifecycle.py +83 -0
- package/python/mindbase/preferences.py +50 -0
- package/python/mindbase/proactive_agent.py +274 -0
- package/python/mindbase/proactive_gate.py +53 -0
- package/python/mindbase/proactive_group.py +119 -0
- package/python/mindbase/proactive_policy.py +422 -0
- package/python/mindbase/proactive_profile.py +196 -0
- package/python/mindbase/proactive_timing.py +28 -0
- package/python/mindbase/pyramid_stages.py +61 -0
- package/python/mindbase/quantum.py +132 -0
- package/python/mindbase/quantum_chaos.py +98 -0
- package/python/mindbase/reflection.py +68 -0
- package/python/mindbase/regulation_posture.py +50 -0
- package/python/mindbase/relationship_session.py +431 -0
- package/python/mindbase/relationship_suite.py +45 -0
- package/python/mindbase/relationships.py +70 -0
- package/python/mindbase/replay.py +22 -0
- package/python/mindbase/reply_diversity.py +53 -0
- package/python/mindbase/reply_gate.py +52 -0
- package/python/mindbase/rerank.py +41 -0
- package/python/mindbase/rerank_local.py +46 -0
- package/python/mindbase/ripple.py +187 -0
- package/python/mindbase/robust_search.py +68 -0
- package/python/mindbase/runtime.py +113 -0
- package/python/mindbase/scoped_identity.py +116 -0
- package/python/mindbase/self_insight.py +51 -0
- package/python/mindbase/self_model.py +116 -0
- package/python/mindbase/selftest.py +118 -0
- package/python/mindbase/semantic_memory.py +108 -0
- package/python/mindbase/session_store.py +175 -0
- package/python/mindbase/session_window.py +31 -0
- package/python/mindbase/shadow_eval.py +42 -0
- package/python/mindbase/sim_env.py +86 -0
- package/python/mindbase/social.py +138 -0
- package/python/mindbase/social_constraint.py +141 -0
- package/python/mindbase/social_feedback.py +95 -0
- package/python/mindbase/social_norm.py +225 -0
- package/python/mindbase/spiking_reservoir.py +204 -0
- package/python/mindbase/spontaneity.py +40 -0
- package/python/mindbase/story_loop.py +203 -0
- package/python/mindbase/survival_gauntlet.py +128 -0
- package/python/mindbase/symbolic.py +170 -0
- package/python/mindbase/teacher.py +244 -0
- package/python/mindbase/theory.py +42 -0
- package/python/mindbase/theory_of_mind.py +98 -0
- package/python/mindbase/translation_layer.py +80 -0
- package/python/mindbase/turn_eval.py +52 -0
- package/python/mindbase/types.py +78 -0
- package/python/mindbase/uncertainty_field.py +61 -0
- package/python/mindbase/user_loop.py +45 -0
- package/python/mindbase/user_profile.py +73 -0
- package/python/mindbase/value_model.py +64 -0
- package/python/mindbase/values.py +102 -0
- package/python/mindbase/vector_store.py +78 -0
- package/python/mindbase/viability.py +41 -0
- package/python/mindbase/virtual_body.py +90 -0
- package/python/mindbase/will.py +60 -0
- package/python/mindbase/working_memory.py +32 -0
- package/python/mindbase/workspace.py +55 -0
- package/ui/panel.css +16 -5
- package/ui/panel.html +25 -1
- package/ui/panel.js +57 -15
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from typing import Dict, List, Sequence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _tokens(text: str) -> List[str]:
|
|
9
|
+
text = (text or "").lower()
|
|
10
|
+
chars = [text[i:i + 2] for i in range(max(0, len(text) - 1))]
|
|
11
|
+
words = [w for w in text.replace("\n", " ").split() if len(w) > 1]
|
|
12
|
+
return chars + words
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BM25:
|
|
16
|
+
def __init__(self, docs: Sequence[str], k1: float = 1.5, b: float = 0.75):
|
|
17
|
+
self.docs = list(docs)
|
|
18
|
+
self.k1 = k1
|
|
19
|
+
self.b = b
|
|
20
|
+
self.freqs: List[Counter] = [Counter(_tokens(d)) for d in self.docs]
|
|
21
|
+
self.lengths = [sum(f.values()) for f in self.freqs]
|
|
22
|
+
self.avgdl = sum(self.lengths) / max(1, len(self.lengths))
|
|
23
|
+
df: Dict[str, int] = {}
|
|
24
|
+
for f in self.freqs:
|
|
25
|
+
for t in f:
|
|
26
|
+
df[t] = df.get(t, 0) + 1
|
|
27
|
+
n = max(1, len(self.docs))
|
|
28
|
+
self.idf = {t: math.log(1 + (n - v + 0.5) / (v + 0.5)) for t, v in df.items()}
|
|
29
|
+
|
|
30
|
+
def score(self, query: str, doc_index: int) -> float:
|
|
31
|
+
q = Counter(_tokens(query))
|
|
32
|
+
f = self.freqs[doc_index]
|
|
33
|
+
dl = self.lengths[doc_index] or 1
|
|
34
|
+
total = 0.0
|
|
35
|
+
for term, qf in q.items():
|
|
36
|
+
tf = f.get(term, 0)
|
|
37
|
+
if not tf:
|
|
38
|
+
continue
|
|
39
|
+
denom = tf + self.k1 * (1 - self.b + self.b * dl / max(1e-6, self.avgdl))
|
|
40
|
+
total += self.idf.get(term, 0.0) * tf * (self.k1 + 1) / denom * qf
|
|
41
|
+
return total
|
|
42
|
+
|
|
43
|
+
def rank(self, query: str, k: int = 5) -> List[int]:
|
|
44
|
+
scores = [(self.score(query, i), i) for i in range(len(self.docs))]
|
|
45
|
+
scores.sort(reverse=True)
|
|
46
|
+
return [i for _, i in scores[:k]]
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Bootstrap local appraisal heads from silver labels, then run without the LLM.
|
|
2
|
+
|
|
3
|
+
The LLM is optional and offline-only:
|
|
4
|
+
texts -> DSH/LLM labeler -> StorySample dataset -> local linear AppraisalModel
|
|
5
|
+
Runtime _blend_appraisal() uses the local head; no LLM call.
|
|
6
|
+
|
|
7
|
+
The default ScriptedLabeler makes the pipeline testable without any API.
|
|
8
|
+
DSHLabeler is cache-friendly: frozen system prompt + appended JSON payload.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import math
|
|
15
|
+
import os
|
|
16
|
+
import random
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Dict, List, Optional, Sequence, Tuple
|
|
20
|
+
|
|
21
|
+
from .config import MindConfig
|
|
22
|
+
from .engine import MindBase
|
|
23
|
+
from .lexicon import text_affect
|
|
24
|
+
from .teacher import ScriptedTeacher
|
|
25
|
+
from .training.appraisal import AppraisalModel, TARGETS
|
|
26
|
+
from .training.dataset import StorySample
|
|
27
|
+
from .training.features import feature_names
|
|
28
|
+
from .training.persist import load_heads, save_heads
|
|
29
|
+
from .types import Event
|
|
30
|
+
|
|
31
|
+
DATA = Path(__file__).resolve().parent / "data"
|
|
32
|
+
DEFAULT_HEADS = DATA / "bootstrap_heads.json"
|
|
33
|
+
DEFAULT_DATASET = DATA / "bootstrap_labels.jsonl"
|
|
34
|
+
|
|
35
|
+
_LABEL_SYSTEM = (
|
|
36
|
+
"You are an annotation teacher for a digital companion. Return ONLY JSON. "
|
|
37
|
+
"Rate the user text on five appraisal dimensions."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _clamp(v: float, lo: float, hi: float) -> float:
|
|
42
|
+
try:
|
|
43
|
+
x = float(v)
|
|
44
|
+
except Exception:
|
|
45
|
+
return lo
|
|
46
|
+
return lo if x < lo else hi if x > hi else x
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _extract_json(text: str) -> Dict[str, object]:
|
|
50
|
+
raw = str(text or "").strip()
|
|
51
|
+
fence = chr(96) * 3
|
|
52
|
+
if raw.startswith(fence):
|
|
53
|
+
raw = raw.strip(chr(96))
|
|
54
|
+
if raw.lower().startswith("json"):
|
|
55
|
+
raw = raw[4:]
|
|
56
|
+
i = raw.find("{")
|
|
57
|
+
j = raw.rfind("}")
|
|
58
|
+
if i >= 0 and j > i:
|
|
59
|
+
raw = raw[i:j + 1]
|
|
60
|
+
return json.loads(raw)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ScriptedLabeler:
|
|
64
|
+
"""Deterministic silver labels; no network, no LLM. Used for tests/fallback."""
|
|
65
|
+
|
|
66
|
+
SOCIAL_WORDS = ("你", "陪我", "谢谢", "喜欢", "想", "晚安", "在吗", "我们", "一起")
|
|
67
|
+
|
|
68
|
+
def label(self, text: str, directed: bool = True, context: Optional[Dict[str, object]] = None) -> Dict[str, float]:
|
|
69
|
+
v, a = text_affect(str(text or ""))
|
|
70
|
+
social = 0.65 if directed or any(w in str(text or "") for w in self.SOCIAL_WORDS) else 0.35
|
|
71
|
+
relevance = 0.70 if directed else 0.45
|
|
72
|
+
threat = max(0.0, -float(v)) * 0.85
|
|
73
|
+
return {
|
|
74
|
+
"valence": round(_clamp(v, -1.0, 1.0), 4),
|
|
75
|
+
"arousal": round(_clamp(a, 0.0, 1.0), 4),
|
|
76
|
+
"threat": round(_clamp(threat, 0.0, 1.0), 4),
|
|
77
|
+
"social": round(_clamp(social, 0.0, 1.0), 4),
|
|
78
|
+
"relevance": round(_clamp(relevance, 0.0, 1.0), 4),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DSHLabeler:
|
|
83
|
+
"""LLM/DSH silver labeler with a stable cached prefix and scripted fallback."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, client=None, fallback: Optional[ScriptedLabeler] = None):
|
|
86
|
+
self.client = client
|
|
87
|
+
self.fallback = fallback or ScriptedLabeler()
|
|
88
|
+
self.calls = 0
|
|
89
|
+
|
|
90
|
+
def _call(self, system: str, user: str) -> str:
|
|
91
|
+
if self.client is None:
|
|
92
|
+
raise RuntimeError("no DSH/LLM client configured")
|
|
93
|
+
from .llm_adapter import build_prompt
|
|
94
|
+
from .types import LLMRequest
|
|
95
|
+
req = LLMRequest(mode="semantic", urgency=0.5, max_tokens=300,
|
|
96
|
+
reason="bootstrap_labeler", context={"system": system, "user": user})
|
|
97
|
+
self.calls += 1
|
|
98
|
+
return str(self.client.generate(req))
|
|
99
|
+
|
|
100
|
+
def label(self, text: str, directed: bool = True, context: Optional[Dict[str, object]] = None) -> Dict[str, float]:
|
|
101
|
+
payload = json.dumps({
|
|
102
|
+
"text": str(text or ""),
|
|
103
|
+
"directed": bool(directed),
|
|
104
|
+
"context": context or {},
|
|
105
|
+
"schema": {k: "number" for k in TARGETS},
|
|
106
|
+
}, ensure_ascii=False)
|
|
107
|
+
try:
|
|
108
|
+
data = _extract_json(self._call(_LABEL_SYSTEM, payload))
|
|
109
|
+
out: Dict[str, float] = {}
|
|
110
|
+
for k in TARGETS:
|
|
111
|
+
lo, hi = (-1.0, 1.0) if k == "valence" else (0.0, 1.0)
|
|
112
|
+
out[k] = round(_clamp(data.get(k, 0.0), lo, hi), 4)
|
|
113
|
+
return out
|
|
114
|
+
except Exception:
|
|
115
|
+
return self.fallback.label(text, directed=directed, context=context)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def build_bootstrap_dataset(n_scenarios: int = 50, turns_per_scenario: int = 5,
|
|
119
|
+
labeler: Optional[object] = None, seed: int = 1,
|
|
120
|
+
mind_seed: int = 42) -> List[StorySample]:
|
|
121
|
+
labeler = labeler or ScriptedLabeler()
|
|
122
|
+
teacher = ScriptedTeacher(n_scenarios=max(1, int(n_scenarios)), seed=int(seed))
|
|
123
|
+
mind = MindBase(MindConfig(seed=int(mind_seed)))
|
|
124
|
+
samples: List[StorySample] = []
|
|
125
|
+
for i in range(max(1, int(n_scenarios))):
|
|
126
|
+
scenario = teacher.scenario(i)
|
|
127
|
+
for turn in scenario.turns[:max(1, int(turns_per_scenario))]:
|
|
128
|
+
event = Event(text=str(turn.text), speaker="user", directed=bool(turn.directed),
|
|
129
|
+
ts=mind.t + 60.0, domain="chat")
|
|
130
|
+
try:
|
|
131
|
+
mind.step(event)
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
state = mind._state_features()
|
|
135
|
+
labels = labeler.label(str(turn.text), directed=bool(turn.directed),
|
|
136
|
+
context={"persona": scenario.persona, "scenario": scenario.name})
|
|
137
|
+
samples.append(StorySample(
|
|
138
|
+
event=str(turn.text),
|
|
139
|
+
state=dict(state),
|
|
140
|
+
appraisal=dict(labels),
|
|
141
|
+
action="speak",
|
|
142
|
+
outcome={},
|
|
143
|
+
reward=0.0,
|
|
144
|
+
group=str(scenario.name),
|
|
145
|
+
domain="bootstrap",
|
|
146
|
+
))
|
|
147
|
+
return samples
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _split_by_group(samples: Sequence[StorySample], test_ratio: float = 0.25, seed: int = 0) -> Tuple[List[StorySample], List[StorySample]]:
|
|
151
|
+
groups: Dict[str, List[StorySample]] = {}
|
|
152
|
+
for s in samples:
|
|
153
|
+
groups.setdefault(s.group or "default", []).append(s)
|
|
154
|
+
keys = list(groups)
|
|
155
|
+
random.Random(int(seed)).shuffle(keys)
|
|
156
|
+
n_test = max(1, int(round(len(keys) * float(test_ratio)))) if keys else 0
|
|
157
|
+
test_keys = set(keys[:n_test])
|
|
158
|
+
train: List[StorySample] = []
|
|
159
|
+
test: List[StorySample] = []
|
|
160
|
+
for key, rows in groups.items():
|
|
161
|
+
(test if key in test_keys else train).extend(rows)
|
|
162
|
+
return train, test
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _mae(model: AppraisalModel, samples: Sequence[StorySample]) -> Dict[str, float]:
|
|
166
|
+
if not samples:
|
|
167
|
+
return {}
|
|
168
|
+
err = {k: 0.0 for k in TARGETS}
|
|
169
|
+
for s in samples:
|
|
170
|
+
pred = model.predict(s.event, s.state)
|
|
171
|
+
for k in TARGETS:
|
|
172
|
+
err[k] += abs(float(pred.get(k, 0.0)) - float(s.appraisal.get(k, 0.0)))
|
|
173
|
+
return {f"mae_{k}": round(v / len(samples), 4) for k, v in err.items()}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def run_bootstrap(n_scenarios: int = 50, turns_per_scenario: int = 5,
|
|
177
|
+
epochs: int = 120, seed: int = 1,
|
|
178
|
+
labeler: Optional[object] = None,
|
|
179
|
+
out_heads: Optional[Path | str] = None,
|
|
180
|
+
out_dataset: Optional[Path | str] = None,
|
|
181
|
+
verbose: bool = True) -> Dict[str, object]:
|
|
182
|
+
t0 = time.perf_counter()
|
|
183
|
+
labeler = labeler or ScriptedLabeler()
|
|
184
|
+
samples = build_bootstrap_dataset(n_scenarios=n_scenarios, turns_per_scenario=turns_per_scenario,
|
|
185
|
+
labeler=labeler, seed=seed)
|
|
186
|
+
train, test = _split_by_group(samples, test_ratio=0.25, seed=seed)
|
|
187
|
+
model = AppraisalModel(len(feature_names())).fit(train, epochs=int(epochs))
|
|
188
|
+
metrics = {
|
|
189
|
+
"n_total": len(samples),
|
|
190
|
+
"n_train": len(train),
|
|
191
|
+
"n_test": len(test),
|
|
192
|
+
"epochs": int(epochs),
|
|
193
|
+
"labeler": type(labeler).__name__,
|
|
194
|
+
"train": _mae(model, train),
|
|
195
|
+
"holdout": _mae(model, test),
|
|
196
|
+
"elapsed_sec": round(time.perf_counter() - t0, 3),
|
|
197
|
+
}
|
|
198
|
+
hp = Path(out_heads) if out_heads else DEFAULT_HEADS
|
|
199
|
+
dp = Path(out_dataset) if out_dataset else DEFAULT_DATASET
|
|
200
|
+
hp.parent.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
save_heads({"appraisal": model}, hp)
|
|
202
|
+
if out_dataset is not False:
|
|
203
|
+
dp.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
with dp.open("w", encoding="utf-8") as f:
|
|
205
|
+
for s in samples:
|
|
206
|
+
f.write(json.dumps({
|
|
207
|
+
"event": s.event, "state": s.state, "appraisal": s.appraisal,
|
|
208
|
+
"action": s.action, "group": s.group, "domain": s.domain,
|
|
209
|
+
}, ensure_ascii=False) + "\n")
|
|
210
|
+
metrics["heads_path"] = str(hp)
|
|
211
|
+
metrics["dataset_path"] = str(dp)
|
|
212
|
+
if verbose:
|
|
213
|
+
print(json.dumps(metrics, ensure_ascii=False, indent=2))
|
|
214
|
+
return metrics
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _build_dsh_client():
|
|
218
|
+
try:
|
|
219
|
+
from .llm_openai import OpenAICompatClient
|
|
220
|
+
except Exception:
|
|
221
|
+
return None
|
|
222
|
+
key = os.getenv("DEEPSEEK_API_KEY") or os.getenv("LLM_API_KEY") or os.getenv("OPENAI_API_KEY")
|
|
223
|
+
if not key:
|
|
224
|
+
return None
|
|
225
|
+
try:
|
|
226
|
+
return OpenAICompatClient(model=os.getenv("LLM_MODEL") or "deepseek-chat",
|
|
227
|
+
base_url=os.getenv("LLM_BASE_URL") or "https://api.deepseek.com",
|
|
228
|
+
api_key=key, temperature=0.2)
|
|
229
|
+
except Exception:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def main() -> None:
|
|
234
|
+
parser = argparse.ArgumentParser(description="Bootstrap appraisal head from silver labels (LLM optional)")
|
|
235
|
+
parser.add_argument("--scenarios", type=int, default=50)
|
|
236
|
+
parser.add_argument("--turns", type=int, default=5)
|
|
237
|
+
parser.add_argument("--epochs", type=int, default=120)
|
|
238
|
+
parser.add_argument("--seed", type=int, default=1)
|
|
239
|
+
parser.add_argument("--teacher", choices=["scripted", "dsh"], default="scripted")
|
|
240
|
+
parser.add_argument("--out-heads", default="")
|
|
241
|
+
parser.add_argument("--out-dataset", default="")
|
|
242
|
+
args = parser.parse_args()
|
|
243
|
+
labeler = ScriptedLabeler()
|
|
244
|
+
if args.teacher == "dsh":
|
|
245
|
+
client = _build_dsh_client()
|
|
246
|
+
labeler = DSHLabeler(client=client)
|
|
247
|
+
if client is None:
|
|
248
|
+
print("[bootstrap] DSH client unavailable; using scripted fallback.")
|
|
249
|
+
run_bootstrap(n_scenarios=args.scenarios, turns_per_scenario=args.turns,
|
|
250
|
+
epochs=args.epochs, seed=args.seed, labeler=labeler,
|
|
251
|
+
out_heads=Path(args.out_heads) if args.out_heads else None,
|
|
252
|
+
out_dataset=Path(args.out_dataset) if args.out_dataset else None)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
if __name__ == "__main__":
|
|
256
|
+
main()
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Train local appraisal + timing heads from REAL datasets.
|
|
2
|
+
|
|
3
|
+
Data (already local):
|
|
4
|
+
EmoBank 10k -> valence/arousal
|
|
5
|
+
CPED 11k -> Chinese emotion/DA/Big Five
|
|
6
|
+
ESConv 1.3k -> support strategies
|
|
7
|
+
When2Speak 21.8k -> SPEAK vs SILENT timing
|
|
8
|
+
|
|
9
|
+
No LLM is called. Output: mindbase/data/bootstrap_heads_real.json
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import random
|
|
16
|
+
import re
|
|
17
|
+
import time
|
|
18
|
+
from collections import Counter
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Dict, List, Sequence, Tuple
|
|
21
|
+
|
|
22
|
+
from .training.appraisal import TARGETS, AppraisalModel, CompositeAppraisalModel
|
|
23
|
+
from .training.dataset import StorySample
|
|
24
|
+
from .training.features import feature_names
|
|
25
|
+
from .training.loaders import load_cped, load_emobank, load_esconv
|
|
26
|
+
from .training.persist import save_heads
|
|
27
|
+
from .training.timing import (TimingModel, load_human_thread_pairs, load_spontaneous_pairs,
|
|
28
|
+
load_when2speak_pairs)
|
|
29
|
+
|
|
30
|
+
DATA = Path(__file__).resolve().parent / "data"
|
|
31
|
+
REAL = DATA / "real"
|
|
32
|
+
DEFAULT_OUT = DATA / "bootstrap_heads_real.json"
|
|
33
|
+
DEFAULT_SUMMARY = DATA / "bootstrap_real_summary.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _limit(n: int):
|
|
37
|
+
return None if int(n) <= 0 else int(n)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_real_data(max_per_source: int = 4000) -> Dict[str, List[StorySample]]:
|
|
41
|
+
lim = _limit(max_per_source)
|
|
42
|
+
return {
|
|
43
|
+
"emobank": load_emobank(REAL / "EmoBank" / "emobank.csv", limit=lim),
|
|
44
|
+
"cped": load_cped(REAL / "CPED" / "valid_split.csv", limit=lim),
|
|
45
|
+
"esconv": load_esconv(REAL / "ESConv" / "ESConv.json", limit=lim),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _split_by_group(samples: Sequence[StorySample], test_ratio: float = 0.2, seed: int = 0) -> Tuple[List[StorySample], List[StorySample]]:
|
|
50
|
+
groups: Dict[str, List[StorySample]] = {}
|
|
51
|
+
for s in samples:
|
|
52
|
+
key = s.domain + ":" + (s.group or "default")
|
|
53
|
+
groups.setdefault(key, []).append(s)
|
|
54
|
+
keys = list(groups)
|
|
55
|
+
random.Random(int(seed)).shuffle(keys)
|
|
56
|
+
n_test = max(1, int(round(len(keys) * float(test_ratio)))) if keys else 0
|
|
57
|
+
test_keys = set(keys[:n_test])
|
|
58
|
+
train: List[StorySample] = []
|
|
59
|
+
test: List[StorySample] = []
|
|
60
|
+
for key, rows in groups.items():
|
|
61
|
+
(test if key in test_keys else train).extend(rows)
|
|
62
|
+
return train, test
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _split_appraisal(samples: Sequence[StorySample], seed: int = 0):
|
|
66
|
+
"""Honor EmoBank train/test; group-split CPED/ESConv conversations."""
|
|
67
|
+
train: List[StorySample] = []
|
|
68
|
+
test: List[StorySample] = []
|
|
69
|
+
by_group: Dict[str, List[StorySample]] = {}
|
|
70
|
+
for s in samples:
|
|
71
|
+
if s.domain == "appraisal":
|
|
72
|
+
(test if str(s.group) == "test" else train).append(s)
|
|
73
|
+
else:
|
|
74
|
+
by_group.setdefault(s.domain + ":" + (s.group or "default"), []).append(s)
|
|
75
|
+
keys = list(by_group)
|
|
76
|
+
random.Random(int(seed)).shuffle(keys)
|
|
77
|
+
n_test = max(1, int(round(len(keys) * 0.2))) if keys else 0
|
|
78
|
+
test_keys = set(keys[:n_test])
|
|
79
|
+
for key, rows in by_group.items():
|
|
80
|
+
(test if key in test_keys else train).extend(rows)
|
|
81
|
+
return train, test
|
|
82
|
+
|
|
83
|
+
def _appraisal_mae(model: AppraisalModel, samples: Sequence[StorySample]) -> Dict[str, float]:
|
|
84
|
+
if not samples:
|
|
85
|
+
return {}
|
|
86
|
+
targets = tuple(getattr(model, "targets", TARGETS))
|
|
87
|
+
err = {k: 0.0 for k in targets}
|
|
88
|
+
for s in samples:
|
|
89
|
+
pred = model.predict(s.event, s.state)
|
|
90
|
+
for k in targets:
|
|
91
|
+
err[k] += abs(float(pred.get(k, 0.0)) - float(s.appraisal.get(k, 0.0)))
|
|
92
|
+
return {"mae_" + k: round(v / len(samples), 4) for k, v in err.items()}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _n_speakers(context: str) -> int:
|
|
96
|
+
return len(set(re.findall(r"Speaker_(\d+):", str(context or ""))))
|
|
97
|
+
|
|
98
|
+
def _split_pairs(pairs: Sequence[Tuple[str, int]], test_ratio: float = 0.2, seed: int = 0):
|
|
99
|
+
idx = list(range(len(pairs)))
|
|
100
|
+
random.Random(int(seed)).shuffle(idx)
|
|
101
|
+
n_test = max(1, int(round(len(idx) * float(test_ratio)))) if idx else 0
|
|
102
|
+
test = [pairs[i] for i in idx[:n_test]]
|
|
103
|
+
train = [pairs[i] for i in idx[n_test:]]
|
|
104
|
+
return train, test
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _balance_pairs(pairs: Sequence[Tuple[str, int]], seed: int = 0, cap: int = 3000):
|
|
108
|
+
"""Oversample the rare SPEAK class and keep all SILENT examples."""
|
|
109
|
+
speak = [p for p in pairs if int(p[1]) == 1]
|
|
110
|
+
silent = [p for p in pairs if int(p[1]) == 0]
|
|
111
|
+
if not speak or not silent:
|
|
112
|
+
return list(pairs)
|
|
113
|
+
mult = max(1, min(4, len(silent) // max(1, len(speak))))
|
|
114
|
+
out = list(silent) + speak * mult
|
|
115
|
+
random.Random(int(seed)).shuffle(out)
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _timing_metrics(model: TimingModel, pairs) -> Dict[str, object]:
|
|
120
|
+
if not pairs:
|
|
121
|
+
return {"n": 0}
|
|
122
|
+
y = []
|
|
123
|
+
pred = []
|
|
124
|
+
for item in pairs:
|
|
125
|
+
ctx, label = item[0], int(item[1])
|
|
126
|
+
state = item[2] if len(item) > 2 else None
|
|
127
|
+
y.append(label)
|
|
128
|
+
pred.append(1 if model.predict(ctx, state) == "speak" else 0)
|
|
129
|
+
acc = sum(1 for a, b in zip(y, pred) if a == b) / len(y)
|
|
130
|
+
rec, prec, f1 = [], [], []
|
|
131
|
+
for lab in (0, 1):
|
|
132
|
+
tp = sum(1 for a, b in zip(y, pred) if a == lab and b == lab)
|
|
133
|
+
fn = sum(1 for a, b in zip(y, pred) if a == lab and b != lab)
|
|
134
|
+
fp = sum(1 for a, b in zip(y, pred) if a != lab and b == lab)
|
|
135
|
+
r = tp / (tp + fn) if (tp + fn) else 0.0
|
|
136
|
+
p = tp / (tp + fp) if (tp + fp) else 0.0
|
|
137
|
+
rec.append(r)
|
|
138
|
+
prec.append(p)
|
|
139
|
+
f1.append(2 * p * r / (p + r) if (p + r) else 0.0)
|
|
140
|
+
return {
|
|
141
|
+
"n": len(y),
|
|
142
|
+
"accuracy": round(acc, 4),
|
|
143
|
+
"balanced_accuracy": round(sum(rec) / 2.0, 4),
|
|
144
|
+
"macro_f1": round(sum(f1) / 2.0, 4),
|
|
145
|
+
"speak_recall": round(rec[1], 4),
|
|
146
|
+
"speak_precision": round(prec[1], 4),
|
|
147
|
+
"true": dict(Counter("speak" if x == 1 else "silence" for x in y)),
|
|
148
|
+
"pred": dict(Counter("speak" if x == 1 else "silence" for x in pred)),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def train_real_heads(max_per_source: int = 4000, epochs: int = 60,
|
|
153
|
+
seed: int = 1, out: Path | str = DEFAULT_OUT) -> Dict[str, object]:
|
|
154
|
+
t0 = time.perf_counter()
|
|
155
|
+
data = load_real_data(max_per_source=max_per_source)
|
|
156
|
+
va_rows = list(data["emobank"])
|
|
157
|
+
# CPED is the chat-domain source. ESConv is kept as a support/action
|
|
158
|
+
# source, not mixed into the chat appraisal head (labels/domain differ).
|
|
159
|
+
chat_rows = list(data["cped"])
|
|
160
|
+
es_rows = list(data["esconv"])
|
|
161
|
+
appraisal_rows = va_rows + chat_rows + es_rows
|
|
162
|
+
va_train, va_test = _split_appraisal(va_rows, seed=seed)
|
|
163
|
+
chat_train, chat_test = _split_appraisal(chat_rows, seed=seed)
|
|
164
|
+
_es_train, es_test = _split_appraisal(es_rows, seed=seed)
|
|
165
|
+
ap_test = va_test + chat_test
|
|
166
|
+
|
|
167
|
+
when2_all = load_when2speak_pairs(REAL / "When2Speak" / "finetune_val_token.jsonl")
|
|
168
|
+
when2_1v1 = [p for p in when2_all if _n_speakers(p[0]) <= 2]
|
|
169
|
+
human_all = load_human_thread_pairs(DATA / "human_thread.jsonl")
|
|
170
|
+
spont_all = load_spontaneous_pairs(DATA / "spontaneous_48.jsonl")
|
|
171
|
+
w2_train, w2_test = _split_pairs(when2_1v1, test_ratio=0.2, seed=seed)
|
|
172
|
+
h_train, h_test = _split_pairs(human_all, test_ratio=0.25, seed=seed)
|
|
173
|
+
s_train, s_test = _split_pairs(spont_all, test_ratio=0.25, seed=seed)
|
|
174
|
+
timing_train = w2_train + h_train * 5 + s_train * 5
|
|
175
|
+
timing_bal = _balance_pairs(timing_train, seed=seed, cap=3000)
|
|
176
|
+
|
|
177
|
+
n_features = len(feature_names())
|
|
178
|
+
va_head = AppraisalModel(n_features).fit(va_train, epochs=int(epochs))
|
|
179
|
+
vad_head = AppraisalModel(n_features, targets=("valence", "arousal", "dominance")).fit(va_train, epochs=int(epochs))
|
|
180
|
+
chat_head = AppraisalModel(n_features).fit(chat_train, epochs=int(epochs))
|
|
181
|
+
# The base runtime is chat; use the chat-domain head for all five targets.
|
|
182
|
+
# EmoBank head is kept for calibration/reporting, not runtime.
|
|
183
|
+
appraisal = chat_head
|
|
184
|
+
timing = TimingModel().fit(timing_bal, epochs=100)
|
|
185
|
+
timing.calibrate(h_train + s_train, metric="f1")
|
|
186
|
+
|
|
187
|
+
metrics: Dict[str, object] = {
|
|
188
|
+
"sources": {k: len(v) for k, v in data.items()},
|
|
189
|
+
"when2speak_pairs": len(when2_all),
|
|
190
|
+
"when2speak_1v1_pairs": len(when2_1v1),
|
|
191
|
+
"human_thread_pairs": len(human_all),
|
|
192
|
+
"spontaneous_pairs": len(spont_all),
|
|
193
|
+
"appraisal_rows": len(appraisal_rows),
|
|
194
|
+
"appraisal_train": len(va_train) + len(chat_train),
|
|
195
|
+
"appraisal_test": len(ap_test),
|
|
196
|
+
"appraisal_cped": _appraisal_mae(chat_head, chat_test),
|
|
197
|
+
"appraisal_esconv_generalization": _appraisal_mae(chat_head, es_test),
|
|
198
|
+
"appraisal_emobank": _appraisal_mae(va_head, va_test),
|
|
199
|
+
"appraisal_vad_emobank": _appraisal_mae(vad_head, va_test),
|
|
200
|
+
"timing_train": len(timing_bal),
|
|
201
|
+
"timing_threshold": round(float(timing.threshold), 3),
|
|
202
|
+
"timing_when2_1v1": _timing_metrics(timing, w2_test),
|
|
203
|
+
"timing_human": _timing_metrics(timing, h_test),
|
|
204
|
+
"timing_spontaneous": _timing_metrics(timing, s_test),
|
|
205
|
+
"timing_train_mix": dict(Counter("speak" if int(v) == 1 else "silence" for v in [p[1] for p in timing_bal])),
|
|
206
|
+
"elapsed_sec": round(time.perf_counter() - t0, 3),
|
|
207
|
+
}
|
|
208
|
+
path = Path(out)
|
|
209
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
save_heads({"appraisal": appraisal, "vad": vad_head}, path)
|
|
211
|
+
metrics["heads_path"] = str(path)
|
|
212
|
+
summary = Path(DEFAULT_SUMMARY)
|
|
213
|
+
summary.write_text(json.dumps(metrics, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
214
|
+
metrics["summary_path"] = str(summary)
|
|
215
|
+
return metrics
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main() -> None:
|
|
219
|
+
parser = argparse.ArgumentParser(description="Train real appraisal + timing heads (no LLM)")
|
|
220
|
+
parser.add_argument("--max-per-source", type=int, default=4000)
|
|
221
|
+
parser.add_argument("--epochs", type=int, default=60)
|
|
222
|
+
parser.add_argument("--seed", type=int, default=1)
|
|
223
|
+
parser.add_argument("--out", default=str(DEFAULT_OUT))
|
|
224
|
+
args = parser.parse_args()
|
|
225
|
+
metrics = train_real_heads(max_per_source=args.max_per_source, epochs=args.epochs,
|
|
226
|
+
seed=args.seed, out=args.out)
|
|
227
|
+
print(json.dumps(metrics, ensure_ascii=False, indent=2))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
main()
|