本ページでは,jsPsychを用いた認知課題の作成をRstudioで行う方法について解説をします。以下では,簡単なストループ課題を作ってみて,jsPsychでの課題作成方法を学びます。


0. ストループ課題を作る準備

ストループ課題に限りませんが,認知課題をプログラミングする場合に,いきなりそのままプログラムを書くのは避けたほうが良いです。私が認知課題を作る時にやっている普段やっていることを整理すると以下のような感じになります。

  1. 課題の情報を整理する
  2. 課題の設計図を作る
  3. 教示と課題のメインブロックを最もシンプルな形で作る
  4. 課題のメインブロックに機能を追加をする
  5. メインブロック以外のブロックを追加していく

1~5にまっすぐ進むわけでなく,試行錯誤しつつ,行きつ戻りつしながら進みます。特に初学者にとって大切に思うのは,1と2の課題の情報を整理して紙などに課題の設計図を書いてみること,いきなり実際に行う課題を作ろうとするのではなくメインブロックを一番シンプルな形(例えば,ストループ課題の場合は,文字を画面に出すことです)で作ることです。その後で,徐々に機能を追加していきます。いきなり完成形を作ろうとしなくても良いです。


1. 課題の情報を整理する

まずストループ課題の情報を整理します。

ストループ課題は,以下のように文字の意味と文字の色が同じだったり違ったりすることで,意味と色の情報が干渉しあうストループ効果を調べる課題です。典型的には,以下の左のように意味と色が一致な刺激や右のように不一致な刺激を呈示し,意味ではなく色に関して反応するように求めます。その時の反応時間を計測して,一致条件よりも不一致条件の方が反応時間が長い場合にストループ効果が示されたとします(不一致条件では,色の判断に意味の情報が干渉するので遅くなる)。

ストループ課題にもいろいろなタイプがありますが,今回は,赤色・青色・緑色・黄色の4つの刺激を使って,一致条件と不一致条件を作って,キーボードのボタン押しによって,反応時間を測定することにしましょう。


2. 課題の設計図を作る

それではストループ課題の設計図を作ってみましょう!私はなにかプログラムを書くときは,必ず全体像を紙に書き出してから作業を始めます。がむしゃらにトライアンドエラーを繰り返すのも大切ですが,まずは自分が作ろうとしているものの全体像を紙に書き出します。

全体像を書き出すと,以下のようになります(以下の図は私が紙に書き出したものをGoogle スライドで清書したものです。必ず,紙などに書き出す習慣をつけましょう)。左側の図にあるように,開始メッセージ(画面に「ようこそ!」みたいなのを出す),フルスクリーンにする(ブラウザの一部で実験課題をすると集中できないのでフルスクリーンで実施します),課題の教示(課題について説明する文書です),課題のメインの部分(ストループ刺激を呈示して,反応を取得する。そして,これを刺激の数だけ繰り返す),終了メッセージ(課題終了のメッセージです。今回は正答率などのフィードバックもすることにします)の順番になります。

ここで大切なのは,実験課題は全体で1つというよりは,いくつかのブロックから構成されているとうことです。全体をいきなり作るのは結構たいへんですが,一部のブロックだけ作るなら頑張れるかもしれません。こうやって小さなブロックをいくつか作って,それをLEGOブロックみたいに組み合わせることで全体を作っていきます。こういう小さなブロックに分けて作っていくやり方を意識するようにしてみてください。

そして,図の左側のように刺激の組み合わせも書き出します。赤・緑・黄・青の4種類があるので,色4種と文字4種で16通りがあります。そして,1,6,11,16が一致条件,それ以外は不一致条件ですね。上の図のような順番で刺激を呈示すると,参加者は予想できてしまいますので,刺激呈示する際には,ランダム化して呈示します。

紙に書き出すとか古臭いと思うかもしれませんが,こういう地味な作業をすることが漏れのない設計に繋がります。実験課題などのプログラミングだけでなく,理論やモデルの理解の際にもこういう習慣を身に着けておくと良いです。


3. 教示と課題のメインブロックを最もシンプルな形で作る

