7. キーボードで刺激を調整しよう―ミューラー・リヤー錯視

7.1. この章の実験の概要

この章では有名な錯視のひとつであるミューラー・リヤー錯視を題材として、調整法の手続きをBuilderで実現する方法を解説します。この章まで進んできた皆さんはそろそろ教示画面の作成は各自でできるでしょうから、重要な部分だけを取り上げましょう。 図7.1 に実験に用いる刺激を示します。スクリーン上に左右に並んでテスト刺激とプローブが表示されます。テスト刺激はミューラー・リヤー錯視図形で、水平線(以下主線と呼びます)の長さは0.2、矢羽の長さは0.05です。主線と矢羽のなす角度(以下夾角と呼びます)として0度から30度間隔で150度まで、6種類の図形を用います。0度の時は矢羽と主線がぴったり重なって長さ0.2の水平線だけに見える点に注意してください。プローブは水平な線分で、キーボードのカーソルキーの左右を使って長さを調節することができます。実験参加者は主線とプローブの長さが同じに見えるようにプローブの長さを調整して、スペースキーを押して報告します。この時のプローブの長さと主線の長さの差で錯視量を評価しようというのが本実験の狙いです。試行開始時のプローブの長さが毎回同じだと、参加者が何回キーを押せば主線とプローブが同じ長さになるかを学習してしまう恐れがありますので、試行開始時のプローブの長さは170、190、210、230pixの中から無作為に選択します。テスト刺激、プローブともスクリーンの中央の高さで、水平方向の中心がスクリーン中央から0.2離れた位置に提示されるものとします。

_images/muller-lyer.png

図7.1 実験に用いる刺激。テスト刺激の水平線(主線)の長さを0.2、矢羽の長さを0.05で固定し、夾角を0度から150度まで変化させます。参加者は主線の長さとプローブの長さが等しく見えるようにプローブの長さを調整します。試行開始時のプローブの長さは170、190、210、230pixのいずれかです。

図7.2 に実験手続きを示します。教示画面は省略しましたので、いきなり最初の試行から始まります。各試行の最初に0.5秒間空白のスクリーンを提示した後、テスト刺激とプローブを提示します。テスト刺激は半数の試行で右側に、残り半数の試行で左側に提示され、順番は無作為に決定します。実験参加者がカーソルキーの右を押したら刺激の長さを5pix長く、左を押したら5pix短くします。参加者がスペース―を押したら試行は終了です。テスト刺激(6種類)×テスト刺激の位置(2種類)×プローブの初期長さ(4種類)=48通りの条件を3試行ずつ、合計144試行を行ったら実験は終了です。

_images/muller-lyer-procedure.png

図7.2 実験の流れ。

実験手続きは単純なので、前章までに解説したテクニックで十分実現できるはずです。問題は、「カーソルキーで刺激を調節してスペースキーで試行を終了する」という手続きをどうやってBuilderで実現するかです。Keyboardコンポーネントでは、 [Routineを終了] プロパティを用いてキーが押されたときにルーチンを終了させるか否かを指定できます。しかし、特定のキーが押されたときだけ終了させるといった指定はできません。 第6章 で学んだif文を使うとキーに応じた処理を実行することが出来ます。

それでは実験を作成していきましょう。以下の解説では、Builderで実験を新規作成し、以下の作業を行ってexp07a.psyexpの名前で保存したものとします。 この章から既出のコンポーネントのプロパティについてはタブを省略しますのでご注意ください

  • 実験設定ダイアログ

    • PsychoPyの設定でheight以外の単位を標準に設定している場合は [単位] をheightにしておく。

  • trialルーチン

    • Polygonコンポーネントを6つ配置し、 [名前] をprobe, testline, arrowTR, arrowBR, arrorTL, arrorBLとする(BR=Bottom Right、TL=Top Leftといった命名規則)。すべて [開始] を「時刻 (秒)」の0.5とし、 [終了] を空白にして [形状] を直線にする。「外観」タブの [枠線の色] をwhiteにする。どれかひとつを作成してこれらの設定を行い、残りをコンポーネントのコピーで作成すると楽である。続いて以下の設定を行う。

      • testlineの [位置 [x, y] $] を(testPos, 0)にして、「繰り返し毎に更新」に設定する。

      • testlineの [サイズ [w, h] $] を(0.2,0)にする。

      • probeの [位置 [x, y] $] を(-testPos, 0)にして、「繰り返し毎に更新」に設定する。

      • probeの [サイズ [w, h] $] を(probeLen,0)にして、「フレーム毎に更新する」を設定する。

      • arrowTR、arrowBR、arrorTL、arrorBLの [サイズ [w, h] $] を(0.05,1)にする。

    • Keyboardコンポーネントを1つ配置して、以下の設定を行う。
      • [名前] をkey_responseにする。

      • [開始] を「時刻 (秒)」の0.5とし、 [終了] を空白にする。

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

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

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

    • Codeコンポーネントを1つ配置して、以下の設定を行う。
      • [Routine開始時] にprobeLen = initProbeLenと入力する。

      • trialルーチン内での順序を一番上にする。

  • trialsループ(作成する)

    • [Loopの種類] をfullRandomにする。

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

    • [繰り返し条件] にexp07cnd.xlsxと入力する。

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

    • testPos、angle、initProbeLenの3パラメータを設定する。

    • 実験手続の内容を満たすように、2種類のtestPos(-0.2、0.2)、6種類のangle(0、30、60、90、120、150)、4種類のinitProbeLen(0.17、0.19、0.21、0.23)の組み合わせを入力する。2×6×4=48行の条件ファイルとなる(パラメータ名の列を除く)。testPosはテスト刺激の中心のX座標を表しており、プローブ刺激の位置はtestPosの符号を反転すれば得られるのでパラメータとして用意する必要はない。

