# FruitFly —— 果蝇玩俄罗斯方块
用 **MaleCNS 雄性果蝇全中枢神经系统连接组**(Janelia,166,700 个神经元、2,558 万个化学突触)
驱动一个闭环俄罗斯方块智能体。
棋盘 → 复眼光感受器 → 脉冲神经网络 → 下行神经元 → 动作 → 回灌游戏,每个仿真 tick
走一圈。
---
## 先说结论(诚实版)
**能跑通,但玩不好。** 这不是缺陷,是这个模型如实反映的结果。
### 长局对照(400 块上限,3 局平均)
| 智能体 | 均分 | 均消行 | 每块消行 | 顶出率 |
|---|---|---|---|---|
| Random(下界) | 0.0 | 0.0 | 0.000 | 100% |
| Greedy(只算洞数+高度) | 64.7 | 47.3 | 0.279 | 100% |
| **Dellacherie(强基线)** | **188.0** | **156.3** | **0.391** | **0%** |
| 纯连接组(166,700 细胞,无学习) | 0.0 | 0.0 | 0.000 | 100% |
| 固定接线 + 可塑读出 | 0.0 | 0.0 | 0.000 | 100% |
### 短局对照(60 块上限,5 局平均,正式实验配置)
| 智能体 | 均分 | 均消行 | 每块消行 | 顶出率 |
|---|---|---|---|---|
| Random | 0.20 | 0.20 | 0.011 | 100% |
| Greedy | 27.40 | 19.20 | 0.322 | 20% |
| Dellacherie | 24.60 | 20.00 | 0.333 | 0% |
| 纯连接组 | 0.00 | 0.00 | 0.000 | 100% |
| 可塑读出 | 0.00 | 0.00 | 0.000 | 100% |
> 短局里 Dellacherie 的**分数**低于 Greedy,是因为它偏向求稳、以单行消除为主
> (平均 1.23 分/行),而 Greedy 偶尔吃到多行消除(1.43 分/行)。拉长到 400 块后
> 差距就体现出来了:Dellacherie 消 156 行且从不顶出,Greedy 消 47 行且必定顶出。
> **短局数字是被方块上限截断的,不是这两个基线的真实水平。**
### 为什么神经线玩不好
**下行神经元的自发活动把视觉驱动淹没了**:
| 条件 | 光感受器发放率 | 下行神经元发放率 | DN 每 tick 发放细胞 |
|---|---|---|---|
| 有视觉输入 | 0.0446 | 0.0652 | 85.66 / 1314 |
| 无视觉输入 | 0.0373 | 0.0647 | **84.97 / 1314** |
加上视觉输入后光感受器发放率上升约 **20%**,但下行神经元的发放率只差 **0.8%**。
根因在动力学参数:`tonic_current = 0.180` 配 `decay = exp(-0.02/0.1) = 0.8187`,
稳态电位 `0.180 / (1 − 0.8187) ≈ 0.993`,**正好压在阈值 1.0 上**。
网络被设计成"自发活跃",整脑约 6.3% 的细胞每 tick 都因噪声和张力电流发放,
视觉输入只是一个小扰动。
这一点 Fly64 的作者在仓库里已经明确写过:
> It is not a validated living fly or a trained Mario player.
> Mario can walk into a wall and stay there.
本项目复现并**量化**了这个现象(发放率证据 + 可解码性证据)。要让它真的会玩,
需要的不只是"接上连接组"。
---
## 快速开始
```bash
# 1. 装依赖(四个包,无深度学习框架)
pip install -r requirements.txt
# 2. 下载 MaleCNS 数据(约 1.1 GB)
python scripts/fetch_connectome.py
# 3. 构建连接组(约 30 秒,会缓存到 data/cache/)
python scripts/build_connectome.py
# 4. 跑实验(结果落到 artifacts/main/)
python scripts/run_experiment.py --line all --episodes 3 --max-pieces 60 --out artifacts/main
# 5. 信息流诊断(回答"棋盘信息传到了哪一层",约 30 分钟)
# 51,200 次全脑步进,受内存带宽限制,无法显著加速
python scripts/diagnose_information.py
# 5b.(可选)解码器定标:证明"信息在时解码器解得出来",用于给诊断结果做对照
python scripts/calibrate_decoder.py
# 6. 生成仪表盘(读 artifacts/main/ 与 artifacts/information_flow.json)
python scripts/build_dashboard.py
# 7. 测试(--full 会跑依赖连接组的用例)
python tests/test_pipeline.py --full
```
跑完打开 `dashboard/index.html`。
上面 2–7 步可以直接一条命令串起来:
```bash
python scripts/reproduce_all.py # 全流程
python scripts/reproduce_all.py --skip-data # 数据已下载时跳过下载
```
**路径约定**:实验命令务必带 `--out artifacts/main`,否则默认会写到 `artifacts/` 根目录,
而 `build_dashboard.py` 默认读的是 `artifacts/main/`。诊断脚本的输出路径是固定的
`artifacts/information_flow.json`,不需要传参。
**性能量级**:全脑一步 LIF 约 26–29 ms(内存带宽受限,2,558 万条边就是物理下限),
约 0.7× 实时。一局 500 tick 大约 15 秒。
---
## 仓库结构
```
README.md 本文档
LICENSE MIT(含第三方数据与项目的声明)
requirements.txt 四个依赖,无深度学习框架
scripts/
fetch_connectome.py 下载 MaleCNS 数据(约 1.1 GB)
build_connectome.py 构建连接组并缓存
run_experiment.py 实验入口(pure / plastic / baseline)
diagnose_information.py 信息流诊断(可解码性检验)
calibrate_decoder.py 解码器定标(合成数据上的功效上界)
build_dashboard.py 生成自包含 HTML 仪表盘
reproduce_all.py 一键串起全流程
probe_neuprint.py neuPrint API 探测(备用查询通道)
eval_retina_mapping.py 复眼映射方案对比工具
tests/test_pipeline.py 快测 + --full(需连接组)
fruitfly/ 神经侧代码
console.py 控制台 UTF-8 兼容(Windows 代码页不是 UTF-8 时的兜底)
tetris/engine.py 游戏侧代码,零第三方依赖
.github/workflows/tests.yml CI:只跑快测,不下载数据集
data/raw/ 下载的数据集 ← 不入库
data/cache/ 构建好的连接组缓存 ← 不入库
artifacts/ 实验产物 ← 不入库
main/results.json 正式实验汇总 ← 例外:入库作证据
plastic_long/results.json 可塑长时训练汇总 ← 例外:入库作证据
dashboard/index.html 生成的仪表盘 ← 不入库
```
`data/`、`artifacts/`、`dashboard/*.html` 都写在 `.gitignore` 里:数据集 1.1 GB
且版权属 Janelia,实验产物全部可由脚本重新生成。CI 只跑不依赖数据集的快测,
因为 GitHub Actions 上每次拉 1.1 GB 不划算 —— 需要连接组的用例(`--full`)请在本地跑。
例外是两次正式实验的汇总结果(`artifacts/main/`、`artifacts/plastic_long/` 下的
`results.json` / `run_manifest.json` / `episodes.csv`,合计约 50 KB):这些数字写在
本文档里,入库后审阅者不必先跑几十分钟仿真就能核对。
---
## 架构
```
tetris/engine.py 确定性俄罗斯方块,零第三方依赖
fruitfly/connectome.py 连接组构建、查询、缓存(feather → CSR)
fruitfly/retina.py 棋盘 → 光感受器(逐行展开 + 高斯接受角)
fruitfly/network.py 稀疏 LIF 脉冲网络
fruitfly/motor.py 下行神经元活动 → 动作(含阈值分位数标定)
fruitfly/agent.py 闭环智能体
fruitfly/readout.py 三因子可塑读出(固定接线,只学输出端)
fruitfly/baseline.py Dellacherie / Greedy / Random 对照组(带 BFS 规划)
fruitfly/experiment.py 实验主循环与结果汇总
```
### 神经网络动力学
完全对齐 Fly64 公开的技术说明:
```
每 20 ms: v ← exp(-dt/0.1)·v + 1.5·W·spikes + 0.180 + noise + retina
噪声是带种子的伯努利背景活动(1.2 Hz,幅度 0.22)
阈值 1 复位 0
视网膜电流只注入光感受器
```
这些是**工程标定值,不是实测生理参数**。
---
## 数据
| 文件 | 说明 |
|---|---|
| `connectome-weights-*.feather` | 1.1 GB,分 2,318 个记录批次流式读取 |
| `body-annotations-*.feather` | 类型 / 类别 / 侧向 / 神经递质 |
| `optic-column-type-assignments-v1.0.xlsx` | 光列 (i, j) 与 L1/R7/R8 的 bodyId |
数据源:(免 token 直链下载)。
### 两个关键的字段陷阱
1. **神经递质名是小写的**(`gaba` / `glutamate` / `histamine`)。写首字母大写会导致
抑制性细胞数恒为 0,整个网络变成纯兴奋回路。
2. **侧向标注要用 `somaSide` 而不是 `rootSide`**:
| 字段 | 非空标注数 |
|---|---|
| `somaSide` | 150,726 |
| `rootSide` | 17,939 |
用 `rootSide` 会让**全部下行神经元的侧向为空**,运动解码器无法按左右拆分转向池,
"左右不对称 → 转向"这个唯一有解剖学依据的定向通道整个失效。
顺带一个生物学确认:光感受器是**组胺能**的,其输出权重全为负——这是对的,
果蝇光感受器通过组胺门控氯通道**抑制** L1/L2。
---
## 三个必须讲清的工程问题
### 1. 棋盘要铺到复眼上,但复眼不是矩形
官方光列表的 `(i, j)` 是**六边形轴向坐标**。判别方法:在
`x = i + j/2, y = j·√3/2` 变换下,全部 892 个右眼列的两两最近邻距离恒为
**1.0000,标准差 0.0000**。
但复眼在该空间里是一条**斜向平行四边形带**(长轴自左下至右上,宽度中部最大、
上下两端收窄)—— 因为眼面是曲面,`(i, j)` 是一套曲线坐标。
直接后果:任何"把矩形棋盘线性铺进包围盒"的做法都必然漏掉两个对角角区。
实测确认,早期实现下棋盘**右下角的响应光感受器数为 0**。
解决办法是**逐行展开**:每个 j 行把该行 `i` 方向的实际区间映射到棋盘整宽。
```
逐行展开后: 193/200 格有光感受器
再叠加高斯接受角: 200/200 格 ← 当前实现
```
已知偏差(写进报告):逐行展开假设每一背腹行都张满整个水平视野,而真实复眼中
背侧缘区(DRA)只张一个很窄的方位角范围。
### 2. 光列坐标得靠四级回退链凑齐
| 来源 | 数量 | 占比 | 做法 |
|---|---|---|---|
| `direct` | 2,628 | 43.1% | 官方光列表直接命中(R7 / R8 / L1) |
| `via_L1` | 3,249 | 53.3% | 经最强下游 L1 反查 |
| `via_L3` | 29 | 0.5% | 经最强下游 L3 反查 |
| `proxy` | 185 | 3.0% | 前三级都没命中,用确定性代理位置 |
L3 不在官方表里,它的光列靠**柱传播**推断:L3 与 L1 同属一个小眼柱(同一批
R1–R6 同时投射到两者),所以"找与它共享最多 R1–R6 突触前细胞的 L1,继承其光列"。
这是数据驱动的推断,不是位置猜测。
**`proxy` 那 185 个光感受器的坐标不由数据决定,解读结果时必须计入。**
推理"最强下游"时必须用 `|W|` 而不是 `W`:光感受器权重恒为负,
用 `max(axis=1) > 0` 判断"是否有连接"会把 100% 判成无连接。
### 3. 动作阈值必须按实测分布标定
手工照 Fly64 的马里奥通路定的阈值,用到俄罗斯方块的通路上完全不匹配:实测
前进池 EMA 的最大值只有 **0.091**,而原硬降阈值是 **0.20** —— 硬降这个动作
整局一次都没触发过。
现在启动时跑 400 tick 预热,按 `|ema_steer|` / `ema_burst` / `ema_forward`
的实测分布分位数定阈值。标定用的是随机棋盘输入,**不含任何游戏成绩信息**,
不会把表现泄漏进基线。
---
## 信息流诊断
直接看下行神经元的发放率会被自发活动淹没,但发放率不变不等于信息不在 ——
信息也可能存在于发放**模式**里。所以做两项独立检验:
1. **发放率对比**:有/无视觉输入时 DN 发放率只差 0.8%(率编码里没有)
2. **可解码性**:用交叉验证的岭回归从各层群体活动解码棋盘占用状态
### 解码器先定标,再解读
低 R² 有两种截然不同的解释:**信息不在**,或者**解码器无能**。二者必须分开,
否则结论不可用。`scripts/calibrate_decoder.py` 在已知信息必然存在的合成数据上
测出解码器的功效上界:
| 解码目标 | n=160 | n=400 | n=1000 |
|---|---|---|---|
| 200 格(全盘) | 0.761 | 0.862 | 0.912 |
| 25 维(5×5 降采样) | 0.880 | 0.926 | 0.951 |
| 11 维(10 列高 + 占用率) | 0.956 | 0.985 | 0.990 |
| 1 维(占用率) | 0.951 | 0.984 | 0.990 |
解码器在真实实验的样本量(n=160)下能把全盘 200 格解到 R²≈0.76。因此诊断里
若某层 R² 接近 0,结论只能是"该层线性可解码信息不足",而不是"方法不行"。
实测中内层交叉验证选出的 α 达到 oracle 最优值的 96%,说明 α 选择环节无系统偏差。
### 关于 R² 的零点
目标(棋盘占用)**必须先在训练折内中心化**。否则模型要花容量去学均值,且
α→∞ 时预测塌向 0,R² 趋向 `1 − ΣY²/Σ(Y−Ȳ)²` —— 对"列高"这类均值远非 0 的
目标约为 −1.8,使"只会预测均值"这一零点严重偏离 0,R² 失去可比性。
中心化修正后,纯噪声输入与打乱标签对照的 R² 均落在 0 附近(实测 −0.023),
零点校准正确。
运行 `python scripts/diagnose_information.py` 得到各层的 R² 对照表。
---
## 动作映射
| 下行神经元 | 动作 | 依据 |
|---|---|---|
| DNa02 / DNg13 左右差 | 左移 / 右移 | 与 Fly64 同构,是唯一有解剖学依据的定向通道 |
| DNp01 / DNp10 爆发 | 旋转 | Fly64 里这一对驱动跳跃(爆发式事件),语义迁移 |
| DNg100 前进活动 | 硬降 | "前进"在俄罗斯方块里只能落到"推进时间"上 |
**这是工程重映射,不是果蝇行为学发现。** DNg100 驱动的"前进"在俄罗斯方块中
没有对应物,只能落到"推进时间"上。
---
## 实验线
- `--line pure` 纯连接组,166,700 细胞,无任何学习或参数拟合
- `--line plastic` 固定接线 + 三因子可塑读出(pre × post × reward,等价于
带基线的 REINFORCE)。**接线本身一个突触都没动**
- `--line baseline` 对照组
### 关于奖励
报告分数**永远只用消行分**(0/1/3/5/8)。塑形项(洞数、高度)只用于给学习
提供梯度——否则"从不消行 ⇒ 奖励恒为 0 ⇒ 梯度消失 ⇒ 学不动"。
---
## 来源与致谢
- **BetterGI** —
工程化范式参考:屏幕 → 视觉识别 → 输入自动化 → 可视化层
- **Fly64 / ornata/fly** —
方法论蓝本。LIF 动力学参数、光感受器注入约定、下行神经元到动作的映射思路
均来自其公开的技术说明
- **MaleCNS** —
数据源:Janelia 雄性果蝇全中枢神经系统连接组 v1.0
感谢源神,感谢 BetterGI 社区。
### 一处明确的设计偏离:不做屏幕 CV
BetterGI 走的是**纯图像视觉识别**路线——截屏、CV 识别 UI 元素、驱动键鼠。
它的价值在于对付**没有可编程接口的第三方游戏**。
本项目**不采用**这条路线,而是直接读取游戏内部状态(`TetrisGame.observation()`
返回 `(3, 20, 10)` 的干净矩阵)。理由:
1. **本项目的瓶颈在神经侧,不在感知侧。** 实测全脑一步要 26–29 ms,
而一次截屏 + CV 识别的开销会叠加在这个已经很紧的预算上。
2. **CV 会引入无法分离的误差源。** 如果棋盘识别本身有 3% 的格子判错,
就无法判断"果蝇玩不好"是神经回路的问题还是识别的问题。本项目要回答的
是前者,所以必须让输入是精确的。
3. **状态可精确读取是俄罗斯方块相对游戏截屏的根本优势**,主动放弃它没有收益。
如果将来要把这套东西接回真实游戏客户端(例如用神经网络去玩原神一类的
无接口程序),那时再叠加 BetterGI 式的 CV 感知层——届时应把 CV 层视为
**独立的、可单独测量误差的模块**,而不是藏在链路里。
---
## 声明的工程近似(完整清单)
跑完实验后 `artifacts/main/run_manifest.json` 里会有完整清单,核心几条:
1. 俄罗斯方块**没有光学系统**。棋盘按逐行展开铺到复眼光列上,是工程手段,
不是测量到的 MaleCNS 光学参数
2. 小眼接受角用 σ=1.0 棋盘格的高斯核建模,是标定值而非实测光学
3. 3.0% 的光感受器坐标来自确定性代理映射,不由数据决定
4. LIF 参数对齐 Fly64 的工程标定值,不是实测生理参数
5. 抑制性递质判据(gaba/glutamate/histamine)是近似,不等于普适的递质-符号映射
6. 动作映射是工程重映射,不是行为学结论
7. "神经时间"换算(20 ms/tick,每 N tick 落一格)是工程选择
8. 传入权重按"入边绝对权和"归一化,是数值稳定手段,会改变各细胞的相对驱动强度
9. 可塑读出只改输出层,与真实果蝇的突触可塑性不是一回事
---
## 已知局限与下一步
**为什么玩不好**(按可能性排序):
1. 网络处于自发活动主导的状态。`tonic_current=0.180` 配 `decay=0.8187` 的稳态
电位约 0.993,正好压在阈值 1.0 上——网络被设计成"自发活跃",视觉输入是一个
小扰动。可以考虑降低 tonic 让网络更依赖感觉输入
2. 4 个细胞的动作池太小,左右差被泊松噪声主导。可以扩大读出群体
3. 视觉前端只用亮度 + 时间对比度两路。Fly64 还用了绿通道拮抗
4. 可塑读出学的是 tick 级策略,要学出时序扩展行为需要远超 150 块的样本量
**尚未做**:
- 光流 / 运动检测前端(果蝇的 T4/T5 通路专门做这个,本项目只是把亮度灌进光感受器)
- 多巴胺能奖励通路建模(可塑规则里的奖励是全局标量,没有生物学结构)
- 完整的行为学验证(本项目没有与真实果蝇行为数据对照)