さて,設計図を見るといくつかあるブロックの中で一番メインのプロックは刺激呈示をして反応を取得するブロックです。これを最もシンプルな形で作るとすれば,反応の取得はおいておいて,まずは刺激を呈示するブロックを作ることになります。ということで,以下では,早速,教示と課題のメインブロックの刺激呈示部分を作ることとにします。なぜ教示も作るかというと,教示は一般的には簡単につくれることが多いのと,教示は参加者向けのものではありますが,上記の設計図と同じ働きをして,課題の全体像を意識することにつながります。

さて,ストループ課題用を作るためのjsPsychやR Markdownの準備をします。以下をコンソールに打ち込んで準備をしましょう。

library(jsPsychRmd)
set_jsPsych(file_name = "stroop")


3.1 教示を呈示する

早速,教示を作ってみましょう。以下のような内容を画面に呈示して,説明をします(<>内はその前の単語の色を指定しています)。

この課題では, 以下のような色のついた単語を見ていただきます。
青<赤色>
単語の意味は無視して,それぞれの単語の「色」を以下のキーボードのキーを押して回答してください。
左手の人差し指と中指でdとf,右手の人差し指と中指でjとkを押してください。
・赤色<赤色>の単語ならdを押す
・青色<青色>の単語ならfを押す
・緑色<緑色>の単語ならjを押す
・黄色<黄色>の単語ならkを押す
上の例の場合だと,赤色で「青」と書いてありますので,dを押します
キーボードのキーをどれか押すと課題が始まります

index.Rmdを開いて,jsブロック内で課題の設定をしていきます。教示は,以下のように,“html-keyboard-response”を使って呈示します。stimulusってところで呈示する内容を指定します。post_trial_gapは,教示出してから次に移行する間の時間です。ここでは,1000msに設定します。なお,教示の説明部分は,pで挟まれた文章がありますが,これは,文字の大きさを変えたり,左揃えにする際に必要になります。そして,spanで挟まれた文章は,文字の色を変えることができます。

教示の設定を書く,instructionができたら,timelineを作って,instructionにpushします。最後に,jsPsych.init()で,課題をtimelineにそって走らせましょう。on_finish: function() {jsPsych.data.displayData();}の説明をしておきます。これを追加しておくと,課題の終了時に呈示した刺激や反応などが表示されますので,便利です。

ここまでできたら,「Knit」を押して,教示を出してみましょう。一番基本の教示ブロックが完成しました。


3.2 ストループ刺激を呈示する

さて,次はメインのブロックです。メインのブロックでは,ストループ刺激(色のついた文字)を呈示し,その反応を取得します。ただいきなり反応取得までいくとごちゃごちゃしますので,まずは刺激を順番通りに呈示するだけにします(ランダム化しない)。段階を踏むのは遠回りに思えるかもしれませんが,いきなり色々と入れ込むとわけわからなくなるので,やめておきましょう。

以下のように,stimuliの中に16個のストループ刺激(stimulus)を用意します。p styleで色やフォントサイズの指定をしています(最初のstimulusは赤色の60ポイントの「赤」という文字ですね)。上の課題の設計図のところで作成したように,刺激を順番に指定します。これで,stimuliには,16個の色と文字の異なる刺激が準備できました。

var stimuli = [
  {stimulus: "<p style='color:red;font-size:60pt;'>赤</p>"},
  {stimulus: "<p style='color:green;font-size:60pt;'>赤</p>"},
  {stimulus: "<p style='color:yellow;font-size:60pt;'>赤</p>"},
  {stimulus: "<p style='color:blue;font-size:60pt;'>赤</p>"},
  {stimulus: "<p style='color:red;font-size:60pt;'>緑</p>" },
  {stimulus: "<p style='color:green;font-size:60pt;'>緑</p>"},
  {stimulus: "<p style='color:yellow;font-size:60pt;'>緑</p>"},
  {stimulus: "<p style='color:blue;font-size:60pt;'>緑</p>"},
  {stimulus: "<p style='color:red;font-size:60pt;'>黄</p>"},
  {stimulus: "<p style='color:green;font-size:60pt;'>黄</p>"},
  {stimulus: "<p style='color:yellow;font-size:60pt;'>黄</p>"},
  {stimulus: "<p style='color:blue;font-size:60pt;'>黄</p>"},
  {stimulus: "<p style='color:red;font-size:60pt;'>青</p>"},
  {stimulus: "<p style='color:green;font-size:60pt;'>青</p>"},
  {stimulus: "<p style='color:yellow;font-size:60pt;'>青</p>"},
  {stimulus: "<p style='color:blue;font-size:60pt;'>青</p>"}
];

