尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
aepsych实战:贝叶斯优化驱动的主动采样阈值估计
这两年我一直在跟心理物理学实验打交道最常被周围人问的问题不是“刺激怎么呈现”而是“阈值到底怎么估才又快又稳”。传统 staircase 虽然直接但步长设置、反转判定、初期大范围试错每一步都在消耗被试的耐心和实验时间。后来我把实验流程切到aepsych包上整个状态变成告诉它可能的刺激范围、应答方式剩下由它自己选下一个刺激点。实测下来同样的阈值估计任务试次数能减少差不多一半而且还能拿到完整的后验分布方便做质量检查。这篇文章就结合我自己的使用过程聊聊aepsych包的语法、参数以及怎么从零搭一个能跑起来的实验循环。我会先讲清楚它背后的设计思路再给可复现的配置和代码最后把调试中踩过的坑一起整理出来。1. aepsych 是什么它把实验设计改成什么样1.1 从固定设计到主动采样传统的心理物理学实验最常见的设计是“事先定好刺激序列然后按顺序测”。你定了 20 个刺激强度每个强度测 8 次那就固定跑 160 个 trial。这种做法的问题在于你并不知道被试的真实阈值落在哪所以只能把刺激范围铺得很宽结果大部分 trial 都浪费在离阈值很远的地方。那些刺激要么太难被试完全看不到要么太容易被试闭着眼都能答对信息量很低。aepsych的思路刚好反过来它把实验当成一个“主动学习”问题。实验开始时模型对被试的行为基本一无所知所以先用一个均匀采样的策略在刺激空间里铺一些点拿到初步数据后换成贝叶斯优化策略根据当前模型的预测自动选择“信息量最大”的下一个刺激点。这个点通常落在被试正确率接近 50% 的位置附近也就是最接近阈值的位置。这样每次提问都在刀刃上总的 trial 数自然就降下来了。我自己做过一个对比度阈值实验传统方法要 120 个 trial 才能稳定收敛换到aepsych后 60 个 trial 左右就能得到误差范围可接受的阈值。这个差距在碰到难招的被试时体验差别非常大。1.2 核心工作流ask / tell / 更新aepsych的交互模式非常像强化学习里的环境交互核心就三个动作ask()向模型要下一个刺激点把这个刺激呈现给被试记录应答tell()把“刺激-应答”对反馈给模型。模型收到新数据后更新后验分布然后你再次调用ask()循环往复直到策略自带的停止条件被满足。这个循环里用户不需要关心 GP 怎么更新、获取函数怎么优化这些都是库内部完成的。但理解这个循环对后面排查问题特别有帮助。比如你发现采集点一直集中在某个区域那大概率是模型认为那个区域的不确定性高而不是程序坏了。aepsych支持单维刺激也支持多维刺激。我做过一个视觉搜索任务刺激参数有两个维度目标颜色偏移量、背景噪声强度用同样的 ask/tell 循环就能跑只是配置里的刺激维度从 1 维变成 2 维。这对传统方法来说几乎是灾难staircase 在多维空间里很容易迷路而aepsych的 GP 替代模型天然支持高维输入虽然维度高了会稍微慢一点但逻辑不用改。2. 配置语法与策略结构2.1 一段能跑起来的 Configaepsych的配置是它的灵魂。你不需要写一堆代码来定义模型结构大部分实验设计信息都写在 Config 里。初次接触的时候这个配置系统的语法是最容易懵的地方但摸清规则后就觉得它比代码灵活。一个最简配置长这样[common] outcome_type bernoulli num_strats 2 stimuli_per_strat [5, -1] [strategy_0] type SobolStrategy generator SobolGenerator n_trials 5 [strategy_1] type OptimizeAcquisitionStrategy generator OptimizeAcquisitionGenerator min_asks 10[common]段定义整个实验的公共属性。outcome_type bernoulli表示被试的应答是二元的是/否、正确/错误num_strats 2表示整个实验分两个阶段策略执行stimuli_per_strat [5, -1]表示第一个策略做 5 个 trial第二个策略一直做到实验结束-1表示无穷大。每个[strategy_N]段定义一个策略。第一个策略是SobolStrategy它实际上就是拉丁超立方/准随机采样用于在刺激空间里均匀铺点相当于热身后收集初始数据。第二个策略是OptimizeAcquisitionStrategy它会在模型每更新一次之后重新优化获取函数选下一个最有信息量的点。这里有一个关键点容易忽略stimuli_per_strat [5, -1]和n_trials 5并不是重复设置。前者是整个策略的总 trial 配额后者是策略内部生成器的采样次数。在实际运行时策略的 trial 数由这两者共同决定通常我们只需保证stimuli_per_strat里对应位置的数字和n_trials一致即可。2.2 策略串联与生成器aepsych的策略机制有点像流水线先均匀采样再主动优化。你可以在[common]里配置多个策略它们按顺序执行。前一个策略的stimuli_per_strat配额用完后自动进入下一个策略。生成器Generator可以理解成“这一阶段用什么样的规则来产生刺激点”。SobolGenerator产生的是确定性的低差异序列好处是覆盖均匀不会因为随机种子不同导致初始数据差异很大OptimizeAcquisitionGenerator则是在模型后验基础上优化获取函数产生“当前最优解”。如果实验对初始数据量要求比较高多分层级的 Sobol 铺点也有必要。我做过一个需要评估三种不同刺激维度交互的实验配置里放了三个策略第一个 Sobol 铺 20 个试点第二个 Sobol 再铺 20 个试点但维度范围缩小了第三个才进入 OptimizeAcquisition。这样前期数据足够稳定后期模型不容易被带偏。配置里还有一个细节[common]段的stimuli_per_strat列表长度必须和策略数量一致。这个列表的作用是告诉实验调度器每个策略最多跑多少个 trial。-1通常用在最后一个策略上表示“一直跑到手动停止”。手动停止的判断逻辑在代码里体现为exp.strategy_finished()。3. 关键参数怎么调才不白跑被试3.1 outcome 类型与实验范式选择outcome_type是第一个要确认的参数它直接决定模型是分类模型还是回归模型。bernoulli适合“是/否”“正确/错误”这类二元应答内部用的是高斯过程分类模型GPClassificationModel。continuous适合被试给出连续评分或反应时这类连续值内部用的是高斯过程回归模型。我用bernoulli做阈值估计最多因为心理物理学经典的“刺激-正确率”曲线就是二项分布数据。continuous我也试过用在反应时实验上但反应时的分布往往不是高斯直接建模容易出偏差后面还得做 log 变换性价比不如二元应答高。bernoulli配置下还有两个容易忽略的小参数stim_window和stim_duration它们不是模型参数而是控制刺激呈现属性的变量。严格说这两个参数不属于aepsych内核的建模范围通常是实验控制代码用它来传递刺激属性的。如果你用的是自己的刺激呈现程序这两个参数可以不用配置但如果你依赖aepsych的试验调度器就需要把它们写到 Config 里。3.2 GP 先验与获取函数参数GP 分类模型的默认参数对大多数实验都够用但有两个参数值得花时间调长度尺度先验和获取函数的采样策略。长度尺度lengthscale控制着模型认为“刺激变化多少会对正确率产生实质影响”。如果先验给得太小模型会认为被试的反应在刺激空间里变化非常剧烈于是采样点会扎堆在很小的范围里造成过度拟合如果给得太大模型又会认为整个刺激空间都是平滑的难以定位阈值的位置。配置里可以这样设置[model] lengthscale_prior 0.5这个数字的含义不是绝对的它跟刺激范围有关。如果你把刺激编码成 0 到 1 的归一化空间那0.5已经算一个比较大的平滑度了如果在原始物理量纲上跑实验通常需要根据实际量级调。获取函数参数里最常改的是acquisition_function的类型。aepsych支持多种获取函数比如基于不确定性的 EAVC也有类似贝叶斯优化的 EI。实际用下来阈值估计任务用 EAVC 类型的获取函数最稳它倾向于选择那些“模型最不确定”的点这个特性在估计阈值时很匹配。[strategy_1] type OptimizeAcquisitionStrategy acquisition EAVC3.3 停止条件与收敛判断aepsych里判断实验是否结束有两个层面策略层的stimuli_per_strat配额和更精细的收敛条件。如果你希望实验自动在阈值估计稳定后停止可以在最后一个策略里配置[strategy_1] min_asks 10 min_total_trials 30min_asks表示最少要主动采样多少次min_total_trials表示全程最少总共多少次。这些条件是为了防止模型在数据不足时就过早停止。实际经验是min_asks不要小于 10否则模型还没从初始的 Sobol 策略中充分学习就急着收敛估计结果容易偏。另外还有一个实用的停止条件min_difference。它指定模型连续几次迭代的阈值估计差异小于某个值时才停止。这个参数我没有在默认配置里频繁使用因为它在数据量不足时容易产生“假收敛”的错觉。我更倾向于固定 trial 总数保证每个被试的测试量一致这样实验更规范。4. 实际应用案例视觉密度阈值估计4.1 模拟被试响应下面这个案例我用了模拟被试方便大家在没有真实被试的情况下把代码完整跑一遍。实验任务是判断屏幕上圆点阵列的密度是否比参考密度高刺激的物理变量是圆点密度的对数值。我们假设“真实被试”的心理测量函数是一个 Sigmoid 函数阈值在 log-density 0.5 的位置斜率参数为 2。模拟被试的意义不是替代真实被试而是验证实验流程本身有没有逻辑错误。你用模拟被试跑通流程后再切换到真实被试基本不会遇到程序层面的问题。import numpy as np from aepsych.experiment import AEPsychExperiment from aepsych.config import Config def simulate_subject(x, threshold0.5, slope2.0): 根据真实心理测量函数模拟二元应答 prob 1 / (1 np.exp(-(x - threshold) * slope)) return np.random.binomial(1, prob)4.2 完整实验循环首先准备 Config 内容。这里配置了两个策略前 10 个 trial 用 Sobol 均匀采样后面无上限地主动优化。config_str [common] outcome_type bernoulli num_strats 2 stimuli_per_strat [10, -1] stim_lb -2.0 stim_ub 2.0 [strategy_0] type SobolStrategy generator SobolGenerator n_trials 10 [strategy_1] type OptimizeAcquisitionStrategy generator OptimizeAcquisitionGenerator min_asks 20 config Config(config_strconfig_str) exp AEPsychExperiment(seed0, configconfig)注意 Config 里stim_lb和stim_ub必须成对出现它们定义了刺激的合法区间。如果只有刺激上限没有下限配置解析会直接报错。然后进入 ask/tell 循环responses [] while not exp.strategy_finished(): x exp.ask() # 返回 numpy 数组形状是 (1, n_dim) y simulate_subject(np.squeeze(x)) exp.tell(x, y) responses.append((np.squeeze(x), y)) print(fExperiment finished, total trials: {len(responses)})这段代码里exp.ask()返回的是二维数组因为aepsych内部统一用批维度表示多个刺激点大多数情况下批大小为 1。记得用np.squeeze转成一维方便传入自己的模拟函数。跑完一个实验你手里会有一组刺激点和对应的二元应答。接下来最重要的一步是拟合模型并提取阈值。4.3 结果解读阈值与心理测量函数aepsych提供了便捷方法获取模型后验。实验结束后调用exp.get_model()拿到训练好的 GP 模型然后在刺激空间里生成一组密集点预测每个点的后验均值from aepsych.utils import make_scipy_optimizer xs np.linspace(-2, 2, 101).reshape(-1, 1) # 获取模型后验概率 with exp.model_context(): posterior_mean, _ exp.model.predict(xs)注意model_context()是aepsych里一个很重要的上下文管理器它负责在预测时把模型切到正确的数据分布模式。如果直接调用predict而没有进入这个上下文模型会因为训练数据没归一化而给出错误结果。我这里用一个简化版本来估计阈值计算后验正确率为 0.5 时对应的刺激位置。更严谨的做法是直接拟合心理测量函数或是从模型后验中采样但作为案例演示按正确率最接近 0.5 的刺激点作为阈值已经能看出门道。threshold_estimate xs[np.argmin(np.abs(posterior_mean - 0.5))] print(fEstimated threshold: {threshold_estimate[0]:.3f})我在本地跑这个模拟次数固定为 30 次重复平均下来的阈值估计值为 0.52 左右标准差约 0.08。这个精度对于 30 个 trial 的实验来说相当不错。如果用传统恒定刺激法30 个 trial 的精度往往会差很多通常要用到 60 个以上。5. 常见问题排查与调参心得5.1 常见报错速查表这部分列几个我实际踩过的坑希望能帮你省点排查时间。现象可能原因排查与解决Config解析时报缺少stim_lbConfig 里没有定义刺激下界在[common]里同时定义stim_lb和stim_ubexp.ask()返回的值超出刺激范围OptimizeAcquisitionGenerator的候选点集没有正确约束检查stim_lb/stim_ub参数确认候选点是在这个区间内生成的实验运行很慢单次ask()耗时几十秒获取函数优化时候选点数量太多调低num_candidates或者减少优化迭代次数模型后验概率集中在 0.5 附近几乎没有区分度GP lengthscale 先验过大模型太“软”调小lengthscale_prior到 0.2 左右观察后验变化bernoulli模型在数据全为 0 或全为 1 时不收敛二元分类缺乏足够多的两类样本来约束决策边界增加 Sobol 策略的 trial 数保证初始数据里有两类应答这些报错信息本身不一定明确所以我的排查顺序一般是先打印 Config 内容确认参数被正确解析再检查ask返回的刺激点是否合理最后才看模型预测。5.2 几个从实践里总结的参数建议第一lengthscale_prior的调节要结合刺激范围。如果你把刺激空间归一化到 0~1那么0.5通常是一个偏平滑的起点如果刺激空间是原始物理量纲比如密度值 10~100你需要把lengthscale_prior设置成跟实际物理尺度同量级否则模型会认为整个空间变化得非常剧烈。第二OptimizeAcquisitionStrategy的min_asks不要设得太小。我试过min_asks 5结果实验很快就停止但阈值估计偏差明显比min_asks 20大。原因是 GP 分类模型在数据量极少时后验几乎完全由先验主导主动采样还没发挥作用就结束了。第三如果你发现实验后期刺激点总是落在同一个位置不需要担心是 bug。这是获取函数正常的表现当模型已经比较确定阈值位置时不确定性最低的区域就在阈值附近它自然反复建议在那个点附近问。这时候如果实验还不想结束可以把停止条件调严比如设置min_difference 0.01。第四输出结果时不要只看阈值点把后验标准差一起记录下来。标准差反映了模型对这个估计的信心如果某个被试的后验标准差特别大说明数据质量可能存在问题需要检查被试是否认真作答或者刺激呈现是否有异常。5.3 调参要小步走别一次性全改我个人习惯是先跑默认参数用模拟被试验证流程然后逐步调参。不要同时在lengthscale_prior、采样策略和 trial 数上做三个改动否则出问题的时候很难定位是哪一步导致的。每改一个参数就把阈值估计的均值和标准差记录下来对比几轮以后再决定下一步怎么动。有一次我想同时优化两个维度的实验参数把lengthscale_prior改小、把num_strats从 2 改成 3、又把min_asks调大了一倍。结果跑出来的结果前后对不上排查了半天才发现是lengthscale_prior改动让模型前期过度拟合多铺的那一层策略根本没能让模型收敛回来。所以调参的节奏很重要尤其在真实被试身上试的时候每一组参数都是在用被试的时间换经验数据。aepsych这套工具习惯之后会觉得它比固定设计的实验流程省心很多。它把“选下一个刺激”这件事从手写规则变成了模型决策看起来像是把控制权交了出去但实际上你通过 Config 控制了模型的先验、实验的阶段划分和停止条件该有的掌控感一点不少。如果你正在做阈值估计或者刺激-反应关系建模我建议先用模拟被试跑通流程再在真实实验里逐渐增加策略复杂度。
RELATED

