12. 実験の流れを制御しよう―強化スケジュール

12.1. この章の実験の概要

ある行動に随伴して生じる出来事によって、その行動の発生頻度が高まることを強化と呼び、行動に随伴して生じる出来事を強化子と呼びます。強化子が毎回生じるか、一定間隔で生じるかといった強化子の出現スケジュールの違いによって行動の発生頻度の変化パターンが異なることが知られています。強化子の出現スケジュールのことを強化スケジュールと呼びます。 図12.1 は基本的な強化スケジュールを示しています。ポイントは「強化子が生じるタイミングが時間によって決まっているか、行動回数によって決まっているか」と、「時間や回数が一定であるか、変動するか」です。これらの組み合わせでFixed Interval (FI)、Fixed Ratio (FR)、Variable Interval (VI)、Variable Response (VR)の4種類のスケジュールができます。FI、VIにおいて強化子が得られるまでに必要な時間を「強化時間」、FR、VRにおいて強化子が得られるまでに必要な反応回数を「強化回数」と呼ぶことにします。FI、VIでは強化時間が経過したら自動的に強化子が得られるのではなく、強化時間が経過した後に行われた最初の行動によって得られる点に注意してください。

_images/schedules-of-reinforcement.png

図12.1 基本的な強化スケジュール。

この章では、Codeコンポーネントを活用してこれらの強化スケジュールをBuilderで実現します。 図12.2 に実験の手続きを示します。実験参加者の課題は、キーボードのスペースキーを押してできるだけ早く指定された点数の「得点」を獲得することです(例えば20点)。スクリーンには実験が開始してからの時刻と現在の得点が表示されていて、参加者は強化スケジュールで定められた条件を満たした状態でスペースキーを押すと、1点を獲得することができます。得点を獲得すると同時に2000Hzの音が0.2秒間鳴り、スクリーン上の時刻と得点の表示の背後に赤い長方形が1秒間表示されます。指定された得点に到達したら、その時点で実験は終了します。

今回の実験では単位としてnormを用いて、経過時刻および得点の表示用テキストの大きさ( [文字の高さ $] )は0.1、経過時刻の位置は[0, 0]、得点の位置は[0, -0.2]とします。また、得点獲得時の赤い長方形は大きさ[0.5, 0.5]、位置[0, -0.1]とします。

_images/reinforcement-procedure.png

図12.2 実験の手続き。

得点を獲得できる条件として、FI、FR、VI、VRの4種類のスケジュールを実行時に選択するようにします。いろいろな作成方法が考えられますが、ここでは「FIは強化時間が1種類しかないVI」と考えてみましょう。そうすると、FIはVI用の実験に条件が1種類しか定義されていない条件ファイルを与えることで実現できます。 図12.2 の1.のように実験開始時の実験情報ダイアログに条件ファイルを指定するconditionという項目と、各条件の繰り返し回数を指定するnRepsという項目を用意しておいて、「条件1種類の条件ファイルをconditionに指定して、nRepsを20に」すれば20点獲得で終了するFIです。「条件5種類の条件ファイルをconditionに指定して、nRepsを4に」すれば、5×4=20点獲得で終了するVIです。同様に、「FRは強化回数が1種類しかないVR」と考えれば、VR用の実験を用意すればFRに対応できます。結論として、VI用とVR用の実験を作成するだけで4種類のスケジュールに対応できます。

以上が実験の概要です。この章では、以上の実験を土台として、一定時間経過したら実験が終了したり、参加者の反応によって次の課題が変化したりといった高度な実験の流れの制御を学びたいと思います。

12.2. FI/VI実験の作成

それでは実験の作成に入りましょう。まずはFI/VI用の実験から作成します。以下の解説では、Builderで新規に実験を作成して以下の作業を行い、exp12vi.psyexpという名前で保存したものとします。

  • 実験設定ダイアログ

    • [実験情報ダイアログ] にnRepsとconditionという項目を追加する。

    • [単位] をnormにする。

  • trialルーチン

    • Textコンポーネントをふたつ配置して、それぞれ [名前] をscoreTrial、clockTrialにする。両方とも [終了] を空白にする。

      • scoreTrialの [位置 [x, y] $] を[0.0, -0.2]にする。 [文字列] に$'Score:'+str(score)と入力し、「繰り返し毎に更新」に設定する。

      • clockTrialの [文字列] に----と入力しておく。

    • Keyboardコンポーネントをひとつ配置して、以下のように設定する。

      • [名前] をkey_resp_Trialにする。

      • [終了] を空白にする。

      • [Routineを終了] のチェックを外す。

      • [検出するキー $] に'space'と入力する。

      • [記録] を「全てのキー」にする。

    • Codeコンポーネントをひとつ配置し、 [名前] をcodeTrialにする。

      • [実験開始時] にscore=0と入力する。

      • [Routine終了時] にscore+=1と入力する。

  • reinforcementルーチン(作成する)

    • フローのtrialルーチンの直後に挿入する。

    • polygonコンポーネントをひとつ配置して、以下のように設定する。

      • [形状] 長方形にする。

      • [名前] をbackgroundRectにする。

      • [終了] が「実行時間 (秒)」で1.0(=初期値)であることを確認する。

      • [塗りつぶしの色][枠線の色] をredにする。

      • [位置 [x, y] $] を[0.0, -0.1]にする。

    • Textコンポーネントをふたつ配置して、それぞれ [名前] をscoreRF、clockRFにする。両方とも [終了] が「実行時間 (秒)」で1.0(=初期値)であることを確認する。また、両方ともbackgroundRectより上に描画されるようにルーチンペイン上の順序に配慮する。

      • scoreRFの [位置 [x, y] $] を[0.0, -0.2]にする。 [文字列] に$'Score:'+str(score)と入力し、「繰り返し毎に更新」に設定する。

      • clockRFの [文字列] に----と入力しておく。

    • Soundコンポーネントをひとつ配置して、以下のように設定する。

      • [名前] をSoundRFにする。

      • [終了] を「実行時間 (秒)」の0.1にする。

      • [音] を2000に、 [ボリューム $] を1にする。

  • trialsループ(作成する)

    • trialルーチンとreinforcementルーチンを繰り返すように挿入する。

    • [繰り返し回数 $] にexpInfo['nReps']と入力する。

    • [繰り返し条件] に$expInfo['condition']と入力する。

  • exp12fi.xlsx(条件ファイル)

    • intervalというパラメータを設定し、値として10を入力する。パラメータ名の行を除いて1行1列の条件ファイルとなる。

  • exp12vi.xlsx(条件ファイル)

    • intervalというパラメータを設定し、値として6, 8, 10, 12, 14を入力する。パラメータ名の行を除いて5行1列の条件ファイルとなる。