教示などを省略したので以上で単純なフローの実験となりました。trialルーチンに6個のPolygonルーチンを配置しましたが、その内の5個(testline、arrowTR、arrowBR、arrorTL、arrorBL)を使ってミューラー・リヤー図形を描きます。これらの位置を指定するのはちょっと複雑ですので次節でていねいに見ていきましょう。

7.2. Polygonコンポーネントで線分を描画しよう

Polygonコンポーネントで [形状] に直線を指定することによって、線分を描画することができます。多くのプログラミング言語でスクリーン上に線分を描くライブラリの多くは両端の座標を指定するのですが、Builderでは [位置 [x, y] $] で図形の中央の座標を、 [サイズ [w, h] $] で大きさを指定することになっているため、他のプログラミング言語に慣れている人ほど戸惑うかもしれません。

Polygonコンポーネントで描く線分の長さは、 [サイズ [w, h] $] の幅の値(1番目の値)で指定します。2番目の値は無視されますので、適当な値(例えば0)を入れておきましょう。線分の太さは [枠線の幅 $] で指定します。線分の色は [枠線の色] で指定します。 [塗りつぶしの色] は無視されます。

問題は [位置 [x, y] $] です。多角形を描画する時と同様に中心の座標を指定しないといけませんが、今回は「矢羽用の線分の端点を主線の端点と一致させる」必要がありますので、端点が意図した位置にくるように線分の中心位置を計算して [位置 [x, y] $] に指定しないといけません( 図7.3 上)。線分の中点の座標を(0, 0)、線分の長さをL、回転角度をqとすると、線分の端点の座標は±(L/2×cos(q), L/2×sin(q))です。±が付いているのは線分には端点が2つあるからです。線分の中点を(a, b)の位置へ移動させると、端点の座標は (a, b)±(L/2×cos(q), L/2×sin(q))です。今、端点の一方である(a + L/2×cos(q), b + L/2×sin(q))が(x, y)に一致するように線分の中点の座標を決めたいとしましょう。その時、(a, b)は(a + L/2×cos(q), b + L/2×sin(q)) = (x, y)を満たしますので、これを解いてa = x - L/2×cos(q)、b = y - L/2×sin(q)を得ます。本来ならばこれで線分の中点をどこへ置けばよいかの計算は終わりなのですが、Builderでは正の回転方向が時計回りであり、通常の三角関数の計算の時と回転方向が逆であることを考慮しないといけません。対策は簡単で、回転時のY座標の符号を反転するだけです( 図7.3 下)。従って、a = x- L/2×cos(q)、b = y + L/2×sin(q)が求める答えです。

_images/center-position-of-lines.png

図7.3 Polygonコンポーネントで斜めの線分を描画する時の位置の計算。Builderの回転方向と通常の三角関数における回転方向が逆なので、Y座標の符号を反転させる必要があります。

同様の要領で、ミューラー・リヤー図形を描けるように4本の線分の中心位置を求めたのが 図7.4 です。4本すべての線分の回転角がqで統一できるように符号を決めていますので、確認してください。

_images/arrow-positions.png

図7.4 ミューラー・リヤー錯視の矢羽用のPolygonコンポーネントの座標。テスト刺激の位置(X座標)をtestPosで、ミューラー・リヤー図形の夾角をqで表しています。

それではexp07a.psyexpを開いてこれらの式を入力していきましょう。一点だけ注意しないといけないのは、 図7.4 の式の回転角qの単位がラジアンである点です。条件ファイルに入力した夾角angleは単位が度ですので、qの部分をdeg2rad(angle)に書き換える必要があります。Polygonコンポーネントのプロパティに直接入力しても構わないのですが、式が複雑なのでCodeコンポーネントを使いましょう。矢羽の角度や位置は試行毎に変化しますので、 [Rooutine開始時] に以下のように入力します(cとccは時計回り(clockwise)、反時計回り(counterclockwise)を表しています)。

cx = 25 * cos( deg2rad(angle) )
cy = 25 * sin( deg2rad(angle) )
ccx = 25 * cos( deg2rad(-angle) )
ccy = 25 * sin( deg2rad(-angle) )

このcx, cy, ccx, ccyを使って以下のPolygonコンポーネントに式を入力していきます。

  • arrowTR

    • [回転角度 $] にangleと入力し、「繰り返し毎に更新」を設定する。

    • [位置 [x, y] $] に[testPos+0.1-cx, cy]と入力し、「繰り返し毎に更新」を設定する。

  • arrowBR

    • [回転角度 $] に-angleと入力し、「繰り返し毎に更新」を設定する。

    • [位置 [x, y] $] に[testPos+0.1-ccx, ccy]と入力し、「繰り返し毎に更新」を設定する。

  • arrorTL

    • [回転角度 $] に-angleと入力し、「繰り返し毎に更新」を設定する。

    • [位置 [x, y] $] に[testPos-0.1+ccx, -ccy]と入力し、「繰り返し毎に更新」を設定する。

  • arrorBL

    • [回転角度 $] にangleと入力し、「繰り返し毎に更新」を設定する。

    • [位置 [x, y] $] に[testPos-0.1+cx, -cy]と入力し、「繰り返し毎に更新」を設定する。