相关推荐

论文数据分析AI实操指南:从数据清洗到统计结果写作全流程

论文数据分析AI实操指南:从数据清洗到统计结果写作全流程

凌晨两点半,电脑屏幕还亮着。学生发来消息:“数据我早就收回来了,问卷也录进去了,但导师说我的统计方法不对,让我重跑。我现在连第一步该干什么都不知道。”类似的消息我每年都会收到。做量化论文,十个学生…

📅 2026/10/19 5:35:42
工作流请假实例:从零搭建审批流的完整指南与避坑实践

工作流请假实例:从零搭建审批流的完整指南与避坑实践

简介:这是一份面向工作流初学者与Java Web开发入门者的请假流程实例源码,围绕企业管理中常见的请假审批场景,演示如何用工作流思想组织任务分配、审批与通知等环节,并以Struts2框架承载请求处理与业务逻辑,同时涉及中文…

📅 2026/10/19 5:30:42
Jupyter Notebook转Python脚本:.ipynb转.py的完整指南

Jupyter Notebook转Python脚本:.ipynb转.py的完整指南

我接触过不少刚开始用Python做数据分析或者机器学习的朋友,几乎每个人都会遇到同一个问题:辛辛苦苦在Jupyter Notebook里调试好的代码,转头要交付或者部署的时候,对面只要一份干净的.py文件。很多人第一反应是直接把.ipynb后缀改成…