以上の作業に加えて、trialルーチンのcodeTrialの [フレーム毎] に以下のコードを入力してください。

if len(key_resp_Trial.rt) > 0 and key_resp_Trial.rt[-1] >= interval:
    continueRoutine = False

解説済みのテクニックで作成できる部分は以上です。実はこの時点ですでにFI/VIの実験として使用できる状態まで完成していますので、一度実行してみましょう。実験開始からの時刻が----と表示される以外は、計画通り動作するはずです。条件ファイルにexp12fi.xlsxを指定すれば強化時間10秒のFIスケジュールとなりますし、条件ファイルにexp12vi.xlsxを指定すれば強化時間が6秒から14秒まで変化するVIスケジュールとなります。実験を実行すると作成される記録ファイルの例を 図12.3 に示します。内容を確認すると、key_resp_Trial.rtという列に、実験参加者がキーを押した時刻がルーチン開始時を0秒として記録されています。このデータを利用すると、実験参加者のキー押し反応の発生頻度がどのような時間経過をたどって変化したかを分析することが可能です。

_images/vi-results-xlsx.png

図12.3 trial-by-trial記録ファイルを確認すると、key_resp_Trial.rtにキーを押した時刻がすべて記録されています。このデータを用いて反応の頻度がどのように変化していくかを分析することができます。

ここまでで使用しているテクニックはすでに解説済みであることはすでに述べましたが、Codeコンポーネントの働きが今一つピンとこない人が居るかも知れませんので、念のため補足しておきましょう。復習のつもりで読んでください。まず、 [実験開始時] で得点を保持する変数scoreの値を0に初期化しています。そして、 [フレーム毎] では以下のif文を実行しています。

if len(key_resp_Trial.rt) > 0 and key_resp_Trial.rt[-1] >= interval:
    continueRoutine = False

このif文は、実験参加者の最新のキー押し反応が強化時間経過後に押されていればルーチンを終了するという動作を実現しています。詳しく見ていきましょう。まず、and演算子の左側のlen(key_resp_Trial.rt)>0という式ですが、key_resp_Trial.rtはキーが押された時刻を格納しているデータ属性でした(第6章)。len( )はシーケンス型の要素数を返す関数ですから(第8章)、一回でも反応があってキー押しが記録されていればand演算子の左辺の式はTrueになります。続いてand演算子の右辺を見ましょう。右辺は左辺の式がTrueでなければ評価されませんので(第8章)、必ず1個以上の要素がkey_resp_Trial.rtに含まれています。ですから、key_resp_Trial.rt[-1]が必ず存在しています。intervalは条件ファイルで定義されている変数ですから、右辺の式は記録済みの最後のキー押し反応の時刻がintervalの値以上である時にTrueとなります。左辺、右辺の両方の式がTrueであれば、continueRoutineをFalseにしてルーチンを終了します。このような方法を採ることによって、ルーチンで行われたすべてのキー押し反応の時刻を記録しつつ、強化時間を過ぎて反応が起こったら直ちにルーチンを終了することが可能となります。そして、ルーチンが終了したということは得点を得たという事ですから、 [Routine終了時] でscore += 1を実行するわけです。解説を読んでもよくわからなかった方は、第6章 から 第8章 にかけてしっかりと復習してください。

12.3. Global Clockを用いて実験開始からの経過時間を得よう

すでに前節でFI/VIスケジュールの実験が完成してしまって、この章は一体この後何を学ぶの?と疑問に思われているかたも多いかもしれません。この章の次の目標は、一定時間が経過したら実験を中断するという動作を実装することです。そのための布石として、まずは実験開始からの経過時間をスクリーン上に表示させてみましょう。

Builderには、実験を制御するためにいくつかの「時計」が用意されています。「時計」には実験全体の経過時間を計測するためのグローバルクロックと、各ルーチンが始まってからの経過時間を計測するためのルーチンクロックがあります。これらの時計の実体は、psychopy.core.Clockというクラスのインスタンスで、グローバルクロックはglobalClockという変数に格納されています。ルーチンクロックは"ルーチン名+Clock"という名前の変数に格納されています。ルーチン名がtrialなら、ルーチンクロックはtrialClockです。

psychopy.core.Clockは今まで紹介してきたPsychoPyのクラスと同様にいくつかのデータ属性とメソッドを持ちますが、クロックの働きを変更するメソッド等を実行するとBuilderで作成した実験の動作に悪い影響が出る恐れがありますので、ここでは現在の時間を得るメソッドgetTime( )だけを紹介しておきます。getTime( )メソッドは、現在の時間を小数で返します。時間の単位は秒です。globalClock.getTime( )とすれば、実験開始からの経過時間が得られますし、trialClock.getTime( )とするとtrialルーチン開始からの経過時間が得られます。なお、第5章 において、ルーチン開始からの経過時刻を保持するtという内部変数が出てきましたが、実はこのtはルーチン実行中に各フレームの処理の最初でt = trialCock.getTime( )という具合にルーチンクロックのgetTime( )メソッドの戻り値を格納することで得られています。

さて、実験開始からの経過時間を得る方法がわかりましたので、さっそくexp12vi.psyexpを改造して経過時間をスクリーン上に標示してみましょう。exp12vi.psyexpを開いて、trialルーチンのclockTrialのプロパティ設定ダイアログを開いて、 [文字列] に$globalClock.getTime()と入力して「フレーム毎に更新する」に設定しましょう。同様に、reinforcementルーチンのclockRFの [文字列] にも$globalClock.getTime()と入力して「フレーム毎に更新する」に設定しましょう。変更が終わったら保存して、実行してください。確かに先ほどまで----と表示されていた部分に経過時刻が表示されていますが、 図12.4 に示すように小数点以下の桁数がやたらと多くて非常に見難くなってしまいました。小数点以下はせいぜい1桁もあれば充分でしょう。次の節では、小数点1桁までの表示を実現する方法を解説します。

_images/draw-time-on-screen.png

図12.4 現在の経過時間をそのまま描画すると、小数点以下の桁数が多すぎて非常に見難くなります。

チェックリスト
  • 実験が開始してからの経過時間を得るコードを書くことができる。

