SpikeInterface — 细胞外电生理统一分析框架

23 个 spike sorter、一个接口,和两次让旧代码全部失效的版本变更

来源 ETH Zurich、University of Edinburgh、CNRS Lyon、Flatiron Institute、Allen Institute for Brain Science 等机构联合开发;代码托管于 github.com/SpikeInterface/spikeinterface;发布渠道 PyPI发布时间: 2026-10-21最后更新: 2026-10-21 阅读 24
SpikeInterface — 细胞外电生理统一分析框架

信息速览

数据集名称SpikeInterface — 细胞外电生理统一分析框架
数据类型MIT 许可,58 个 PyPI 版本,23 个 spike sorter,17 个外部 sorter,4 个内部 sorter,0.105.3 当前版,Python 3.10+,10 种以上格式
规模0
接入方式ETH Zurich、University of Edinburgh、CNRS Lyon、Flatiron Institute、Allen Institute for Brain Science 等机构联合开发;代码托管于 github.com/SpikeInterface/spikeinterface;发布渠道 PyPI
AI 就绪度

一个实验室的电生理分析流程,通常是这么长出来的:先用 SpikeGLX 录数据,再写一个 MATLAB 脚本读 .bin,调 Kilosort 排序,导出到 Phy 手工策管,然后用另一个脚本算波形、算质量指标、画图。

三年后,换了探针、换了实验范式、换了学生。这套脚本没人敢动,因为里面每一个魔数都是某次调参调出来的。

SpikeInterface 想解决的就是这件事:把「读什么格式、用什么排序器、怎么算指标」这三件事拆成可替换的部件,用同一套 API 串起来。

它的论文 2020 年 11 月发在 eLife。到今天,PyPI 上已经有 58 个版本,最新是 2026-10-20 发布的 0.105.3。

这一页不是用法教程——官方文档的教程比这里全。这一页要讲的是三个会让人浪费时间的地方:

  1. **版本号断过一次。**2021-07 从 0.13 直接跳到 0.90,前后完全不兼容。你在网上找到的教程,看它写的版本号就知道该不该信。
  2. **「支持 23 个 sorter」这个数字要看年份。**2020 年论文写 10 个,2026 年文档写 23 个。同一句话在六年里含义变了。
  3. **SortingAnalyzer 有个存盘陷阱。**官方文档专门用一段警告讲它,而这段警告最容易被跳过。

§1 它是什么:一个统一的中间层

1.1 一句话定义与四条边界

这是MIT 许可的 Python 框架,把多种细胞外电生理文件格式与多种尖峰排序算法统一到一套 API,覆盖从读取、预处理、排序、后处理到质量评估的完整链路。

四条边界:

  • **不是排序算法。**它自己不发明排序方法。绝大多数 sorter 是别人的代码,SpikeInterface 提供包装。
  • **不是数据集。**不提供任何样本。它的价值在「让别人的数据能被处理」。
  • **不是格式转换器。**虽然能读写很多格式,但它的核心抽象是 Recording 与 Sorting 两个对象,不是文件。
  • **不是一次安装就完事。**外部 sorter 要单独装,依赖冲突是常态。容器化就是为这件事准备的。

第三条最容易被误解。很多人以为装完 SpikeInterface 就能读所有格式——**能,但读进来的是 Recording 对象,不是原来的文件。**要转回文件,得显式调用 write 系列方法。

1.2 为什么需要这么一层

在 SpikeInterface 之前,一个电生理实验室的分析代码大致是这样一个结构:

读数据(依赖格式:SpikeGLX / Open Ephys / Intan / BlackRock ...)
  → 预处理(自己写的滤波、去共模)
  → 排序(Kilosort / Mountainsort / ...,各自不同接口)
  → 读排序结果(各自不同格式:phy / npz / mat)
  → 算指标(自己写)
  → 画图(自己写)

**每一层都在耦合。**换 sorter 就要重写后面全部;换格式就要重写前面。而且每个人写的预处理都不一样,两个人的结果不可比。

SpikeInterface 的做法是把这条链切成两段接口:

任意格式 ──→ [Recording 对象] ──→ 预处理 ──→ 排序器 ──→ [Sorting 对象] ──→ 分析

Recording 抽象「有 N 个通道、采样率 F、能取任意时间窗的一段电压」。Sorting 抽象「有 M 个单元、每个单元有一串尖峰时刻、每个尖峰属于某个通道」。

**只要你的数据能变成 Recording,后面的所有 sorter 都能用;只要 sorter 输出 Sorting,后面的所有分析都能用。**这是整个框架的全部设计思想。

1.3 时间线:两次断点

时间 版本 事件
2019-07-12 — GitHub 仓库创建
2019-10-08 0.9.1 首个 PyPI 版本
2020-11-10 — eLife 论文发表(9:e61834)
2021-07-29 0.13.0 旧五包体系最后一支
2021-07-31 0.90.0 版本号体系切换,不再兼容 0.10–0.13
2023-11-13 0.99.1 0.99 系列收尾
2024-06-08 0.101.0rc0 SortingAnalyzer 取代旧后处理 API
2025-02-07 0.102.0 —
2025-07-31 0.103.0 —
2026-04-20 0.104.2 —
2026-09-21 0.105.0 —
2026-10-20 0.105.3 当前最新

两个断点值得展开。

**第一个断点(0.13 → 0.90)。**旧版是「一个元包 + 五个子包」的结构:spikeextractors(读文件)、spiketoolkit(预处理与策管)、spikesorters(排序器包装)、spikecomparison(比较)、spikewidgets(可视化)。0.90 之后合并成单一包 spikeinterface,子模块变成 spikeinterface.extractors、spikeinterface.preprocessing 等。

后果:所有写 from spikeextractors import ... 的代码全部失效。你在 GitHub 上搜到的、2019–2021 年间写的示例脚本,几乎都要改。

**第二个断点(0.100 → 0.101)。**引入 SortingAnalyzer 对象,取代原来「先建 extractor 再算波形」的流程。旧的 extract_waveforms / WaveformExtractor 系列 API 被废弃。

后果:2023 年前写的后处理代码基本要重写。这不是改个函数名,是换了数据组织方式——SortingAnalyzer 要求显式决定「存盘还是放内存」,并且所有后续计算都挂在这个对象上。

1.4 版本节奏:一年 12 个 patch

2026 年内从 0.104.2(4 月 20 日)到 0.105.3(10 月 20 日),六个月里发了 12 个版本。平均每月两个。

这个节奏对使用者意味着两件事:

  • 好消息:bug 修得快,新 sorter 接得快。
  • 坏消息:**不锁版本就没法复现。**半年后重装环境,pip install spikeinterface 拿到的是另一个版本,某些默认参数可能变了。

实践建议:论文里写明 spikeinterface==0.105.3,不要写 >=0.10。用 requirements.txt 或 environment.yml 锁死。

1.5 许可与治理

项 值
代码许可 MIT(SPDX: MIT)
论文 eLife,另有 eLife 常规条款
GitHub 867 stars / 285 forks / 26 watchers / 136 位贡献者(含匿名)
未结 issue 335
Release GitHub 上没有 Release,只在 PyPI 发布

最后一行是个实际的麻烦。**很多人去 GitHub 的 Releases 页找变更日志,会发现是空的。**变更记录在 PyPI 的版本历史里,或文档的 changelog 页。

MIT 许可是这里最实用的一条:**可以商用、可以闭源分发、可以改。**只有一条要求——保留版权声明。对做产品的团队来说,这比 GPL 系宽松得多。

1.6 能力清单:它能替你做什么

按官方 README 的条目整理,去掉营销词,讲清楚每一项的实际含义:

