Chapter 09

【フランカー課題】教示と練習

snishiyama
snishiyama
2022.08.17に更新

本章の目的

本章では課題の教示,課題の練習をこれまでのコードに追加します。対面実験であれば実験プログラムとは別に作っておいたスライドを用いて教示を行うことも可能ですが,jsPsych の主な利用場面であるオンライン実験ではそうも行きません。実験プログラムに教示を入れておく必要があります。また,練習課題は,参加者の教示の理解を確認したり反応のマッピングを生成したりするために重要です。

これらの導入はこれまでの内容を少し応用するだけでできます。ただ,練習を導入するとコードが少し冗長になるので,それを避けるための関数化にまた取り組んでみましょう。

教示・長文テキスト

フランカー課題の教示は以下のようなものでしょうか。

これから画面上に5つの矢印(>>>>>など)が表示されます。中央の矢印が左向き(<)なら「f」キーを,中央の矢印が右向き(>)なら「j」キーをなるべく速く,正確に押してください。準備ができたら「f」キーか「j」キーを押して課題を開始してください。

jsPsych では様々な方法で教示の提示を実装することができます[1]が,上のような文だけのシンプルな教示であれば,これまでずっと扱ってきたjsPsychHtmlKeyboardResponseを使えばよいです。刺激や注視点を指定していたstimulusに教示文を指定するだけです。

show_instruction.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script src="../jspsych/dist/jspsych.js"></script>
    <script src="../jspsych/dist/plugin-html-keyboard-response.js"></script>
    <link rel="stylesheet" href="../jspsych/dist/jspsych.css" />
  </head>
  <body></body>
  <script>
    const jsPsych = initJsPsych();

    const instruction = {
      type: jsPsychHtmlKeyboardResponse,
      stimulus:
        'これから画面上に5つの矢印(>>>>>など)が表示されます。中央の矢印が左向き(<)なら「f」キーを,中央の矢印が右向き(>)なら「j」キーをなるべく速く,正確に押してください。準備ができたら「f」キーか「j」キーを押して課題を開始してください。',
      choices: ['f', 'j'],
      data: { task: 'instruction' },
    };

    jsPsych.run([instruction]);
  </script>
</html>

実際表示してみると,画面幅に合わせて改行されるので読みづらいです。改行位置を指定して,読みやすくしていきます。教示文中に<br>を入れるとその位置で改行されます。

const instruction = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus:
    'これから画面上に5つの矢印(>>>>>など)が表示されます。<br>中央の矢印が左向き(<)なら「f」キーを<br>中央の矢印が右向き(>)なら「j」キーを<br>なるべく早く,正確に押してください<br><br>準備ができたら「f」キーか「j」キーを押して課題を開始してください。',
  choices: ['f', 'j'],
  data: { task: 'instruction' },
};

改行を入れることでかなり見やすくなったのではないでしょうか。改行記号は 2 つ重ねることもできて,その場合は空白行ができます。段落などのまとまりを作る場合に便利です[2]

これで参加者は教示文を読みやすくなりました。しかし,実験を作っている立場からすると,1 行で書くのは見にくく,編集しにくく不便です。これを解消する方法としては,教示文をいくつかの文字列に分割して,+で連結するようことが挙げられます。+を使っている部分ではコード上で改行することができるので,コードを見やすくすることができます。

const instruction = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus:
    'これから画面上に5つの矢印(>>>>>など)が表示されます。<br>' +
    '中央の矢印が左向き(<)なら「f」キーを<br>' +
    '中央の矢印が右向き(>)なら「j」キーを<br>' +
    'なるべく早く,正確に押してください<br><br>' +
    '準備ができたら「f」キーか「j」キーを押して課題を開始してください。',
  choices: ['f', 'j'],
  data: { task: 'instruction' },
};

jsPsych.runの注意点

課題の教示用の試行変数を作成できたので,教示の表示後に課題が実施されるようにしましょう。これまで作成してきたblocksRandomと今回作成したinstructionを並べてjsPsych.runに入れれば良いように思います。

jsPsych.run([instruction, blocksRandom]);

実はこれではエラーになってしまいます。これまで説明してきませんでしたが,jsPsych.runの引数には,オブジェクト(より具体的には試行変数あるいはブロック変数)の配列を指定する必要があります。

上のコードではjsPsych.run([])の配列[]の中にinstructionblocksRandomが入っていて,

  • instructiontypeなどのプロパティを持つ試行変数オブジェクトです。一方で,
  • blocksRandomtimelineなどのプロパティを持つブロック変数オブジェクトの配列です。

jsPsych.run()には試行・ブロック変数の配列を指定する必要があるということ,blocksRandomが配列ということを合わせて考えると,これまでなんとなしに使い続けてきたjsPsych.run(blocksRandom)がうまく動作していたことが理解できると思います。

