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

9.1. この章の実験の概要

突然ですが、野鳥観察を趣味にしている人が居るとします。この人が、とある湖畔に明け方に訪れた時にずっと探していた野鳥を見つけました。その後も時々同じ湖畔で明け方にその野鳥を見かけることがあって、今では機会があればいそいそと朝早くに家を出てその湖畔へ向かうようになりました。―いきなり何のことかと思われるかも知れませんが、この例は「明け方に湖畔に行く」という行動をすると、「目当ての野鳥が観察できる」という結果が時々生じて、その結果によって「明け方に湖畔に行く」という行動が生じる頻度が高まったと言うことが出来ます。このようにある行動に随伴して生じる出来事によって、その行動の発生頻度が高まることを強化と呼び、行動に随伴して生じる出来事を強化子と呼びます。この例では「湖畔に行く」という行動に対して「目当ての野鳥が観察できる」という強化子が毎回ではなく「時々」生じているのですが、毎回生じるのか、一定間隔で生じるのか、といった強化子の出現スケジュールの違いによって行動の発生頻度の変化パターンが異なることが知られています。強化子の出現スケジュールのことを強化スケジュールと呼びます。

図9.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

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

この章では、Codeコンポーネントを活用してこれらの強化スケジュールをBuilderで実現します。 図9.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

図9.2 実験の手続き。

得点を獲得できる条件として、FI、FR、VI、VRの4種類のスケジュールを実行時に選択するようにします。いろいろな作成方法が考えられますが、ここでは「FIは強化時間が1種類しかないVI」と考えてみましょう。そうすると、FIはVI用の実験に条件が1種類しか定義されていない条件ファイルを与えることで実現できます。 図9.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種類のスケジュールに対応できます。

以上が実験の概要です。この章では、以上の実験を土台として、一定時間経過したら実験が終了したり、参加者の反応によって次の課題が変化したりといった高度な実験の流れの制御を学びたいと思います。実験の作成に入る前に、この章で初登場のSoundコンポーネントについて解説しておかなければいけません。ついでですので、Movieコンポーネントの使い方についても簡単に説明しておきます。

9.2. SoundコンポーネントとMovieコンポーネント