12.4. 文字列のformat()メソッドを使って経過時間の表示を整えよう

前節で問題となった経過時間の表示ですが、これはPythonが小数を文字列として表示する際に標準の動作としてあのように小数点以下の桁を表示してしまうことが原因で生じます。Pythonにはプログラマが小数点以下何桁程度を表示したいと考えているかを知る方法がありませんので、Pythonの標準動作が気に入らない場合はプログラマが明示的に表示方法を指定してやる必要があります。ここで重要な役割を果たすのが文字列に対するformat()メソッドです。format()は数値や文字列等のデータを引数にとり、引数の値を文字列へ指定された書式で埋め込みます。以下に例を挙げます。

'平均反応時間:{} 正答率:{}'.format(mean_rt, n_correct)

format()は文字列オブジェクトのメソッドなので、このように文字列の後ろに.format()と続けて書くことができます。文字列の中に{}という部分が2か所あることに注目してください。format()は、引数として与えられた値を文字列中の{}の場所へ順番に埋め込んでいきます。いま、引数として与えている変数mean_rtの値が1218.7、n_correctの値が31ならば、このメソッドの実行結果は

'平均反応時間:1218.7 正答率:31'

という文字列になります。{0}や{1}のように{}の中に整数を書くと、何番目の引数と対応づけるかを指定できたり、{mean_rt}のように名前を書くと引数名で対応づけを指定できるなど、便利な指定法もありますが、Builderでの実験作成で使う程度なら、{}の中に何も書かずに自動的に引数順に割り当てる方法を覚えておけば十分でしょう。

format()は引数が整数であろうがリストオブジェクトであろうが、自動的に文字列に変換して埋め込んでくれます。しかし、自動変換の結果がこちらの望み通りとは限りません。実際、前節で問題となったglobalClock.getTime()の戻り値を埋め込んでみると、Builderに任せた時と同様に小数点以下の桁数が非常に多くなってしまいます。format文のいいところは、文字列への変換する際の書式を柔軟に指定できる点です。以下の例では、Builderの内部変数tを小数点以下3桁までで表すように指定しています(4桁目で四捨五入されます)。

'実験開始から{:.3f}秒'.format(t)

ちょっと難しいですが、{}内の : が「これ以降は変換方式の指定」ということを表す記号だと思ってください。 : の後ろの .3f というのが書式指定で、fは小数として出力することを指定してします。fの前の.3が小数点以下3桁を出力するという指定です。.の前に整数を書くと全体の桁数の指定となり、さらに+記号をつけると符号つきで出力されます。文章で説明してもなかなかわかりにくいと思いますので、具体例を 図12.5 に示します。

_images/flag-string.png

図12.5 数値の書式指定の例。変数pには3.14159264359という値が代入されているものとします。

書式指定には小数を指定するfの他にも、10進数の整数を指定するdや、16進数の整数を指定するxなどがあります。小数を10進数整数や16進整数で出力しようとするとエラーで実験が停止してしまうのでご注意ください。小数の値を整数で出力する場合は以下のようにint()を使って整数に変換するか、小数点以下の桁数として0を指定すればよいでしょう。

'実験開始から{:d}秒'.format(int(t))  # intで整数に変換
'実験開始から{:.0f}秒'.format(t)     # 小数点以下の桁数として0を指定

文字列を埋め込む場合は、特に変換の必要がないので{}だけでよいでしょう。以下の例では実験情報ダイアログの値(=文字列)を埋め込んでいます。

'あなたの参加者IDは{}です'.format(expInfo['participant'])

{ や } の記号を出力したい場合は {{ や }} といった具合に2つ並べて書きます。

format()メソッドにはまだまだ様々な使い方があるのですが、とりあえずこのくらい覚えておけば多くの実験に対処できるはずです。 今回の場合は実験開始からの経過時間を小数点1桁まで表示したいので、以下の式で目的を達成できます。

'%.1f'.format(globalClock.getTime())

exp12vi.psyexpを開いて、この式をtrialルーチンのclockTrialとreinforcementルーチンのclockRFの [文字列] に入力してください。もちろん先頭に$を付け忘れないようにしてください。変更したら保存して実行してみましょう。経過時間の表示が小数点1桁までになったはずです。

このformat()を使った文字列への値の埋め込みは、教示文や刺激文などを作る時にも便利です。今までは+演算子とstr( )関数を駆使して埋め込みを行っていましたが、埋め込む値が複数個ある場合はformat()を使った方が簡潔に記述できますので、ぜひマスターしてください。

さて、これで 図12.2 に示した実験手続のうち、FI/VIに関する実験が完成しました。ここからさらにステップアップする前に、exp12vi.psyexpを改造してFR/VRスケジュールの実験を作成しておきましょう。

チェックリスト
  • 変数の値を文字列に埋め込むことができる。

  • 浮動小数点数を文字列に埋め込む時に、小数点以下の桁数を指定することができる。

  • 数値を文字列に埋め込む際に、指定した桁数の右寄せで埋め込むことができる。

  • 数値を文字列に埋め込む際に、符号付きで埋め込むことができる。

12.5. FR/VR実験を作成しよう

この節では、exp12vi.psyexpをベースにして、FR/VRスケジュールの実験を作成します。具体的には、exp12vi.psyexpをexp12vr.psyexpという別名で保存した後、exp12vr.psyexpに以下の変更を加えます。

  • ratioというパラメータを定義する条件ファイルexp12fr.xlsxとexp12vr.xlsxを作成する。exp12fr.xlsxではratioは10のみ、exp12vr.xlsxではratioは2、6、10、14、18の5種類の値をとるようにする。

  • trialルーチンにおいて、ルーチン開始後にratioの回数だけスペースキーを押したらルーチンを終了する。

これまでに解説してきたことだけでできる変更なので、腕試しをしたい人は以下のヒントを見ずに作業してみてください。

さて、以下は「まだちょっと自力では難しいかな」という方向けのヒントです。条件ファイルの作成は、exp12fi.xlsxとexp12vi.xlsxを少し修正するだけです。exp12fi.xlsxのパラメータ名intervalをratioに書き換えればexp12fr.xlsxになります。exp12vi.xlsxのパラメータ名intervalをratioに書き換えて、値を2、6、10、14、18の5種類に修正すればexp12vr.xlsxになります。

