用Python和PsychoPy从零搭建一个n-back工作记忆测试游戏(附完整代码)
用Python和PsychoPy构建n-back工作记忆测试:从实验设计到代码实现
九宫格中的绿色方块快速闪烁,你需要记住它前几次出现的位置——这就是经典的n-back测试,心理学研究中衡量工作记忆的黄金标准。作为认知神经科学领域最常用的实验范式之一,n-back任务不仅被用于评估注意力、记忆保持能力,还在脑机接口、认知训练等前沿领域发挥着重要作用。本文将带你用Python的PsychoPy库,从零搭建一个可定制、可扩展的n-back测试系统。
1. 实验原理与PsychoPy环境搭建
n-back任务的核心在于要求被试者持续监控并回忆前n次刺激出现的位置或内容。当n=1时只需记忆前一次刺激,而n=3则需在脑海中维持三个连续刺激的"记忆栈"。这种阶梯式难度设计使其成为研究工作记忆负荷的理想工具。
PsychoPy作为开源的心理学实验构建工具,其优势在于:
- 精确的时间控制:刺激呈现时间可精确到毫秒级
- 跨平台一致性:在Windows、macOS和Linux上保持相同表现
- 丰富的刺激类型:支持文本、图像、视频等多种刺激形式
- 数据记录完备:自动记录反应时、正确率等关键指标
安装PsychoPy只需一行命令:
pip install psychopy pandas openpyxl提示:建议使用Python 3.8及以上版本,某些PsychoPy功能在旧版本中可能受限
2. 实验界面设计与刺激呈现
我们先构建九宫格的基本视觉元素。九宫格位置采用像素坐标表示,中心为原点(0,0):
from psychopy import visual, event, core # 窗口设置 win = visual.Window(size=(1000, 618), color='white', units='pix') # 九宫格位置参数 positions = [ (-150, 150), (0, 150), (150, 150), (-150, 0), (0, 0), (150, 0), (-150, -150),(0, -150),(150, -150) ] cube_size = 145 # 方块尺寸刺激序列生成需要确保相邻刺激不重复,这通过以下算法实现:
import random def generate_sequence(length=30, num_positions=9): sequence = [] while len(sequence) < length: pos = random.randint(0, num_positions-1) if not sequence or pos != sequence[-1]: sequence.append(pos) return sequence刺激呈现的核心逻辑是循环遍历序列,在每个位置显示绿色方块1秒:
def show_stimulus(position, duration=1.0): for i in range(9): color = 'green' if i == position else '#afafaf' cube = visual.Rect(win, width=cube_size, height=cube_size, pos=positions[i], fillColor=color) cube.draw() win.flip() core.wait(duration)3. 实验流程控制与用户交互
完整的n-back实验包含以下几个阶段:
- 指导语阶段:解释任务要求
- 位置记忆阶段:展示九宫格位置编号
- 测试阶段:呈现刺激序列并收集反应
- 结果反馈阶段:显示正确率和反应时
- 数据保存阶段:记录实验数据到Excel
关键的用户交互代码如下:
# 指导语显示 def show_instruction(text, y_offset=0, is_title=False): text_stim = visual.TextStim(win, text=text, pos=(0, y_offset), height=50 if is_title else 20, color='black', bold=True) text_stim.draw() win.flip() event.waitKeys() # 显示位置编号 def show_position_numbers(): for i, pos in enumerate(positions): cube = visual.Rect(win, width=cube_size, height=cube_size, pos=pos, fillColor='#afafaf') cube.draw() num = visual.TextStim(win, text=str(i+1), pos=pos, height=cube_size/2) num.draw() win.flip() event.waitKeys()4. 数据收集与结果分析
n-back实验通常关注两个核心指标:
| 指标类型 | 具体测量 | 数据分析意义 |
|---|---|---|
| 准确率 | 正确判断次数/总测试次数 | 反映工作记忆容量 |
| 反应时 | 从问题呈现到按键反应的时间 | 反映认知处理速度 |
数据记录采用pandas库实现:
import pandas as pd def save_to_excel(data, filename='nback_results.xlsx'): df = pd.DataFrame(data) try: existing = pd.read_excel(filename) combined = pd.concat([existing, df], ignore_index=True) combined.to_excel(filename, index=False) except FileNotFoundError: df.to_excel(filename, index=False)实验主循环中,我们随机选择n值(1-3)并记录每次测试结果:
data = {'trial': [], 'n': [], 'correct': [], 'response_time': []} for trial in range(30): show_stimulus(sequence[trial]) # 每6次测试提问一次 if (trial + 1) % 6 == 0: n = random.randint(1, 3) question = f"前{n}次绿色方块出现的位置是?" show_instruction(question, y_offset=150) timer = core.Clock() keys = event.waitKeys(keyList=[str(i) for i in range(1, 10)]) rt = timer.getTime() correct = (int(keys[0]) - 1) == sequence[trial - n] feedback = "正确!" if correct else "错误!" show_instruction(f"{feedback} 反应时间:{rt:.3f}秒") data['trial'].append(trial//6 + 1) data['n'].append(n) data['correct'].append(correct) data['response_time'].append(rt) save_to_excel(data)5. 高级功能扩展与优化建议
基础版本实现后,可以考虑以下增强功能:
- 自适应难度:根据被试表现动态调整n值
- 多模态刺激:增加声音或图像刺激
- 脑电同步:通过并行端口发送事件标记
- 网络部署:使用PsychoJS实现浏览器版本
反应时分析的改进方法:
# 剔除异常反应时(如<200ms或>3000ms) clean_rt = [rt for rt in data['response_time'] if 0.2 < rt < 3.0] # 计算各n水平的平均反应时 n_levels = sorted(set(data['n'])) rt_by_n = {n: [] for n in n_levels} for n, rt in zip(data['n'], data['response_time']): rt_by_n[n].append(rt) avg_rt = {n: sum(rts)/len(rts) for n, rts in rt_by_n.items()}窗口关闭前的数据完整性检查:
def validate_data(data): required_fields = ['trial', 'n', 'correct', 'response_time'] if not all(field in data for field in required_fields): raise ValueError("缺失必要数据字段") if len(data['trial']) != len(data['correct']): raise ValueError("数据长度不一致") return True6. 常见问题排查与调试技巧
遇到问题时,可以尝试以下调试方法:
视觉刺激不显示:
- 检查窗口单位(pix/cm/deg)是否与坐标匹配
- 确认draw()和flip()被正确调用
反应键无响应:
- 验证keyList参数是否包含所有有效键
- 检查键盘焦点是否在实验窗口
时间控制不精确:
- 避免使用time.sleep(),改用core.wait()
- 关闭不必要的后台进程
记录调试信息的实用代码:
# 在关键操作前后添加时间戳 debug_log = [] trial_start = core.getTime() # ...执行操作... debug_log.append(f"Trial {trial}: {core.getTime() - trial_start:.3f}s") # 保存日志文件 with open('debug_log.txt', 'w') as f: f.write("\n".join(debug_log))实验结束后,记得正确释放资源:
win.close() core.quit()