図9.3 にSoundコンポーネントとMovieコンポーネントのアイコン及びプロパティ設定ダイアログを示します。Soundコンポーネントのプロパティの内、これまでに紹介済みのコンポーネントと共通ではないのは ボリューム $ です。 には無圧縮WAV形式の音声ファイルを指定できるほか、AやBfl (B♭)、Csh (C#)のようにキーコードで音を指定することも出来ます。また、2000という具合に正の数値を入力すると、その周波数の音がなります。実行環境によってはWAV以外にOGGなどの音声ファイルを再生できますが、無圧縮WAVを用いるのが確実です。 ボリューム $ は0.0から1.0の範囲でボリュームを指定します。 開始 および 終了 で定められた時間が音声ファイルの時間より短い場合は、音声ファイルの再生が途中で終了します。

Movieコンポーネントのプロパティの内、これまでに紹介済みのコンポーネントと共通ではないのは バックエンド動画ファイル です。 動画ファイル には再生する動画ファイル名を指定します。 サイズ [w, h] $ を動画ファイルと異なる値に設定することによって、動画を縦横に拡大縮小して再生することが出来ます。動画ファイルの元の解像度のまま再生する場合は サイズ [w, h] $ は空白でも構いません。Soundコンポーネントと同様に、 開始 および 終了 で定められた時間が動画ファイルの時間より短い場合は、動画ファイルの再生が途中で終了します。 Routineを終了 をチェックしておくと、動画再生が終了すると同時にルーチンが終了します。 バックエンド については、とりあえず「avbin」のままにしておくのが良いと思います。詳しくは Movieコンポーネントのバックエンド をご覧ください。

_images/properties-of-sound-and-movie.png

図9.3 SoundコンポーネントとMovieコンポーネントのアイコン及びプロパティ設定ダイアログ

動画ファイルのフォーマットはお勧めできる定番がないのですが、筆者はMPEG4形式をよく使用しています。いずれのフォーマットを用いるにせよ、動画再生はかなりPCへの負担が大きい処理なので、解像度をむやみに上げるべきではありません。例えば動画撮影に使用したカメラの記録フォーマットが1920×1080のFull HD画質で、実験に使用する時の表示サイズが480×270であるならば、実験に使用する前に動画編集ソフトを用いて480×270に縮小すべきです。先ほど サイズ [w, h] $ を指定することで動画の拡大縮小が出来ると書きましたが、 サイズ [w, h] $ を用いる方法はPCに十分な処理能力がある場合のみ利用してください。PCの処理能力は皆さんの実行環境に寄りますので一概には言えませんが、再生させてみて動画がカクカクする場合は確実に処理能力が不足しています。

音声ファイルや動画ファイルを用いた実験を行う時にしばしば困るのが、「音声ファイルが再生されている間文字列が表示され、再生終了と共に消える」といった処理や、「動画ファイルの再生が終わったら文字列が表示されるようにしたいが、ルーチンは継続したいので Routineを終了 は使いたくない」という場合です。使用する音声ファイルや動画ファイルの再生時間がすべて同じであれば 開始終了 の値を再生時間に合わせて設定すればいいのですが、ファイルによって再生時間が異なる場合は工夫が必要です。具体的には、SoundコンポーネントやMovieコンポーネントに対応するPsychoPyクラスが持っているstatusというデータ属性を利用します。音声または動画ファイルが再生されていなければ、statusはNOT_STARTEDという値が設定されています。再生中であればPLAYING (またはSTARTED)、再生が終了していればSTOPPED (またはFINISHED)です。これを利用すると、Codeコンポーネントを用いて以下のようにstim_Soundの再生終了時にルーチンを強制終了させることが出来ます。

if stim_Sound.status == FINISHED:
    continueRoutine = False

ルーチン全体を終了させるのではなく、特定のコンポーネントの描画を開始したり終了したりしたい場合は、第2章で「とりあえず無視」した機能を利用します。各コンポーネントの 開始 および 終了 のプルダウンメニューには 「条件式」 という選択項目があります。これを選択すると、コンポーネントの開始や終了のタイミングを条件式によって指定することが出来ます。第2章の時点では条件式について説明していなかったので無視しましたが、この章まで進んだ皆さんならもう説明は不要でしょう。 図9.4 のように入力することによって、音声ファイルや動画ファイルの再生開始、終了に合わせてコンポーネントの開始、終了を設定できます。もちろん 「条件式」 に入力するのは条件式であれば何でも構いませんので、Codeコンポーネントを利用すれば多彩な制御が可能となります。ぜひ覚えておいてください。

_images/start-stop-by-condition.png

図9.4 開始 および 終了 に 「条件式」 を指定すると、条件式によってコンポーネントの開始、終了を制御できます

なお、非常にファイルサイズの大きい音声ファイルや動画ファイルを再生しようとすると、読み込みが間に合わずに正常に再生されない場合があります。このような場合に便利なのが第2章からずっと実験の作成手順でtrialルーチンから消去され続けてきたStaticコンポーネントです。StaticコンポーネントのアイコンはコンポーネントペインのCustomのグループにあります。Staticコンポーネントを配置すると、ルーチン上に赤い領域が出現します( 図9.5 )。それと同時に、ルーチンに配置されている各コンポーネントのプロパティ設定ダイアログの、更新方法のメニューに「trialのISIの間に更新」という項目が追加されます。「trialのISI」の部分はStaticコンポーネントを配置したルーチン名と、Staticコンポーネントの 名前 によって決まります。instructionというルーチンにprereadという名前でStaticコンポーネントを配置したのであれば、「instructionのprereadの間に更新」という項目が追加されます。

_images/static-component.png

図9.5 Staticコンポーネントを利用して、ファイルサイズの大きい刺激をPCの負荷が小さい時間帯に読み込むことが出来ます。

図9.5 のようにMovieコンポーネントの 動画ファイル で「…の間に更新」を選択すると、Staticコンポーネントで指定されている時間内に動画ファイルの読み込みが行われます。「Staticコンポーネント」の名前の通り、この期間には刺激を描画したりキー押しを検出したりするべきではありません。やって出来ない事はないのですが、精度が保障されません。 図9.5 のfixpointというTextコンポーネントのように、Staticコンポーネントの期間中に静止した刺激を描画しておくことには何の問題もありません。ファイルの読み込みタイミングとして他のルーチンに配置したStaticコンポーネントを選択することも可能なので、実験期間中で都合がよいタイミングにファイルを読み込んでくことが可能です。Staticコンポーネントはファイルサイズが大きくなりがちな動画ファイルの読み込みに特に力を発揮しますが、音声ファイルを読み込む時や、画像ファイルを十数枚一気に読み込む必要がある時などにも役に立ちます。

以上でSoundコンポーネントとMovieコンポーネントの解説は終了ですが、最後にひとつ注意点を挙げておきます。Soundコンポーネントによる音声の再生タイミングはかなり「いいかげん」です。例えば「ぴぴっ」と音を短い音を2回鳴らしたいとします。再生時間0.1秒のSoundコンポーネントを2個配置して、それぞれの 開始 を0.5秒ずらしてやると「ぴぴっ」となるはずですが、実行するPCによっては1回しか音がならなかったり、全く音がならなかったりします。元々、PCのオーディオ機能はエラー音などを鳴らしたり、ひとつの音声ファイルを鳴らしたりするためのもので、短時間に複数の音声を正確に再生する機能は保証されていません。高性能なサウンドモジュールを追加することによって多少改善しますが、視覚-聴覚の相互作用の研究を考えておられる方はBuilderで正確な実験をするのはかなり厳しいと思っておいてください。刺激を動画として作成するのもひとつの対策でしょう。

チェックリスト
  • 無圧縮WAV形式の音声ファイルを再生できる。
  • 指定された周波数の音を鳴らすことが出来る。
  • 指定されたキーコードの音を鳴らすことが出来る。
  • 音声のボリュームを指定できる。
  • MPEG2形式の動画ファイルを再生できる。
  • 動画ファイルを拡大縮小して再生することが出来る。
  • 音声ファイル、動画ファイルの再生を指定された時刻に途中終了できる。
  • 様々な再生時間の音声ファイル、動画ファイルの再生開始、終了に合わせて他のコンポーネントを開始または終了させることが出来る。
  • なぜ短時間に複数のSoundコンポーネントを鳴らそうとした時に期待した結果が得られないのかを説明することが出来る。

9.3. FI/VI実験の作成

Soundコンポーネントの説明が終わったところで、実験の作成に入りましょう。まずはFI/VI用の実験から作成します。以下の解説では、Builderで新規に実験を作成して以下の作業を行い、exp09vi.psyexpという名前で保存したものとします。

  • 実験設定ダイアログ

    • 実験情報ダイアログ にnRepsとconditionという項目を追加する。
    • 単位 をnormにする。
  • trialルーチン

    • 最初からStaticコンポーネントが配置されている場合は削除する。

    • 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’]と入力する。
  • exp09fi.xlsx(条件ファイル)

    • intervalというパラメータを設定し、値として10を入力する。パラメータ名の行を除いて1行1列の条件ファイルとなる。
  • exp09vi.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の実験として使用できる状態まで完成していますので、一度実行してみましょう。実験開始からの時刻が—-と表示される以外は、計画通り動作するはずです。条件ファイルにexp09fi.xlsxを指定すれば強化時間10秒のFIスケジュールとなりますし、条件ファイルにexp09vi.xlsxを指定すれば強化時間が6秒から14秒まで変化するVIスケジュールとなります。実験を実行すると作成される記録ファイルの例を 図9.6 に示します。「xlsx形式のデータを保存」をチェックしていませんので、作成されるのはtrial-by-trial記録ファイルだけです。内容を確認すると、key_resp_Trial.rtという列に、実験参加者がキーを押した時刻がルーチン開始時を0秒として記録されています。このデータを利用すると、実験参加者のキー押し反応の発生頻度がどのような時間経過をたどって変化したかを分析することが可能です。