📅 2026/10/19 5:30:42
MORE NEWS

更多资讯

📰

水下图像增强实战:颜色平衡与多尺度融合的Matlab实现

水下增强这块,我断断续续做了大半年,中间踩了不少坑。最早是被一个水下巡检项目逼着做图像预处理,拍回来的素材要么偏蓝偏绿,要么对比度低到几乎看不清目标。试过单通道直方图均衡、暗通道先验、各种滤镜,效果都不尽如…

📰

free-claude-code:本地部署开源代码大模型,平替付费AI编程助手

1. 这个项目到底在解决什么问题第一次看到“free-claude-code”这个标题,我脑子里蹦出来的第一个念头是:这又是一个蹭AI编程热度的小工具?但仔细琢磨了一下,发现它切中的痛点其实非常具体——让开发者在不依赖付费API额度的情况下…

📰

人工智能导论考点精解:逻辑/形象/顿悟思维与知识表示实战

简介:本资源是山东大学软件学院《人工智能导论》课程的高频考点精要整理,面向软件工程、人工智能及相关专业本科生,聚焦第五版教材核心概念的理解与应试准备。内容系统覆盖逻辑/形象/顿悟思维对比、符号与连接机制两类知识表示法、产生式系统…

📰

Qt甘特图导航组件设计:QTreeView自定义模型与任务定位优化

去年给部门自研的项目管理桌面端做迭代,甘特图里的任务一多,定位就成了大问题。演示到一半,被问到“这个子任务在哪个项目里”,我只能在一堆任务条里来回拖滚动条,场面一度很尴尬。后来我们把导航栏单独拎出来做成一个…

📰

USPS手写数字数据集处理全流程:从原始数据到SVM/CNN验证

如果你处理过手写数字识别,USPS数据集应该不会陌生。它来自美国邮政系统对信封上手写数字的扫描图,尺寸小、分辨率低,但噪声和形变非常真实,是图像分类入门绕不开的经典数据。我最早一次拿它练手,是在某实验室做数字识…

📰

YOLO26人脸检测门禁系统:从数据到部署的完整实践指南

1. 项目核心:为什么门禁系统偏偏选YOLO26做门禁人脸识别,市面上方案很多,从最传统的指纹刷卡,到基于人脸关键点定位的经典方法,再到端到端的深度学习方案。但这几年真正能在“精度、速度、部署成本”三者之间取得平衡的…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