能力 实际含义 什么时候用得上
读写多种细胞外格式 统一成 Recording 对象 换设备、合并多来源数据
预处理 带通滤波、共模抑制、相位偏移校正、去漂移 每次都要做
运行外部 sorter 17 个,统一 run_sorter() 接口 比较不同算法
运行内部 sorter 4 个,纯 Python 无外部依赖 不想装 MATLAB/CUDA
容器化运行 Docker / Singularity 镜像 依赖地狱的出口
SortingAnalyzer 后处理 波形、模板、相关图、振幅、主成分 排序之后的一切
比较与基准测试 多组排序结果的单元匹配 方法学研究
质量指标与策管 隔离度、不应期违例、漂移 决定哪些单元能用
可视化 matplotlib / sortingview / ephyviewer 快速检查
导出报告 / 导出 phy HTML 报告、交给 Phy 手工策管 交付与人工复核
spikeinterface-gui 独立包,Qt 或 web 界面 替代 Phy 做手工策管
排序组件 可拼装自己的 sorter 研发新算法
漂移校正框架 motion correction 长时间记录

**这张表里最容易被低估的是「容器化运行」。**它看起来是个部署细节,实际是整个框架能否在真实实验室落地的关键——因为外部 sorter 的依赖实在难搞。

§2 规模:23 个 sorter 这个数字怎么算

2.1 三类 sorter 的官方口径

当前文档(stable)把 sorter 分成三组:

外部 sorter(run_sorter 封装,17 个)

名称 标识符
dartsort dartsort
HerdingSpikes2 herdingspikes
IronClust ironclust
Kilosort / 2 / 2.5 / 3 / 4 kilosort / kilosort2 / kilosort2_5 / kilosort3 / kilosort4
PyKilosort pykilosort
Mountainsort4 / 5 mountainsort4 / mountainsort5
RTSort rtsort
SpyKING Circus spykingcircus
Tridesclous tridesclous
Wave clus waveclus
Combinato combinato
HDSort hdsort

内部 sorter(4 个):Lupin、Spyking Circus2、Tridesclous2、Simple。这些基于 spikeinterface.sortingcomponents,不依赖外部工具。

已弃用的 legacy(2 个):Klusta、YASS。文档原话是「can still be run with an older version of SpikeInterface」。

17 + 4 + 2 = 23。

2.2 2020 年论文只有 10 个

论文列的 sorter 是:Klusta、Mountainsort4、Kilosort、Kilosort2、SpyKING Circus、HerdingSpikes2、Tridesclous、IronClust、Wave clus、HDsort。

十项。注意其中 Klusta 现在已被弃用——也就是说论文时代的十个里有三个(Klusta、YASS 同期、HDSort 的边缘维护状态)今天已经不好用。

六年里发生的事:

  • Kilosort 从一个变成五个(1、2、2.5、3、4);
  • Mountainsort 从 4 变成 4 与 5;
  • 新增 dartsort、RTSort、Combinato、PyKilosort;
  • 内部 sorter 从 0 个变成 4 个;
  • Klusta 弃用。

**所以「SpikeInterface 支持 N 个 sorter」这个说法,N 是时间的函数。**写论文时必须标年份和版本号。

2.3 内部 sorter 为什么重要

四个内部 sorter(Lupin、Spyking Circus2、Tridesclous2、Simple)看起来是补充,实际是战略性的。

外部 sorter 的依赖问题在文档里写得很直白:

“Sometimes they need MATLAB, specific versions of CUDA, specific gcc versions or outdated versions of Python/NumPy.”

**IronClust 要 MATLAB。**一个实验室如果没买 MATLAB 授权,就用不了它。
**Kilosort 系要 CUDA。**没有 NVIDIA 显卡的笔记本跑不动。
**部分老 sorter 要求旧版 Python。**装在一个环境里会污染其他依赖。

内部 sorter 的意义:**只要 pip install spikeinterface,立刻有一个能跑的排序器。**不需要 MATLAB、不需要 GPU、不需要旧版依赖。

README 里有一句很自信的话:这些自研 sorter「compete with kilosort4」。这句话值得记着,但也值得自己验证——内部 sorter 的成熟度评价,最好来自你自己的数据。

2.4 两级架构的代价

把 sorter 分成「外部包装」与「内部实现」两类,带来一个不对称。

项 外部 sorter 内部 sorter
安装 各自安装,依赖可能冲突 pip install 即得
容器化 支持,镜像预装 sorter 不需要
数据拷贝 需要写临时文件再读 不需要(直接处理对象)
速度 取决于算法本身 通常较慢(纯 Python 部分多)
成熟度 多数经过多年打磨 较新
参数体系 各算法自己的参数 统一的组件参数

「数据拷贝」这一行是内部的实质优势。文档原话:「they directly handle SpikeInterface objects, so they do not need any data copy」。

**一次 30 分钟、384 通道、30 kHz 的记录,原始数据约 40 GB。**外部 sorter 要走一遍「写二进制临时文件 → sorter 读 → 输出结果」的流程,磁盘 I/O 是实打实的开销。内部 sorter 直接在内存映射的 Recording 上跑。

**代价是速度。**Kilosort 的核心是 CUDA 加速的模板匹配,纯 Python 实现很难追上。所以内部 sorter 在「小数据、无 GPU、要快速试」的场景最合适。

2.5 一个必须先做的检查

文档给了一个很实用的小函数:

from spikeinterface.sorters import installed_sorters
installed_sorters()

输出形如:

['herdingspikes', 'klusta', 'mountainsort4', 'mountainsort5',
 'spykingcircus', 'tridesclous']

**这个列表是「当前环境真实可用的」,不是「文档声称支持的」。**两者差别常常很大。

如果调一个没装的 sorter,报错信息会告诉你怎么装,但格式往往让人看不懂:

AssertionError: This sorter ironclust is not installed.
    Please install it with:
To use IronClust run:
    >>> git clone https://github.com/jamesjun/ironclust
    and provide the installation path by setting the IRONCLUST_PATH
    environment variables or using IronClustSorter.set_ironclust_path().

**注意这条报错的第一句是 AssertionError,不是 ImportError 或 ModuleNotFoundError。**如果你在 try/except ImportError 里包了 run_sorter,这个错不会被捕获,会直接中断整个批处理。

§3 架构:四个对象撑起整个框架

3.1 Recording:能取任意时间窗的电压

Recording 是读取层的统一抽象。它最少要实现:

  • get_num_channels():通道数
  • get_sampling_frequency():采样率
  • get_traces(start_frame, end_frame, channel_ids):取一段电压

**关键在最后一条:「任意时间窗」。**这意味着 Recording 可以是:

  • 磁盘上的一个二进制文件(按偏移量 seek);
  • 内存里的一个 NumPy 数组;
  • 一个 Zarr 分块;
  • 一个远程对象——IBL 的 streaming recording 就是通过 HTTP 按需拉数据,不落地。

这个设计让「取 30 秒数据算个波形」和「在一份 5 TB 数据上算波形」用同一行代码。

3.2 Sorting:单元与尖峰

Sorting 抽象排序结果:

  • get_num_units():单元数
  • get_unit_spike_train(unit_id):某单元的全部尖峰时刻(采样点序号)
  • get_unit_property(name):单元属性(质量、标签等)

注意单位是采样点序号,不是秒。要换成秒需要除以采样率——这是新手最常踩的单位错误。

3.3 SortingAnalyzer:把两者绑在一起

SortingAnalyzer 是 0.101 引入的,把 Recording 与 Sorting 绑成一个对象,所有后处理挂在它上面。

import spikeinterface.core as si

# 内存格式
analyzer = si.create_sorting_analyzer(
    sorting=sorting,
    recording=recording,
    format="memory",
)