さて,このstimliを呈示していくわけですが,前に学んだようにfor文で呈示することもできますが,jsPsychでは,「Timeline variables」を使って,もう少し簡単に書くこともできます(Timeline variablesの詳細は,こちらを参照ください)。教示をhtml-keyborad-responseで呈示しましたが,それは1回だけでした。jsPsychでは,instructionや以下のstroopといったオブジェクトの中にもtimelineを作ることができます(なんだかよくわからないと思いますが,stroopの中に16個の刺激を呈示するtimelineを持つことができて,これができるとコードがすっきりします)。var stroop ={の後ろにtimeline:[{}]を作ります。timeline:[{}]内で,type, trial_duration, stimulus, promptを指定します。そして,。timeline:[{}]の後ろに,timeline_variablesで先程作ったstimuliを指定します。なお,今回は,stimulusを入れ替えて呈示するので,jsPsych.timelineVariable()を使って,timeline_variablesで指定したstimliの中のstimulusをstimulus: jsPsych.timelineVariable(‘stimulus’)と指定します。このように,timeline_variablesに16個のストループ刺激が入ったstimuliを指定したので,timeline:[{}]のstimulusをstimuliのstimulusを使って変更しつつ16回呈示します。

ちょっと分かりにくいですが,呈示したい刺激のリストを作って,それをtimeline_variablesに入れます。そうすると,jsPsych.timelineVariable()を使って,その前のtimeline内の内容を変更することができます(今回の場合は,stimulusを施行ごとに変更しています)。これを上手く使うと,シンプルかつ柔軟に課題を作ることができます。

これで,作成したリストの順番でストループ刺激が呈示されるかと思います。試してみましょう。


演習課題1

  • 今回は漢字で刺激を呈示しましたが,「赤」ではなく,「あか」のようにひらがなで刺激を呈示してみましょう。



デバッグについて

さて,ここまで上手く作れたでしょうか?なかなか動かなくて困った人もいるかもしれません。RmdファイルからHTMLは作れるけど,ちゃんと動作しないことも多いです。その時に,以下の方法でバグを探すと良いです。

上手く動作しないときは,タイトルと作者だけ表示されることが多いですが,その上手く動いていないHTMLファイルをブラウザ(Chrome)で開きます。そして,右クリックをしてメニューをだして,Inspectをクリックします。そうすると以下の右側のような画面がでてくるので,そのConsoleあたりをみるとエラーが出ています。エラーをみると,コードのどこに問題があるか確認できます。これにしたがって,コードを修正していくと,ちゃんと走るかと思います。


4. 課題のメインブロックに機能を追加をする

メインブロックの刺激呈示はできましたが,いくつかやり残したことがあります。ここでは,提示する刺激のランダム化と反応の取得をします。


4.1 ストループ刺激をランダムに呈示する

まず,刺激のランダム化は簡単で,timeline_variablesの後ろに,randomize_order: trueを入れればできます(ランダム化の詳細は,こちらを参照ください)。

var stroop = {
  timeline: [{
    type: "html-keyboard-response",
    trial_duration: 2000,
    stimulus: jsPsych.timelineVariable('stimulus'),
    prompt: '赤色ならd,  青色ならf,  緑色ならj,  黄色ならk',
  }],
  timeline_variables: stimuli,
  randomize_order: true
};

ただ,実際の課題は,刺激リストを数回繰り返すことも多いかと思います。その場合は,sampleを使います。sampleでは,以下のような方法があります。

  • with-replacement: timeline variablesから特定の数ランダムにサンプルする(数はsizeで指定する)。同じアイテムが何度も選ばれることがある。

  • without-replacement: timeline variablesから特定の数ランダムにサンプルする(数はsizeで指定する)。それぞれのアイテムが1回だけ選ばれる。

  • fixed-repetitons: timeline variablesのアイテムがランダムに特定の回数サンプルされる(繰り返しの数はsizeで指定する)。

今回の場合は,ストループ刺激のリストをランダムに2回(リストを2周する)呈示するので,fixed-repetitonsが適していますね。timeline_variables: stimuliの次にsample: {type: ‘fixed-repetitions’,size: 2}を追加しましょう。これで,simuliリストのアイテムを2回ランダムに呈示します。

var stroop = {
  timeline: [{
    type: "html-keyboard-response",
    trial_duration: 2000,
    stimulus: jsPsych.timelineVariable('stimulus'),
    prompt: '赤色ならd,  青色ならf,  緑色ならj,  黄色ならk',
  }],
  timeline_variables: stimuli,
  sample: {type: 'fixed-repetitions',size: 2}
};


4.2 ボタン押しの記録と正誤判定

今回作成しているストループ課題では,キーボードのタイピングで反応を取得します(d,f,j,k)。なお,Java Script内で処理する際に文字よりも数字のコードのほうが便利です。そこで,キーボードのそれぞれ割り振られたコード使います。それぞれのキーボードにどういうコードが割り振られているかは,https://keycode.info/で簡単に確認できます。なお,今回使う,d,f,j,kの場合は,以下のようになります。

  • 赤色の単語ならdを押す = 68
  • 緑色の単語ならjを押す = 74
  • 黄色の単語ならkを押す = 75
  • 青色の単語ならfを押す = 70

この情報も踏まえて,stimuliのリストに手を加えます。先程は,stimulusのみを書いていましたが,その後の正誤判定のためや解析で使えるように,dataにno(項目番号),stim_type(一致か不一致か),correct_key_press(正答のキーボードのキーのコード)を以下のように追加します。

var stimuli = [
  {
    stimulus: "<p style='color:red;font-size:60pt;'>赤</p>",
    data: { no:'1', stim_type: 'congruent', correct_key_press: '68'}
  },
  {
    stimulus: "<p style='color:green;font-size:60pt;'>赤</p>",
    data: { no:'2', stim_type: 'incongruent', correct_key_press: '74'}
  },
  {
    stimulus: "<p style='color:yellow;font-size:60pt;'>赤</p>",
    data: { no:'3', stim_type: 'incongruent', correct_key_press: '75'}
  },
  {
    stimulus: "<p style='color:blue;font-size:60pt;'>赤</p>",
    data: { no:'4', stim_type: 'incongruent', correct_key_press: '70'}
  },
  {
    stimulus: "<p style='color:red;font-size:60pt;'>緑</p>",
    data: { no:'5', stim_type: 'congruent', correct_key_press: '68'}
  },
  {
    stimulus: "<p style='color:green;font-size:60pt;'>緑</p>",
    data: { no:'6', stim_type: 'incongruent', correct_key_press: '74'}
  },
  {
    stimulus: "<p style='color:yellow;font-size:60pt;'>緑</p>",
    data: { no:'7', stim_type: 'incongruent', correct_key_press: '75'}
  },
  {
    stimulus: "<p style='color:blue;font-size:60pt;'>緑</p>",
    data: { no:'8', stim_type: 'incongruent', correct_key_press: '70'}
  },
  {
    stimulus: "<p style='color:red;font-size:60pt;'>黄</p>",
    data: { no:'9', stim_type: 'congruent', correct_key_press: '68'}
  },
  {
    stimulus: "<p style='color:green;font-size:60pt;'>黄</p>",
    data: { no:'10', stim_type: 'incongruent', correct_key_press: '74'}
  },
  {
    stimulus: "<p style='color:yellow;font-size:60pt;'>黄</p>",
    data: { no:'11', stim_type: 'incongruent', correct_key_press: '75'}
  },
  {
    stimulus: "<p style='color:blue;font-size:60pt;'>黄</p>",
    data: { no:'12', stim_type: 'incongruent', correct_key_press: '70'}
  },
  {
    stimulus: "<p style='color:red;font-size:60pt;'>青</p>",
    data: { no:'13', stim_type: 'congruent', correct_key_press: '68'}
  },
  {
    stimulus: "<p style='color:green;font-size:60pt;'>青</p>",
    data: { no:'14', stim_type: 'incongruent', correct_key_press: '74'}
  },
  {
    stimulus: "<p style='color:yellow;font-size:60pt;'>青</p>",
    data: { no:'15', stim_type: 'incongruent', correct_key_press: '75'}
  },
  {
    stimulus: "<p style='color:blue;font-size:60pt;'>青</p>",
    data: { no:'16', stim_type: 'incongruent', correct_key_press: '70'}
  }
];

そして,以下のようにstroopブロックに追加をしていきます。

  • typeの下に,choices: [“d”,“f”,“j”,“k”]をおいて,stroopブロックで使う選択肢を指定する。
  • キーを押したら刺激呈示が終わるように,response_ends_trial: trueを追加する。
  • 刺激呈示or反応後にすぐに次の刺激がでてくるとまずいのでpost_trial_gapでブランクをいれます。その際に,post_trial_gap: function() {return Math.floor(Math.random() * 1500) + 500;}として,ランダムになるようにします(Math.random()は0~1の間の値をランダムにとるので,最小500ms,最大2000msになります)。

以下が設定できたら,一度走らせてみましょう!それでうまくいっていれば,キーボードで反応をすると,最後にその選択が記録されます。

var stroop = {
  timeline: [{
    type: "html-keyboard-response",
    choices: ["d","f","j","k"],
    trial_duration: 2000,
    response_ends_trial: true,
    stimulus: jsPsych.timelineVariable('stimulus'),
    prompt: '赤色ならd,  青色ならf,  緑色ならj,  黄色ならk',
    data: jsPsych.timelineVariable('data'),
    post_trial_gap: function() {
        return Math.floor(Math.random() * 1500) + 500;
    }
  }],
  timeline_variables: stimuli,
  sample: {type: 'fixed-repetitions',size: 2}
};

さて,その後の解析なども含めて,反応の正誤判定をしておくと便利です。stimuliのリストのアイテムの正答反応を意味するcorrect_key_pressと実際の反応が記録されdataに保存されたdata.key_pressとが一致していれば,その試行の反応は正答です。それをif文を使って表現すると以下になります。基本的にはcorrectは0としておいて,もしアイテムの正答反応と反応が一致したら,correctを1にして,dataに保存します。

var correct = 0;
if(data.correct_key_press == data.key_press){
  correct = 1;
} 
data.correct = correct;

この作業は試行の最後にしてほしいので,試行終了時の作業を指示するon_finish: function()を使います。また,ここではstimuli内のdataを使うので,data: jsPsych.timelineVariable(‘data’)も指定します。そうすると以下のようになります。

/*ストループ課題*/
var stroop = {
  timeline: [{
    type: "html-keyboard-response",
    choices: ["d","f","j","k"],
    trial_duration: 2000,
    response_ends_trial: true,
    stimulus: jsPsych.timelineVariable('stimulus'),
    prompt: '赤色ならd,  青色ならf,  緑色ならj,  黄色ならk',
    data: jsPsych.timelineVariable('data'),
    on_finish: function(data){
      var correct = 0;
      if(data.correct_key_press == data.key_press){
        correct = 1;
      } 
      data.correct = correct;
    },
    post_trial_gap: function() {
        return Math.floor(Math.random() * 1500) + 500;
    }
  }],
  timeline_variables: stimuli,
  sample: {type: 'fixed-repetitions',size: 2}
};

走らせてみて,最後に表示される結果から,ちゃんと正答した時にcorrectが1になっていますか?それができていれば,メインブロック部分は完成です。


演習課題2

  • 上記では繰り返しが2回でしたが,繰り返しが3回のストループ課題を作ってみましょう。


5. メインブロック以外のブロックを追加していく

さて,メインブロックは完成したので,残りのブロックを追加していきます。まずは,課題が始まった時に表示されるウェルカムメッセージとフルスクリーンで実施するための教示などのブロックを追加して,最後に終了の教示(今回はデブリーフィングとして,結果のフィードバックをする)を表示します。


5.1 ウェウカムメッセージとフルスクリーン化の追加

最初にウェウカムメッセージとして以下のwelcomブロックを追加します。

/* 最初のメッセージ*/
var welcome = {
  type: "html-keyboard-response",
  stimulus: "<p><span style='font-size:20pt;'>実験にお越しいただき,ありがとうございます!!!</span></p>"+
  "<p>キーボードのキーをどれか押して,開始してください</p>"
};

timelineにもwelcomを追加します。

var timeline = [];
timeline.push(welcome);
timeline.push(instructions);
timeline.push(stroop);

さらに,認知課題をブラウザで実行する場合は,全画面表示で行う必要があります(気が散るので)。全画面表示については,set_jsPsych(file_name = "stroop")で用意をすると,すでに使うプラグインとしてjspsych-fullscreen.jsが追加されていますし,以下のようなブロックが用意されています。

/* 全画面表示化 */
var fullscreen = {
  type: 'fullscreen',
  message: '<p>以下のボタンをクリックすると,画面は全画面表示に切り替わります。全画面表示を止めたい場合はEscキーを押してください。</p>',
  button_label: "全画面表示に切り替え",
  fullscreen_mode: true
};

welcomeの次にこのfullscreenブロックをtimelineにプッシュします(//timeline.push(fullscreen);となっているので,コメントアウトを取ります)。これで,ウェルカムメッセージが表示されて,全画面表示ができるようになっていると思います。

var timeline = [];
timeline.push(welcome);
timeline.push(fullscreen);
timeline.push(instructions);
timeline.push(stroop);


5.2 デブリーフィング

いよいよ最後です。最後に,終了時の教示(今回は結果のフィードバックをする)のブロックを追加します。html-keyboard-responseを使って画面を表示しますが,stimulusにて,dataから結果を引っ張ってきて,一致試行と不一致試行の平均反応時間を計算しています。ややこしそうですが,dataからfilterで絞り込みをかけて,countなどで数えるという単純な作業をしているだけになります。その計算結果をいれこんだ文章をつくって表示しています。

/*デブリーフィングの設定*/
var debrief = {
  type: "html-keyboard-response",
  stimulus: function() {
    var incongruent_trials = jsPsych.data.get().filter({stim_type: 'incongruent'}).count();
    var congruent_trials = jsPsych.data.get().filter({stim_type: 'congruent'}).count();
    var accuracy = Math.round(jsPsych.data.get().filter({correct: 1}).count() / (incongruent_trials + congruent_trials) * 100);
    var congruent_rt = Math.round(jsPsych.data.get().filter({correct: 1, stim_type: 'congruent'}).select('rt').mean());
    var incongruent_rt = Math.round(jsPsych.data.get().filter({correct: 1, stim_type: 'incongruent'}).select('rt').mean());
    return "<p>あなたの正答率は,<strong>"+accuracy+"%</strong>でした。</p> " +
    "<p>色と単語が一致していた時にボタンを押すまでにかかる時間の平均は, <strong>" + congruent_rt + "ms</strong>でした。</p>"+
    "<p>色と単語が不一致だった時にボタンを押すまでにかかる時間の平均は, <strong>" + incongruent_rt + "ms</strong>でした。</p>"+
    "<p>キーボードのキーをどれか押すと画面に結果の記録が表示されます。ブラウザを閉じて終了してください。ご参加ありがとうございました。</p>";
  }
};

上記のdebriefブロックができたら,timelineにdebriefをpushします。

var timeline = [];
timeline.push(welcome);
timeline.push(fullscreen);
timeline.push(instructions);
timeline.push(stroop);
timeline.push(debrief);

さて,これで,ストループ課題は完成です!


演習課題3

  • キーボードのキーを,“d”,“f”,“j”,“k”ではなく,“z”,“x”,“n”,“m”に変えてストループ課題を作ってみよう!