_images/vi-results-xlsx.png

図9.6 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章にかけてしっかりと復習してください。

9.4. 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( )メソッドの戻り値を格納することで得られています。

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

_images/draw-time-on-screen.png

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

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

9.5. 文字列に対する%演算子を使って経過時間の表示を整えよう

前節で問題となった経過時間の表示ですが、これはPythonが小数を文字列として表示する際に標準の動作としてあのように小数点以下の桁を表示してしまうことが原因で生じます。Pythonにはプログラマが小数点以下何桁程度を表示したいと考えているかを知る方法がありませんので、Pythonの標準動作が気に入らない場合はプログラマが明示的に表示方法を指定してやる必要があります。ここで重要な役割を果たすのが文字列に対する%演算子です。%演算子は左辺に文字列、右辺に数値や文字列等のデータまたはタプルと呼ばれるシーケンス型の一種をとり、右辺の値を左辺の文字列へ指定された書式で埋め込みます。左辺の文字列をフォーマット文字列と呼びます。C++言語など、Python以外にも類似の機能を持っている言語はたくさんありますので、他のプログラミング言語をご存じのかたは「ああ、あれか」と思われるのではないかと思います。しかし、初めての方には何のことやらさっぱりわからないと思いますので、ていねいに見ていきましょう。他の言語をご存じの方も復習のつもりでご覧ください。

