1. 为什么选择PsychoPy构建SST实验第一次接触Stop Signal TaskSST是在研究生课题组的组会上。导师要求我们设计一个能够配合fNIRS设备的行为实验用来研究抑制控制能力的神经机制。当时我翻遍了各种实验程序代码库发现现成的SST程序要么过于复杂要么无法满足我们的定制化需求。这就是我决定用PsychoPy从零搭建SST实验的起点。PsychoPy作为一款开源的心理学实验构建工具最大的优势在于它的灵活性和跨平台特性。相比传统的E-Prime或MATLAB Psychtoolbox它不需要昂贵的软件授权也不需要复杂的开发环境配置。更重要的是它支持Python编程这意味着我们可以轻松实现各种复杂的实验逻辑比如SST中最关键的动态SSDStop Signal Delay调整算法。在实际使用中我发现PsychoPy特别适合需要高度定制化的实验场景。比如我们的fNIRS实验需要精确控制刺激呈现时间同时还要与设备同步打marker。PsychoPy的Python接口让我们可以轻松实现这些功能而不用像其他软件那样受限于图形化界面的功能限制。2. 环境配置的坑与解决方案2.1 安装过程中的版本陷阱刚开始安装PsychoPy时我犯了一个新手常犯的错误——直接安装了最新版本。结果发现2023.2.x版本在Mac系统上存在严重的兼容性问题特别是在处理音频刺激时经常崩溃。后来在论坛上看到其他研究者的反馈才知道这是这个版本的已知bug。经过多次尝试我最终选择了2022.2.5这个相对稳定的版本。安装过程其实很简单访问PsychoPy官网下载对应版本的安装包对于Mac用户直接将应用拖入Applications文件夹在系统偏好设置中允许运行来自未知开发者的应用这里有个小技巧安装完成后建议立即创建一个新的Python虚拟环境专门用于PsychoPy开发。这样可以避免与其他Python项目的依赖冲突。我在实际使用中就遇到过因为numpy版本不兼容导致实验程序崩溃的情况。2.2 音频设备的配置要点SST实验通常需要使用声音作为Stop信号这就涉及到音频设备的配置。我发现PsychoPy在不同操作系统上的音频表现差异很大在Windows系统上建议使用ASIO音频驱动以获得最低延迟在Mac系统上Core Audio通常表现稳定Linux系统则需要配置PulseAudio一个实用的测试方法是使用以下代码检查音频延迟from psychopy import sound, core s sound.Sound(A, secs0.1) for i in range(10): s.play() core.wait(0.5)如果听到的声音有明显的延迟或卡顿就需要调整音频设置。在我的MacBook Pro上将音频缓冲区大小设置为256 samples可以获得较好的平衡。3. SST实验的核心逻辑实现3.1 动态SSD调整算法SST实验最核心的部分就是动态调整Stop Signal DelaySSD。这个参数决定了Stop信号在Go信号之后出现的时间间隔直接影响任务的难度和被试的抑制成功率。标准的跟踪算法通常采用阶梯法staircase procedure根据被试的表现动态调整SSD。我实现的算法逻辑如下# 初始化SSD expInfo[ssd] 0.25 # 初始值为250ms # 在每个Stop试次后调整SSD if currentTrial[is_stop]: if resp.keys: # 抑制失败 expInfo[ssd] max(0.05, expInfo[ssd] - 0.05) # 减少50ms else: # 抑制成功 expInfo[ssd] min(0.8, expInfo[ssd] 0.05) # 增加50ms # 记录数据 thisExp.addData(inhibition_success, int(not resp.keys)) thisExp.addData(ssd, expInfo[ssd])这个实现有几个关键点需要注意设置SSD的最小值和最大值我设为50ms和800ms避免参数变得过于极端调整步长设为50ms这是文献中常用的值每次调整后都要立即记录数据防止意外丢失3.2 试次序列的随机化策略SST实验通常采用25%的Stop试次比例但如何随机分布这些试次很有讲究。完全随机可能导致连续出现多个Stop试次给被试带来额外压力。我采用的解决方案是使用伪随机序列确保不会连续出现3个以上的Stop试次每个block中Stop试次的比例严格控制在25%相同条件的试次不会连续出现太多实现代码示例from random import shuffle def generate_trial_sequence(n_trials, stop_prob0.25): n_stop int(n_trials * stop_prob) trial_types [stop]*n_stop [go]*(n_trials-n_stop) # 确保不会连续3个stop while True: shuffle(trial_types) valid True for i in range(len(trial_types)-2): if trial_types[i]stop and trial_types[i1]stop and trial_types[i2]stop: valid False break if valid: break return trial_types4. PsychoPy项目的最佳实践4.1 项目文件组织结构经过多次项目迭代我总结出一个比较合理的PsychoPy项目结构SST_Experiment/ ├── stimuli/ # 存放所有刺激材料 │ ├── images/ # 图片刺激 │ └── sounds/ # 声音文件 ├── instructions/ # 指导语图片 ├── conditions/ # 试次条件文件 │ ├── block1.xlsx │ ├── block2.xlsx │ └── ... ├── data/ # 实验数据输出目录 ├── utils/ # 工具脚本 └── SST_main.psyexp # 主实验文件这种结构的好处是不同类型的文件分类存放便于管理条件文件与主程序分离方便修改实验设计自动生成的数据不会与程序文件混在一起4.2 版本控制策略PsychoPy项目经常会遇到需要反复修改的情况一个好的版本控制策略可以避免很多麻烦。我推荐的做法是使用Git进行版本控制每次重大修改都创建一个新分支在实验文件中明确标注版本号和修改日期对条件文件和数据文件使用时间戳命名一个实用的命名约定示例SST_v2.3_20230815.psyexp # 主程序文件 conditions_block1_v1.2_20230814.xlsx # 条件文件 sub001_SST_20230815_1030.csv # 数据文件5. 常见问题排查指南5.1 刺激呈现时间不准确这是我在fNIRS实验中最常遇到的问题。PsychoPy的刺激呈现时间受到多种因素影响显示器刷新率设置不正确没有启用高精度计时模式系统后台进程占用资源解决方案在Builder视图的Experiment Settings中设置正确的屏幕刷新率通常60Hz启用Enable high precision timing在代码中添加以下检查from psychopy import visual, core win visual.Window() print(实测刷新率, win.getActualFrameRate())5.2 与fNIRS设备的同步问题fNIRS实验需要精确的时间同步我采用的解决方案是使用PsychoPy的并行端口发送marker在每个关键事件发生时发送特定编码在数据记录中包含精确的时间戳示例代码from psychopy import parallel port parallel.ParallelPort(address0x378) def send_trigger(code): port.setData(code) core.wait(0.01) port.setData(0) # 在试次开始时发送marker send_trigger(1) # 1表示试次开始5.3 程序崩溃与数据保存PsychoPy偶尔会出现崩溃的情况特别是在长时间运行的实验中。为了确保数据安全定期保存数据到多个位置使用try-except捕获异常实现自动恢复机制我的数据保存策略import os from datetime import datetime def safe_save(data, filename): tempname filename .temp backupname filename .bak # 先保存到临时文件 data.saveAsExcel(tempname) # 如果存在旧文件先备份 if os.path.exists(filename): os.rename(filename, backupname) # 重命名临时文件 os.rename(tempname, filename) # 在实验循环中定期调用 if trialNum % 10 0: safe_save(thisExp, fdata/sub{participant}_autosave_{datetime.now().strftime(%H%M)}.xlsx)6. 性能优化技巧6.1 刺激预加载为了提高实验的流畅性建议在实验开始前预加载所有刺激# 预加载图片刺激 images {} for img_file in os.listdir(stimuli/images): images[img_file] visual.ImageStim(win, imagefstimuli/images/{img_file}) # 预加载声音刺激 beep sound.Sound(stimuli/sounds/beep.wav)6.2 实时性能监控在调试阶段可以添加性能监控代码from psychopy import monitors def check_performance(): perf monitors.Monitor(test).getPsychopyVersion() print(f帧率: {perf[frameRate]} Hz) print(f丢帧率: {perf[droppedFrames]}%) print(f平均延迟: {perf[meanDelay]*1000:.1f} ms) # 定期调用 if trialNum % 20 0: check_performance()6.3 多线程处理对于需要同时呈现刺激和收集反应的场景可以使用多线程from threading import Thread def present_stimulus(stim, duration): stim.draw() win.flip() core.wait(duration) # 在试次中使用 stim_thread Thread(targetpresent_stimulus, args(stim, 0.5)) stim_thread.start()7. 实验设计的进阶考量7.1 不同实验条件的平衡在我们的fNIRS实验中需要比较两种不同条件下的抑制控制表现。这就涉及到条件间的平衡设计使用ABBA或ABAB的block顺序设计确保每个条件下的试次数量相等平衡不同条件下的刺激材料实现代码示例def balance_conditions(n_blocks4): # ABBA设计 conditions [A, B, B, A] blocks [] for cond in conditions: trials generate_trials_for_condition(cond) blocks.append({condition:cond, trials:trials}) return blocks7.2 个性化难度调整为了适应不同被试的能力差异我实现了基于表现的动态难度调整根据前几个block的表现调整初始SSD动态改变Stop试次的比例20%-30%根据反应时调整Go试次的时间限制def adjust_difficulty(previous_blocks): success_rate np.mean([b[inhibition_success] for b in previous_blocks]) if success_rate 0.4: # 太难 new_ssd max(0.1, initial_ssd - 0.05) stop_prob min(0.3, stop_prob 0.05) elif success_rate 0.6: # 太容易 new_ssd min(0.5, initial_ssd 0.05) stop_prob max(0.2, stop_prob - 0.05) return new_ssd, stop_prob8. 数据记录与分析准备8.1 关键数据的记录为了后续分析需要确保记录以下关键数据每个试次的类型Go/Stop反应时仅Go试次抑制是否成功Stop试次当前的SSD值刺激呈现的精确时间戳PsychoPy的Excel输出配置示例thisExp.addData(trial_type, currentTrial[type]) thisExp.addData(rt, resp.rt if resp.keys else None) thisExp.addData(correct, resp.corr) thisExp.addData(ssd, expInfo[ssd]) thisExp.addData(timestamp, globalClock.getTime())8.2 与fNIRS数据的同步标记为了与fNIRS数据同步我们需要在每个试次开始时发送同步标记记录标记发送的精确时间在数据文件中包含这些时间信息实现代码# 试次开始标记 send_trigger(1) # 1表示试次开始 thisExp.addData(trigger_start, globalClock.getTime()) # 刺激呈现标记 if currentTrial[type] stop: send_trigger(2) # 2表示Stop信号 thisExp.addData(trigger_stop, globalClock.getTime())经过三个月的反复调试和优化这个基于PsychoPy的SST实验程序最终在我们的fNIRS研究中表现出色。最让我自豪的是它不仅稳定运行完成了50多名被试的数据采集还被实验室的其他同学采用作为他们研究的基准程序。在这个过程中积累的经验告诉我一个好的实验程序不仅需要严谨的逻辑设计更需要充分考虑实际使用中的各种边界情况和用户体验。