以上で条件ファイルのパラメータを読み込んでミューラー・リヤー図形を描く準備ができました。次はプローブの長さの変更に挑戦します。

チェックリスト
  • Polygonコンポーネントを使って線分を描画することができる。

  • Polygonコンポーネントを使った線分の長さ、角度が決まっている時に、その端点が指定された座標に一致するように [位置 [x, y] $] を指定することができる。

7.3. Codeコンポーネントを使って刺激のパラメータとルーチンの終了を制御しよう

Codeコンポーネントでは、押されたキーを判別してカーソルキーの右であればプローブの長さを5pix長く、左であれば5pix短くし、スペースキーであればルーチンを終了します。第6章 を読んだ皆さんであれば「if文を使うとよい」ということはすぐにわかると思いますが、今回は処理が複雑です。第6章 では

  1. 押されたキーの名前が変数correctAnsの値と一致している

  2. 押されたキーの名前が変数correctAnsの値と一致していない

の2通りに分岐しましたが、今回は

  1. 押されたキーの名前が'left'である

  2. 押されたキーの名前が'right'である

  3. 押されたキーの名前が'space'である

  4. 押されたキーの名前がいずれにも一致しない

の4通りに分岐しなければいけません。このように3通り以上の分岐を処理するために、Pythonのif文ではelifという語を使うことができます。elifを使ったif文の書式は以下の通りです。n個の式を連ねて書くことができます。最初はif、最後はelseで、それ以外はすべてelifでなければいけません。

if 式1:
    式1が真の時の処理
elif 式2:
    式1が偽で式2が真の時の処理
(中略)
elif 式n:
    式1から式n-1が偽で式nが真の時の処理
else:
    式1から式nがすべて偽であった時の処理

最初に真になった式に対応する処理だけが実行されますので、例えば式1が偽で式2が真であれば、「式1が偽で式2が真の時の処理」だけが実行されます。その後に続く式3、式4…が真であっても、対応する処理は一切実行されません。

押されたキーの名前が変数keyに入っているとすれば、今回の処理は以下のように書けます。上記の書式でn=3の場合に該当します。

if key == 'left':
    プローブの長さから0.005を引く
elif key == 'right':
    プローブの長さに0.005を足す
elif key == 'space':
    ルーチンを終了する処理

elseはどこへいった?と思われるかもしれませんが、elseに対応する処理が何もない場合はelseを省略することができます。今回はカーソルキーの左、右、スペースキーのいずれも押されていない場合は何もする必要がないのでelseを省略できます。

プローブの長さを変更する処理については、プローブ刺激に対応するPolygonコンポーネントprobeの [サイズ [w, h] $] に[probeLen, 1]と書いているのですから、変数probeLenの値を増減すればいいだけです。0.005を加えるにはprobeLen += 0.005、0.005を引くにはprobeLen -= 0.005です。

ルーチンを終了させる処理については、まだ解説していないBuilderの機能を使用する必要があります。Builderには、1フレーム描画する毎に1回、continueRoutineという変数を確認し、値がFalseであれば直ちにルーチンを終了するという機能があります。if文を用いて、ルーチンを終了させたい条件を満たした時にcontinueRoutine=Falseという文を実行すれば、ルーチンを終了させることができるわけです。

以上を踏まえて、if文の処理内容を記述すると以下のようになります。

if key == 'left':
    probeLen -= 0.005
elif key == 'right':
    probeLen += 0.005
elif key == 'space':
    continueRoutine = False

残るは押されているキー名の取得です。第6章 の内容を踏まえると、Keyboradコンポーネントの [名前] がkey_responseですから、key_responseのデータ属性keysを利用すればよいだけのような気がします。しかし、 表6.3 をよく読みなおしてほしいのですが、keysには押されたキー名が格納されているのではなく、そのルーチンでKeyboardコンポーネントが保存するキー名が格納されています。今回の実験ではkey_responseの [記録] を「なし」に設定しているのですから、データ属性keysにはキー名が格納されません。

ではどうにすれば良いかといいますと、Builderが自動的に用意する変数theseKeysを利用します。theseKeysは最後に実行したKeyboardコンポーネントの結果を格納している変数です。 [検出するキー $] に記述されたキーがいずれも押されていなければ空の(要素数ゼロの)リスト、押されていたキーがあれば、それらの名前をすべて列挙したリストが格納されています。「それらの名前」と複数形になっているのは、同時に複数のキーが押される場合があるからです。キーの同時押しについての詳細は「 7.7.1:複数キーの同時押しの検出について 」を参考にしてください。

theseKeysを利用する場合、theseKeysの中身はリストですからtheseKeys == 'left'という具合に直接文字列と等しいか比較しても意味がありません。theseKeysに格納されたリストの中に'left'という文字列があるかを検査しなければいけません。Pythonにはこの用途にうってつけの in という演算子があります。in演算子はx in aという形で使用して、aの中にxがあればTrue、なければFalseとなります。theseKeysとin演算子を用いてif文を書くと以下の通りになります。

if 'left' in theseKeys:
    probeLen -= 0.005
elif 'right' in theseKeys:
    probeLen += 0.005
elif 'space' in theseKeys:
    continueRoutine = False

