简介这是一份面向心理学、神经科学及机器学习研究者的 PsychoPy 实验编程入门文档以 Word 格式系统梳理了从基础概念到环境配置的完整学习路径。内容先介绍 PsychoPy 的诞生背景与发展阶段再围绕实验设计、数据分析、模型训练三大功能展开并覆盖心理物理学、反应时间、视觉与听觉等常用范式帮助读者理解如何用 Python 编写、运行和管理实验。资源包共 1 个 docx 文件大小仅 31KB轻量便于阅读、批注与打印适合作为课程讲义或自学笔记使用。目前已有 1027 人学习编者 zhuzhi 将安装步骤、依赖库配置及常见实验编程要点整理成章节式文档读者可据此快速搭建 Python 环境并从零开始构建自己的心理学实验是入门到进阶过程中一份实用的参考资料。1. 实验编程绕不开 PsychoPy它到底解决了什么心理学、认知神经科学领域做行为实验最头疼的往往不是数据统计而是刺激呈现的精确性和反应时间记录的可靠性。PsychoPy 正是为这件事而生的 Python 实验编程框架它把视觉刺激、听觉刺激、按键反馈、事件计时全部封装成可调用的对象让实验脚本能毫秒级控制呈现顺序同时把每个试次的行为数据自动落盘。我拆这份《实验编程PsychoPy 从入门到精通》文档时最大的感受是它把“从安装到一个能跑的完整实验”整条链路讲全了适合刚上手做实验编程的研究生也适合需要移植旧实验、重构刺激程序的熟手。它能帮你解决的核心问题很简单用一套 Python 代码把实验设计变成可复现、可审计的实验程序。2. 环境安装与第一个程序先把库装对再谈实验逻辑2.1 安装路线pip 安装前的三个前置检查PsychoPy 本质上是 Python 库所以第一步不是急着pip install psychopy而是先把解释器和包管理器确认好。文档里的顺序是安装 Python、确认 pip、安装 PsychoPy、安装依赖库。这个顺序本身没错但我在实际拆解中发现有几处容易卡住的地方值得先说清楚。python --version pip --version python -m ensurepip --upgrade先运行这三条命令确认 Python 版本和 pip 是否可用。PsychoPy 对 Python 版本有要求较新版本需要 3.8 以上环境如果本机还是 3.6 或者干脆没有 Python后续的依赖安装会翻车。python -m ensurepip --upgrade是补装 pip 的兜底手段通常只在新装解释器或系统自带 Python 的特殊环境里才需要执行。接着用虚拟环境隔离依赖是个好习惯尤其是同一台机器上还跑着老实验脚本、机器学习项目时。文档虽然没提虚拟环境但这是实践中最常被忽略的坑python -m venv psychopy_env source psychopy_env/bin/activate # Linux / macOS psychopy_env\Scripts\activate # Windows激活虚拟环境后再执行pip install psychopy之后再用pip install numpy matplotlib jupyter pandas补齐依赖库。numpy 负责数值计算matplotlib 用在数据可视化jupyter 方便交互调试pandas 是后续数据处理的主力。这套组合跟文档里强调的完全一致我一般会顺手多装一个opencv-python某些图像刺激预处理场景会用到。2.2 第一个可运行脚本Window、TextStim 与 flip 的关系文档里 2.3 节的 Hello World 示例看着简单但它其实串起了 PsychOpy 三个核心模块的分工。我把代码整理成可直接运行的完整版本并补上了程序退出前的清理逻辑from psychopy import visual, core, event win visual.Window([800, 600], color[1, 1, 1], unitspix) hello visual.TextStim(win, textHello World, color[-1, -1, -1], height40) hello.draw() win.flip() core.wait(2.0) win.close() core.quit()visual.Window创建实验窗口两个参数决定了后续所有刺激的坐标参照系[800, 600]是窗口宽高像素color[1, 1, 1]是背景色。注意这里的颜色空间不是 0-255 的 RGB而是 -1 到 1 的线性区间[1, 1, 1]是白色[-1, -1, -1]是黑色。这个细微的差异是新手最容易看晕的地方写颜色值前需要先明确当前窗口的颜色空间。visual.TextStim创建文字刺激text参数决定显示内容color[-1, -1, -1]是黑色文字height40代表文字高度 40 像素。我在原文档基础上加了unitspix这样height和后续的位置参数都按像素理解否则默认单位下坐标含义会变得抽象。hello.draw()只是把刺激画到后台缓冲区屏幕上还看不见win.flip()才把后台内容一次性推到前台。这个“先画后翻”的机制是 PsychoPy 精确计时的根基所有刺激在同一个缓冲区里准备好翻转时同步刷新避免逐帧绘制的时间误差。core.wait(2.0)让窗口停留 2 秒win.close()关闭窗口core.quit()退出程序。不加后两行的话实验结束后进程会悬挂终端里看到的就是“窗口关了但 Python 没退出”。2.3 帧同步与等待逻辑为什么你的窗口会一闪而过很多第一次跑这段脚本的人会遇到窗口一闪而过、看不清内容的情况。原因不是脚本写错了而是忘了让程序“等一等”。虽然win.flip()之后画面确实显示了出来但脚本紧接着就执行到下一行如果没有core.wait()或者等待按键的代码程序会立刻跑到win.close()把窗口关了。这跟实验设计的播放节奏是同一套逻辑呈现刺激之后必须明确告诉程序下一步做什么要么等待固定时间要么等待被试按键。core.wait(2.0) # 方式一等待固定秒数 event.waitKeys(keyList[space]) # 方式二等待被试按空格两种方式对应不同的实验场景。core.wait(2.0)适合固定时长的刺激呈现比如注视点显示 500ms、刺激显示 1000msevent.waitKeys(keyList[space])适合被试自己控制节奏的任务按了键才进入下一步。后者是反应时间实验的基础后面的章节会展开讲。3. 刺激呈现与实验流程设计从 Hello World 到完整试次3.1 视觉刺激的三类基础对象什么场景用哪个PsychoPy 的视觉刺激对象都挂在visual模块下除了文档里演示的TextStim我拆这份文档时顺手整理了最常见的三类文字刺激、图片刺激、形状刺激。它们的使用逻辑完全一致创建对象、设置属性、draw()绘制、flip()翻转但各自的参数侧重点不同。from psychopy import visual import numpy as np # 文字刺激适合指导语、注视点、反馈信息 text_stim visual.TextStim(win, text请注视十字, height30, color[-1,-1,-1]) # 图片刺激适合面孔、物体、场景类实验材料 image_stim visual.ImageStim(win, imageface001.jpg, size[200, 250], pos[0, 0]) # 形状刺激适合色块、边框、掩蔽刺激 rect_stim visual.Rect(win, width100, height100, fillColor[1,-1,-1], pos[150, 0]) # 圆形刺激适合空间线索、视觉搜索目标 circle_stim visual.Circle(win, radius50, fillColor[-1,1,-1], pos[-150, 0])参数上imageface001.jpg是图片路径size[200, 250]控制图片显示尺寸而不是原始像素大小。Rect和Circle的关键参数是width/height与radiusfillColor控制填充色不带fillColor时只画出描边。这些对象创建后可以随时改属性再重新绘制实现刺激的动态变化——比如通过循环修改pos参数让圆形移动。3.2 反应时间任务如何组织一个完整试次文档第三章给出了反应时间实验的设计思路但只有思路没有完整脚本。我按照文档的步骤补了一个可运行的简单反应时间任务。它包含典型的三段式试次结构注视点、目标刺激、按键反应。from psychopy import visual, core, event import random win visual.Window([1024, 768], color[1, 1, 1], unitspix) fixation visual.TextStim(win, text, height40, color[-1, -1, -1]) target visual.Circle(win, radius60, fillColor[-1, 1, -1], pos[0, 0]) trials [{target_color: [-1, 1, -1]} for _ in range(10)] # 10 个试次 random.shuffle(trials) for i, trial in enumerate(trials): # 呈现注视点 500ms fixation.draw() win.flip() core.wait(0.5) # 呈现目标刺激等待按键记录反应时 target.fillColor trial[target_color] target.draw() win.flip() clock core.Clock() keys event.waitKeys(keyList[space, escape], timeStampedclock) if keys is None or keys[0][0] escape: break # 被试主动退出 rt keys[0][1] # 反应时秒 print(fTrial {i1}: RT {rt:.3f}s) # 简单的刺激间间隔 win.flip() core.wait(0.3) win.close() core.quit()这段脚本的试次流程是先画注视点win.flip()后等待 500ms然后画目标圆形翻转后立刻创建core.Clock()计时器用event.waitKeys(timeStampedclock)同时获取按键和按键发生的时间戳。timeStampedclock是关键它让返回的每个按键都带上相对于clock创建时点的时间也就是反应时。event.waitKeys返回的格式是列表每个元素是(按键名, 时间戳)这样的元组所以代码里用keys[0][1]取出时间戳。keyList[space, escape]限制了有效按键范围被试按空格正常反应、按 escape 退出实验。这个设计比文档里的方案多了一层退出机制长时间运行的实验必须保留这个出口否则被试中途想退出只能靠拔电源。3.3 试次随机化与条件平衡别让顺序效应毁掉数据上面代码里我用random.shuffle(trials)打乱了 10 个试次的顺序这背后是实验设计的基本原则。行为实验里被试的疲劳、练习、期望都会随试次累积而影响表现如果把所有条件的试次按固定顺序排列条件之间会被顺序效应污染。before: [A, A, A, A, A, B, B, B, B, B] # 错误的条件内集聚 after: [A, B, A, B, B, A, A, B, A, B] # 正确的完全随机或伪随机对简单设计random.shuffle就够了。更复杂的实验还要考虑条件在试次间的平衡常见做法是用itertools.product生成所有条件组合的笛卡尔积再打散重复若干遍。from itertools import product import random conditions list(product([face, house], [left, right])) trials conditions * 5 # 每个条件重复5次 random.shuffle(trials)conditions是四组条件组合乘以 5 得到 20 个试次再 shuffle 保证被试无法预测下一个试次的类型。这种做法在识别任务、记忆任务里几乎是标准操作文档里提到的“随机化程度”参数化配置本质上就是这么实现的。4. 数据采集与避坑记录、导出、排查的完整方案4.1 数据记录的两种姿势逐试次写入还是最后统一导出PsychoPy 提供了.csv数据保存函数文档里提到文本文件、Excel、JSON 三种输出格式我在实际拆解中强烈推荐用.csv它跟 pandas 配合最顺手Excel 格式在几百个试次后打开速度会明显变慢。数据写入有两种常见姿势各有适用场景。方式一实验结束后统一导出。把每个试次的数据累积到 Python 列表里循环结束再用 pandas 一次性写文件。import pandas as pd results [] for i, trial in enumerate(trials): # ... 实验逻辑 ... results.append({ trial: i 1, condition: trial[target_color], rt: rt, correct: 1 # 或者根据实际按键判断 }) df pd.DataFrame(results) df.to_csv(data/exp1_results.csv, indexFalse)方式二边跑边存。用data.ExperimentHandler组件每个试次结束自动落盘。这种方式的好处是实验中途崩溃不会丢失已完成的数据坏处是代码复杂度略高、对新手不友好。我一般做正式实验用第二种做演示和调试用第一种。数据清洗是数据处理的第一步文档第四章提到的“去除异常和无关数据”落到实处通常是两步过滤掉没按键的试次、剔除反应时小于 100ms 或大于 3 个标准差的极端值。前者是实验协议问题后者是统计惯例但标准要根据具体任务调整不能一刀切。4.2 新手最容易翻车的五个注意点写 PsychoPy 实验脚本的翻车现场翻来覆去就是这几个每条都是我看过不少人踩过的坑。坑一窗口一片白文字看不到。现象窗口正常弹出但只有背景色没有刺激内容。原因visual.TextStim创建时忘了调用hello.draw()或者draw()放在了win.flip()之后。draw 是把内容画到后台缓冲区flip 才上台面顺序反了就什么都不显示。解决把stim.draw()放在win.flip()之前确保每次 flip 前都把所有要显示的刺激 draw 一遍。坑二第一次按键没反应第二次才生效。现象被试按空格实验窗口没动静再按一次才进入下一步。原因上一阶段留下的按键事件没有清空。event.waitKeys是等到新按键出现才返回的如果上次实验阶段结束前被试多按了几下这些按键会留在缓冲区里被下一次waitKeys立刻读到。解决在每个关键阶段前调用event.clearEvents()把缓冲区清干净。特别是从指导语页面切换到正式试次时这条几乎是一定要加的。坑三中文刺激显示成方块。原因PsychoPy 默认字体不支持中文字符Windows 上尤其常见TextStim的font参数没设成中文字体。text_stim visual.TextStim(win, text请注视屏幕中央, fontSimHei, height30)解决显式设置fontSimHeiWindows 黑体或fontArial Unicode MSmacOS。Linux 环境可以设fontWenQuanYi Zen Hei这类中文字体。坑四实验数据没保存文件是空的。现象跑完实验控制台没报错但输出文件不存在或内容为空。原因路径问题通常是主因。用相对路径data/results.csv时如果data文件夹不存在写入直接失败to_csv在路径不存在时会静默报错控制台未必能看到。import os os.makedirs(data, exist_okTrue) df.to_csv(data/results.csv, indexFalse)解决写文件前用os.makedirs(data, exist_okTrue)确保目录存在。另一个常见坑是用了中文文件名在某些编码环境里也会写入失败。坑五反应时间记录不准整体偏大或抖动严重。现象跟外部设备比对时发现反应时总是多几十毫秒或者忽大忽小。原因最常见的是逻辑里用了core.wait(0.5)代替计时或者在按键之后处理了太多额外逻辑才记录时间。core.wait本身就有几毫秒的调度误差反应时任务里计时起点必须和刺激出现严格对齐。解决用core.Clock()计时器并在win.flip()之后立即获取时间戳已经写在上一节的反应时脚本里了。这是 PsychoPy 反应时任务的推荐做法别用time.time()当计时工具。4.3 数据导出与初步分析从 csv 到描述统计实验跑完拿到.csv文件后后续分析在 PsychoPy 生态内也能完成。文档里提到 PsychoPy 支持文本文件、Excel、JSON 输出也支持内置分析工具但正式写论文时大家通常还是回到 pandas 和 matplotlib 这套更通用的工具链。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(data/exp1_results.csv) df df[df[rt].notna()] # 去掉没有反应的试次 df[rt_ms] df[rt] * 1000 # 秒转毫秒 summary df.groupby(condition)[rt_ms].agg([mean, std, count]) print(summary) df.boxplot(columnrt_ms, bycondition) plt.savefig(data/rt_boxplot.png, dpi150)这段代码做了三件事读入数据、转换单位、按条件分组做描述统计。groupby(condition)的前提是保存数据时把条件字段写进了每一行这也是为什么上一节我强调手动构造results列表时要把条件也记录进去。很多新手只保存了反应时和正确率没存条件变量后期再想按条件分组分析就得回去重跑实验这是数据记录层面最亏的一件事。5. 进阶把实验脚本提升为可复用工具的配置分离技巧用 PsychoPy 写了十来个实验之后你会意识到一个问题每次改实验参数都得翻脚本改错了还可能把已有的逻辑搞坏。我拆完这份文档后的最大收获是把实验参数从代码里抽出来用配置文件统一管理。做法是创建一个简单的.json或者.yaml文件存放所有可调参数实验脚本启动时读入整个实验逻辑只跟配置字典打交道。改字号、改刺激颜色、改试次数量都不用动核心代码。import json from psychopy import visual, core, event with open(config.json, r, encodingutf-8) as f: config json.load(f) win visual.Window([config[screen][width], config[screen][height]], colorconfig[screen][bg_color], unitspix) fixation visual.TextStim(win, text, heightconfig[fixation][size], colorconfig[fixation][color]) for trial in range(config[exp][n_trials]): fixation.draw() win.flip() if config[exp][debug_mode]: # 调试模式跳过等待 core.wait(0.1) else: core.wait(config[timing][fixation_duration])对应的config.json长这样{ screen: {width: 1024, height: 768, bg_color: [1, 1, 1]}, fixation: {size: 40, color: [-1, -1, -1]}, timing: {fixation_duration: 0.5, isi: 0.3}, exp: {n_trials: 60, debug_mode: true} }配置分离带来一个直接福利调试时可以临时把debug_mode设为true所有刺激呈现等待时间缩短到十分之一跑完整个流程只需要几秒方便快速验证逻辑有没有 bug。正式收集数据前再切回false等待时间恢复正常。这个习惯救过我很多次——某次正式实验前半小时我发现改了刺激大小后位置偏移了靠着调试模式在两分钟内跑完了 60 个试次的完整流程并定位到问题出在坐标计算如果按正常速度得磨蹭将近十分钟。从那以后我每次写新实验第一步永远是建config.json再写脚本逻辑。实验参数、刺激大小、颜色、等待时长、试次数全部集中在外置配置里脚本里再找不到一个裸数字。希望这个思路帮你在 PsychoPy 上少走几趟弯路把时间花在实验设计本身而不是调参数上。本文还有配套的精品资源点击获取