それではどうすればいいかというと,一つはblocksRandomtimelineとするより大きな単位のブロック変数を作成するという方法が上げられます。

const flanker = {
  timeline: blocksRandom,
};
jsPsych.run([instruction, flanker]);

もう一つは,blocksRandomの配列の先頭にinstructionを挿入する方法もあります。ただ,個人的には好みではありません。一つの実験で複数の課題をやる必要がある場合には,一つ目の解決策を取る必要があるからです。

blocksRandom.splice(0, 0, instruction);
jsPsych.run(blocksRandom); // この場合,[]はいらない

spliceについては第 5 章を参照してください。

課題の練習

課題の練習は,本番と同じ設定で実施され異なるのは試行数だけです。つまり,これまでの章で作成したブロック変数やそれらの配列であるblocksはそのまま利用できます。そのため,randomization.repeat()を使って,練習用と本番用のランダム化済み配列を作成することができます。

const blocks = [block1, block2, block3, block4];
// ↑↑↑ ここより上はこれまでと同様 ↑↑↑
const flankerPractice = {
  timeline: jsPsych.randomization.repeat(blocks, 2),
};
const flankerMain = {
  timeline: jsPsych.randomization.repeat(blocks, 5),
};

// instruction の定義を追加する

jsPsych.run([instruction, flankerPractice, flankerMain]);

基本的にこれで OK なんですが,実はこのままだと,練習と本番のデータの区別がつかないため,分析の際に苦労してしまいます。もちろん,練習のほうが本番よりも先に実施されるはずなので,それを基準に分析を進めることができますが,可能であれば,本番(そして必要があれば練習)のデータだけをスッと抽出できるようにしておきたいです。他の課題も取り入れたりし始めると必要なデータを見つけるのがさらに難しくなるでしょう。

そこで,前章でfixationtrialのデータを識別しやすくするためにそれぞれをtaskというプロパティ名でデータに追加したのと同様に,課題の練習か本番かをtaskプロパティに保存するようにしたほうがいいでしょう。そのためにcreateTrialを以下のように編集してみます。合わせてcreateBlockも編集しています。

// 引数で taskName を渡せるようにする
const createTrial = (stim, fontSize, corrKey, cond, taskName) => {
  const trial = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: `<p style="font-size: ${fontSize}px">${stim}</p>`,
    choices: ['f', 'j'],
    post_trial_gap: 500,
    data: {
      task: taskName, // <--
      correctKey: corrKey,
      condition: cond,
    },
    on_finish: (data) => {
      data.isCorrect = Number(jsPsych.pluginAPI.compareKeys(data.response, data.correctKey));
    },
  };
  return trial;
};

// fixationの定義

// 引数で taskName を渡せるようにする
const createBlock = (stim, fontSize, corrKey, cond, taskName) => {
  const trial = createTrial(stim, fontSize, corrKey, cond, taskName);
  const block = {
    timeline: [fixation, trial],
  };
  return block;
};

これらの変更に伴って,ランダマイズまでのコードも以下のようになります。

const blockPractice1 = createBlock('<<<<<', 48, 'f', '一致', 'flanker_practice');
const blockPractice2 = createBlock('>>>>>', 48, 'j', '一致', 'flanker_practice');
const blockPractice3 = createBlock('<<><<', 32, 'j', '不一致', 'flanker_practice');
const blockPractice4 = createBlock('>><>>', 30, 'f', '不一致', 'flanker_practice');

const blockMain1 = createBlock('<<<<<', 48, 'f', '一致', 'flanker_main');
const blockMain2 = createBlock('>>>>>', 48, 'j', '一致', 'flanker_main');
const blockMain3 = createBlock('<<><<', 32, 'j', '不一致', 'flanker_main');
const blockMain4 = createBlock('>><>>', 30, 'f', '不一致', 'flanker_main');

const blocksPractice = [blockPractice1, blockPractice2, blockPractice3, blockPractice4];
const blocksMain = [blockMain1, blockMain2, blockMain3, blockMain4];
const flankerPractice = {
  timeline: jsPsych.randomization.repeat(blocksPractice, 2),
};
const flankerMain = {
  timeline: jsPsych.randomization.repeat(blocksMain, 5),
};

なんとも長いコードになってしまいました。これでも動くのですが,各試行の設定や,試行の順番をランダマイズするという処理は練習と本番で共通しているので,うまく関数化してこの冗長さを回避したいところです。演習として関数の作成に取り組んでみましょう。

演習

  • taskに割り当てられる値が変えられるように,フランカー課題のブロック変数の配列(ブロック化済み)をtimelineとするブロック変数を出力する関数を作成しよう。
コード例

練習と本番で試行数も変えられるように,jsPsych.randomization.repeat()にわたす繰り返し回数も引数で指定できるようにしておきましょう。