# 存盘格式
analyzer = si.create_sorting_analyzer(
    sorting=sorting,
    recording=recording,
    format="binary_folder",
    folder="my_analyzer",
)

format 只有两个值:memory 或 binary_folder。这个二选一决定了后面所有的存盘行为。

3.4 AnalyzerExtension:一切后处理都是扩展

波形、模板、相关图、尖峰振幅、主成分、质量指标——全部是 AnalyzerExtension。

analyzer.compute("random_spikes")
analyzer.compute("waveforms")
analyzer.compute("templates")
analyzer.compute("noise_levels")
analyzer.compute("spike_amplitudes")

print(analyzer.get_loaded_extension_names())
# ['random_spikes', 'waveforms', 'templates', 'noise_levels', 'spike_amplitudes']

ext = analyzer.get_extension("spike_amplitudes")
data = ext.get_data()

print(analyzer.get_computable_extensions())   # 还有哪些能算

这个设计的好处:**扩展与主对象强绑定。**保存、载入、按单元筛选,都会自动作用到所有扩展上。文档原话:「operations done on the SortingAnalyzer, such as saving, loading, or selecting units, will be automatically applied to all extensions」。

3.5 那个存盘陷阱

这是本页最想说清的一处,因为官方专门用一段 Note 警告,而这段最容易跳过。

错误写法:

analyzer = si.create_sorting_analyzer(
    sorting=sorting, recording=recording, format="memory"
)
analyzer.save_as(folder="my_analyzer")     # 拍了个快照
analyzer.compute("random_spikes", save=True)   # ← 这里不会写进 my_analyzer

结果:my_analyzer 目录里没有 random_spikes。而且不会报错。

原因(文档原文):「The save_as method only made a snapshot of the sorting analyzer which is saved in a folder. Hence compute doesn’t know about the folder and doesn’t save anything.」

正确写法:一开始就用 binary_folder。

analyzer = si.create_sorting_analyzer(
    sorting=sorting, recording=recording,
    format="binary_folder", folder="my_analyzer"
)
analyzer.compute("random_spikes", save=True)   # 写进 my_analyzer

为什么这个陷阱特别坑:它不报错。你在 notebook 里跑完,看起来一切正常,get_loaded_extension_names() 也有内容——因为内存里那个对象确实算了。等到第二天重新载入,才发现磁盘上没有。

文档给的建议是一句纪律:

“We recommend choosing a workflow and sticking with it. Either keep everything on disk or keep everything in memory until you’d like to save.”

**选一条路走到底。**混着来就会出现「算过的没存、存了的算错」。

3.6 传旧的 API 变化:到底变了什么

0.101 之前的后处理流程是这样:

extractor = extract_waveforms(recording, sorting, folder)
extractor.get_waveforms(unit_id)
compute_quality_metrics(extractor, ...)

0.101 之后:

analyzer = create_sorting_analyzer(sorting, recording, format=..., folder=...)
analyzer.compute('waveforms')
analyzer.get_extension('waveforms').get_waveforms(unit_id)
compute_quality_metrics(analyzer, ...)

变化的三处实质:

  1. **参数顺序反了。**旧版是 extract_waveforms(recording, sorting, ...),新版 create_sorting_analyzer(sorting=, recording=, ...) 推荐用关键字。位置传参会把 sorting 和 recording 搞反,而且不报错——两个都是对象,类型检查抓不到。
  2. **扩展要显式 compute。**旧版 extract_waveforms 一次性算完波形与模板。新版要分别 compute("waveforms") 与 compute("templates")。
  3. **存盘语义变了。**旧版 folder 是必需参数,天然落盘。新版 format 决定落不落盘,默认仍是 memory。

第三点是那个陷阱的根源。

§4 预处理的现实:这一步比排序更能决定结果

4.1 一个常被低估的事实

大多数人把注意力放在「用哪个 sorter」。实际影响结果更大的,往往是预处理。

同一份数据,用不同的滤波频段、不同的共模抑制方式、不同的漂移校正策略,排序出来的单元数可能差一倍。

原因:sorter 的核心是「在电压轨迹里找重复的波形模式」。如果预处理把低频漂移去掉了,波形更干净,sorter 更容易找对;如果带通太窄,尖峰的形态被削,sorter 就分不清两个相似的单元。

4.2 常用预处理的顺序

一个稳妥的顺序(不是唯一,但不会出错):

步骤 作用 典型参数
相位偏移校正(phase shift) 纠正多路复用 ADC 的采样时刻差 按探针型号
带通滤波 去掉 LFP 与高频噪声 300–6000 Hz(Kilosort 惯例)
共模抑制(CMR) 去掉所有通道共有的噪声 中位数参考
漂移校正(motion correction) 补偿探针在组织内的位移 见 §4.4
坏通道剔除 去掉阻抗异常或断裂的通道 按噪声水平

**注意带通参数不是普适的。**300–6000 Hz 是 Kilosort 的默认,适合皮层 Neuropixels。对海马的高频振荡、对深部脑区的慢尖峰,参数要调。照抄参数是最常见的隐性错误。

4.3 坏通道:不处理会毁掉整根探针

一根 384 通道的 Neuropixels,进入组织后常有几到几十个通道坏掉:阻抗异常高、或短路、或断路。

这些通道的表现:要么几乎平坦(断路),要么持续饱和平顶(短路),要么噪声极大。

不处理会怎样:共模抑制会用一个被污染的中位数做参考,把所有好通道一起拉坏;sorter 会把这些通道的噪声当成单元。

处理方式:先算每个通道的噪声水平,把离群值剔除。

from spikeinterface.preprocessing import detect_bad_channels
rec = detect_bad_channels(recording, bad_threshold=2.0)

**这一步应该在共模抑制之前做。**顺序错了,参考信号已经被污染。

4.4 漂移校正:长时间记录的关键

Neuropixels 插在活体脑组织里,会随时间缓慢位移:呼吸、心跳、组织松弛、动物运动,都会造成几十微米的漂移。

漂移的后果:同一个神经元在记录开始和结束时落在不同通道上。sorter 不知道这是同一个细胞,会把它切成两个单元。

这就是「同一个细胞被算两次」的主要来源。

SpikeInterface 提供一套运动校正框架,近年还接入了 MEDiCINe(eNeuro 12(3) ENEURO.0529-24.2025)。基本思路是:把记录切成时间块,估计每块的位移,然后对齐。

代价:校正本身要算很久,而且校正参数选错会引入新的伪影。

4.5 一个真实的取舍

预处理做多少,是个权衡:

做得少 做得多
快,原始形态保留 慢,形态被改动
漂移与坏通道会污染 漂移被纠正
换 sorter 时结果差异大 结果更稳定
适合探索 适合正式产出

建议:探索阶段少做,锁定 sorter 与参数后再把完整预处理加上,对比两次结果。如果差异很大,说明你的预处理在改变结论,要写进方法论。

4.6 格式层:为什么「能读」和「读对」是两件事

SpikeInterface 声称支持几十种格式。但「能不能读出来」和「读出来是不是对的」是两个问题,后者经常被跳过。

**每个格式都有它的隐藏约定。**举四个例子。

**SpikeGLX。**一个 run_g0 目录里有多个流:imec0.ap(宽带动作电位)、imec0.lf(低频 LFP)、nidq(外部信号)。默认读哪个流、stream_id 怎么填,直接决定你拿到的是宽带还是低通。读错流不会报错,只会让波形看起来「特别平滑」。

**Open Ephys。**老版本用 .continuous 二进制文件,新版本用 .dat + 结构记录。同一个实验室换一次软件版本,目录结构就变了。读的时候要看清是 read_openephys 的哪个分支。