_images/format-operator.png

図9.8 %演算子による数値の文字列への埋め込み。%演算子の左辺の文字列に含まれている%dという部分に、%演算子の右辺の数値を整数に変換して埋め込みます。%dのdは整数に変換するという「変換型」を指定する文字です。

表9.1 フォーマット文字列における主な変換型
変換型 意味
dまたはi 符号付き10進数。小数点以下は切り捨てられます。
o 符号なし8進数
u 符号なし10進数。小数点以下は切り捨てられます。
x 符号なし16進数(小文字) 例: 2000 → ‘7d0’
X 符号なし16進数(大文字) 例: 2000 → ‘7D0’
e 指数表記の浮動小数点数(小文字) 例: 2000 → ‘ 2.000000e+03’
E 指数表記の浮動小数点数(大文字) 例: 2000 → ‘ 2.000000E+03’
fまたはF 10 進浮動小数点数
s 文字列
% %という文字

図9.8 は、変数varに格納されている数値を整数に変換して文字列を生成する例です。%演算子の左辺の文字列の中に含まれた%dという部分に、変数varの値が整数として挿入されます。演算の結果は必ず文字列になる点に注意してください。%があちこちに出てきて非常に説明しにくいのですが、%演算子の左辺に含まれている%dに注目してください。%dのdは「変換型」を示す文字列で、この文字列を変更することでさまざまな書式を指定することが出来ます。 表9.1 に主な変換型を示します。だいたいお分かり頂けると思いますが、最後の%がわかりにくいかもしれません。変換型としての%は、変換後の文字列に%という文字そのものを含みたいときに使います。例えば「あなたの正答率は60%でした」という文字列の60の部分に変数correctRatioの値を埋め込みたい場合は、以下のように記述します。得られる文字列では%%が%に変換されます。

u'あなたの正答率は%d%%でした' % correctRatio

変換後の文字列で、数値の桁数を指定したい場合は%と変換型の間にフラグ文字列と呼ばれる文字列を挿入します。主なフラグ文字列を 表9.2 に示します。 表9.2 では数値で例を示していますが、桁数の指定は文字列を埋め込む場合でも使えます。今回の場合、実験開始からの経過時間を小数点1桁まで表示したいので、小数を表示する変換型であるfと、小数点以下の桁数を指定するフラグを組み合わせればうまくいきます。具体的には、以下の式で目的を達成できます。

'%.1f' % globalClock.getTime( )
表9.2 主なフラグ文字列
フラグ 意味
正の整数
桁数を指定します。
例: u’値は%7dです’ % 17 → u’値は   17です’
0+正の整数
桁数を指定します。足りない桁は0で埋められます。
例: u’値は%07dです’ % 17 → u’値は0000017です’
負の整数
桁数を指定し、左詰めで文字列化します。
例: u’値は%-7dです’ % 17 → u’値は17   です’
小数
小数点の前の値は全体の、後の値は小数点以下の桁数を指定します。小数点前の値は省略することが出来ます。値は四捨五入されます。
例: u’値は%.3fです’ % 3.14159 → u’値は3.142です’
例: u’値は%07.3fです’ % 3.14159 → u’値は0003.14です’

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

_images/embed-multiple-values.png

図9.9 %演算子による複数の値の埋め込み。フォーマット文字列に出現する変換型の個数と、%演算子の右辺のシーケンスの要素数が一致している必要があります。右辺のシーケンスは ( )で囲んだ「タプル」でなければいけません。[ ]で囲んだリストを右辺に置くとエラーになります。