続いてexp12vr.psyexpの変更です。trialルーチンの終了条件を変更するだけですので、変更点はtrialルーチンのCodeコンポーネント(codeTrial)です。trialルーチンが開始されてから実験参加者がスペースキーを押した時刻がkey_resp_Trial.rtにリストとして格納されているのですから、このリストの長さが変数ratio以上になればルーチンを終了すればよいのです。例えば以下のようなコードを書けばよいでしょう。

if len(key_resp_Trial.rt) >= ratio:
    continueRoutine = False

少なくとも筆者の経験上、Keyboardコンポーネントの [検出するキー $] にキー名がひとつしかない場合は一度に1回しかキー押しはカウントされないので、if文の条件式は >= ではなく == でも恐らく正常に動作すると思います。しかし、一度に2回以上キー押しがカウントされてしまうことが万一起こったら == では永遠にルーチンが終了しなくなってしまいますので、上記のコードでは念のために >= としています。

以上の変更でFR/VRスケジュールへの対応ができました。次は再びexp12vi.psyexpをベースにして、実験開始後一定時間が経過したら実験を終了するように改造しましょう。

12.6. 条件を満たしたら実験を強制終了するようにしよう

Builderは、基本的に条件ファイルで定められたすべてのパラメータを、ループで指定された回数だけ必ず繰り返すように設計されています。言い換えると、指定された試行数を実行するまで実験は終了しません。しかし、心理学の実験の中には、「直近の20試行の正答率が80%以上になれば終了」とか、「実験開始から20分経過したら終了」といった具合に、試行数以外の条件で終了する実験もあります。このような実験はBuilderとの相性が悪いのですが、Codeコンポーネントを使うと実現することができます。 なお、本節の方法はPavloviaによるオンライン実験ではうまく動作しませんので、別の方法を用いる必要があります。 詳しくは「 12.9.2:Pavloviaでのオンライン実験にも視野に入れたループの中断方法 」をご覧ください。

この節では、「試行数以外の条件で終了する実験」の例として、実験開始から指定された時間が経過したら実験を終了する実験をexp12vi.psyexpをベースに作成してみましょう。exp12vi.psyexpをexp12vi_time_limited_1.psyexpという別名で保存して、以後の作業はexp12vi_time_limited_1.psyexpに対しておこないます。

すでに前節までの作業で、グローバルクロックを用いて実験開始からの経過時間を得る方法は解説しました。先ほど「Codeコンポーネントを使えば実現できる」と言ったのですから、exp12vi_time_limited_1.psyexpのどこかに

if globalClock.getTime( ) > 制限時間を保持している変数:
    実験を終了させる

というコードを書けばいいということは想像がつくと思います。実行中の実験を終了させると言えば思い出されるのが、実験設定ダイアログで[Enable Escape]をチェックしていればESCキーを押すことで実験を強制終了できる機能です(第2章)。この機能は、Builderの内部でpsychopy.core.quit( )という関数を呼び出すことで実現されています。この関数は名前通り、すべてのPsychoPyのウィンドウを破棄して実験を終了します。Builder内部ではこの関数をcore.quit( )という名前で呼び出すようにimportが行われているので、

if globalClock.getTime( ) > limitTime:
    core.quit( )

と書けば、実験を終了できます。なお、制限時間を保持している変数をlimitTimeとしました。

残る問題は、このコードをどの時点で実行すればよいかということです。いろいろな候補が考えられますが、重要なポイントは「指定された時間が経過したら、課題遂行中であろうが直ちに終了してほしい」のか、「指定された時間が経過したら、現在遂行中の課題を通常通り終えてから終了してほしい」のかの違いです。前者であれば、Codeコンポーネントの [フレーム毎] に上記のコードを記入して、フレーム毎に経過時間をチェックするべきです。後者であれば [Routine終了時][Routine開始時] にコードを記入するべきです。今回の実験では、intervalに大きな値を指定した時には特に、指定時間が経過してからtrialルーチンが終了するまで時間がかかりますから、trialルーチンの途中で直ちに終了するべきでしょう。reinforcementルーチンはわずか1秒しかありませんし、強化子が途中で途切れるのは不適切だと思われますので、reinforcementルーチンは途中で中断しないことにしましょう。そうすると、コードを挿入すべきなのはtrialルーチンの [フレーム毎] ということになります。

trialルーチンにはすでにCodeコンポーネントを配置済みですので、プロパティ設定ダイアログを開いて [フレーム毎] に以下のコードを追加してください。追加する位置は入力済みのコードの前でも後でもどちらでも構いません。

if globalClock.getTime( ) > limitTime:
    core.quit( )

あとは変数limitTimeの準備です。ここでは実験情報ダイアログからlimitTimeの値を指定できるようにしましょう。実験設定ダイアログを開いて、[実験情報ダイアログ] にtime limit (s)という項目を追加してください。そして、trialルーチンのCodeコンポーネントのプロパティ設定ダイアログを再び開いて、 [実験開始時] に以下のコードを追加します。やはり入力済みのコードの前でも後でもどちらでも構いません。

limitTime = float(expInfo['time limit (s)'])

以上で変更は終了です。exp12vi_time_limited_1.psyexpを保存して実行してみてください。実験情報ダイアログにtime limit (s)という項目がありますので、実験の終了時間を秒で指定しましょう。とりあえず動作確認のために30秒くらいの値を指定するのがお勧めです。条件ファイルと繰り返し回数も忘れずに指定して実験を実行しましょう。30秒経過した時点で実験が自動的に終了するはずです。trial-by-trial記録ファイルを開いて、強化まで進んだ試行についてはキー押しの記録が残っていることを確認してください。当然、中断された試行については保存処理なしにいきなり実験が終了していますので、キー押し記録は残りません。

中断された試行のキー押し記録も残したい場合は、psychopy.core.quit( )以外の方法で実験を終了させる必要があります。また、制限時間が経過したら実験全体を終了させてしまうのではなく、現在の課題を終了させて次の課題を実行したいという場合にも、psychopy.core.quit( )は使えません。これらの場合にはどのような方法が有効でしょうか。勘のいい人は、psychopy.core.quit( )の代わりにcontinueRoutine = Falseとすればいいじゃないかと思われるかもしれません。確かに、trialルーチンとreinforcementルーチンにCodeコンポーネントを置いて、 [フレーム毎] で以下のコードを実行すれば、グローバルクロックの値がlimitTimeを超えた以後はtrialルーチンもreinforcementルーチンも一瞬で終了してしまいます。この方法ならルーチン終了の処理もすべて通常通り行われますから中断された試行のキー押し記録も残りますし、終了させたい課題にだけこのコードを挿入しておけば、「現在の課題を終了させて次の課題を実行する」ことも可能です。