Intan。.rhd 文件里通道是按放大器分块的,不是按物理顺序。如果探针几何设错,通道映射会整体错位,排序出来的单元深度全错。

**NWB。**一个文件里可能有多组 ElectricalSeries(原始宽带、滤波后、LFP),electrical_series_path 选错就读到别的。文档里给的做法是先 nwbfile.electrodes 看一遍再决定路径。

四条共同规律:

规律 后果
同一格式有多个流/通道组 选错不报错,只是结果错
探针几何要手工指定 指定错,空间信息全废
采样率写在元数据里,也可能要手工给 给错,所有时间轴偏移
通道编号顺序 ≠ 物理顺序 深度图左右颠倒

唯一可靠的检查方法:读完之后画一遍。

import matplotlib.pyplot as plt
# 画一段原始电压,看是不是有尖峰、有没有饱和、通道顺序对不对
fig = plt.figure(figsize=(12, 6))
si.plot_traces(rec, time_range=(0, 2), channel_ids=rec.channel_ids[:16])

看三件事:

  1. 有没有明显的尖峰(说明是宽带,不是 LFP);
  2. 有没有通道平顶(饱和)或近乎直线(断路);
  3. 相邻通道的尖峰是否在时间上递进(说明通道顺序与探针物理顺序一致)。

**第三点是最容易忽略的。**如果通道顺序错了,图看起来依然「正常」,只是尖峰在不同通道间的关系乱了。这个错误在事后极难发现,因为排序器不在乎通道顺序,它照样能切出单元——只是单元的深度位置全错。

4.7 探针几何:一个必须手工填的参数

Neuropixels 的通道不是均匀分布。以 NP 1.0 为例,384 个通道排布在一段约 3.84 mm 的探针上,相邻通道的垂直间距约 20 µm,但每四个通道构成一组,组内还有横向偏移。

SpikeInterface 用「探针几何」描述这件事:每个通道的三维坐标。

为什么必须手工指定:原始数据文件里通常不含这份几何信息。文件只告诉你「第 137 号通道的电压是多少」,不告诉你「137 号在组织里的哪个位置」。

# 按探针型号取预定义几何
rec = rec.set_probe(probe=si.core.Neuropixels1p0())

# 或自定义
from spikeinterface.core import generate_probe
probe = generate_probe(num_elec=384, contact_shape='square',
                       x_pitch=32, y_pitch=20)

几何错了会怎样:排序器算「空间邻域」时用了错的坐标,会去比较物理上不相邻的通道。结果是单元数量下降、噪声单元增加,但表面上看不出原因。

结论:探针几何是这条链路里最容易被省略、后果最隐蔽的一个参数。每次用新探针型号,第一件事是把几何核对一遍。

4.8 采样率与时间轴:三次转换

一个尖峰从电压样本到「秒」要过三道转换,每道都可能出错。

第一道:样本序号 → 秒。除以采样率。这一步最容易错,因为 Sorting 对象里的尖峰时刻是样本序号,不是秒。

**第二道:本机时钟 → 全局时钟。**采集系统有自己的时钟。如果一个 session 里有多块采集卡(Neuropixels + 行为 + 视频),它们各自的时钟要走同步信号对齐。同步信号缺失,两个通道的时间轴就漂了——这正是 IBL 2025-Q1 那个 audio sync 补丁要解决的问题。

**第三道:全局时钟 → 任务事件。**行为软件记录的刺激时刻用的是它自己的时间戳,需要映射到采集时钟上。

三道转换里,第二道最危险,因为它不报错。你算出的「刺激后 200 ms 的发放率」可能实际是「刺激后 350 ms」,而数据看起来完全正常。

检查方法:找一个已知的强响应做交叉验证。比如视觉任务里,初级视觉皮层对刺激的响应峰值应该在刺激后几十毫秒。如果算出来是 300 ms,时间轴八成错了。

§5 质量指标与策管:7.5 万与 62 万的分界

5.1 为什么需要指标

排序算法倾向于「多切」:宁可多输出候选,也不漏掉一个可能存在的细胞。

**结果就是你拿到 100 个单元,但真正的单细胞可能只有 12 个。**这个比例不是夸张——本站另一篇关于 IBL Brain-Wide Map 的条目里,621,733 个排序单元最终只有 75,708 个通过质控,良品率 12.2%。

SpikeInterface 的价值在于把「筛」这一步标准化,让不同实验室的筛选口径可比。

5.2 四类核心指标

指标 问的问题 差的表现
隔离度(isolation / SNR) 这个单元与背景噪声分得开吗 波形淹没在噪声里
不应期违例(refractory violation) 尖峰间隔是否短于生理下限 大量 <1.5 ms 的间隔 → 两个细胞混在一起
存在比例(presence ratio) 这个单元在整个记录时段都在吗 只在前 10% 出现 → 可能是漂移残留
漂移(drift) 单元的空间位置稳定吗 沿探针深度持续移动

**不应期违例是最有判别力的一个。**原理很朴素:一个神经元发放后有一段绝对不应期(通常 1–2 ms),这段时间内不可能再发放。如果一个「单元」里出现大量 0.5 ms 间隔的尖峰对,那它一定是两个以上细胞的混合。

5.3 自动策管工具

近年接入了几个自动策管工具:

工具 作用
Bombcell 自动分类单元质量(good / MUA / noise)
UnitRefine 社区工具箱,多种自动策管方法
SLAy 处理「过度切分」(一个细胞被切成多个)
dartsort 漂移跟踪排序,从源头减少漂移导致的重复

**自动策管不是万能的。**它的训练数据来自特定探针、特定脑区、特定物种。换场景后准确率会掉。正确用法是「自动筛一遍 + 人工抽查」,不是「自动筛完直接用」。

5.4 从 62 万到 7.5 万:一条可复用的筛选纪律

把 IBL 那条链路抽象出来,是一套通用做法:

  1. 算法标签先过一遍(如 label == 'good');
  2. 再看关键指标的分布,不要只看标签——标签是别人定的阈值;
  3. 按脑区/按 session 检查良品率,某个区良品率异常低说明探针位置或组织状态有问题;
  4. 人工抽查 5–10% 的单元,看波形与自相关图;
  5. 记录最终口径:用了哪几列、阈值多少、剩下多少单元。

**第五条最容易被省略,也最影响复现。**写清楚「筛完剩 12,481 个单元,用了 A 列 > x 且 B 列 < y」,比写「经过质量控制」有用一百倍。

§6 怎么用:五个场景的完整链路

6.0 七个高频坑点

坑点 1:把 version 0.90 之前的教程当现行文档

  • 问题:2021-07 之前的代码用 spikeextractors / spiketoolkit 等五个子包。
  • 症状:ModuleNotFoundError: No module named 'spikeextractors'。
  • 解决:看教程里的 import 行。出现 spikeextractors、spiketoolkit、spikesorters 的,一律过时。
  • 参考:0.90(2021-07-31)起合并为单包。

坑点 2:create_sorting_analyzer 位置传参

  • 问题:新版推荐 sorting= / recording= 关键字。位置传参顺序是 sorting 在前。
  • 症状:不报错,但结果全错——因为它把你的 recording 当成了 sorting。
  • 解决:永远用关键字。
  • 参考:0.101 起的 API。

坑点 3:memory 格式 + save_as + compute(save=True)

  • 问题:见 §3.5,快照与后续计算脱钩。
  • 症状:磁盘上缺扩展,不报错。
  • 解决:一开始就用 format="binary_folder"。

坑点 4:run_sorter 抛 AssertionError 而不是 ImportError

  • 问题:sorter 没装时报的是 AssertionError。
  • 症状:try/except ImportError 捕不到,批处理整个中断。
  • 解决:先用 installed_sorters() 检查,或 except (ImportError, AssertionError)。
  • 参考:文档给出的真实报错文本。

