.. title:: Pythonで心理実験 - 例題19-2 例題19-2:Builderと見せかけてCoder ================================================== **A:** さて、今回は作者のSがテンパっているので雑談はなし。いきなり本題に入る。 **B:** 雑談がないと気が引き締まりますね。で、今日のお題はなんですか? **A:** PsychoPyのBuilderで直接Pythonのコードを挿入する方法を紹介する。 **B:** へ、なんですかそれ。そんなこと出来るんですか? **A:** BuilderのコンポーネントにCodeというのがある。こいつを使うと直接Pythonのコードをごりごり書くことが出来る。 .. figure:: img/19-2-01.png **B:** なんでニヤついてるんですか。気持ち悪い。 **A:** いやいやいや。便利そうだろう? 次は何の解説をしようと思っていじっていて、これだッ!ってピーンときたね。 **B:** Builderの良さを最大限にいかすには、まずは普通にBuilderのコンポーネントを使った実験を作る解説をいくつかするべきだと思いますが。これだからこの変態は…。 **A:** これだってフツーのBuilderのコンポーネントじゃんか♪ **B:** …(呆れ顔) **A:** まあまあ、まずはサンプルを見てくれよ。 + PsychoPy Builderのpsyexpファイルをダウンロード→ `19-2a.psyexp `_ + 上記psyexpファイルと同一ディレクトリに置く条件ファイル→ `19-2a.xlsx `_ **B:** ダウンロードしました。 **A:** じゃさっそく開いて。まずFlowから見ていこうか。このサンプルは非常にシンプルなフローで、教示を表示するRoutineがひとつ、そして刺激を提示するRoutineがひとつ。Loopがひとつ。 .. figure:: img/19-2-02.png **B:** うわっ、なんだ1×160って。160条件? **A:** それは後で詳しく触れるとして、Routineの中身を見てみよう。 .. figure:: img/19-2-03.png **B:** いっぱいありますね。えーと。 **A:** 難しいことは何もしていない。単にカーソルキーの左右が押されるまで次のような画面を表示するだけだ。例題18の復習として各自で確認しておいてほしい。 .. figure:: img/19-2-04.png **A:** 続いて刺激を提示するのRoutine。見ての通り1.5秒でひとつの試行が自動的に終了して直ちに次の試行へ進む。休憩は一切ないので参加者は集中力を切らさないようにしないといけない。 .. figure:: img/19-2-05.png **B:** で、参加者は何をするんですか? **A:** さっき教示画面が出ただろ。以下のように画面中央に白い + が表示されるのでそれをじっと注視して、+ の周囲にグレーティングが表示されたらその向きをカーソルキーの左右で答える。 .. figure:: img/19-2-06.png **B:** + を注視してなんて書いてなかったじゃないですか…って、これ、グレーティングなんてどこにあります? **A:** よく見ろよ。右下に出てるだろ。 **B:** ああっ、こんなに薄いんですか? こりゃ手ごわいぞ。 **A:** 教示画面みたいにコントラストが高かったら全部見えちゃって実験にならないだろ。とにかく1.5秒×160試行=4分間休憩なしに次々と刺激が出てくるので、やってみようと思う人は覚悟しておいてほしい。まあ知覚の研究をしている院生さんとかなら楽勝だろうけど。 **B:** このグレーティングがじわっと出てくるのはどういう仕組みになっているんですかね。 **A:** trial Routineで表示されるグレーティングの設定ダイアログを見たまえ。まあこれは例題18の集大成というかいい練習問題になっているので、ぜひ自分で理解してみてほしい。なお、orientation、gx、gy、frequencyはxlsxファイルから読み込まれる変数である。 .. figure:: img/19-2-07.png **B:** ええと、Opacityってのがじわっと出てくる部分のカギですかね。maxというのは引数の中で最大のものを返すんだから…、図を描いてみるとわかりやすいぞ。最初の1秒はsinの値の方が大きくて、1から1.5秒の間は0の方が大きくなるんだ。 .. figure:: img/19-2-08.png **A:** お、いいぞいいぞ。その調子。 **B:** Opacityは0だと完全に透明だから、要するに1.5秒間のうち最初の1秒間だけしか画面に表示されていないんですね。 **A:** よしよし。 **B:** あとMaskの欄のgaussっていうのは… **A:** グレーティングのボケに対応している。ここが空欄なら長方形のグレーティング、circleなら円形に切り抜かれたグレーティングになる。 **B:** ふむふむ。 **A:** さて、問題はxlsxファイルなんだ。 .. figure:: img/19-2-09.png **B:** 問題って何が問題なんですか。 **A:** グレーティングの空間周波数が0.5から16cpdの10種類、グレーティングの方向が-30度と30度の2種類。さらにグレーティングの出現位置が注視点から5.0度の距離で0度、45度、90度…の8方向。10×2×8=160条件。 **B:** ええ、さっきぼくが言った事ですね。 **A:** 今回のサンプルでは単に空間周波数と正答率の関係だけを見たいという設定なんだ。だから刺激の位置や方向は分析の時には潰してしまいたいんだが、Builderが作るデータファイルでは位置違い、方向違いはすべて別条件とされるので、例えば空間周波数0.5cpdの時の正答率を計算しようと思ったら赤で囲った行の青い列の値をずっと拾っていかないといけない。 .. figure:: img/19-2-10.png **B:** 拾えばいいじゃないですか。 **A:** 面倒くさいだろ。 **B:** 普段そういうデータファイルを分析するスクリプトを書いてるくせにこのおっさんは…。 **A:** それに何より、こういう条件ファイルを作るのが面倒くさいし、間違ってないか確認するのはもっと面倒くさい。 **B:** 普段そういう条件リストを作るスクリプトを書いてるくせにこの(ry **A:** あー、うるさいうるさい。話の腰を折るな。とにかく、例題18で扱った範囲でのBuilderの使い方で最大の弱点は試行毎に変わる刺激のパラメータは全て条件ファイルに書く必要があることだ。今回はCodeコンポーネントを使ってちょいちょいっとこの弱点を乗り越えてみせる。 **B:** ほほう。そういう前振りですか。 **A:** というわけで次のサンプル、19-2b.psyexpをダウンロードしてほしい。 + PsychoPy Builderのpsyexpファイルをダウンロード→ `19-2b.psyexp `_ + 上記psyexpファイルと同一ディレクトリに置く条件ファイル→ `19-2b.xlsx `_ **B:** 開きましたよ。 **A:** じゃあ変更点を確認しようか。まずFlow、10条件×10回繰り返しになっている。 .. figure:: img/19-2-11.png **A:** これに対応してxlsxファイルの中身はぐっとシンプルになった。 .. figure:: img/19-2-12.png **B:** うわ、本当にシンプルになりましたね。frequencyの一列だけですか。 **A:** instructionのRoutineは変更なしなのでパス。trialのRoutineはcodeコンポーネントが追加された。 .. figure:: img/19-2-13.png **B:** こいつがキモですね。 **A:** これがcodeコンポーネントの設定ダイアログ。だいたい想像がつくと思うんだが、それぞれの欄の意味は以下の通り。 .. figure:: img/19-2-14.png .. csv-table:: :delim: $ name$Codeコンポーネントの名前。Pythonの変数名として使える文字列しか使用できない。例えば日本語は使用できない。 Begin Experiment$実験の開始時に一度だけ実行されるコード。特殊なモジュールのimportや変数の初期値の設定などを行う。 Begin Routine$codeコンポーネントが含まれるRoutineの開始時に実行されるコード。Loopを使ってRoutineを繰り返し実行する時には、繰り返しの度に一度実行されることになる。 Each Frame$codeコンポーネントが含まれるRoutineの実行中にフレーム毎に実行されるコード。フレーム毎に刺激の位置や色を変更したりしたい場合や、Routineの途中で変更された変数の値に応じて刺激を切り替えたい場合などに用いる。 End Routine$codeコンポーネントが含まれるRoutineの終了時に実行されるコード。参加者の反応に応じて次の試行の条件を変更したい場合などに用いる。Loopを使ってRoutineを繰り返し実行する時には、繰り返しの度に一度実行されることになる。 End Experiment$実験の終了時に一度だけ実行されるコード。独自のデータ処理、保存などを行いたい場合などに用いる。 **B:** ふむふむ結構いろんなことが出来そうですね。 **A:** このサンプルでは19-1a.psyexpではいちいち条件ファイルで指定していたorientation、gx、gy、answerをcodeコンポーネントを使って生成している。まずBegin Experimentで乱数生成用のモジュールrandomをimport。ついでにgx、gy、orientationは初期値がないとエラーが生じる場合があるので適当な値を代入しておく。 **B:** エラーが「生じる場合がある」ってのはどういうことですかね。 **A:** あー。多分constant/set every repeat/set every frameの設定と関係あるだろうってアタリはつけてるけど、きちんと検証してないんで今のところはテキトーな値を代入しといてって言っておく。代入しとけば設定に関係なくちゃんと動く。 **B:** テキトーって、どんな値でもいいんですか? **A:** もちろんorientation(回転角)なのに文字列とかいれちゃ駄目だぞ。そこは常識で。実験で使う値のどれかを入れておけばいいんじゃないかな。 **B:** あの、eccというのは…。 **A:** ああ、これは偏心度を実験開始時のinfoダイアログ( :doc:`例題18-4参照 <18-4>` )で設定できるようにしている。infoダイアログから毎回入力して変更したい場合のサンプルだね。 **infoダイアログで設定した値はexpInfoというdictオブジェクトに格納されている** ので、eccという項目であればexpInfo['ecc']とすればユーザーが入力した値を取得できる。値は文字列なので、今回のように数値として使う場合は小数ならfloat()、整数ならint()を使うなどして変換する。 :: ecc = float(expInfo['ecc']) **B:** なるほど。このテクニックは応用範囲が広いかもしれませんね。 **A:** で、続き。Routineの最初では、その試行での刺激の位置と方向をrandom.randintを使って設定している。B君、random.randintは何をする関数だった? **B:** 整数の乱数を返す関数でしたかね。random.randint(m,n)ならmからnまでの整数。 **A:** その場合、nは含む? **B:** ええと、ええと…どうでしたっけ。 **A:** **random.randint(m,n)はnを含む。一方、よく似た名前だがnumpy.random.randint(m,n)ではnを含まない** 。非常に間違えやすいので気を付けるように。 **B:** (これは自分が間違えたパターンだな…) **A:** 何か言ったか? **B:** いや、別に~。 **A:** まず0から7の乱数を発生させてposIndexに代入。この値とeccを使って刺激の座標(gx, gy)を決定。ここはこれ以上解説の必要はないよね。 :: posIndex = random.randint(0,7) gx = ecc*cos(posIndex/4.0*pi) gy = ecc*sin(posIndex/4.0*pi) **B:** はい、この程度なら。 **A:** そして次は刺激の方向の決定。これはキー押しの正答(answer)の決定と連動しているのでちょっとややこしい。まず0か1の乱数を発生させて、ansIndexに代入。これは問題ないだろう。 :: ansIndex = random.randint(0,1) answer = ['left','right'][ansIndex] orientation =[-30,30][ansIndex] **B:** はいはい。右か左の2通りだから0か1ですよね。 **A:** それに続く2つの文はわかるか。 **B:** ふふっ、僕も日々成長しているのです。この程度なら僕にもわかるのです! まず前の[ ]は'left'と'right'を要素とする長さ2のリストを作る演算子です。そして後ろの[ ]は前の[ ]で作れたリストの何番目の要素を取り出すかを示す演算子です。だから、ansIndexの値が0なら'left'、1なら'right'が得られるのです!! .. figure:: img/19-2-15.png **A:** ありゃ、正解だわ。ここはボケる流れじゃないの? **B:** えっへん、僕を甘く見ないでほしいですね! **A:** うーん、予想外の流れだが、B君の言うとおり。そうしたらorientationを決めている行は説明する必要ないよね。 **B:** はい、ansIndexが0なら-30、1なら30が得られるます。 **A:** なんだよ、得られるますって。 **B:** ちょ、ちょっと舌をかんだだけじゃないですか! **A:** ま、B君らしいなあというところで解説終了。簡単だろ? **B:** んー。今回解説してもらった範囲は簡単でしたが、自分で応用が利くかといわれると難しいかなあ。 **A:** ある程度分かっている人か、あれこれ失敗を楽しみながら試行錯誤できる時間的余裕がある人じゃないとしんどいかも知れんね。私も最初は **こんなの直接スクリプト書ける人じゃないと使えねえし、書ける人はわざわざBuilderなんか使わんでも最初からエディタで書くやろうが** と思っていたが、最近はBuilderをある程度マスターした人がステップアップして直接コードを書くようになる最初のステップとして使う分には面白いんじゃないかと思っている。だからこそ今回取り上げたわけだが。 **B:** ふーん。確かにゼロからスクリプトを書くよりはまずこういうちょっとした部分から始めるのはいいのかも知れませんね。 **A:** 最後にひとつだけ注意。19-2b.psyexpでは乱数で刺激の方向を決定しているので、右と左の試行数が一致するとは限らない。ていうか一致しないと思っておいた方がいい。きちんと揃えたい人はrandom.randintに頼らず工夫する必要がある。 **B:** どう工夫すればいいんですか? **A:** そりゃーそこまで言っちゃったら甘過ぎよ。練習問題ね。 **B:** えー。 **A:** ヒントとしては、実験の最初に長さが総試行数と一致していて'left'と'right'を同数含むリストを作っておいて、random.shuffleで無作為に並び替えておけばいい。現在第何試行か保持しておく変数を用意しておく必要があるかな。random.shuffleを使う例は今までの例題で繰り返し出ているので参考にしてほしい。えーと、例題18あたりから読んでおられる読者の皆様向けとしては…やっぱり例題1かな。例題1はVisionEggを使っているので、PsychoPyを使う場合は…、ちょっと初心者向けではありませんが例題19-1かな。 **B:** ヒントはそれだけ? **A:** 本当に初心者の方以外はまずこの程度のヒントだけで頑張ってほしいな。さて、19-2b.psyexpで実験を行った場合のデータファイルがこれ。赤枠のところは正に今回欲しかった各空間周波数の時の正答率になっているので、後処理が必要なくなった。また、青枠のところにeccの値が出力されているので、注視点から刺激の距離を何度に設定したかもここを見ればわかる。 .. figure:: img/19-2-16.png **B:** ふむ。計算したい正答率がズバリと出力されているというのは気持ちいいですね。8cpdあたりでスカッと正答率が落ちているぞ。 **A:** まあこの辺りが閾値ということだな。同一実験参加者で偏心度(注視点と刺激の距離)を変えながらデータを取ってみると面白いかもしれないね。まあこれも練習問題ということで。 **B:** 最初にcodeコンポーネントを扱うと聞いた時にはまた変化球な例題を…と思いましたが、意外と面白かったです。いやはや。 **A:** そうそう。今回のサンプルは16cpdまで刺激を用意しましたが、使用するディスプレイや観察距離によっては16cpdは表示の限界を超えてエイリアシングが起きるかもしれません。サンプルを実行してみて高空間周波数の刺激が提示されているはずの時にムラがある荒いグレーティングが表示される場合は、観察距離を近くする、ドットピッチの細かいディスプレイを使用する、条件ファイルを書き換えるなどの工夫をしてみてください。 **B:** エイリアシング? **A:** サンプリング周波数の半分以上の周波数成分を持つ信号をサンプリングすると、元の信号には含まれていなかった成分があたかも存在するように見えてしまうことだ。詳しくはサンプリング定理とかエイリアシングとかナイキスト周波数で検索してみてくれ。というわけで、今回はこの辺りでおしまいにしておこうかな。ではでは。