上一篇教程中,我们做了一个简单的实验程序 demo,演示了一下 jsPsych 编写实验程序的基本逻辑。这篇教程,我们会在上一篇教程的基础上,进行扩展和重构,把它做成一个完整的、像模像样的实验程序。

这篇教程的主要目的是认识 timeline 这一重要的组件,并学会用它简化实验程序的设计,或实现额外的功能。

0. 提前规划——我们要做什么?

我们再看看上一篇教程中我们完成的实验程序代码:

let jsPsych = initJsPsych();

let instruction = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: `
    <p>在实验中,屏幕中央会呈现一个圆形</p>
    <p>如果呈现的是蓝色圆形,请尽快按 F 键</p>
    <p>如果呈现的是橙色圆形,请尽快按 J 键</p>
    <p>按任意键开始实验</p>
    `,
    post_trial_gap: 500
}

let blue_trial = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: `<img src="./images/blue.png">`,
    choices: ['f','j'],
    post_trial_gap: 500
}

let orange_trial = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: `<img src="./images/orange.png">`,
    choices: ['f','j'],
    post_trial_gap: 500
}

jsPsych.run([
    instruction,
    blue_trial, orange_trial, blue_trial, orange_trial, blue_trial, orange_trial
])

很显然,其中最大的问题有二:

  1. 试次顺序固定,没有实际意义,且在运行时过于繁琐;
  2. 在反应时实验中,通常需要使用“线索”(cue)来提示被试,马上会出现刺激,这个实验中没有做到。

当然,还有其他各种各样的小问题,不过在这篇文章中,我们主要集中在上面两个问题。

那么,我们先大概规划一下,改进后的实验应该做到什么程度呢?

  1. 有 60 个试次,橙蓝各 30 个,试次的呈现顺序随机;
  2. 在每个试次开始前,要有 500 ms 的提示点,500 ms 的空屏。

1. 从简单的开始——组织一个 timeline

首先我们来实现提示点和空屏的问题。还是拿 PsychoPy 举个例子:在 PsychoPy 的 trial 里,想要实现这个功能,大概率是这样的一个结构:

用 PsychoPy 写的类似功能,先呈现 0.5 s 的 cue,再显示刺激

当然,我们已经知道了,jsPsych 中的 trial 比 PsychoPy 里的离散很多,是一个更小的功能单元。把两个 trial 连接起来,就需要使用 timeline 了。

Timeline(时间线)是一种可以用来组织各个 trial 的顺序,把多个 trial 合并到同一流程内的组件。

1.1 创建 timeline

先让我们把上面示例中的橙蓝刺激改成 timeline 看看。

let blue_timeline = {
    timeline: [
        {
            type: jsPsychHtmlKeyboardResponse,
            stimulus: `+`,
            choices: ["NO_KEYS"],
            trial_duration: 500,
            post_trial_gap: 500
        },
        {
            type: jsPsychHtmlKeyboardResponse,
            stimulus: `<img src="./images/blue.png">`,
            choices: ['f','j'],
            post_trial_gap: 500
        }
    ]
}

let orange_timeline = {
    timeline: [
        {
            type: jsPsychHtmlKeyboardResponse,
            stimulus: `+`,
            choices: ["NO_KEYS"],
            trial_duration: 500,
            post_trial_gap: 500
        },
        {
            type: jsPsychHtmlKeyboardResponse,
            stimulus: `<img src="./images/orange.png">`,
            choices: ['f','j'],
            post_trial_gap: 500
        }
    ]
}

基于这个示例,一个 timeline 的基本结构就成型了:它把两个单独的 trial 连接了起来。我们给前一个注视点的刺激规定了试次的时间长度(500 ms)和它后续的 gap 长度(500 ms)。连接试次是 timeline 的基本功能。现在,我们替换一下运行试次的代码:

jsPsych.run([
    instruction,
    blue_timeline, orange_timeline, blue_timeline, orange_timeline, blue_timeline, orange_timeline
])

注视点和空屏的功能就完成了!

1.2 利用 timeline 简化实验代码

Timeline 还可以起到简化实验代码的作用——你可以把重复的部分抽离出来,放到 timeline 里面,timeline 会自动把它们填入试次里。比如说,我们可以这样:

let blue_timeline = {
    type: jsPsychHtmlKeyboardResponse,
    post_trial_gap: 500,
    timeline: [
        {
            stimulus: `+`,
            choices: ["NO_KEYS"],
            trial_duration: 500
        },
        {
            stimulus: `<img src="./images/blue.png">`,
            choices: ['f','j']
        }
    ]
}

let orange_timeline = {
    type: jsPsychHtmlKeyboardResponse,
    post_trial_gap: 500,
    timeline: [
        {
            stimulus: `+`,
            choices: ["NO_KEYS"],
            trial_duration: 500
        },
        {
            stimulus: `<img src="./images/orange.png">`,
            choices: ['f','j']
        }
    ]
}

我们把定义试次类型(type)和规定试次后空屏(post_trial_gap)的部分抽离了出来,放在了和 timeline 同级的地方。这样做之后,代码仍然可以正常执行,和修改前的效果是一致的。这意味着,我们可以把繁复的代码中的共同部分剥离出来,减少我们编辑每一个 trial 的工作量。

当然,到这里你肯定会有一个问题:如果同一个参数,我在 timeline 中定义了一次,又在单独的 trial 里定义了一次,会发生什么呢?

为什么不自己试试呢?(笑)

let blue_timeline = {
    type: jsPsychHtmlKeyboardResponse,
    post_trial_gap: 500,
    timeline: [
        {
            stimulus: `+`,
            choices: ["NO_KEYS"],
            trial_duration: 500,
            post_trial_gap: 2000
        },
        {
            stimulus: `<img src="./images/blue.png">`,
            choices: ['f','j']
        }
    ]
}