これでif文は完成しましたが、問題はこのif文をCodeコンポーネントのどこへ入力すれば良いのかという点です。ここで覚えておいてほしいのが、「あるルーチンを実行している最中に何かをするのであれば、コードを入力する欄は [フレーム毎] でなければいけない」ということです。今回のコードはルーチンの実行中に押されたキーを判別して処理を振り分けるのですから、 [フレーム毎] に入力しなければいけません。タイプミスしないように気を付けて [フレーム毎] にこのif文を入力してください。leftやright、spaceの前後のシングルクォーテーションや、if、elifが出て来る最後のコロン、if、elif以外の行は行頭にスペースが必要な点などが間違えやすいポイントです。

以上でCodeコンポーネントによる刺激パラメータとルーチン終了の制御ができるようになりました。作業内容をexp07a.psyexpに保存して実行してみましょう。カーソルキーの左右を押すとプローブの長さが変化し、スペースキーを押すと次の試行へ進むはずです。残念ながらカーソルキーを押しっぱなしにしていても連続的に長さは変化しませんので、何度もカチカチとボタンを押して長さを調節する必要があります。

一見、これで 図7.1 および 図7.2 に示した実験が完成したように思えます。しかし、実験を最後まで実行するかEscキーで中断してtrial-by-trial記録ファイルを確認してみるとわかりますが、probeLenの列に試行開始時の値が出力されていて、参加者が調整後のprobeLenの値が記録ファイルのどこにも出力されていません。Builderは、Codeコンポーネントで独自に使用した変数や、ルーチン開始後に変更されたパラメータの値を記録ファイルには出力しないのです。自動で出力してくれたらいいのにと思われるかもしれませんが、本当にそんなことをしたら記録ファイルが分析に不要な変数だらけで大変なことになりかねません。次節では、Coderコンポーネントを用いて実験記録ファイルに出力する変数を追加する方法を解説します。

チェックリスト
  • 3通り以上の分岐を処理させるif文を書くことができる。

  • リストの中にある要素が含まれているか否かで処理を分岐させることができる。

  • Codeコンポーネントからルーチンを終了させることができる。

7.4. Codeコンポーネント使って独自の変数の値を記録ファイルに出力しよう

Builderが実験記録ファイルに出力する変数はどのように管理されているのでしょうか。ここで思い出していただきたいのは、条件ファイルで定義した変数はすべて実験記録ファイルに出力されるという点です。条件ファイルはループのプロパティ設定ウィンドウで指定するのですから、実験記録ファイルに出力される値を管理しているのはループであるはずです。

では、Builderにおけるループの「実体」とは何でしょうか。その答えは [Loopの種類] によって異なるのですが、この本で使用しているrandom、sequential、fullRandomの場合はいずれもpsychopy.data.TrialHandler (以下TrialHandler)というクラスのインスタンスです。TrialHandlerはその名が示す通り、試行を制御するためのクラスです。繰り返しの度にパラメータの値を更新したり、パラメータや反応を実験記録ファイルに保存したりするBuilderの機能はこのクラスによって実現されています。

表7.1 にTrailHandlerの主なデータ属性を示します。これらのデータ属性を利用すると、画面上に「現在第n試行」、「残りn試行」といったメッセージを提示することができます。例えばtrialsという名前を付けたループに対応するTrailHandlerのインスタンスはCoder上では変数trialsに格納されているので、trials.thisNと書くと現在trialsループで何回繰り返しを終えているかが得られます。 表7.1 に書かれているように1回目の繰り返しではthisN=0なので、1を加えてtrials.thisN+1とすれば「第n試行」のnに当てはめる数値が得られます。

表7.1 TrialHandlerの主なデータ属性

データ属性

概要

thisIndex

条件ファイルの何行目の条件がこのループで用いられているかを示します。ただしパラメータ名の行は行数に数えず、1行目を0と数えます。Trial-by-trial記録ファイルのthisIndexと同じです。

nTotal

このループで実行される繰り返しの総数。

nRemaining

このループで実行される残りの繰り返し回数。1回目の繰り返しの実行中にnTotal-1で、繰り返しの度に1ずつ減少します。

thisN

このループで実行済みの繰り返し回数。1回目の繰り返しの時に0で、繰り返しの度に1ずつ増加します。Trial-by-trial記録ファイルのthisNと同じです。

thisRepN

Trial-by-trial記録ファイルのthisRepNと同じです。

thisTrialN

Trial-by-trial記録ファイルのthisTrialNと同じです。

ちょっと脱線なのですが、第5章 の復習がてら実際に「第○試行」とTextコンポーネントを使って提示する場合の注意点を述べておきましょう。trials.thisN+1は数値ですので、そのまま文字列と結合することができません。ですからTextコンポーネントの [文字列] に$'第' + (trials.thisN+1) + '試行'と書くと当然エラーになります。ここで 第5章 において実験情報ダイアログからターゲットの大きさや偏心度の値を得たときの処理を思い出してください。あの時は実験情報ダイアログの値は文字列で、刺激のパラメータとして利用する時には数値に変換しないといけないのでした。今回はこの逆で、数値であるtrials.thisN+1を文字列にしないといけません。第5章 で紹介したように、この変換には関数str( )を用います。$'第' + str( trials.thisN+1 ) + '試行'とすれば、TextコンポーネントのTextの値として使用できます。