坑点 5:以为装了 spikeinterface 就有 sorter

  • 问题:外部 sorter 要单独装。
  • 症状:installed_sorters() 返回的列表比文档说的短很多。
  • 解决:要么装依赖,要么用容器镜像,要么用 4 个内部 sorter。

坑点 6:尖峰时刻的单位当成秒

  • 问题:get_unit_spike_train 返回采样点序号。
  • 症状:算出的发放率差了 30,000 倍。
  • 解决:除以 recording.get_sampling_frequency()。

坑点 7:不锁版本

  • 问题:pip install spikeinterface 每次都装最新。
  • 症状:半年后重跑,结果不一样。
  • 解决:spikeinterface==0.105.3 写进 requirements。

6.1 场景一:从零跑通一个 sorter

最小可跑链路:读数据 → 预处理 → 排序 → 建 analyzer → 算指标。

import spikeinterface.full as si

# 1) 读:SpikeGLX 目录
rec = si.read_spikeglx('/path/to/run_g0', stream_id='imec0.ap')

# 2) 预处理:坏通道 + 相位偏移 + 带通 + 共模抑制
rec = si.detect_bad_channels(rec)
rec = si.phase_shift(rec)
rec = si.bandpass_filter(rec, freq_min=300, freq_max=6000)
rec = si.common_reference(rec, reference='global', operator='median')

# 3) 排序:内部 sorter,无需外部依赖
sorting = si.run_sorter('tridesclous2', rec, folder='/tmp/tdc2_out')

# 4) 建 analyzer(注意:直接磁盘格式,别用 memory 再 save_as)
analyzer = si.create_sorting_analyzer(
    sorting=sorting, recording=rec,
    format='binary_folder', folder='/tmp/analyzer',
)

# 5) 算扩展
analyzer.compute(['random_spikes', 'waveforms', 'templates',
                  'noise_levels', 'spike_amplitudes'])

# 6) 质量指标
analyzer.compute('quality_metrics')
qm = analyzer.get_extension('quality_metrics').get_data()
print(qm.head())

这段代码有三个刻意的选择:

  • 用 tridesclous2(内部 sorter),因为不依赖外部工具,第一次跑通概率最高;
  • 预处理顺序按 §4.2(坏通道 → 相位 → 带通 → 共模);
  • 直接 binary_folder,避开 §3.5 的陷阱。

6.2 场景二:比较两个 sorter

import spikeinterface.full as si
from spikeinterface.comparison import compare_two_sorters

s1 = si.run_sorter('kilosort4', rec, folder='/tmp/ks4')
s2 = si.run_sorter('mountainsort5', rec, folder='/tmp/ms5')

cmp = compare_two_sorters(s1, s2)
print(cmp.agreement_scores)     # 单元匹配矩阵

读这个矩阵的方法:

  • 对角线附近有强匹配 → 两个算法找到的是同一批细胞;
  • 一方多出很多未匹配单元 → 那个算法在「多切」;
  • 匹配但波形差异大 → 两个算法切的是同一群细胞的不同子集。

**这一步的真正价值不是「选哪个更好」,而是「哪些单元是两个算法都同意的」。**交集部分的置信度最高。

6.3 场景三:用容器跑 IronClust(没有 MATLAB)

import spikeinterface.full as si

sorting = si.run_sorter(
    'ironclust', rec,
    folder='/tmp/ironclust_out',
    docker_image=True,       # 用官方容器
)

docker_image=True 会让 SpikeInterface 拉官方镜像、把数据挂进去、跑完把结果取回来。

两个注意点:

  • 镜像里不含 spikeinterface 本身(官方设计如此),启动时容器内会装。所以第一次启动慢。
  • 容器内装哪个版本的 spikeinterface,取决于你本机是怎么装的:pip 装 → 装最新版;源码装 main → 装开发版;本地有改动 → 设 SPIKEINTERFACE_DEV_PATH。

**第二条是复现的隐患。**你今天用 pip 装 0.105.3,容器里装 0.105.3;三个月后别人用同样代码跑,容器里装的是 0.106,结果可能不同。

解决办法:自定义镜像,把版本钉死。

FROM spikeinterface/kilosort3-compiled-base:0.1.0
RUN pip install "spikeinterface[full]==0.105.3"

6.4 场景四:策管与导出 phy

# 自动策管:按指标阈值合并过度切分的单元
from spikeinterface.curation import MergeUnitsSorting

# 导出到 phy 做手工复核
si.export_to_phy(analyzer, output_folder='/tmp/phy_out',
                 compute_pc_features=True, copy_binary=True)

# 或导出 HTML 报告
si.export_report(analyzer, output_folder='/tmp/report')

export_to_phy 的 copy_binary=True 会复制原始二进制,几十 GB。如果只是想看波形,可以设 False 但要在路径上做符号链接。

export_report 生成的是一个自包含 HTML,包含每个单元的波形、自相关图、深度分布、指标表。这是交付给合作者最省事的形式——对方不需要装任何东西。

6.5 场景五:从 NWB 读 IBL 风格数据

import spikeinterface.full as si

rec = si.read_nwb_recording('/path/to/file.nwb')
sorting = si.read_nwb_sorting('/path/to/file.nwb')

# 或者只有 ecephys 部分
rec = si.read_nwb_ecephys('/path/to/file.nwb', electrical_series_path='...')

NWB 的坑:一个 NWB 文件里可能有多组 ElectricalSeries,electrical_series_path 选错会读到 LFP 而不是原始宽带。先列一遍路径再读。

6.6 一个决策表:该用哪个 sorter

没有普适答案,但可以按条件缩小范围。

你的条件 建议起点 理由
第一次用,想快速跑通 tridesclous2 或 simple 内部实现,无外部依赖
有 NVIDIA GPU,Neuropixels kilosort4 生态最成熟,参数文档最全
没有 GPU mountainsort5 或 herdingspikes CPU 可跑,速度可接受
要发表方法学论文 至少跑 3 个,做一致性分析 单一算法的结论不牢
数据有严重漂移 先做 motion correction,再选 sorter 漂移不解决,换 sorter 没用
要实时/在线 rtsort 毫秒级延迟设计

最后一行要说明白:rtsort 的设计目标是实时检测与排序(RT-Sort,PLoS ONE 10.1371/journal.pone.0312438),延迟在毫秒量级。它不是「更快的离线 sorter」,是不同场景的工具。

6.7 完整案例:一份数据,三个 sorter,看清差异在哪

这一步值得亲手做一遍,因为它能把本页前面的所有抽象概念落到地面。

**准备。**取同一段记录(建议 5–10 分钟,太长会等很久),做同一套预处理:

import spikeinterface.full as si

rec = si.read_spikeglx('/path/to/run_g0', stream_id='imec0.ap')
rec = si.detect_bad_channels(rec)
rec = si.phase_shift(rec)
rec = si.bandpass_filter(rec, freq_min=300, freq_max=6000)
rec = si.common_reference(rec, reference='global', operator='median')
rec = rec.frame_slice(start_frame=0, end_frame=int(600 * rec.get_sampling_frequency()))

**跑三个。**选一个内部、两个外部,覆盖不同算法家族:

runs = {}
runs['tdc2'] = si.run_sorter('tridesclous2', rec, folder='/tmp/s_tdc2')
runs['ms5']  = si.run_sorter('mountainsort5', rec, folder='/tmp/s_ms5')
runs['ks4']  = si.run_sorter('kilosort4', rec, folder='/tmp/s_ks4')

先看数量。

for k, s in runs.items():
    print(k, s.get_num_units())

典型的输出形状是这样:内部 sorter 数量中等,Mountainsort 偏少(密度聚类对低发放率单元保守),Kilosort 偏多(模板匹配倾向于多切)。差两三倍是常见的。