let orange_timeline = {
    type: jsPsychHtmlKeyboardResponse,
    post_trial_gap: 500,
    timeline: [
        {
            stimulus: `+`,
            choices: ["NO_KEYS"],
            trial_duration: 500,
            post_trial_gap: 2000
        },
        {
            stimulus: `<img src="./images/orange.png">`,
            choices: ['f','j']
        }
    ]
}

在以上的代码中,我们在 timeline 中指定了 post_trial_gap 这一参数为 500,又在线索提示点的 trial 里指定了 2000。把这段代码运行一下,它最后的结果是 500 ms,还是 2000 ms 呢?

答案是 2000 ms。在 trial 中定义的参数具有更高的优先级,会覆盖掉 timeline 的默认参数。

2 深入研究——认识时间线变量

我们解决了如何组合一个完整的刺激流程的问题,接下来,让我们开始接触 timeline 的更多功能,实现我们的第一个目标:重复并随机试次。

到这里,就要介绍时间线变量(timeline variables)了。先上示例代码:

// Timeline Variables Demo
let timeline_demo = {
    type: jsPsychHtmlKeyboardResponse,
    post_trial_gap: 500,
    timeline: [
        {
            stimulus: `+`,
            choices: ["NO_KEYS"],
            trial_duration: 500,
        },
        {
            stimulus: jsPsych.timelineVariable('picture'),
            choices: ['f','j']
        }
    ],
    timeline_variables: [
        {picture: '<img src="./images/blue.png">'},
        {picture: '<img src="./images/orange.png">'},
    ]
}

这段代码在我们写的两个 timeline 基础上做了更进一步的抽象化,把两个 timeline 统一成了一个。

在 trial 中,我们将刺激指定为一个变量 picture,随后在参数中将 picture 的内容定义好——在这里是两张图片。jsPsych 会自动按顺序执行完所有的变量,再结束实验。所以如果运行这个实验,你会分别接受一次蓝色圆刺激,一次橙色圆刺激。

使用时间线变量,可以方便地定义试次中的各种内容,进一步简化我们的代码复杂程度。

3. 循环和随机化——时间线的采样方法

再下一步,那自然是试次的重复和随机化了。Timeline 中有一个独特的参数,用来控制时间线的循环和随机化:

// Timeline Variables Demo
let timeline_demo = {
    type: jsPsychHtmlKeyboardResponse,
    post_trial_gap: 500,
    timeline: [
        {
            stimulus: `+`,
            choices: ["NO_KEYS"],
            trial_duration: 500,
        },
        {
            stimulus: jsPsych.timelineVariable('picture'),
            choices: ['f','j']
        }
    ],
    timeline_variables: [
        {picture: '<img src="./images/blue.png">'},
        {picture: '<img src="./images/orange.png">'},
    ],
    sample: {
        type: 'fixed-repetitions',
        size: 30
    }
}

注意我们新增的 sample 参数,这规定了试次的“采样方法”——如何选取试次,试次用什么方式运行。在这里,我们选择了 fixed-repetitions,要求试次以随机的顺序总共重复 30 次。

再保存一下,运行试试?

4. 总结

本篇教程结束后,完整的实验程序代码如下:

let jsPsych = initJsPsych();

let instruction = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: `
    <p>在实验中,屏幕中央会呈现一个圆形</p>
    <p>如果呈现的是蓝色圆形,请尽快按 F 键</p>
    <p>如果呈现的是橙色圆形,请尽快按 J 键</p>
    <p>按任意键开始实验</p>
    `,
    post_trial_gap: 500
}

// Timeline Variables Demo
let timeline_demo = {
    type: jsPsychHtmlKeyboardResponse,
    post_trial_gap: 500,
    timeline: [
        {
            stimulus: `+`,
            choices: ["NO_KEYS"],
            trial_duration: 500,
        },
        {
            stimulus: jsPsych.timelineVariable('picture'),
            choices: ['f','j']
        }
    ],
    timeline_variables: [
        {picture: '<img src="./images/blue.png">'},
        {picture: '<img src="./images/orange.png">'},
    ],
    sample: {
        type: 'fixed-repetitions',
        size: 30
    }
}

jsPsych.run([
    instruction,
    timeline_demo
])

到这里,你已经知道了 jsPsych 程序的基本编写原理——当然,这篇教程太浅了(毕竟是快速上手,以入门了解基本方法为主)。你需要补充阅读原始的教程来了解时间线的控制方法究竟有多少种,有哪些可以使用的东西。

jsPsych 的 Timeline 官方教程:https://www.jspsych.org/7.3/overview/timeline/

下一篇教程将从实战出发,从头开始设计一个实验,主要讲解设计 jsPsych 实验的思路——应该从什么角度开始思考?选择哪些东西?如何组合它们?等等等等。

此外,你也可以阅读这个实验的原始教程:https://www.jspsych.org/7.3/tutorials/rt-task/,看看官方的实现方法和我们教程中的做法有哪些不同,这些不同有哪些影响。

照例,在本文末尾留几个习题,作为进一步的思考。

习题 1

为了避免被试的习惯化,通常我们要求线索之后的空屏有一定的随机量(举例而言,400 - 600 ms)。如何实现这一点呢?

习题 2

如果你完成了上一篇教程中的习题 2(修改元素的风格格式),那么请完成:增大指导语的字体大小到 200%,增大注视点的刺激大小到 400%。

最后修改:2024 年 08 月 25 日
虽然点赞什么的确实没什么意义但是也可以点一个再走呗?(