話を元に戻しましょう。TrialHandlerクラスのインスタンスに実験記録ファイルへ出力する変数を追加するには、TrialHandlerのメソッドを使用する必要があります。 表7.2 に主なTrialHandlerのメソッドを示します。メソッドの引数の書き方がPsychoPyのヘルプドキュメントと異なりますが、この点について解説するにはPythonの文法に関する詳しい解説が必要です。興味がある方は「 7.7.2:メソッドの第一引数について(上級) 」をご覧ください。 表7.2 の最初に挙げられているaddData( )が今回の目的を達成するためのメソッドです。第6章 でも少しだけ例が出ていたのですが、メソッドはデータ属性と同様にインスタンスが格納されている変数とメソッド名をドット演算子で連結して呼び出します。trialsループに出力する変数を追加する場合はtrials.addData( )と書くわけです。 表7.2 に書かれている通り、addData( )には引数が必要です。第1引数には、trial-by-trial記録ファイルやxlsx記録ファイルに置いてその値が出力される列の名前(記録ファイルの1行目の見出し)を文字列として渡します。第2引数には、出力したい変数や値を記述します。今回の例では、responseという列名で実験参加者が調整した後のprobleLenの値を出力してみることにしましょう。記入すべき文は以下の通りです。

trials.addData('response', probeLen)

この文をCodeコンポーネントに追加すればいいのですが、どの欄に追加すればいいでしょうか。参加者がスペースキーを押して反応を確定した後に保存しないと意味がありませんから、 [Routine終了時] に追加するのが正解です。追加して変更を保存してください。

表7.2 TrialHandlerの主なメソッド。

メソッド

概要

addData(thisType, value)

実験記録ファイルに出力する値を追加します。thisTypeに実験記録ファイルにおける列名、valueに値を指定します。

getEarlierTrial(n)

n回前に用いられたパラメータを得ます。1回前であればn=-1という具合に負の整数で指定します。nが省略された場合はn=-1と見なされます。n回前が存在しない(2回目で-5を指定するなど)場合はNone、存在する場合はn回前のパラメータが辞書オブジェクトとして得られます。

getFutuerTrial(n)

n回後に用いられるパラメータを得ます。1回後であればn=1という具合に正の整数で指定します。nが省略された場合はn=1と見なされます。n回後が存在しない場合はNone、存在する場合はn回後のパラメータが辞書オブジェクトとして得られます。

保存したら実験を実行してみましょう。終了後にtrial-by-trial記録ファイルとxlsx記録ファイルを開くと、 図7.5 のようにresponseという名前の列が存在していて、そこに調整後のprobeLenの値が出力されているのがわかります。xlsx記録ファイルにはKeyboardコンポーネントの出力と同様に平均値や標準偏差も出力されています。

_images/adddata-output.png

図7.5 addData( )メソッドによる変数の出力。

これでこの章の実験は完成です。ですが、せっかく 表7.2 にaddData( )以外のTrialHandlerのメソッドを紹介しましたので、少し触れておきましょう。 表7.2 に書かれている通り、TrialHandlerにはgetEarlierTrial( )とgetFutuerTrial( )というメソッドがあります。これらのメソッドを使うと、それぞれ現在のループのn回前、およびn回後の繰り返しで条件ファイルから読み込まれたパラメータのどの値が用いられた(用いられる)かを知る事ができます。例えばtrialsループの内部のルーチンにCodeコンポーネントを配置して、 [Routine開始時][フレーム毎][Routine終了時] のいずれかで以下の文を実行すると、2回前の繰り返しで用いられたパラメータが変数prevParamに格納されます。

prevParam = trials.getEarlierTrial(-2)

prevParamに格納されているのは辞書オブジェクトです。 第4章第5章 でも触れたように、辞書オブジェクトとは実験情報ダイアログの値を保持するのにつかわれているデータ形式です。ですから、実験情報ダイアログと同様に、以下のように書くとangleというパラメータの値を取り出すことができます。

prevParam['angle']

記憶課題の一種に「左右の選択肢のうち、n試行前に提示されていた刺激と一致する選択肢を選べば正解」という課題(n-back課題)がありますが、 [Loopの種類] にrandomやfullRandomを選んでいる場合、n試行前に提示した刺激は無作為に決定されているので条件ファイルで正答を定義することができません。そのような場合にgetEarlierTrial( )メソッドは非常に有効です。

チェックリスト
  • TrialHandlerのインスタンスから現在ループの何回目の繰り返しを実行中かを取得できる。

  • TrialHandlerのインスタンスから現在ループの繰り返し回数が残り何回かを取得できる。

  • TrialHandlerのインスタンスから現在ループの総繰り返し回数を取得できる。

  • 上記3項目の値を使って「現在第n試行」、「残りn試行」、「全n試行」といったメッセージをスクリーン上に提示できる。

  • Codeコンポーネントを用いて、実験記録ファイルに出力するデータを追加することができる。

  • 現在実行中の繰り返しのn回前、n回後に使われるパラメータを取得するコードを記述することができる。

7.5. プローブの長さが一定範囲に収まるようにしよう

すでにこの章で目的とする実験は完成しているのですが、if文の練習を兼ねて少し改造してみましょう。exp07a.psyexpでは、実際にする人がいるかどうかは別として、テスト刺激に重なったりスクリーンからはみ出してしまったりするくらいプローブを大きくすることができてしまいます。また、プローブをどんどん小さくしていけばいずれ長さは0になり、負の値になってしまいます。長さは負の値をとることができませんので、そこで実験はエラーとなり停止してしまいます。このような事態を避けるために、if文を使ってプローブの長さが0.05から0.35の範囲を超えて短くしたり長くしたりできないようにしてみましょう。