再看一致性。

from spikeinterface.comparison import compare_multiple_sorters
mcmp = compare_multiple_sorters(list(runs.values()), name_list=list(runs.keys()))
print(mcmp.agreement_scores)

agreement_scores 是三方两两的匹配矩阵。关注三件事:

现象 含义 该怎么办
三方都匹配 高置信单元 这三个可以用
只有 KS4 有,另两个没有 可能是 KS4 切的碎片 查这些单元的 firing_rate 是否极低
只有 MS5 缺 MS5 对低发放率保守 从 KS4 结果里补,但要单独验证

然后算指标做交叉验证。

for k, s in runs.items():
    a = si.create_sorting_analyzer(s, rec, format='memory')
    a.compute(['random_spikes', 'waveforms', 'templates',
               'noise_levels', 'quality_metrics'])
    qm = a.get_extension('quality_metrics').get_data()
    print(k, qm['snr'].median(), qm['isi_violations_ratio'].median())

两个数的意义:

  • snr 中位数:应该在 3 以上。三个 sorter 之间差太多,说明有一个把噪声当单元了。
  • isi_violations_ratio 中位数:应该接近 0。哪个 sorter 这个值高,说明它有更多「两个细胞混在一起」的单元。

最后人工抽查。

si.plot_unit_waveforms(a, unit_ids=list(a.unit_ids)[:20])
si.plot_unit_templates(a)

看二十个波形。真的单细胞,波形在各个尖峰之间高度一致;混合单元,波形会明显分层。

**这个案例的价值不在于「选出最好的 sorter」,而在于建立判断力。**跑过三五次之后,你看一眼单元数量与指标中位数,就能大致判断这次排序是否正常。

6.8 三个常被问到的具体问题

「我的数据是 2 小时的长记录,内存装不下怎么办?」

不要一次读进来。用 frame_slice 或者让 sorter 自己分块。Kilosort 系本身是分块处理的,Mountainsort 有 batch_size 参数。关键是把「读」和「算」都做成流式,别在一个 get_traces() 里要整段。

「我的采样率是 20 kHz,不是 30 kHz,参数要改吗?」

要。Kilosort 系里有几个参数是按采样点数定义的(如 nt、波形长度),换采样率后同样的数值对应不同的时间长度。尤其 whitening_range、sigmaMask 这类涉及空间的参数,也要按通道间距重算。

一个稳妥做法:先按默认参数跑一小段,看单元数与波形是否合理,再微调。别一开始就照搬别人的参数表——那张表是在别人的探针、别人的采样率下调出来的。

「能不能不装 MATLAB 就跑 IronClust?」

不能,但可以用容器。IronClust 本身是 MATLAB 代码,官方镜像里带了 MATLAB Runtime(不是完整 MATLAB)。注意 Runtime 与完整 MATLAB 的版本兼容性不一致,某些工具箱函数在 Runtime 里不可用。

如果就是要 IronClust 且没授权,最实际的选择是换一个 sorter(Mountainsort5 或 HerdingSpikes2),而不是折腾容器。

6.9 一条最容易被跳过的步骤:先看数据,再跑算法

几乎所有教程的第一行是 read_spikeglx,然后直接进预处理。中间少了一步:看原始数据长什么样。

这一步省掉,后面所有的参数都是在猜。

具体要看四件事。

**第一,噪声水平与共模成分。**画一条通道的原始电压(不滤波)。如果看到明显的低频大波动(几十到几百微伏的慢波),说明 LFP 成分很强,带通的下限不能设太低。如果看到所有通道同步的快速抖动,说明有共模干扰,需要共模抑制。

**第二,饱和与断路通道。**找那些电压长期贴在 ± 最大值的通道(饱和),和电压几乎为 0 方差的通道(断路)。这两个都必须剔除,否则共模参考会被污染。

**第三,尖峰的信噪比量级。**在一段无刺激的静息期,找几个自发的尖峰,看峰峰值与背景噪声的比值。如果只有 2–3 倍,说明探针离细胞群较远,这个位点的单元质量会差。这是「这个 session 值不值得花时间」的最快判据。

**第四,通道顺序。**如 §4.6 所说,看相邻通道的尖峰是否在时间上递进。

# 四件事一次看完
si.plot_traces(rec, time_range=(0, 1), channel_ids=rec.channel_ids[:32],
               mode='line', backend='matplotlib')

**「先看数据」这件事听起来像常识,实际是新手与熟练者最大的差别。**熟练的人花五分钟看图,能省掉后面两小时的调参。

6.10 输出交付:给别人什么

一个排序项目最终要交给别人什么?不是一堆 .npy 文件。四样东西。

第一,一个可复现的环境。requirements.txt 或 environment.yml,写死所有版本,包括 sorter。

**第二,一份参数记录。**预处理参数、sorter 参数、质控阈值,全部明文。不要写「默认参数」四个字——默认值会随版本变。

第三,一个 HTML 报告。export_report 生成,包含每个单元的波形、自相关图、深度、指标。这是别人判断「你的单元可不可信」的唯一依据。

**第四,一份单元清单。**CSV 或 Parquet,每行一个单元:标识、脑区(如果有)、深度、发放率、各项指标、是否通过质控。

四样都齐了,别人才能在你停下地方继续。缺第三样,对方只能相信你的结论;缺第二样,对方连相信都做不到。

§7 质量评估与局限

7.1 六个真实局限

局限 具体表现 影响
版本不兼容跨度大 0.13→0.90、0.100→0.101 两次断点 旧代码与旧教程大范围失效
外部 sorter 依赖重 MATLAB、CUDA、旧 gcc、旧 Python 环境搭建本身是门槛
GitHub 无 Release 变更日志不在 Releases 页 想追变更要另找
335 个未结 issue 冷门 sorter 的问题可能长期无响应 踩到边角问题要自己解决
不发明算法 排序质量取决于所调用的 sorter 别指望它提升算法效果
预处理参数无普适值 300–6000 Hz 只是皮层惯例 照抄参数会出错

第五条值得强调:**SpikeInterface 是「协调层」不是「算法层」。**它不会让 Kilosort 变得更准。它的价值在让你能换、能比、能复现。

7.2 版本兼容性矩阵

这是实践中最需要的一张表。

你的需求 可用版本 说明
旧五包体系(spikeextractors 等) ≤ 0.13.0 2021-07 止
单包 + 旧后处理(extract_waveforms) 0.90 – 0.100.x 2021-07 至 2024-06
单包 + SortingAnalyzer ≥ 0.101.0 2024-06 起
当前最新 0.105.3 2026-10-20

看一份旧代码属于哪一代,最省事的判据是 import 行:

  • from spikeextractors import ... → 一代(≤0.13)
  • from spikeinterface.extractors import ... + extract_waveforms → 二代(0.90–0.100)
  • from spikeinterface.core import create_sorting_analyzer → 三代(≥0.101)

7.3 AI 就绪度评估

按 DAIMS 的维度逐项看(该框架原本用于数据集,这里套用到软件产品上,只取适用项):

维度 项 评估
动机 目的 ✅ 论文明确:统一 spike sorting 技术栈
动机 资助与治理 ✅ 多机构合作,MIT 许可,公开仓库
构成 功能边界 ✅ 文档清晰区分「包装外部」与「内部实现」
构成 接口稳定性 ⚠️ 两次破坏性变更,无 LTS 分支
构成 测试覆盖 ✅ 持续集成,官方强调「CI 覆盖的代码库」
收集 文档完整性 ✅ readthedocs 全量文档 + 多个 Colab 教程
收集 示例可运行 ⚠️ 部分教程依赖外部 sorter,环境未备时跑不通
预处理 参数透明 ✅ 预处理函数参数全部显式
预处理 默认值来源 ⚠️ 部分默认值沿用 Kilosort 惯例,未说明适用边界
使用 许可 ✅ MIT,商用无阻
使用 安装门槛 ⚠️ 核心包易装,外部 sorter 难装
使用 可复现 ⚠️ 需自行锁版本;容器内版本随本机安装方式变
使用 社区支持 ✅ 136 位贡献者,但 335 个未结 issue
使用 长期维护承诺 ⚠️ 无公开的 LTS 承诺