この%演算子を使った文字列への値の埋め込みは、教示文や刺激文などを作る時にも便利です。今までは+演算子とstr( )関数を駆使して埋め込みを行っていましたが、埋め込む値が複数個ある場合は%演算子を使った方が簡潔に記述できます。複数の値を埋め込む例を 図9.9 に示します。%演算子の右辺に埋め込みたい値を並べたシーケンスを置くのですが、このシーケンスは「タプル」と呼ばれる( )で囲まれたものでなければいけません。今までの章で出てきた「リスト」は[ ]で囲みましたが、%演算子の右辺にリストを置くとエラーになります。タプルについて詳しくは リストとタプル をご覧ください。左辺のフォーマット文字列に含まれる変換型の個数と右辺のタプルの要素数は一致している必要があります。n番目の変換型の位置へ、タプルのn番目の要素が埋め込まれます。 図9.9 の式では、変数whoにu’太郎’、whenに10、whereにu’ホームセンター’が格納されているとすると、u’太郎は10時にホームセンターへ行った’という文字列が得られます。ここで%dと対応しているwhenにu’公園’のように整数に変換できない値が格納されていた場合はエラーとなります。埋め込む値の個数が多い時には本当に%演算子は便利なので、ぜひマスターしておいてください。

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

チェックリスト
  • ひとつの数値を8進数、10進数、16進数整数の書式で文字列に埋め込むことが出来る。
  • ひとつの浮動小数点数を10進数表記と指数表記の書式で文字列に埋め込むことが出来る。
  • 数値を文字列に埋め込む際に、指定した桁数の右寄せで埋め込むことが出来る。桁が足りない時に0で埋めることが出来る。
  • 数値を文字列に埋め込む際に、指定した桁数の右寄せ埋めで込むことが出来る。桁が足りない時に空白文字で埋めることが出来る。
  • 数値を文字列に埋め込む際に、指定した桁数の左寄せで埋め込むことが出来る。
  • 浮動小数点数を文字列に埋め込む時に、整数部の桁数と小数点以下の桁数を指定することが出来る。
  • 複数の値をひとつの文字列にそれぞれ書式を指定して埋め込むことが出来る。

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

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

  • ratioというパラメータを定義する条件ファイルexp09fr.xlsxとexp09vr.xlsxを作成する。exp09fr.xlsxではratioは10のみ、exp09vr.xlsxではratioは2、6、10、14、18の5種類の値をとるようにする。
  • trialルーチンにおいて、ルーチン開始後にratioの回数だけスペースキーを押したらルーチンを終了する。

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

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

続いてexp09vr.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スケジュールへの対応が出来ました。次は再びexp09vi.psyexpをベースにして、実験開始後一定時間が経過したら実験を終了するように改造しましょう。

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

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

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

すでに前節までの作業で、グローバルクロックを用いて実験開始からの経過時間を得る方法は解説しました。先ほど「Codeコンポーネントを使えば実現できる」と言ったのですから、exp09vi_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の値を指定出来るようにしましょう。実験設定ダイアログを開いて、Experiment infoにtime limit (s)という項目を追加してください。そして、trialルーチンのCodeコンポーネントのプロパティ設定ダイアログを再び開いて、 実験開始時 に以下のコードを追加します。やはり入力済みのコードの前でも後でもどちらでも構いません。

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

以上で変更は終了です。exp09vi_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

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

exp09vi_time_limited_1.psyexpを開いて、今度はexp09vi_time_limited_2.psyexpという名前で保存してください。以後、exp09vi_time_limited_2.psyexpに対して作業をするものとします。保存したらさっそくexp09vi_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'

作業が終わったら、exp09vi_time_limited_2.psyexpを保存して、実行してください。条件ファイルにexp09fi.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文です。

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

_images/while-statement.png

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

図9.11 は、Builderが実験のフローをどのようにPythonのコードへ変換するかを示しています。フローにおけるループはfor文に変換されます。条件ファイルや Loopの種類繰り返し回数 $ などに基づいて各試行で用いられるパラメータ一覧のリストが作成され、for文へ渡されます。このfor文を実行することで、パラメータを変更しながら指定された回数の繰り返しが実行されます。一方、各ルーチンはwhile文に変換されます。 図9.11 は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

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

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

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