if globalClock.getTime( ) > limitTime:
    continueRoutine = False

この方法を自力で思いついた方は、前章までの内容をとてもよく理解しておられると思います。ですが、残念なことに、この方法にはひとつ問題があります。どのような問題が生じるのか、実際に試してみましょう。

exp12vi_time_limited_1.psyexpを開いて、今度はexp12vi_time_limited_2.psyexpという名前で保存してください。以後、exp12vi_time_limited_2.psyexpに対して作業をするものとします。保存したらさっそくexp12vi_time_limited_2.psyexpを開いて、trialルーチンのcodeTrialの [フレーム毎] のcore.quit( )をcontinueRoutine = Falseに書き換えましょう。書き換え後の [フレーム毎] は以下のようになっているはずです(ふたつのif文の順番が逆でも構いません)。

if len(key_resp_Trial.rt) > 0 and key_resp_Trial.rt[-1] >= interval:
    continueRoutine = False
if globalClock.getTime( ) > limitTime:
    continueRoutine = False

続いてreinforcementルーチンを開いて、Codeコンポーネントを配置してください。 [名前] はcodeRFとしておきましょう。codeRFの [フレーム毎] に以下のコードを追加します。

if globalClock.getTime() > limitTime:
    continueRoutine = False

これで作業は完了ですが、trialルーチンとreinforcementルーチンの実行を中断した後に別の課題を行えることを確認するために、trialsループの後にルーチンをひとつ置きましょう。以下の作業を行ってください。

  • feedbackルーチン(作成する)

    • フローのtrialsループの後ろ(つまりフローの最後)に挿入する。

    • Textコンポーネントをひとつ配置し、以下のように設定する。

      • [名前] をtextFBにする。

      • [終了] を「実行時間 (秒)」に設定し、5と入力する。

      • [文字列] を$feedbackMessageにし、「繰り返し毎に更新」に設定する。

    • Codeコンポーネントをひとつ配置し、以下のように設定する。

      • [名前] をcodeFBにする。

さらに、Codeコンポーネントの [Routine開始時] に以下のコードを入力してください。

if globalClock.getTime() > limitTime:
    feedbackMessage = 'Time out'
else:
    feedbackMessage = 'Completed'

作業が終わったら、exp12vi_time_limited_2.psyexpを保存して、実行してください。条件ファイルにexp12fi.xlsxを指定して、nRepsを100、time limit (s)を5にしてみましょう。実行したら、スペースキーを押さずに制限時間の5秒が過ぎるのを待ってください。いかがでしょうか。10秒経過した後、PCからピピピピーッと音がなってからスクリーンにTime outと表示されたのではないかと思います。何が起きたのかわかりましたか?

グローバルクロックがlimitTimeを超えたらcontinueRoutine = Falseを実行するというコードを挿入することによって、確かに「trialルーチンとreinforcementルーチンを(nRepsで指定された)100回繰り返さずに」feedbackルーチンへ進むことができました。しかし、いくら一瞬で終了するとはいえtrialルーチンとreinforcementルーチンは実行されているので、reinforcementルーチンに含まれていたSoundコンポーネントの音が鳴り響いたのです。これでは「trialルーチンとreinforcementルーチンの実行を中断した」とは言えません。これらのルーチンを一瞬たりとも実行させずにfeedbackルーチンへ進まなければなりません。

この難題を解決するためには、Builderの仕組みにさらに踏み込まなければなりません。Builderはルーチンを実行している時、実際にはどのようなコードを実行しているのでしょうか。そして、continueRoutineという変数の値をFalseにするとルーチンが終了するというのはいったいどういう仕組みによるものなのでしょうか。これらの点を理解していただくためには、if文、for文と並ぶPythonの重要構文であるwhile文を覚えていただく必要があります。

while文とは、for文と同様に繰り返し処理を行うための文です。for文では、与えられたシーケンス型データに含まれる要素を先頭から取り出して順番に処理をしていきました。あらかじめ繰り返し処理の対象が決まっている時にはfor文はとても便利なのですが、心理学実験でよくある「キーが押されるまでスクリーンに刺激を描画し続ける」といった処理では、いつまで描画を繰り返せばいいのか実際に実行してみるまでわかりません。このような「ある条件が満たされるまで同じ処理を繰り返す」という処理を行いたいときに用いるのがwhile文です。

図12.6 にwhile文の概要を示します。while文は条件式と組み合わせて使用し、条件式がTrueである間、処理を繰り返します。繰り返す範囲はfor文やif文と同様に字下げで示します。注意しないといけないのは、while文と組み合わせる条件式に誤って絶対にFalseにならない式を書いてしまうと、Pythonインタプリタ自体が正常に動作している限り永遠に処理が終了しないという点です。ただし、for文と同様にbreakとcontinueを使うことができますので、繰り返し中にbreakを実行すれば条件式に関わらず繰り返しを中断することはできます。なお、while文にはif文のようにelseを伴わせることができますが、その時の動作については「 12.9.1:while文に伴うelse 」を参照してください。

_images/while-statement.png

図12.6 while文。条件式がTrueである限り処理を繰り返します。for文と同様にbreakやcontinueを使うことができます。

図12.7 は、Builderが実験のフローをどのようにPythonのコードへ変換するかを示しています。フローにおけるループはfor文に変換されます。条件ファイルや [Loopの種類][繰り返し回数 $] などに基づいて各試行で用いられるパラメータ一覧のリストが作成され、for文へ渡されます。このfor文を実行することで、パラメータを変更しながら指定された回数の繰り返しが実行されます。一方、各ルーチンはwhile文に変換されます。 図12.7 はCodeコンポーネントの解説も含んでいて複雑になっていますので、以下にwhile文の部分を抜粋して示します。

continueRoutine = True
while continueRoutine and ルーチン継続に関する条件:
    各コンポーネントのフレーム毎の処理

ルーチンが開始される直前に、continueRoutineという変数にTrueが代入されます。そしてwhile文によってルーチン内の各コンポーネントのフレーム毎の処理を繰り返します。while文の条件式には、continueRoutineとその他の条件の論理積(and)が渡されます。その他の条件というのは、例えばルーチンが5.0秒で終了するように各コンポーネントの [終了] が設定されていた場合には「ルーチン開始からまだ5.0秒経過していない」という条件が入ります。continueRoutineがFalseの場合、他にどのような条件が指定されていてもwhile文の条件式はFalseになるので、continueRoutine = Falseを実行するとwhile文の繰り返しが終了します。これが今まで呪文のようにcontinueRoutine = Falseと書いていた時に実際に生じていたことなのです。