评估总结:作为软件产品,它的文档、测试、许可三项做得扎实;扣分集中在**「接口稳定性」与「可复现」**——这两项互为因果:接口变得快,可复现就必须靠锁版本;而锁版本这件事官方没有强制机制(比如不提供 LTS 分支)。

对使用者的净结论:这是一个值得依赖但必须自己加护栏的工具。护栏就是 requirements.txt + 容器镜像 + 写清版本号。

7.4 与同类工具的位置

工具 定位 与本框架的关系
Phy 手工策管的 GUI 标准 本框架可导出到 phy;也有 spikeinterface-gui 替代
Kilosort 单一排序算法 被封装,是最常用的外部 sorter
MountainSort 另一系算法 被封装(4 与 5)
Brainbox / ibllib(IBL 自研) IBL 数据分析 并行生态,不是上下游
Pynapple 时间序列分析 互补,可接在排序之后
NeuroConv 转 NWB 复用 SortingAnalyzer 对象

关键区分:Kilosort 是「一个算法」,SpikeInterface 是「装算法的架子」。两者不竞争。

§8 生态位置与影响

8.1 它在电生理流水线里的坐标

一条完整的电生理研究链路,大致是这样分布工具的:

采集(SpikeGLX / Open Ephys)
  ↓
读取与预处理(SpikeInterface)← 这里
  ↓
排序(Kilosort / Mountainsort / ...,经 SpikeInterface 调用)
  ↓
后处理与质控(SpikeInterface 的 SortingAnalyzer)
  ↓
手工策管(Phy / spikeinterface-gui)
  ↓
分析与建模(Pynapple / Brainbox / 自研)
  ↓
归档(NWB → DANDI)

**它可以吞掉中间三层。**这是它在六年里被广泛采用的结构性原因——不是因为它算法最好,而是因为它占的位置最居中。

8.2 复用效应:被别的工具当地基

SortingAnalyzer 已经不只是 SpikeInterface 内部的对象。文档明确说它「is already used by SpikeInterface-GUI, SortingView and NeuroConv」。

这三家分别代表三个方向:

  • SpikeInterface-GUI:交互式策管,替代 Phy;
  • SortingView:云端协作查看;
  • NeuroConv:格式转换到 NWB。

也就是说,**「排序结果如何被组织」这件事,已经在这个生态里形成了事实标准。**你在 SpikeInterface 里建的 analyzer,可以直接被另外三个工具读。

这种「对象级互操作」比「文件级互操作」更深一层。文件可以转换,对象不能——用谁的类,就得进谁的生态。

8.3 与 IBL 的关系:两个平行世界

本站另一篇讲 IBL Brain-Wide Map。两者的关系值得说清,因为容易搞混。

维度 SpikeInterface IBL(Brainbox / ibllib)
定位 通用排序框架 特定项目的分析库
排序器 包装 17 个外部 + 4 个内部 自研流水线(基于 Kilosort2.5 系)
数据格式 多种,统一为 Recording/Sorting ALF 约定 + Alyx 元数据
是否互依赖 否 否
交集 都能读 NWB、都能处理 Neuropixels 数据 同

**它们是并行的两个世界,不是上下游。**IBL 用自己的流水线排序,发布聚合表;SpikeInterface 用户可以拿 IBL 的原始数据自己重排一遍——这恰好是「独立复现」的一条现实路径。

8.4 六年做对的一件事

回头看,SpikeInterface 在 2020 年选的那条路——不做算法,只做协调层——是对的。

如果当年它自己也去做一个 sorter,今天它会是「十几个 sorter 里的一个」,和 Kilosort 竞争。而它选择做「所有 sorter 的公共接口」,结果变成所有人都绕不开的那一层。

**这个策略的代价是它必须一直跟。**Kilosort 出 4,它要接;Mountainsort 出 5,它要接;新的漂移校正方法出来,它要接。累积 335 个未结 issue 就是这个代价。

**收益是位置。**在工具链里,位置比性能更值钱。

8.5 对使用者的三条现实建议

第一,锁版本,写进论文。

spikeinterface==0.105.3
kilosort==4.0.31

审稿人问「你怎么保证可复现」时,这两行就是答案。

第二,先跑内部 sorter 验证链路通不通。

装完环境第一件事不是上 Kilosort,是跑 tridesclous2 或 simple。**链路通了再换重的。**这样可以区分「环境问题」与「sorter 问题」。

第三,把预处理写进方法学。

「用 SpikeInterface 做了预处理」这句话没有信息量。要写:坏了几个通道、带通多少、共模用什么参考、有没有做漂移校正、参数多少。

第三点最常被省略,也最影响别人复现你的结果。

8.6 排序算法的四类思路:为什么结果会不一样

框架把 23 个 sorter 统一了接口,但它们的内部逻辑差别很大。理解这四类思路,能帮你判断「两个 sorter 不一致」是正常还是异常。

第一类:模板匹配型(Kilosort 系)。

先用一个简化模型检测出候选尖峰,再把这些尖峰的波形聚成若干「模板」,然后用模板在原始数据上重新扫一遍,找所有匹配位置。

  • 强项:高密度探针、单元多、速度快(GPU)。
  • 弱项:模板数量要预设或自动估计,估少会合并真单元,估多会切碎。
  • 典型问题:同一个细胞被拆成两个(over-splitting),因为它的波形在记录期间漂了。

第二类:密度聚类型(Mountainsort、Tridesclous)。

在特征空间(波形的主成分、或小波系数)里做聚类,把密度相连的点归为一类。

  • 强项:不需要预设单元数,能处理形状不规则的簇。
  • 弱项:特征维度要选,选少了混,选多了散。
  • 典型问题:低发放率单元被当成噪声丢掉,因为密度不够。

第三类:模板块 + 图优化型(SpyKING Circus、HerdingSpikes)。

先把数据切成小块(patch),每块独立聚类,再用图算法把相邻块的同类合并。

  • 强项:能并行,适合长记录。
  • 弱项:块的边界处理不好会切碎单元。
  • 典型问题:同一单元在块边界被分成两个。

第四类:组件化拼装型(Lupin、Spyking Circus2、Tridesclous2、Simple)。

把检测、特征提取、聚类、合并每一步做成可替换组件,用户自己拼。

  • 强项:透明,每一步都能看、能调。
  • 弱项:参数多,需要理解每一步在干什么。
  • 典型问题:参数组合没调好,整体效果不如成熟算法。

**这四类解释了为什么「两个 sorter 结果差 30% 是正常的」。**它们对「什么算一个单元」的判断本来就不一样。交集部分才是高置信度的。

8.7 一个被忽略的工程量:analyzer 目录有多大

SortingAnalyzer 存盘时写什么、占多大空间,很少有人事先算。

以 384 通道、30 kHz、1 小时记录、约 500 个单元为例:

内容 量级 说明
recording 引用 极小 只存路径,不复制原始二进制
sorting 几 MB 尖峰时刻 + 单元标签
random_spikes 极小 抽取的样本索引
waveforms 数 GB 每个尖峰 × 每条通道 × 波形长度(float32)
templates 几十 MB 每单元 × 通道 × 波形长度
spike_amplitudes 几 MB 每个尖峰一个值
principal_components 数 GB 与 waveforms 同量级
correlograms 几十 MB 每单元的自/互相关
quality_metrics 几 MB 每单元一行