exp09vi_time_limited_2.psyexpを、exp09vi_time_limited_3.psyexpという別名で保存してください。以下の作業はexp09vi_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がルーチンペインの一番上に並んでいる必要があります( 図9.12 )。trialルーチンとreinforcementルーチンを開いて、codeTrialとcodeRFがそれぞれ一番上になるように並び替えましょう。

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

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

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

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

_images/break-routine-xlsx-output.png

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

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

チェックリスト
  • 条件式がTrueである間処理を繰り返すPythonのコードを書くことが出来る。
  • Codeコンポーネントを用いて、ある条件を満たしたときに実行が中断される実験を作ることが出来る。
  • Codeコンポーネントを用いて、ある条件を満たしたときに実行がスキップされるルーチンを作ることが出来る。

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

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

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

_images/concurrent-schedule-procedure.png

図9.14 並立スケジュール。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を駆使する方法でも実現可能ですが、この節ではループの実行そのものを省略する方法を紹介します。

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

_images/concurrent-schedule-flow.png

図9.15 exp09concurrent.psyexpのフロー。

  • 実験設定ダイアログ

    • Experiment infoから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にする。
  • exp09concurrentVI.xlsx(条件ファイル)

    • exp09vi.xlsxをコピーして、intervalの値を2, 6, 10, 14, 18にする。
  • exp09concurrentVR.xlsx(条件ファイル)

    • exp09vr.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 = ‘exp09concurrentVI.xlsx’
  • key_resp_Choiceで押されたキーが’right’であれば、以下の変数を設定する。

    • nRepsVI = 0
    • nRepsVR = 1
    • condition = ‘exp09concurrentVR.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 = 'exp09concurrentVI.xlsx'
elif 'right' in theseKeys:
    nRepsVI = 0
    nRepsVR = 1
    condition = 'exp09concurrentVR.xlsx'

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

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

_images/concurrent-xlsx-output.png

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

チェックリスト
  • ある条件に当てはまる時にループを実行しない実験を作成することが出来る。
  • ループを実行しない実験を作成した時のtrial-by-trial記録ファイルから、ループを実行しなかった時の記録を判別できる。

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

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

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

    • 教示文は各自に任せます。
    • 教示画面でスペースキーを押したら実験が始まるようにしてください。
  • 問2:exp09vi.psyexpをベースとして、 図9.14 の並列スケジュールの実験を以下の条件で作成してください。

    • 左キーが押された場合は強化時間が2秒、11秒、20秒、29秒、38秒の5種類のVIスケジュール
    • 右キーが押された場合は強化時間が12秒、16秒、20秒、24秒、28秒の5種類のVIスケジュール
    • 左キーと右キーのスケジュールの実行には同一のループおよびルーチンを使用し、条件ファイルの切り替えによってスケジュールの違いに対応する。
  • 問3:exp09vi_time_limited_3.psyexpをベースとして、time limit (s)で指定された制限時間を過ぎてしまったときに、スキップされた試行のデータがtrial-by-trial記録ファイルに出力されないようにする。ただし、制限時間によって中断された試行の、中断直前までのキー押しはtrial-by-trial記録ファイルに出力されているようにすること。

9.10. この章のトピックス

9.10.1. Movieコンポーネントのバックエンド

バックエンドにはいろいろな意味がありますが、動画再生に利用するライブラリのことを指しています。PsyhcoPy Builderのユーザーから見ると「Movieコンポーネント」が操作画面に見えていて実際に操作する対象であり、これを「フロントエンド」と呼びます。それに対して、Movieコンポーネントが動画再生のために内部で利用しているライブラリが「バックエンド」です。

PsychoPyが動画再生をサポートした当初は、avbinというライブラリが利用されていました。ところがこのavbinは実験の実行時にうまく読み込めないことがあるなどのトラブルが多く、動画再生機能を全く利用していない実験にまで 図9.17 のようなエラーメッセージが表示されることがよくありました。動画再生を利用していないのであれば実験は正常に動作するのでこのエラーは無視して良いのですが、そのことを知らなくて「なぜエラーメッセージが出ているんだろう、実験に何か問題があるのだろうか」と頭を悩ませる人が後を絶ちませんでした。