_images/flow-to-code.png

図12.7 BuilderによるフローからPythonコードへの変換。フローにおけるループはfor文に、ルーチンのフレーム毎の処理はwhile文に置き換えられます。

以上を踏まえて改めて 図12.7 をご覧ください。 図12.7 では、Codeコンポーネントで [Routine開始時][フレーム毎][Routine終了時] にコードを記入した時にどの位置にコードが挿入されるかを示しています。これをご覧になったら、continueRoutine = Falseを [フレーム毎] に書かなければwhile文を終了させることができないのがおわかりいただけるかと思います。

さて、なぜこんなBuilderの裏側の世界まで足を踏み入れているのかと言えば、何とかしてルーチンを一瞬たりとも実行させずに中断させたいのでした。continueRoutineにはwhile文が実行される直前にTrueが代入されます。ですからCodeコンポーネントの [Routine開始時] にcontinueRoutine = Falseと書いても実行直前にTrueに書き換えられてしまい、ルーチン(=while文)の実行を止めることができません。一度でもwhile文が実行されるとSoundコンポーネントが実行されてしまうので、いくら [フレーム毎] にcontinueRoutine = Falseと書いても音が鳴ってしまいます。これがexp12vi_time_limited_2.psyexpで生じていた問題です。では、どうすればいいでしょうか? ここで思い出していただきたいのがbreakです。breakが実行されると、break以降に処理があろうと直ちに繰り返しが中断されるのでした。ということは、ルーチンが実行された後、どのコンポーネントの処理よりも先にbreakを実行すれば、実質的にルーチンを実行しなかったのと同じ結果になるはずです。さっそく試してみましょう。

exp12vi_time_limited_2.psyexpを、exp12vi_time_limited_3.psyexpという別名で保存してください。以下の作業はexp12vi_time_limited_3.psyexpに対して行うものとします。別名で保存したら、trialルーチンを開いてcodeTrialの [フレーム毎] を確認してください。globalClock.getTime( ) > limitTimeだったらcontinueRoutine = Falseするというコードが入力されているはずですが、このcontinueRoutine = Falseをbreakに書き換えてください。書き換えた後の [フレーム毎] は以下のようになっているはずです(ふたつのif文の順番が逆でも構いません)。

if len(key_resp_Trial.rt) > 0 and key_resp_Trial.rt[-1] >= interval:
    continueRoutine = False
if globalClock.getTime( ) > limitTime:
    break

同様に、reinforcementルーチンのcodeRFの [フレーム毎] も以下のようにbreakに書き換えてください。

if globalClock.getTime() > limitTime:
    break

そして、次の作業が重要です。これらの修正したコードが、同一ルーチン内の他のコンポーネントの処理が行われる前に実行されるようにする必要があります。そのためには、codeTrialやcodeRFがルーチンペインの一番上に並んでいる必要があります( 図12.8 )。trialルーチンとreinforcementルーチンを開いて、codeTrialとcodeRFがそれぞれ一番上になるように並び替えましょう。

_images/move-code-components-to-top.png

図12.8 trialルーチンとreinforcementルーチンのCodeコンポーネントを一番上へ配置してください。

作業が終わったら、exp12vi_time_limited_3.psyexpを保存して実行しましょう。先ほどと同様に、条件ファイルにexp12fi.xlsxを指定して、nRepsを100にしてください。時間内に得点が得られた試行のデータも欲しいので、time limit (s)は30にしてください。実行したら、スペースキーを押して得点を獲得しながら30秒経過するのを待ちましょう。30秒経過したら実験が中断されてスクリーンにTime outと表示されますが、今度はexp12vi_time_limited_2.psyexpの時のように音が鳴らなかったはずです。

psychopy.core.quit( )で終了した場合と異なり、作成されたtrial-by-trial記録ファイルには、 図12.9 のように中断された試行もすべて記録されています。breakで中断された試行ではキー押しが記録されませんので、key_resp_Trial.keysとkey_resp_Trial.rtはいずれもNoneと出力されています。かなり苦戦しましたが、これでなんとか目的を達成することができました。

_images/break-routine-xlsx-output.png

図12.9 psychopy.core.quit( )で中断した場合と異なり、ルーチンをbreakで中断した場合は中断された試行もすべて記録されています。

なお、皆さんの中には、exp12vi_time_limited_3.psyexpを実行した時に、30秒経過してからTime outと表示されるまでに少し「30.0」という表示のままPCが停止したかのような動作をしたことが気になった方がいるかも知れません。これは、Builderが残りの全て実際に開始して、即breakして、結果をtrial-by-trial記録ファイルに書きこむという作業を繰り返しているために生じる現象です。breakされた試行もすべて記録に残すべきか否かはどちらが良いか一概には言えませんが、この待ち時間が問題になる実験ももしかしたらあるかも知れません。この節のテクニックを応用することで待ち時間をなくすことも可能ですが、これは練習問題としておきましょう。

チェックリスト
  • 条件式がTrueである間処理を繰り返すPythonのコードを書くことができる。

  • Codeコンポーネントを用いて、ある条件を満たしたときに実行が中断される実験を作ることができる。

  • Codeコンポーネントを用いて、ある条件を満たしたときに実行がスキップされるルーチンを作ることができる。

12.7. 繰り返し回数を変更して並立スケジュールを実現しよう

Builderに対する不満として非常によく聞くのが、フローを分岐させることができないというものです。例えば、スクリーン上に複数の選択肢が提示されて、実験参加者がひとつを選択すると、それに応じた課題が行われるといった実験を現在のBuilderは想定していません。しかし、Codeコンポーネントを利用するとフロー上では分岐できなくても動作上は分岐する実験を作成することが可能です。

具体的な例題が無いと話がしづらいので、 図12.10 のような並列スケジュールの実験を考えてみましょう。exp12vi.psyexpの前にスクリーンをひとつ挿入して、そこで「カーソルキーの左右を押して課題を選択してください」と教示します。参加者は適切なタイミングでスペースキーを押すと得点が得られること、できるだけ速く50点を得るように努力することだけが告げられていて、左右の課題がそれぞれどのようなスケジュールであるのか知らされていません。左右いずれかのキーを押すと、押したキーに応じたスケジュールで課題が始まります。

