biosnn-mcp 0.2.0__tar.gz
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.
- biosnn_mcp-0.2.0/.gitignore +102 -0
- biosnn_mcp-0.2.0/PKG-INFO +15 -0
- biosnn_mcp-0.2.0/pyproject.toml +33 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/__init__.py +53 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/__main__.py +3 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/_version.py +5 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/consolidate.py +193 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/context.py +129 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/core.py +243 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/dialogue.py +281 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/embeddings.py +81 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/episodic.py +121 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/llm.py +31 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/llm_ollama.py +64 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/roc.py +91 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/scheduling.py +51 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/server.py +335 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/trace.py +68 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/trigger.py +45 -0
- biosnn_mcp-0.2.0/src/biosnn_mcp/trigger_spiking.py +188 -0
- biosnn_mcp-0.2.0/tests/test_consolidate.py +80 -0
- biosnn_mcp-0.2.0/tests/test_context.py +76 -0
- biosnn_mcp-0.2.0/tests/test_episodic.py +96 -0
- biosnn_mcp-0.2.0/tests/test_m1.py +68 -0
- biosnn_mcp-0.2.0/tests/test_m2.py +25 -0
- biosnn_mcp-0.2.0/tests/test_m3b.py +97 -0
- biosnn_mcp-0.2.0/tests/test_mcp_server.py +188 -0
- biosnn_mcp-0.2.0/tests/test_mcp_writes.py +190 -0
- biosnn_mcp-0.2.0/tests/test_ollama.py +65 -0
- biosnn_mcp-0.2.0/tests/test_roc.py +62 -0
- biosnn_mcp-0.2.0/tests/test_scheduling.py +39 -0
- biosnn_mcp-0.2.0/tests/test_trigger_spiking.py +87 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# ---- Python ----
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
.eggs/
|
|
12
|
+
|
|
13
|
+
# ---- 虚拟环境 ----
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
env/
|
|
17
|
+
ENV/
|
|
18
|
+
|
|
19
|
+
# ---- uv ----
|
|
20
|
+
# uv.lock 需要提交(复现清单要求精确依赖版本)
|
|
21
|
+
# 见 docs/reproducibility.md
|
|
22
|
+
|
|
23
|
+
# ---- 数据 ----
|
|
24
|
+
# 计划书 §12.1:本仓库不提供数据镜像。scripts/download_data.py 把数据集落到这里,
|
|
25
|
+
# 并校验哈希。不下发,因为再分发要另看各数据源自己的许可。
|
|
26
|
+
data/
|
|
27
|
+
|
|
28
|
+
# ---- 测试与覆盖率 ----
|
|
29
|
+
.pytest_cache/
|
|
30
|
+
.coverage
|
|
31
|
+
.coverage.*
|
|
32
|
+
htmlcov/
|
|
33
|
+
.tox/
|
|
34
|
+
.nox/
|
|
35
|
+
coverage.xml
|
|
36
|
+
*.cover
|
|
37
|
+
|
|
38
|
+
# ---- 类型检查与缓存 ----
|
|
39
|
+
.mypy_cache/
|
|
40
|
+
.dmypy.json
|
|
41
|
+
dmypy.json
|
|
42
|
+
.ruff_cache/
|
|
43
|
+
.pytype/
|
|
44
|
+
|
|
45
|
+
# ---- Jupyter ----
|
|
46
|
+
.ipynb_checkpoints/
|
|
47
|
+
*.ipynb~
|
|
48
|
+
|
|
49
|
+
# ---- 数据与实验产物 ----
|
|
50
|
+
# 数据集不入库:按计划书 §12.1 不镜像数据,只提供 scripts/download_data.py(逐文件校验 + 打印哈希)。
|
|
51
|
+
data/
|
|
52
|
+
datasets/
|
|
53
|
+
# 但**实验产物入库**(2026-09-23 起。原来这里忽略了 sweep_results/,即那些数据只存在于一块硬盘上)。
|
|
54
|
+
# 理由:论文的图表要从它们出;目录清单与保留策略见 sweep_results/README.md。
|
|
55
|
+
outputs/
|
|
56
|
+
runs/
|
|
57
|
+
wandb/
|
|
58
|
+
*.ckpt
|
|
59
|
+
*.pth
|
|
60
|
+
*.pt
|
|
61
|
+
*.npz
|
|
62
|
+
*.h5
|
|
63
|
+
|
|
64
|
+
# ---- 编辑器 / 系统 ----
|
|
65
|
+
.idea/
|
|
66
|
+
.vscode/*
|
|
67
|
+
!.vscode/settings.json
|
|
68
|
+
!.vscode/extensions.json
|
|
69
|
+
*.swp
|
|
70
|
+
*~
|
|
71
|
+
.DS_Store
|
|
72
|
+
Thumbs.db
|
|
73
|
+
desktop.ini
|
|
74
|
+
|
|
75
|
+
# ---- 构建与文档 ----
|
|
76
|
+
site/
|
|
77
|
+
docs/_build/
|
|
78
|
+
|
|
79
|
+
# ---- CI artifact ----
|
|
80
|
+
# scripts/gap_report.py 在 CI 里生成并上传,不入库:它由记录现算,
|
|
81
|
+
# 入库反而会多出一份需要跟着记录走、却没人会去核对的副本。
|
|
82
|
+
gap-report.md
|
|
83
|
+
gap-report.json
|
|
84
|
+
|
|
85
|
+
# ---- 环境变量 ----
|
|
86
|
+
# 本项目不使用任何环境变量;这里只是防止贡献者把本地的 .env 误提交。
|
|
87
|
+
.env
|
|
88
|
+
.env.*
|
|
89
|
+
|
|
90
|
+
# ---- paper/ 的 LaTeX 中间产物(可再生成,不入库)----
|
|
91
|
+
# PDF 是本地构建产物(latexmk -pdf main.tex),入库会每次重编译都产生一个二进制改动
|
|
92
|
+
# (PDF 内嵌生成时间,哈希必变)。可编译性由 CI 的 latex-build job 保证。
|
|
93
|
+
paper/*.pdf
|
|
94
|
+
paper/*.aux
|
|
95
|
+
paper/*.log
|
|
96
|
+
paper/*.out
|
|
97
|
+
paper/*.blg
|
|
98
|
+
paper/*.bbl
|
|
99
|
+
|
|
100
|
+
# ---- 本地编辑器 / 工具链(不进库)----
|
|
101
|
+
.vscode/
|
|
102
|
+
ToolsRuby/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: biosnn-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: BioSNN-Plug 阶段五 MCP 服务器:4 个只读工具 + 5 个提议工具(propose.*,只改内存替身核),默认只读、逐调用 JSONL 审计,不依赖 LLM
|
|
5
|
+
Project-URL: Homepage, https://github.com/lzmd-arch/BioSNN-Plug
|
|
6
|
+
Project-URL: Repository, https://github.com/lzmd-arch/BioSNN-Plug
|
|
7
|
+
Author-email: 离中梦蝶 <269058959+lzmd-arch@users.noreply.github.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: mcp,orchestration,spiking-neural-network
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "biosnn-mcp"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "BioSNN-Plug 阶段五 MCP 服务器:4 个只读工具 + 5 个提议工具(propose.*,只改内存替身核),默认只读、逐调用 JSONL 审计,不依赖 LLM"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
authors = [{ name = "离中梦蝶", email = "269058959+lzmd-arch@users.noreply.github.com" }]
|
|
12
|
+
keywords = ["mcp", "spiking-neural-network", "orchestration"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
]
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
biosnn-mcp = "biosnn_mcp.server:main"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/lzmd-arch/BioSNN-Plug"
|
|
27
|
+
Repository = "https://github.com/lzmd-arch/BioSNN-Plug"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/biosnn_mcp"]
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.sdist]
|
|
33
|
+
include = ["src/", "tests/"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""BioSNN-Plug 阶段五 MCP:只读骨架(M0)+ 显式可写工具(M1)。"""
|
|
2
|
+
|
|
3
|
+
from .consolidate import ConsolidationReport, MemoryItem, SleepConsolidator
|
|
4
|
+
from .context import ContextCode, ZScoreTopKEncoder
|
|
5
|
+
from .core import CognitiveCore, InMemoryCore, MemoryEntry, ToolError
|
|
6
|
+
from .dialogue import Scenario, Turn, WritableMemory, build_scenarios, run_session
|
|
7
|
+
from .embeddings import HashEmbedding, OllamaEmbedding
|
|
8
|
+
from .episodic import EpisodicStore
|
|
9
|
+
from .llm import LLMAdapter, MockLLM
|
|
10
|
+
from .llm_ollama import OllamaLLM
|
|
11
|
+
from .scheduling import SchedulingPolicy
|
|
12
|
+
from .server import PROTOCOL_VERSION, TOOLS, AuditLog, handle_message, serve_stdio
|
|
13
|
+
from .trace import SCHEMA_VERSION, DialogueTrace, TurnRecord
|
|
14
|
+
from .trigger import TriggerDecision, TriggerPolicy
|
|
15
|
+
from .trigger_spiking import SpikingTrigger
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"PROTOCOL_VERSION",
|
|
19
|
+
"SCHEMA_VERSION",
|
|
20
|
+
"TOOLS",
|
|
21
|
+
"AuditLog",
|
|
22
|
+
"CognitiveCore",
|
|
23
|
+
"ConsolidationReport",
|
|
24
|
+
"ContextCode",
|
|
25
|
+
"DialogueTrace",
|
|
26
|
+
"EpisodicStore",
|
|
27
|
+
"HashEmbedding",
|
|
28
|
+
"InMemoryCore",
|
|
29
|
+
"LLMAdapter",
|
|
30
|
+
"MemoryEntry",
|
|
31
|
+
"MemoryItem",
|
|
32
|
+
"MockLLM",
|
|
33
|
+
"OllamaEmbedding",
|
|
34
|
+
"OllamaLLM",
|
|
35
|
+
"Scenario",
|
|
36
|
+
"SchedulingPolicy",
|
|
37
|
+
"SleepConsolidator",
|
|
38
|
+
"SpikingTrigger",
|
|
39
|
+
"ToolError",
|
|
40
|
+
"TriggerDecision",
|
|
41
|
+
"TriggerPolicy",
|
|
42
|
+
"Turn",
|
|
43
|
+
"TurnRecord",
|
|
44
|
+
"WritableMemory",
|
|
45
|
+
"ZScoreTopKEncoder",
|
|
46
|
+
"__version__",
|
|
47
|
+
"build_scenarios",
|
|
48
|
+
"handle_message",
|
|
49
|
+
"run_session",
|
|
50
|
+
"serve_stdio",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
from ._version import __version__ # 单一版本源
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""阶段五 M3d:睡眠期噪声回放的巩固协议(纯 stdlib,协议层)。
|
|
2
|
+
|
|
3
|
+
计划书 §七 第五阶段任务 4 要求「LLM 输出的脉冲编码回注和记忆巩固(**含睡眠期噪声巩固**)」。
|
|
4
|
+
本模块只实现**巩固协议**:
|
|
5
|
+
|
|
6
|
+
1. 睡眠期对每个条目的嵌入做高斯噪声回放(``rng.gauss``,种子固定 → 可复现);
|
|
7
|
+
2. 回放后两两算余弦相似度,``≥ merge_threshold`` 的条目**合并**(保留先出现的 id 与文本,
|
|
8
|
+
嵌入取噪声回放后的均值);
|
|
9
|
+
3. 重复 ``rounds`` 轮,直到没有合并或轮数用尽;
|
|
10
|
+
4. 返回合并后的条目与报告(合并对、轮数、平均余弦漂移)。
|
|
11
|
+
|
|
12
|
+
**边界**:它不连接真实记忆库、不做语义改写、不生成新知识;噪声是各向同性的,
|
|
13
|
+
合并判据是余弦阈值。这些都要写进结果 README,不能把它写成「睡眠巩固能力」。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import math
|
|
19
|
+
import random
|
|
20
|
+
from collections.abc import Sequence
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
|
|
23
|
+
__all__ = ["ConsolidationReport", "MemoryItem", "SleepConsolidator"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class MemoryItem:
|
|
28
|
+
"""一条待巩固的记忆:id、文本、嵌入。"""
|
|
29
|
+
|
|
30
|
+
id: str
|
|
31
|
+
text: str
|
|
32
|
+
embedding: tuple[float, ...]
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
if not self.id:
|
|
36
|
+
raise ValueError("id 不能为空。")
|
|
37
|
+
if not self.embedding:
|
|
38
|
+
raise ValueError("embedding 不能为空。")
|
|
39
|
+
if not all(math.isfinite(value) for value in self.embedding):
|
|
40
|
+
raise ValueError("embedding 含非有限值。")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ConsolidationReport:
|
|
45
|
+
"""巩固报告。
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
merged_pairs: 每轮合并掉的 (保留 id, 被并入 id)。
|
|
49
|
+
rounds_used: 实际执行的轮数。
|
|
50
|
+
mean_cosine_drift: 噪声回放前后嵌入的余弦相似度均值(衡量回放扰动)。
|
|
51
|
+
original_count / final_count: 输入与输出的条目数。
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
merged_pairs: list[tuple[str, str]] = field(default_factory=list)
|
|
55
|
+
rounds_used: int = 0
|
|
56
|
+
mean_cosine_drift: float = 0.0
|
|
57
|
+
original_count: int = 0
|
|
58
|
+
final_count: int = 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _cosine(left: Sequence[float], right: Sequence[float]) -> float:
|
|
62
|
+
dot = sum(a * b for a, b in zip(left, right, strict=True))
|
|
63
|
+
norm = math.sqrt(sum(a * a for a in left)) * math.sqrt(sum(b * b for b in right))
|
|
64
|
+
if norm == 0.0:
|
|
65
|
+
return 0.0
|
|
66
|
+
return dot / norm
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SleepConsolidator:
|
|
70
|
+
"""睡眠期噪声回放 + 余弦合并的协议实现。
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
noise_sigma: 每个维度的噪声标准差(相对嵌入的 L2 范数归一化后施加)。
|
|
74
|
+
merge_threshold: 合并阈值,余弦 ≥ 该值合并(应在 [-1, 1])。
|
|
75
|
+
rounds: 最多巩固轮数(≥1)。
|
|
76
|
+
seed: 随机种子;同种子 → 同结果。
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
*,
|
|
82
|
+
noise_sigma: float = 0.05,
|
|
83
|
+
merge_threshold: float = 0.95,
|
|
84
|
+
merge_margin: float = 0.0,
|
|
85
|
+
rounds: int = 3,
|
|
86
|
+
seed: int = 0,
|
|
87
|
+
) -> None:
|
|
88
|
+
if not 0.0 <= noise_sigma <= 1.0:
|
|
89
|
+
raise ValueError(f"noise_sigma 应在 [0, 1],收到 {noise_sigma!r}。")
|
|
90
|
+
if not -1.0 <= merge_threshold <= 1.0:
|
|
91
|
+
raise ValueError(f"merge_threshold 应在 [-1, 1],收到 {merge_threshold!r}。")
|
|
92
|
+
if not 0.0 <= merge_margin <= 2.0:
|
|
93
|
+
raise ValueError(f"merge_margin 应在 [0, 2],收到 {merge_margin!r}。")
|
|
94
|
+
if not isinstance(rounds, int) or isinstance(rounds, bool) or rounds < 1:
|
|
95
|
+
raise ValueError(f"rounds 必须是 ≥1 的整数,收到 {rounds!r}。")
|
|
96
|
+
self.noise_sigma = float(noise_sigma)
|
|
97
|
+
self.merge_threshold = float(merge_threshold)
|
|
98
|
+
# M3f:并入前还要求「最近邻与次近邻的余弦间隔 ≥ margin」,0 = 历史行为逐位不变。
|
|
99
|
+
self.merge_margin = float(merge_margin)
|
|
100
|
+
self.rounds = rounds
|
|
101
|
+
self.seed = int(seed)
|
|
102
|
+
|
|
103
|
+
def _replay(self, item: MemoryItem, rng: random.Random) -> tuple[float, ...]:
|
|
104
|
+
"""对嵌入加各向同性噪声并重新归一化。"""
|
|
105
|
+
norm = math.sqrt(sum(value * value for value in item.embedding))
|
|
106
|
+
if norm == 0.0:
|
|
107
|
+
return item.embedding
|
|
108
|
+
# 每维标准差 = σ·‖x‖/√dim,使**总**噪声范数 ≈ σ·‖x‖(逐维用 σ·‖x‖ 会让
|
|
109
|
+
# 总噪声随 √dim 放大:768 维时达到信号的 1.4 倍,回放余弦被压到 0.58)。
|
|
110
|
+
scale = self.noise_sigma * norm / math.sqrt(len(item.embedding))
|
|
111
|
+
noisy = [value + rng.gauss(0.0, scale) for value in item.embedding]
|
|
112
|
+
noisy_norm = math.sqrt(sum(value * value for value in noisy))
|
|
113
|
+
if noisy_norm == 0.0:
|
|
114
|
+
return item.embedding
|
|
115
|
+
return tuple(value / noisy_norm * norm for value in noisy)
|
|
116
|
+
|
|
117
|
+
def consolidate(
|
|
118
|
+
self, items: Sequence[MemoryItem]
|
|
119
|
+
) -> tuple[list[MemoryItem], ConsolidationReport]:
|
|
120
|
+
"""执行睡眠巩固,返回(合并后的条目,报告)。"""
|
|
121
|
+
for item in items:
|
|
122
|
+
if not isinstance(item, MemoryItem):
|
|
123
|
+
raise TypeError(f"条目必须是 MemoryItem,收到 {type(item)!r}。")
|
|
124
|
+
ids = [item.id for item in items]
|
|
125
|
+
if len(set(ids)) != len(ids):
|
|
126
|
+
raise ValueError("条目 id 必须唯一。")
|
|
127
|
+
|
|
128
|
+
rng = random.Random(self.seed)
|
|
129
|
+
report = ConsolidationReport(original_count=len(items))
|
|
130
|
+
current = list(items)
|
|
131
|
+
drifts: list[float] = []
|
|
132
|
+
|
|
133
|
+
for round_index in range(1, self.rounds + 1):
|
|
134
|
+
replayed = [self._replay(item, rng) for item in current]
|
|
135
|
+
drifts.extend(
|
|
136
|
+
_cosine(item.embedding, noisy)
|
|
137
|
+
for item, noisy in zip(current, replayed, strict=True)
|
|
138
|
+
)
|
|
139
|
+
survivors: list[MemoryItem] = []
|
|
140
|
+
merged_this_round = 0
|
|
141
|
+
for item, noisy in zip(current, replayed, strict=True):
|
|
142
|
+
merged = False
|
|
143
|
+
if self.merge_margin > 0 and survivors:
|
|
144
|
+
# 先选最近邻,再检查它与次近邻的间隔;间隔不足就不合并。
|
|
145
|
+
scored = sorted(
|
|
146
|
+
(
|
|
147
|
+
(_cosine(noisy, survivor.embedding), index)
|
|
148
|
+
for index, survivor in enumerate(survivors)
|
|
149
|
+
),
|
|
150
|
+
reverse=True,
|
|
151
|
+
)
|
|
152
|
+
best_score, best_index = scored[0]
|
|
153
|
+
margin_ok = len(scored) == 1 or best_score - scored[1][0] >= self.merge_margin
|
|
154
|
+
if best_score >= self.merge_threshold and margin_ok:
|
|
155
|
+
merged = True
|
|
156
|
+
merged_this_round += 1
|
|
157
|
+
survivor = survivors[best_index]
|
|
158
|
+
report.merged_pairs.append((survivor.id, item.id))
|
|
159
|
+
survivors[best_index] = MemoryItem(
|
|
160
|
+
id=survivor.id,
|
|
161
|
+
text=survivor.text,
|
|
162
|
+
embedding=tuple(
|
|
163
|
+
(a + b) / 2.0
|
|
164
|
+
for a, b in zip(survivor.embedding, noisy, strict=True)
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
if not merged:
|
|
168
|
+
survivors.append(MemoryItem(id=item.id, text=item.text, embedding=noisy))
|
|
169
|
+
continue
|
|
170
|
+
for index, survivor in enumerate(survivors):
|
|
171
|
+
if _cosine(noisy, survivor.embedding) >= self.merge_threshold:
|
|
172
|
+
merged = True
|
|
173
|
+
merged_this_round += 1
|
|
174
|
+
report.merged_pairs.append((survivor.id, item.id))
|
|
175
|
+
survivors[index] = MemoryItem(
|
|
176
|
+
id=survivor.id,
|
|
177
|
+
text=survivor.text,
|
|
178
|
+
embedding=tuple(
|
|
179
|
+
(a + b) / 2.0
|
|
180
|
+
for a, b in zip(survivor.embedding, noisy, strict=True)
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
break
|
|
184
|
+
if not merged:
|
|
185
|
+
survivors.append(MemoryItem(id=item.id, text=item.text, embedding=noisy))
|
|
186
|
+
current = survivors
|
|
187
|
+
report.rounds_used = round_index
|
|
188
|
+
if merged_this_round == 0:
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
report.final_count = len(current)
|
|
192
|
+
report.mean_cosine_drift = sum(drifts) / len(drifts) if drifts else 1.0
|
|
193
|
+
return current, report
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""阶段五 M3c:z-score 标准化 top-k 群体编码(纯 stdlib,写入情景记忆用)。
|
|
2
|
+
|
|
3
|
+
计划书 §七 第五阶段任务 2:把 LLM 嵌入做 **z-score 标准化 + top-k**,再用群体编码写成
|
|
4
|
+
脉冲模式,供 SNN 情景记忆写入 [Savage, 2026]。本模块只做「嵌入 → 脉冲模式」这一段,
|
|
5
|
+
不连接任何记忆库,也不依赖 torch/numpy(包本身零依赖)。
|
|
6
|
+
|
|
7
|
+
编码步骤(写死,便于复现):
|
|
8
|
+
|
|
9
|
+
1. 对输入向量做**总体** z-score:``z_i = (x_i − mean) / std``(``std = 0`` 时全零);
|
|
10
|
+
2. 取 ``|z_i|`` 最大的 **top-k** 维(同值按下标升序,保证确定性);
|
|
11
|
+
3. 每个被选维在 ``[−span, +span]`` 上铺 ``n_centers`` 个高斯中心,
|
|
12
|
+
激活 ``a_j = exp(−(z_i − c_j)² / (2σ²))``;
|
|
13
|
+
4. 激活 ≥ ``threshold`` 记 1,否则 0;按「先被选维、后中心」的顺序展平。
|
|
14
|
+
|
|
15
|
+
输出长度恒为 ``k × n_centers``;它是**脉冲模式**,不是连续嵌入。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import math
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
__all__ = ["ContextCode", "ZScoreTopKEncoder"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class ContextCode:
|
|
29
|
+
"""一次编码的结果。
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
indices: 被选中的维度下标(按 ``|z|`` 降序、同值按位置升序)。
|
|
33
|
+
zscores: 对应维度的 z-score。
|
|
34
|
+
activations: 展平后的群体激活,长度 ``len(indices) × n_centers``。
|
|
35
|
+
spikes: 二值化后的脉冲模式,长度同上,取值 ``{0.0, 1.0}``。
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
indices: tuple[int, ...]
|
|
39
|
+
zscores: tuple[float, ...]
|
|
40
|
+
activations: tuple[float, ...]
|
|
41
|
+
spikes: tuple[float, ...]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ZScoreTopKEncoder:
|
|
45
|
+
"""把任意维度的嵌入编码成定长脉冲模式。
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
top_k: 选多少个维度(必须 ≥1)。
|
|
49
|
+
n_centers: 每个维度的群体中心数(必须 ≥2)。
|
|
50
|
+
sigma: 高斯宽度(>0)。
|
|
51
|
+
span: 中心覆盖范围 ``[−span, +span]``(>0)。
|
|
52
|
+
threshold: 二值化阈值,激活 ≥ 该值记 1(应在 (0, 1])。
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: 参数越界,或输入为空、含非有限值。
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
top_k: int = 16,
|
|
62
|
+
n_centers: int = 8,
|
|
63
|
+
sigma: float = 1.0,
|
|
64
|
+
span: float = 3.0,
|
|
65
|
+
threshold: float = 0.5,
|
|
66
|
+
) -> None:
|
|
67
|
+
if not isinstance(top_k, int) or isinstance(top_k, bool) or top_k < 1:
|
|
68
|
+
raise ValueError(f"top_k 必须是 ≥1 的整数,收到 {top_k!r}。")
|
|
69
|
+
if not isinstance(n_centers, int) or isinstance(n_centers, bool) or n_centers < 2:
|
|
70
|
+
raise ValueError(f"n_centers 必须是 ≥2 的整数,收到 {n_centers!r}。")
|
|
71
|
+
if not sigma > 0:
|
|
72
|
+
raise ValueError(f"sigma 必须为正,收到 {sigma!r}。")
|
|
73
|
+
if not span > 0:
|
|
74
|
+
raise ValueError(f"span 必须为正,收到 {span!r}。")
|
|
75
|
+
if not 0 < threshold <= 1:
|
|
76
|
+
raise ValueError(f"threshold 应在 (0, 1],收到 {threshold!r}。")
|
|
77
|
+
self.top_k = top_k
|
|
78
|
+
self.n_centers = n_centers
|
|
79
|
+
self.sigma = float(sigma)
|
|
80
|
+
self.span = float(span)
|
|
81
|
+
self.threshold = float(threshold)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def dimension(self) -> int:
|
|
85
|
+
"""脉冲模式的长度 ``top_k × n_centers``。"""
|
|
86
|
+
return self.top_k * self.n_centers
|
|
87
|
+
|
|
88
|
+
def centers(self) -> tuple[float, ...]:
|
|
89
|
+
"""群体中心,``n_centers`` 个,均匀铺在 ``[−span, +span]``。"""
|
|
90
|
+
if self.n_centers == 1: # pragma: no cover - 构造器已拒绝
|
|
91
|
+
return (0.0,)
|
|
92
|
+
step = 2.0 * self.span / (self.n_centers - 1)
|
|
93
|
+
return tuple(-self.span + step * index for index in range(self.n_centers))
|
|
94
|
+
|
|
95
|
+
def encode(self, embedding: Sequence[float]) -> ContextCode:
|
|
96
|
+
"""把嵌入编码成脉冲模式。"""
|
|
97
|
+
values = [float(value) for value in embedding]
|
|
98
|
+
if not values:
|
|
99
|
+
raise ValueError("embedding 不能为空。")
|
|
100
|
+
if not all(math.isfinite(value) for value in values):
|
|
101
|
+
raise ValueError("embedding 含非有限值(nan/inf)。")
|
|
102
|
+
|
|
103
|
+
mean = sum(values) / len(values)
|
|
104
|
+
variance = sum((value - mean) ** 2 for value in values) / len(values)
|
|
105
|
+
std = math.sqrt(variance)
|
|
106
|
+
if std == 0.0:
|
|
107
|
+
zeros = (0.0,) * self.dimension
|
|
108
|
+
return ContextCode(indices=(), zscores=(), activations=zeros, spikes=zeros)
|
|
109
|
+
|
|
110
|
+
zscores = [(value - mean) / std for value in values]
|
|
111
|
+
order = sorted(range(len(zscores)), key=lambda index: (-abs(zscores[index]), index))
|
|
112
|
+
chosen = order[: min(self.top_k, len(order))]
|
|
113
|
+
centers = self.centers()
|
|
114
|
+
|
|
115
|
+
activations: list[float] = []
|
|
116
|
+
spikes: list[float] = []
|
|
117
|
+
for index in chosen:
|
|
118
|
+
z = zscores[index]
|
|
119
|
+
for center in centers:
|
|
120
|
+
activation = math.exp(-((z - center) ** 2) / (2.0 * self.sigma**2))
|
|
121
|
+
activations.append(activation)
|
|
122
|
+
spikes.append(1.0 if activation >= self.threshold else 0.0)
|
|
123
|
+
|
|
124
|
+
return ContextCode(
|
|
125
|
+
indices=tuple(chosen),
|
|
126
|
+
zscores=tuple(zscores[index] for index in chosen),
|
|
127
|
+
activations=tuple(activations),
|
|
128
|
+
spikes=tuple(spikes),
|
|
129
|
+
)
|