两处占空间的大头是 waveforms 与 principal_components。

粗算一下 waveforms:假设 500 个单元、每单元 500 个尖峰、每个尖峰取 384 通道 × 61 个采样点 × 4 字节。

500 × 500 × 384 × 61 × 4 ≈ 23 GB

**23 GB。**这就是为什么官方建议「要先想清楚存不存盘」——format="memory" 时它躺在内存里,binary_folder 时它躺在磁盘上。

两个优化方向:

  • 用 max_spikes_per_unit 限制每单元的尖峰数(默认通常几百,可以降到 100)。波形是统计量,不需要全部尖峰。
  • 用 sparse=True 只存每个单元附近的通道(Neuropixels 上通常每单元只涉及十几到几十个通道,不是 384 个)。这一项能把波形存储降一个数量级。

**这两项没设,一个小时的记录就能吃掉几十 GB 磁盘。**批量处理几十个 session 时,这是硬盘爆掉的直接原因。

8.8 并行与批处理:批量跑 100 个 session 的现实问题

实验室里真正的工作量不是「跑一个 session」,是「跑一百个」。

SpikeInterface 提供 run_sorter_jobs 之类的批量接口,但真实的瓶颈往往不在框架本身。

瓶颈 表现 对策
GPU 显存 同时跑 2 个 Kilosort 就 OOM 串行跑,或按显存排队
磁盘 I/O 100 个 session 同时读写,磁盘打满 分批,每批 4–8 个
内存 waveforms 累积不释放 每跑完一个 session 显式 del analyzer
sorter 自身的临时文件 Kilosort 写几 GB 到临时目录 定期清临时目录

**第三条最容易忽略。**Python 的垃圾回收对大型 NumPy 数组不总是及时,跑几十个 session 后内存会一直涨。每轮循环结束显式 del 并 gc.collect(),能避免半夜 OOM。

8.9 一句实在话:这套工具不能替你做判断

框架能做的:把格式统一、把接口统一、把指标算出来、把报告生成出来。

框架不能做的:判断「这个单元是真的还是假的」、「这个 sorter 的结果可不可信」、「这个脑区的活动意味着什么」。

**这三件事只能靠看数据。**所以官方文档里所有教程都会让你画图——不是走过场,是因为「看一眼波形」目前无法被自动化替代。

一个真实的比例:跑完排序大概花 10% 的时间,看波形和策管花 50%,剩下的时间在整理和写方法学。

如果你的流程里策管只占 5%,那大概率是没有认真策管。

§9 相关资源与引用

9.1 核心引用

Buccino AP, Hurwitz CL, Garcia S, Magland J, Siegle JH, Hurwitz R, Hennig MH.
SpikeInterface, a unified framework for spike sorting.
eLife 2020;9:e61834. doi:10.7554/eLife.61834

BibTeX 键(官方提供):buccino2020spikeinterface

注意:官方要求「cite other relevant papers for the specific components you use」。也就是说,用了 Kilosort4 要引 Kilosort4 的论文,用了 Bombcell 要引 Bombcell。只引 SpikeInterface 论文是不够的。

9.2 官方入口

用途 地址
源码 https://github.com/SpikeInterface/spikeinterface
稳定版文档 https://spikeinterface.readthedocs.io/en/stable/
开发版文档 https://spikeinterface.readthedocs.io/en/latest/
PyPI https://pypi.org/project/spikeinterface/
教程仓库 https://github.com/SpikeInterface/spiketutorials
引用清单 https://spikeinterface.readthedocs.io/en/latest/references.html
GUI https://github.com/SpikeInterface/spikeinterface-gui
博客 https://spikeinterface.github.io
视频教程 YouTube @Spikeinterface

9.3 关键文档页

  • Sorters 模块:支持列表、容器化运行、内部 sorter
  • Postprocessing 模块:SortingAnalyzer 与扩展体系
  • references 页:每个被封装工具的原始论文,引用必查

9.4 相关条目

条目 关系
IBL Brain-Wide Map 使用类似技术栈的公开数据集,可作为练手数据
DANDI Archive NWB 数据的托管方,SpikeInterface 可直读
MICrONS 联接组数据,与电生理互补

§10 AI 使用声明

10.1 本页的数据来源

数据 来源 时点
版本号、发布日期、版本总数 PyPI JSON API(/pypi/spikeinterface/json) 2026-10-21
stars / forks / 贡献者 / issue 数 GitHub REST API 2026-10-21
sorter 列表与分类 官方文档 stable 版 Sorters 模块 2026-10-21
SortingAnalyzer 行为与陷阱 官方文档 Postprocessing 模块 2026-10-21
论文信息 eLife 文章页 2020-11-10 发表

10.2 明确的不一致与口径说明

事项 说明 处理
sorter 数量 2020 论文 10 个,2026 文档 23 个 本页并列,明确标年份
Klusta 状态 论文里是支持项,现在是 legacy 本页归入 legacy 组
GitHub Release 一个都没有,但 PyPI 有 58 个版本 本页明确指出这个坑
版本号连续性 0.13 之后是 0.90,不是 0.14 本页作为断点专门说明

10.3 编辑部复核说明

  • 「17 + 4 + 2 = 23」为按官方文档分组逐项计数所得,非官方汇总数字。
  • 「12.2% 良品率」引用自本站 IBL 条目,其原始数据来自 IBL 官方 75,708 ÷ 621,733。
  • 本页引用的报错文本(AssertionError: This sorter ironclust is not installed.)为官方文档原文示例,非本环境实测。
  • 本页代码段为示意性写法,未在本环境实际安装 spikeinterface 执行;语法与 API 名称按官方文档校对。

10.4 使用建议

  • 做正式分析:锁版本 + 自定义容器镜像 + 写清预处理参数。
  • 做快速验证:先用内部 sorter(tridesclous2 / simple)确认链路。
  • 做方法学研究:至少 3 个 sorter 交叉,看交集单元。
  • 长期项目:把 analyzer 目录纳入版本管理或归档,因为重算成本高。

一句话总结:这是一个「位置比性能更值钱」的工具——它不发明排序算法,但决定了你用什么方式接触所有排序算法。用它之前先记住两件事:版本号断过两次,SortingAnalyzer 的 memory 格式不会替你存盘。


相关数据集导航

以下为站内 AI-Ready 数据集百科中与本词条共享多个主题标签的相关数据集,按相关度降序排列:

  • ibl-brain-wide-map — 共享标签:神经科学 / 生理信号 / 脑电信号 / 多模态
  • stroke-eeg-fnirs — 共享标签:神经科学 / 生理信号 / 脑电信号 / 多模态
  • mental-fnirs — 共享标签:神经科学 / 生理信号 / 脑电信号 / 多模态
  • multimodal-gait-dataset — 共享标签:神经科学 / 生理信号 / 脑电信号 / 多模态
  • openneuro — 共享标签:神经科学 / 生理信号 / 脑电信号
  • cves — 共享标签:神经科学 / 生理信号 / 多模态
  • bigp3bci — 共享标签:生理信号 / 脑电信号 / 多模态
  • dandi-archive — 共享标签:神经科学 / 生理信号 / 多模态
  • tuh-eeg-corpus — 共享标签:神经科学 / 生理信号 / 脑电信号
  • cinc-2023 — 共享标签:神经科学 / 生理信号 / 脑电信号

导航说明:本章节由全站统一标签体系自动计算生成(标签重合度算法),双向可达;点击链接可跳转至对应数据集词条。

返回 AI-Ready 数据集