_images/concurrent-schedule-procedure.png

図12.10 並立スケジュール。1.のスクリーンで実験参加者がカーソルキーの左右どちらを押すかによって次のブロックの課題が変化します。

並列スケジュールの実験の作成において、

  • 二つの課題がどちらもVI

  • 左キーの場合は強化時間が2秒、11秒、20秒、29秒、38秒の5種類

  • 右キーの場合は強化時間が12秒、16秒、20秒、24秒、28秒の5種類

といった具合に、左右の課題の違いがパラメータの違いだけであれば、実現はあまり難しくありません。ルーチンとループは共通のものを利用して、条件ファイルを押されたキーに応じて変更すれば実現できます。これは練習問題にしておきます。厄介なのは

  • 左キーの場合はVIで強化時間が2秒、6秒、10秒、14秒、18秒の5種類

  • 右キーの場合はVRで強化回数が12回、16回、20回、24回、28回の5種類

といった具合に、押されたキーによって実験内容が異なる場合です。前節のbreakを駆使する方法でも実現可能ですが、この節ではループの実行そのものを省略する方法を紹介します。

この節の実験では、前節までのような時間制限がないので、exp12vi.psyexpをベースに改造するのがよいでしょう。exp12vi.psyexpを開いて、exp12concurrent.psyexpという別名で保存してください。保存したら、exp12concurrent.psyexpに対して以下の作業を行ってください。かなり複雑なフローになりますので、完成後のフローを 図12.11 に示しておきます。

_images/concurrent-schedule-flow.png

図12.11 exp12concurrent.psyexpのフロー。

  • 実験設定ダイアログ

    • [実験情報ダイアログ] からnRepsとconditionを削除する。

  • trialsループ(vi_trialsループに名称変更)

    • [名前] をvi_trialsにする。

    • [繰り返し回数 $] をnRepsVIにする。

    • [繰り返し条件] を$conditionにする。

  • trialVRルーチン(作成する)

    • vi_trialsループの直後に挿入する。

    • Textコンポーネントをふたつ配置して、それぞれ [名前] をscoreTrialVR、clockTrialVRにする。両方とも [終了] を空白にする。

      • scoreTrialVRの [位置 [x, y] $] を[0.0, -0.2]にする。 [文字列] に$'Score:'+str(score)と入力し、「繰り返し毎に更新」に設定する。

      • clockTrialVRの [文字列] に$'%.1f' % globalClock.getTime( )と入力し、「フレーム毎に更新する」に設定する。

    • Keyboardコンポーネントをひとつ配置して、以下のように設定する。

      • [名前] をkey_resp_TrialVRにする。

      • [終了] を空白にする。

      • [Routineを終了] のチェックを外す。

      • [検出するキー $] に'space'と入力する。

      • [記録] を「全てのキー」にする。

    • Codeコンポーネントをひとつ配置し、 [名前] をcodeTrialVRにする。

      • [Routine終了時] にscore+=1と入力する。

  • reinforcementルーチン

    • trialVRルーチンの直後に挿入する。フロー内に2個のreinforcementルーチンが配置されることになる。

  • vr_trialsループ(作成する)

    • trialVRルーチンと、その後ろにあるreinforcementルーチンを繰り返すように挿入する。

    • [繰り返し回数 $] をnRepsVRに、 [繰り返し条件] を$conditionにする。

  • choiceルーチン(作成する)

    • vi_trialsループの前に挿入する。

    • Textコンポーネントを3つ配置して、 [名前] をそれぞれtextInstruction、scoreChoice、clockChoiceとする。すべて [終了] を空白にする。以下のように設定する。

      • textInstructionの [位置 [x, y] $] を[0.0, 0.2]にする。 [文字列] に「カーソルキーの左右を押して課題を選択してください」と入力する。

      • scoreChoiceの [位置 [x, y] $] を[0.0, -0.2]にする。 [文字列] に$'Score:'+str(score)と入力し、「繰り返し毎に更新」に設定する。

      • clockChoiceの [文字列] に$'%.1f' % globalClock.getTime( )と入力し、「フレーム毎に更新する」に設定する。

    • Keyboardコンポーネントをひとつ配置して、以下のように設定する。

      • [名前] をkey_resp_Choiceにする。

      • [終了] を空白にする。

      • [検出するキー $] を'left','right'にする。

      • [記録] を「なし」にする。

    • Codeコンポーネントを配置し、 [名前] をcodeChoiceにしておく。現時点ではコードは入力しない。

  • blocksループ(作成する)

    • フロー全体を繰り返すように挿入する。すなわち、choiceルーチンの前からvr_trialsループの終わりまでを繰り替えす。

    • [名前] をblocksにする。

    • [繰り返し回数 $] を10にする。

  • exp12concurrentVI.xlsx(条件ファイル)

    • exp12vi.xlsxをコピーして、intervalの値を2, 6, 10, 14, 18にする。

  • exp12concurrentVR.xlsx(条件ファイル)

    • exp12vr.xlsxをコピーして、ratioの値を12, 16, 20, 24, 28にする。

さらに、trialVRルーチンのcodeTrialVRの [フレーム毎] に以下のコードを入力してください。

if len(key_resp_TrialVR.rt) >= ratio:

continueRoutine = False

準備はよろしいでしょうか。それでは最後の仕上げ、ChoiceルーチンのCodeコンポーネント(codeChoice)にコードを入力しましょう。codeChoiceでは、以下の処理を行います。

  • key_resp_Choiceで押されたキーが'left'であれば、以下の変数を設定する。

    • nRepsVI = 1

    • nRepsVR = 0

    • condition = 'exp12concurrentVI.xlsx'

  • key_resp_Choiceで押されたキーが'right'であれば、以下の変数を設定する。

    • nRepsVI = 0

    • nRepsVR = 1

    • condition = 'exp12concurrentVR.xlsx'

もうすでに何をしようとしているかお分かり頂けたと思います。VIスケジュールを行うループvi_trialsの繰り返し回数は、nRespVIという変数で決まります。nRepsVIに0を代入すれば、繰り返し回数が0回となって一度もVIスケジュールの試行が行われないというわけです。同様に、nRepsVRに0を代入すれば、VRスケジュールの試行は行われません。押されたキーに従って変数に異なる値を設定するのはすでに 第6章 で、 [記録] が「なし」の時に押されたキーを取得する方法は 第7章 で学びましたから、新しい作業は何もありません。以下のコードをcodeChoiceの [Routine終了時] に入力してください。