_images/avbin-vlc-error.png

図9.17 動画再生バックエンドに関するエラーメッセージ

avbinを利用した動画刺激クラスはpsyhcopy.visual.MovieStimという名称ですが、MovieStimの問題を解消するために新たにOpenCVというライブラリを通じてVLC media playerというソフトウェアの動画再生機能を利用する新しい動画刺激クラスMovieStim2が導入されました。確かにMovieStimに比べていろいろと改善された点が多いのですが、VLC media playerをインストールしていないと利用でいないという問題を抱え込んでしましました。厄介なことに、PsychoPyはVLC media playerを見つけられない時に「MovieStim2をimport出来ないから利用できない」というメッセージを表示するため、VLC media playerをイントールしないといけないということが非常にわかりにくいのです。

以上を踏まえたうえで、Movieコンポーネントの バックエンド について解説します。 バックエンド は「avbin」と「opencv」を選ぶことができますが、「avbin」を選ぶとMovieStimがバックエンドとして使用されます。「opencv」ならばMovieStim2が使用されます。VLC media playerのことを知らずに「opencv」を選ぶと「MovieStim2が利用できない」とエラーメッセージが出てきて、その解消方法が「VLC media playerをインストールする」だなんて普通わかるはずがないと筆者は思うのですが、仕方がありません。利用する際はご注意ください。

9.10.2. リストとタプル

タプルについては、第5章で「シーケンス型」というデータ型が初登場したときに、一応その名前だけは紹介していたのですが、その性質については全く解説しませんでした。タプルはリストと非常によく似ているのですが、なぜ「リスト」と「タプル」という類似したデータ型が用意されているかを第5章の時点で解説すると却って混乱すると考えたからです。

タプルはdata = (1, -7, ‘psychoplogy’)といった具合に、リストと同様に要素をカンマで並べて作成します。リストとの違いは、リストでは[ ]で要素を囲んだのに対してタプルでは( )で要素を囲む点です。作成したタプルは、リストと同様に[ ]演算子を適用することによって要素にアクセスすることが出来ます。先の例でdata[2]とすれば’psychology’が得られますし、data[-3]とすれば1が得られます。機能的な意味でのリストとタプルの最大の違いは、リストは要素を追加したり変更したりできるのに対して、タプルはそのような変更ができないという点にあります。例えば、リストであれば

data = [1, -7, 'psychology']
data[1] = 5

とすれば、dataは[1, 5, ‘psychology’]となります。一方、タプルに対して同様の処理をしようとするとエラーとなってプログラムの実行が停止します。

data = (1, -7, 'psychology')
data[1] = 5    #エラーとなる

また、リストにおけるappend( )やextend( )といったメソッドもタプルには存在しません。

どう考えてもタプルは不便なだけのような気がするのですが、なぜタプルなどというデータ型が用意されているのでしょうか。それは、タプルの方がリストよりも効率的かつ高速に処理できるからです。なぜそうなるのかを説明するのは難しいのですが、製本されたノートとルーズリーフの違いのようなものを思うと少しイメージしやすいかもしれません。ルーズリーフは途中に新しいページを挿入したり、順番を入れ替えたりすることが容易にできますが、本当に将来挿入や入れ替えをする必要があるのなら、複数件のメモを一枚のルーズリーフに書くことはできません。ほんの数行だけのメモだけで一枚のルーズリーフを使ってしまい、大変効率が悪いです。後で挿入や入れ替えをする必要がないのなら、ルーズリーフを使わなくても通常のノートに隙間なくメモを書き込む方がいいでしょう。恐らく使用するページ数も少なく済み、ルーズリーフを用意するより安価でかさばらないはずです。これはあくまで例え話に過ぎませんが、変更できないようにすることで得られるメリットがあるからPythonにはタプルというデータ型が用意されていると理解しておいてください。

なお、リストをはじめとする各種シーケンス型データをタプルに変換したいときにはtuple( )という関数を用います。以下のコードを実行すると、変数data_tupelには(1, -7, ‘psychology’)というタプルが格納されます。

data = [1, -7, 'psychology']
data_tuple = tuple(data)

タプルをリストに変換したいときにはlist( )という関数を用います。これらの関数を覚えておくと役に立つかもしれません。

9.10.3. 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のままになります。