作成済みのexp07a.psyexpをexp07b.psyexpという別名で保存してください。そしてtrialルーチンのCodeコンポーネントを開いてください。 [フレーム毎] に入力してあるコードによってプローブの長さが変わるのですから、ここのコードを書きかえると長さを一定の範囲に制限することができるはずです。ひとつの問題を解決するための方法を一度に何通りも紹介するのはよくないかも知れませんが、ここではif文の練習なので2通りの方法を考えます。

_images/restrict-probe-length.png

図7.6 プローブの長さを制限する方法その1。exp07a.psyexpのCodeコンポーネントに書いたコードに組み込む処理を日本語で記入したものを左側に、組み込む処理に対応するコードを右側に示しています。

第一の方法は、長さを0.005増加させた時に0.35より大きくなっていないか確認して、なっていれば0.35に修正し、長さを0.005減少させた時に0.05未満になっていないか確認して、なっていれば0.05に修正するというものです。入力済みのコードに日本語で処理を書きこむと 図7.6 の左のようになります。ご覧のとおり、if文で分岐した後にまた「もし~なら…」という分岐処理が含まれる形になっています。if文では、このような「入れ子」になった条件分岐も書くことができます。「probeLenが0.05未満なら0.05にする」という部分だけを考えると、これは0.05以上なら何もしないということですから、elseは省略できて 図7.6 右上のように書けます。同様に「probeLenが0.35より大きければ0.35にする」という処理も 図7.6 右下のように書けます。 図7.6 左側のコードの日本語で記入した部分に、 図7.6 右側の対応するコードを埋め込むと、 図7.7 左に示すコードが得られます。これだけで完成です。

_images/restrict-probe-length-code.png

図7.7 if文を組み込んで得られたコード。if、elif、elseの及ぶ範囲は、下方向に向かってこれらの語が出現した行と字下げ幅が狭いか同じ行に出会うまでです。if (2)が半角スペース4文字字下げされているので、if (2)の次の行はif (2)よりさらに4文字字下げして8文字字下げとなります。

図7.7 左のコードをもう少ししっかり見ておきましょう。 図7.7 左のコードの冒頭部分を拡大したのが 図7.7 右です。ifが及ぶ範囲は字下げの量で決まります。Pythonはifを発見すると、コードを下へ読み進めていって、これらの語が出現した行と字下げ量が同じか少ない行の直前の行までをifの条件式が及ぶ範囲と見なします。このことを念頭に置いて 図7.7 右のコードを見ると、1行目のif文(if (1)とします)の及ぶ範囲は5行目のelif文の手前まで、すなわち4行目までであることがおわかりいただけると思います。if (1)の条件式が真であれば、4行目までのコードが実行されます。一方、3行目のif文(if (2)とします)の範囲はどこまでかと言いますと、これも5行目のelif文の手前まで、すなわち4行目までです。ですからif (2)の条件式が真であれば、4行目だけ実行されます。もちろんif (2)はif (1)の範囲に入っているので、if (2)が実行されるためにはif(1)が真でなければいけません。if (1)が真でif (2)が偽であれば、2行目だけが実行されます。elif、elseが及ぶ範囲もifと同様に決まります。なお、本書では字下げをすべて4文字としているので 図7.7 の説明で問題ないのですが、web上で誰かが書いたPythonのコードを流用する時にはそのコードが異なる字下げルールを使っているかもしれません。そのようなコードをコピーするときの注意点を「 7.7.3:Pythonコードの字下げについて」に記しておきますので参考にしてください。

では、exp07b.psyexpを開いて、 図7.7 左のコードをtrialルーチンのCodeコンポーネントの [フレーム毎] に入力してください。すでに細字の部分は入力済みのはずなので太字部分を入力するだけでいいはずです。入力したら実験を保存して実行し、プローブの長さが一定以上伸びたり縮んだりしないことを確認してください。プローブ長が0.05や0.35に達するまでカーソルキーを連打するのが面倒くさい!という方は実験をいったん終了して、Codeコンポーネントを編集してカーソルキーの左右を押したときにprobeLenが増減する量を±0.005から±0.05などに変更して実行してみましょう。

続いて第二の方法の解説です。exp07b.psyexpを閉じてexp07a.psyexpを開きなおして、exp07c.psyexpという名前で保存して作業しましょう。第一の方法では、probeLenの長さを増減した直後に範囲外に出てしまっていないかを確認しましたが、第二の方法では一連のif-elif-elseが終わってからprobeLenを確認します。 図7.8 にこの方法を用いたコードを示します。ここでのポイントは、ifに対応するelif、elseが置ける範囲です。 図7.8 に記した通り、1行目のif文(if (1)とします)に対応するelif、elseが置けるのは、if (1)と字下げが同じでelif、else以外から始まる行が出てくるか、if (1)より字下げが少ない行が出てくる直前の行までです。 図7.8 のコードでは、2つ目のif (if (2)とします)が出現した時点でif (1)が終了していますので、if (1)から続く一連のif、elifの結果がどうであろうと必ずif (2)の条件式は評価されます。elifで条件式を列挙した場合は手前のifやelifで真になった時に全く評価されなかったのと対照的です。

_images/restrict-probe-length-code-2.png

図7.8 プローブの長さを制限する方法その2。ifに対応するelif、elseを置ける範囲に注意。