if 'left' in theseKeys:
    nRepsVI = 1
    nRepsVR = 0
    condition = 'exp12concurrentVI.xlsx'
elif 'right' in theseKeys:
    nRepsVI = 0
    nRepsVR = 1
    condition = 'exp12concurrentVR.xlsx'

入力したら、exp12concurrent.psyexpを保存して実行してみましょう。選択画面が出てきたら教示に従ってカーソルキーの左右いずれかを押し、左を押せばVIスケジュール、右を押せばVRスケジュールの課題が5得点分ずつ実行されることを確認してください。実行後に作成されるtrial-by-trial記録ファイルの例を 図12.12 に示します。繰り返し回数を0に設定することによって飛ばされたループに該当するセルは、空白になります。どのような順番で課題を選択したか、それぞれの課題においてキーは何回押したか、いつ押したかといった情報を全てtrial-by-trial記録ファイルから読み取ることができます。

さて、ずいぶん解説が長くなってしまいましたので、この章もそろそろおしまいにしたいと思います。この章で紹介したテクニックを用いると、現状のBuilderでもフローが分岐する実験を作成することができます。次章はいよいよ最終章です。次章では無作為化に関する話題を取り上げます。

_images/concurrent-xlsx-output.png

図12.12 exp12concurrent.psyexpを実行した時に作成されるtrial-by-trial記録ファイルの例。繰り返し回数を0に設定することによって飛ばされたループに該当するパラメータやキー押し記録のセルは、空白になります。

チェックリスト
  • ある条件に当てはまる時にループを実行しない実験を作成することができる。

  • ループを実行しない実験を作成した時のtrial-by-trial記録ファイルから、ループを実行しなかった時の記録を判別できる。

12.8. 練習問題:さまざまなフロー制御をマスターしよう

この章の練習問題は以下の3問です。第2問と第3問は本文中で「練習問題」としたものです。

  • 問1:exp12vi.psyexpをベースとして、フローの先頭に教示画面を挿入してください。そして、trialルーチンやreinforcementルーチンで表示される経過時間を実験開始からの時間(globalClockが示す時間)ではなく、教示画面を終了して実際に強化スケジュールが開始してからの時間を表示してください。

    • 教示文は各自に任せます。

    • 教示画面でスペースキーを押したら実験が始まるようにしてください。

  • 問2:exp12vi.psyexpをベースとして、 図12.10 の並列スケジュールの実験を以下の条件で作成してください。

    • 左キーが押された場合は強化時間が2秒、11秒、20秒、29秒、38秒の5種類のVIスケジュール

    • 右キーが押された場合は強化時間が12秒、16秒、20秒、24秒、28秒の5種類のVIスケジュール

    • 左キーと右キーのスケジュールの実行には同一のループおよびルーチンを使用し、条件ファイルの切り替えによってスケジュールの違いに対応する。

  • 問3:exp12vi_time_limited_3.psyexpをベースとして、time limit (s)で指定された制限時間を過ぎてしまったときに、スキップされた試行のデータがtrial-by-trial記録ファイルに出力されないようにする。ただし、制限時間によって中断された試行の、中断直前までのキー押しはtrial-by-trial記録ファイルに出力されているようにすること。

12.9. この章のトピックス

12.9.1. while文に伴うelse

Pythonではwhile文にelseを伴わせることができます。if文に伴うelseは他の多くのプログラミング言語で使用することができますので、他言語でのプログラミング経験がある人はすぐにわかったと思うのですが、while文に伴うelseはC言語などには無いので戸惑われるかたもいるかもしれません。

if文のelseは、if文の条件式がFalseだった時に行う処理を指定するためのものでした。while文のelseも同様に、while文の条件式がFalseだった時に行う処理を指定します。以下のwhile文を考えてみましょう。

x = y = 0
while x<10:
    x += 1
else:
    y = x

最初にxとyに0が代入され、while文でx+=1を繰り返します。x=10になった時点でwhile文の条件式がFalseになるので、elseで記述されているy = xが実行されます。結果として、xとyの値は10となります。続いて以下の式を考えてみましょう。while文で繰り返す処理の中に「xの値が5であればbreakする」という処理を追加しています。

x = y = 0
while x<10:
    x += 1
    if x == 5:
        break
else:
    y = x

この場合、while文を繰り返しているうちにxの値が5になってbreakが実行されます。while文の条件式であるx<10がFalseになったわけではないので、elseの処理は実行されません。結果として、xの値は5になり、yの値は0のままになります。

12.9.2. Pavloviaでのオンライン実験にも視野に入れたループの中断方法

5.6:複雑な式にはCodeコンポーネントを使ってみよう 」で触れたように、現在のBuilderはオンライン実験サービスPavloviaで動作する実験プログラムを出力することができるようになりました。このプログラムはJavaScriptという言語とPsychoJSというライブラリが用いられており、Pythonで書かれたプログラムとは大きく異なります。そのため、本章で紹介したループの中断方法はPavloviaの実験プログラムでは期待通りに動作しません。そのため本章の内容を全面的に書き直すことも検討したのですが、

  • ローカルPCで実行するPythonのプログラムをBuilderで作成する場合には本章の内容は今なお有効であること

  • BuilderがループをPythonのプログラムとして実現する仕組みを理解して、将来的にBuilderに頼らずに実験コードが書けるようになること

を考慮してそのまま残すこととしました。代わりに、このトピックでPavloviaでループを中断する方法を解説しておきたいと思います。

仰々しい前置きとなりましたが、方法は非常にシンプルです。現在のPsychoPyのTrialHandlerオブジェクト(「 7.4:Codeコンポーネント使って独自の変数の値を記録ファイルに出力しよう 」)には finished というデータ属性が用意されています。このfinishedにTrueを代入すると、ループ内のルーチンの実行が終わったときにまだ繰り返しが残っていてもTrialHandlerはループを終了します。つまり、何らかの条件が満たされてtrialsという名前のループを中断したいときは

trials.finished = True

という文を実行すればよいのです。現在のルーチンの中断を同時に行いたい場合は

trials.finished = True
continueRoutine = False

とすれば目的を達成できます。この方法が素晴らしいのは、Pavlovia用のJavaScriptでも(文法上の違いを除いて)まったく同じコードで動作するということです。具体的には以下のように書きます。

trials.finished = true;
continueRoutine = false;

Builderでオンライン実験の作成を考えている人は覚えておいてください。