const createFlanker = (taskName, repeatN) => {
  const block1 = createBlock('<<<<<', 48, 'f', '一致', taskName);
  const block2 = createBlock('>>>>>', 48, 'j', '一致', taskName);
  const block3 = createBlock('<<><<', 32, 'j', '不一致', taskName);
  const block4 = createBlock('>><>>', 30, 'f', '不一致', taskName);

  const blocks = [block1, block2, block3, block4];
  return {
    timeline: jsPsych.randomization.repeat(blocks, repeatN),
  };
};

const flankerPractice = createFlanker('flanker_practice', 2);
const flankerMain = createFlanker('flanker_main', 5);

jsPsych.run([instruction, flankerPractice, flankerMain]);

map を使う(オブジェクトを引数に用いる)場合

mapメソッドを使う場合は,少し違った変更が必要です。全体的な変更の量は少なく済んでいる印象です。

map を使う場合

createTrialdataに指定しているオブジェクトのtaskプロパティの値を,引数のオブジェクトのtaskNameプロパティに変更します。

// setting は stim, fontSize, corrKey, cond, taskName をプロパティとするオブジェクト
const createTrial = (setting) => {
  const trial = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: `<p style="font-size: ${setting.fontSize}px">${setting.stim}</p>`,
    choices: ['f', 'j'],
    post_trial_gap: 500,
    data: {
      task: setting.taskName, // <--
      correctKey: setting.corrKey,
      condition: setting.cond,
    },
    on_finish: (data) => {
      data.isCorrect = Number(jsPsych.pluginAPI.compareKeys(data.response, data.correctKey));
    },
  };
  return trial;
};

オブジェクトを引数に用いない場合と違って,createBlockを編集する必要はありません。trialSettingsも編集する必要はありません。その代わり,mapメソッドを利用する際に,少しトリッキーなことをします。具体的には,mapの引数に指定している無名関数内で,流し込んでいるオブジェクトにプロパティを追加します。

/* fixationの定義
   createBlockの定義
   trialSettingsの定義 はそのまま */

const blocksPractice = trialSettings.map((s) => {
  // プロパティの追加
  const new_s = Object.assign({ taskName: 'flanker_practice' }, s);
  return createBlock(new_s);
});

const blocksMain = trialSettings.map((s) => {
  // プロパティの追加
  const new_s = Object.assign({ taskName: 'flanker_main' }, s);
  return createBlock(new_s);
});

const flankerPractice = {
  timeline: jsPsych.randomization.repeat(blocksPractice, 2),
};
const flankerMain = {
  timeline: jsPsych.randomization.repeat(blocksMain, 5),
};

jsPsych.run([instruction, flankerPractice, flankerMain]);

プロパティの追加のためにObject.assign()というものを使っています。これは,第 1 引数のオブジェクトに第 2 引数のオブジェクトのプロパティをコピーするという関数です。今回のチュートリアルの範囲であればs.taskName = ... でプロパティを追加しても構わないのですが,場合によっては意図しない挙動が生じることがあるので,そのリスクを避けるためにObject.assign()を使っています。よくわからない人はこういうプロパティの追加方法もあるんだなくらいに思っておいてください。詳しく知りたい方は,「javascript 浅いコピー 深いコピー」などのキーワードでググってみてください。

mapを用いない場合と同様に,フランカー課題作成用の関数を作成してみてもいいかもしれません。

const createFlanker = (settings, taskName, repeatN) => {
  const blocks = settings.map((s) => {
    const new_s = Object.assign({ taskName: taskName }, s);
    return createBlock(new_s);
  });
  return { timeline: jsPsych.randomization.repeat(blocks, repeatN) };
};

const flankerPractice = createFlanker(trialSettings, 'flanker_practice', 2);
const flankerMain = createFlanker(trialSettings, 'flanker_main', 5);

おわりに

本章では教示と練習の導入について説明しました。どちらも導入するだけであれば新しいことを用いる必要はないです。ですが,読みやすくしたり編集しやすくしたりするための工夫を加えました。

教示や練習を追加して実際の実験らしくなってきました。次は参加者情報を収集できるようにしましょう。

脚注
  1. 例えば,jsPsychImageKeyboardResponseを使えば,画像で作成した教示を提示できます。教示中に図を使用したい場合はこちらのほうが楽でしょう。もちろん,jsPsychHtmlKeyboardResponsestimulusに指定するのは html なので,自由なレイアウトが可能ですが html タグや css を勉強する必要があります。また,複数ページを自由に行き来できるような教示を作成したいのであればjsPsychInstructionsというタイプ(プラグインのファイル名はplugin-instructions.js)が便利です。 ↩︎

  2. 段落タグ<p></p>でもいいと思います。 ↩︎