exp07c.psyexpはexp07a.psyexpを別名で保存して作成したので、trialルーチンのCodeコンポーネントの [フレーム毎]図7.8 のコードのif (2)の手前まですでに入力済みのはずです。そこへ、 図7.8 のコードの最後の4行(if (2)に対応する部分)を追加入力してください。continueRoutine = Falseという行とif (2)の最初の行の間は空白行を入れても入れなくても動作しますが、1行空白を入れておいた方が後から見直した時にここで新たなif文が始まることがわかりやすくてよいでしょう。入力を終えたら、exp07c.psyexpを保存して実験を実行してみてください。exp07b.psyexpの時と同様に、プローブの長さが一定以上伸びたり縮んだりしないはずです。キーを連打するのが面倒な方はやはりexp07b.psyexpの時と同様に、一回のキー押しでprobeLenを増減する量を大きくして試してみましょう。

以上でif文の練習は終わりですが、最後にひとつ補足しておきます。第一の方法は入れ子になったif文の練習のためにまず「カーソルキーの左が押されたか」を判定してprobeLenの長さを変更してから「probeLenが0.05未満か」を判定するという二段階の判定を行いましたが、第6章 で学んだ論理演算子を使えば一度に判定することができます。probeLenの初期値は170、190、210、230の4通りしかなくて、±0.005ずつしか増減しないのですから、0.005を引いて0.05未満になるのはprobeLenが0.05の時のみです。ということは、「カーソルキーの左が押されていて、なおかつprobeLenが0.05より大きい」時にはprobeLenから0.005を引いても0.05を下回ることはありません。したがって、論理演算子andを使って一つの条件式として記述できます。

if 'left' in theseKeys and probeLen > 0.05:
    probeLen -= 0.005

同様に、probeLen に0.005を加えて0.35より大きくなるのはprobeLenが0.35に達しているときだけですから、「カーソルキーの右が押されていて、なおかつprobeLenが0.35未満」の時にはprobeLenに0.005を足しても0.35を超えません。したがって、この条件はandを使って一つの式として記述できます。

elif 'right' in theseKeys and probeLen < 0.35:
    probeLen -= 0.005

この方法の弱点は、probeLenの増減量が可変である時にはかえって複雑になってしまうことです。その場合は 図7.7図7.8 に示した方法を用いた方がすっきりとしたコードが書けます。この「増減量が可変」な実験を作成することを練習問題として、この章を終えることにしましょう。

チェックリスト
  • if文の中に入れ子上にif文を組み込んだコードを記述することができる。

  • ifやelifの条件式が真であった時に実行されるコードがどこまで続いているかを判断することができる。if、elifの条件式が全て偽でelseまで進んだときに実行されるコードがどこまで続いているかを判断することができる。

  • 一連のif-elif-elseの組み合わせがどこまで続いているかを判断することができる。

  • 論理演算子を用いて複数の条件式をひとつの式にまとめることができる。

7.6. 練習問題:プローブ刺激の伸縮量を切り替えられるようにしよう

exp07a.psyexpをベースにして、プローブの伸縮量(probeLenの増減量)を切り替えられるようにしてみましょう。二通りの実現方法を挙げますので、ぜひ両方の方法の実現に挑戦してください。

  • 実現方法その1

    • Shiftキーを押すと、伸縮量が±2と±10で切り替わる。余力がある人はTextコンポーネントを使って現在の伸縮量を画面上に提示すること。

      • ヒント1:伸縮量の絶対値を保持する変数を一つ用意して、Shiftキーが押されたら値を切り替える。

      • ヒント2:伸縮量の絶対値を保持する変数には、ルーチン開始時に初期値を与える必要がある。

  • 実現方法その2

    • Xキーを押すと-10、Cキーを押すと-2、ピリオド( . )キーを押すと+2、スラッシュ( / )キーを押すと+10伸縮する。

      • この方法についてはヒントなし。

「どちらもあっさりできてしまった」という方は、以下の問題にも取り組んでみてください。

  • 実現方法その1、その2共通

    • 図7.8 の例のように、キー入力に関する処理を終えた後にprobeLenが範囲を超えていないかを確認して必要があれば値を修正すること。ただし、その際if文を使わずに、第5章 に出てきた関数を使って「1行で」処理を記述すること。

      • ヒント:二種類の関数を使う必要がある。

7.7. この章のトピックス

7.7.1. 複数キーの同時押しの検出について

キーボードは製品によってテンキーと呼ばれる独立した数字や四則演算のキーがあったり、音量を調節するためのキーがあったり、いろいろなものがありますが、特殊なものを除けば80個以上のキーがあります。これらのキーは物理的には複数個同時に押すことはできますが、文書を書くなどの一般的な用途では「PとSとYのキーを同時押しする」といった具合に複数の文字キーを押すことはありません。ですから、市販されているキーボードの中には、ShiftやCtrlといった特殊なキーを除いて、複数キーの同時押しを検出することを前提に設計されていないものがあります。例えば筆者が使用しているキーボードの中には、F、G、H、Jのキーを同時に押すとFとJのみ、GとHのみといった具合にいずれか2つのみしか同時に認識しないものがあります。一方、これらの4個のキーの同時押しを認識させることができるキーボードもあります。

通常の文書入力では4つのキーの同時押しを検出できなくても困ることはまず無いのですが、キーボードを使用して操作するアクションゲームの場合は大きな問題になり得ます。ですから、ゲーム用と銘打って販売されているキーボードは多くのキーを同時押しできるように設計されています。中にはすべてのキーの同時押しを検出できる製品もあります。

Builderは、同時押しに対応したキーボード、対応していないキーボードのどちらが接続されていて同じコードで処理できるように、変数theseKeysに押されたキー名を必ずリストとして保存するように作られています。複数キーを同時押ししているにも関わらずtheseKeysに押したキー名が含まれていない場合は、そのキーの組み合わせが使用中のキーボードで検出できない組み合わせである可能性があります。

7.7.2. メソッドの第一引数について(上級)

TrialHandlerの主要クラスメソッドの表( 表7.2 )において、getEarlierTrial( )の引数はnの1個だけしか示されていません。しかし、Pythonインタプリタ上でpsychopy.dataをimportしてhelp(psychopy.data.TrialHandler)を実行してgetEarlierTrial( )のヘルプを見ると、selfとnという2つの引数が記載されています。 表7.2 で第一引数selfを省略している理由について簡単に解説します。

公式ヘルプに記載されている第一引数selfですが、これはTrialHandlerに限らずすべてのクラスのメソッドに必ず存在します。これは「インスタンス自身」を指し示す引数です。C言語やC++言語を御存知の方には、「インスタンスへのポインタが渡される」と言えばわかりやすいかも知れません。クラスの定義に関するPythonの文法を解説していないのでどうしても不正確な説明にしかならないのですが、このselfはインスタンスが自分自身に格納されたデータ属性の値を知るために必要なもの、と思っておいてください。

Pythonの文法では、メソッドを呼び出す時にselfは省略して記述すると定められています。ですから、引数がselfのみしかないfooというメソッドを呼び出す場合はfoo( )という具合に括弧の中は空白にします。TrialHandlerのgetEarlierTrial( )を呼び出す場合には、このメソッドにはselfとnという2つの引数があるので、selfを省略してgetEarlierTrial(n)と書きます。 表7.2 では、実際にコードを書くときの表記と一致させることを重視してgetEarlierTrial( )の引数としてnのみを記載しています。

なお、同じくヘルプのaddData( )メソッドの引数を見ると、self、thisType、valueに加えてさらにposition=Noneと書かれています。これはデフォルト値付き引数と呼ばれるもので、「引数positionが渡されなかった場合はNoneが渡されたと解釈する」ということを意味します。ですから、本文中でtrials.addData('response', probeLen)としたようにpositionに相当する引数を渡さなくてもエラーにならなかったのです。getEarlierTrial( )の引数nもn=-1という具合にデフォルト値付き引数として定義されているので、本文中や 表7.2 で述べたようにnを省略することができるのです。

7.7.3. Pythonコードの字下げについて

第6章 において、Python Enhancement Proposals (PEP)という公式文書で半角スペース4文字の字下げが推奨されていることを紹介しました。PEPはPythonの言語仕様やPythonプログラマのコミュニティ向けの情報などを記述した文書の集合で、その中のPEP-8というPythonのコードの書き方を定めた文書に「半角スペース4文字を使いなさい」と記されています。

しかし、Python以外のプログラミング言語では字下げにTab文字や8文字の半角スペースなど、さまざまな字下げが使用できるためか、Pythonでも半角スペース4文字以外の字下げを使用できるようになっています。 図7.9 は、2文字や6文字の字下げが混在しているコードの例を示しています。 図7.9 左のように字下げが混在していてもそれぞれのブロック内で字下げが統一されていれば動作します。例えば 図7.9 の(3)は直前のifに対する字下げが6文字、(4)は直前のelseに対する字下げが4文字であり、一連のif-else文にも関わらず字下げが一致していません。しかし、(3)、(4)のブロック内でそれぞれ字下げが一貫しているのでPythonは適切にこのコードを解釈して実行することができます。それに対して 図7.9 の(5)では、最初の2行の字下げが4文字であるにもかかわらず最後のi+=1の字下げは3文字であり、(5)のブロック内で一貫していません。従って、 図7.9 右のコードはエラーとなり実行できません。

_images/indent-in-python-code.png

図7.9 4文字以外の半角スペースによる字下げ。それぞれのブロック内で字下げが一貫していればエラーにはなりません。

スペースとTab文字が混在しているとさらに事態は複雑になります。基本的には、Tab文字は半角スペース8文字に置き換えられます。しかし、半角スペース8文字未満にTab文字が続く場合は、半角スペースとTab文字を合わせて半角スペース8文字と解釈されます。 図7.10 の例をご覧ください。 図7.10 左のコードの最終行は、4文字の半角スペースより前にTab文字がありますから、Tab文字が半角スペース8文字分に解釈されて合計半角スペース12文字と解釈されます。従って、 図7.10 左のコードはエラーとならず実行できます。一方、 図7.10 右の最終行は、左と同じTab文字と半角スペース4文字の組み合わせなのですが、半角スペースがTab文字より前にあります。この場合、半角スペースとTab文字を合わせて半角スペース8文字と解釈されますので、直前のif文と字下げ量が同じとなってしまいエラーになります。

以上のように、Pythonのスクリプトでは字下げは半角スペース4文字でなくても動作します。しかし、混乱を避けるためにはやはりPEP-8に従って半角スペース4文字で統一するべきだと思われます。

_images/tab-in-python-code.png

図7.10 Tab文字による字下げ。基本的にはTab文字は半角スペース8文字に置換されると考えておけばよいですが、左の例のように半角スペースの後ろにTabがある場合は半角スペースとTabをまとめて8の倍数個のスペースとして解釈されてしまいます。