11. 無作為化しよう―視覚探索

11.1. この章の実験の概要

ここまでの解説において、Builderがおこなう無作為化は「繰り返し順序の無作為化」でした。つまり、繰り返されるパラメータはすべて事前に条件ファイルで与えられていて、繰り返し順序だけが無作為になっていました。これで充分な実験手続きも多いのですが、パラメータ自体を無作為に決定したい場合もあります。そういった手続きの例として、本章では視覚探索課題を取り上げます。

図11.1 に本章の実験の概要を示します。 図11.1 の(1)では、スクリーン上に切れ目が入っている円(以下Cと表記)が複数個提示されていますが、50%の確率で一つだけ切れ目がない円(以下Oと表記)が含まれています。実験参加者は、できるだけ速く正確に、Oが含まれているか居ないかを判断しなければいけません。容易に予想できる事ですが、Oの有無を判断するまでに要する平均時間はスクリーン上に提示されている図形(以下アイテムと表記)の個数にほぼ比例して増加します。ところが、 図11.1 の(2)のように、OとCを入れ替えて、複数個のOの中からCの有無を判断する課題に切り替えると、アイテム数が増えても(1)ほど反応時間が増加しません。この現象を視覚探索の非対称性と呼び、 図11.1 下のようにアイテム数と平均探索時間の関係をプロットしたグラフを探索関数と呼びます。参加者が探し出すべきアイテムをターゲットと呼び、それ以外のアイテムをディストラクタと呼びます。(1)の課題ではOがターゲットでCがディストラクタ、(2)の課題ではCがターゲットでOがディストラクタです。この章では、アイテム数を5個、10個、15個と変化させながら(1)の課題と(2)の課題を行う実験を作成します。

_images/search-asymmetry.png

図11.1 視覚探索の非対称性。スクリーン上に提示されているアイテム数が多いほど判断に時間がかかりますが、CのなかからOを探すより、Oの中からCを探す方がアイテム数増加に伴って反応時間が急激に増加します。

図11.2 に具体的な手続きを示します。実験を実行すると、準備ができたらカーソルキーの左右を押すように促す教示が提示されます。このスクリーンを1.とします。実験参加者が自分でキーを押すことによって実験が始まります。各試行の最初には、スクリーン中央に固視点として [文字の高さ $] が24pixの「+」の文字が提示されます。このスクリーンを2.とします。実験参加者は固視点を注視しながら刺激の提示を待ちます。待ち時間は試行毎に1.0秒、1.5秒、2.0秒のいずれかから無作為に選びます。待ち時間が終了したら、スクリーン上に刺激が提示されます。刺激はアイテム数が3種類(5個、10個、15個)×アイテムの種類が4種類(すべてO、すべてC、Oの中にひとつだけC、Cの中にひとつだけO)=12種類のいずれかです。実験参加者は、刺激の中に「ひとつだけ周囲と異なるアイテムが存在するか否か」を判断します。ひとつだけ周囲と異なるアイテムが存在する場合はカーソルキーの右、すべて同じアイテムの場合はカーソルキーの左をできるだけ速く、正確に押します。反応に制限時間は設けず、参加者が反応するまで刺激を提示します。参加者が反応したら自動的に次の試行が開始されスクリーン2.(固視点が提示される画面)へ戻りますが、80試行毎に休憩のためにスクリーン1.へ戻って参加者のスペースキー押しを待ちます。12種類の条件に対して20試行ずつ、合計240試行を無作為な順序に実施したら実験は終了します。総試行数が240試行で80試行毎にスクリーン1.が挿入されるのですから、スクリーン1.は実験開始直後、80試行終了時、160試行終了時の3回提示されます。

_images/search-asymmetry-procedure.png

図11.2 実験の手続き。

刺激の詳細を 図11.3 に示します。アイテムの位置はスクリーン中央に設定された仮想的な6×6の格子上から無作為に選ばれます。格子の各マスの幅および高さは100pixです。スクリーン中央の座標が[0, 0]で、グリッドの全幅は500pixですから、グリッドの一番右上の座標は[250,250]、一番左下の座標は[-250, -250]です。一番上の段の座標を左から右に向かって順番に書くと、[-250, 250]、[-150, 250]、[-50, 250]、[50, 250]、[150, 250]、[250, 250]です。

_images/search-asymmetry-stimulus.png

図11.3 刺激の配置。アイテムの位置は仮想的な6×6のグリッド上から試行毎に無作為に選択されます。

アイテムの直径はO、Cともに30pixとし、Cの場合は円の一部に幅10pixの切れ目を入れます。切れ目の位置は、アイテムの中心から見て右を0度として、時計回りに0度、90度、180度、270度の4種類の中からアイテム毎に無作為に選択します。

以上が実験の概要です。Builderが苦手とするポイント、できる事ならBuilderで作りたくないなあと思ってしまうポイントがいくつか含まれています。これらのポイントはお互いに関連しあっているのですが、以下の4つにまとめられると思います。

  1. 独立に位置や形状が変化するアイテムが最大15個スクリーン上に存在する。

  2. 試行毎にスクリーン上に出現するアイテムの個数が異なる。

  3. 無作為に設定するパラメータが複数個ある。

  4. 休憩が挿入される試行が条件数の倍数になっていない。

まず1.と2.についてですが、大量の視覚刺激が配置されている実験を準備するとき、まっさきに考えたいのが「複数の刺激をまとめてひとつの画像にできないか」ということです。ひとつの画像にできるのならImageコンポーネントひとつで解決するので話は簡単です。しかし、今回の実験の場合、個々の刺激がそれぞれOになったりCになったり、位置が変化したりするので、事前に画像ファイルとして用意するなら膨大な枚数が必要となります。頑張って画像ファイルを作成するのも選択肢の一つですが、本書の目的はBuilderでのテクニックを解説することなので、Builderの力で何とかしたいところです。 一方、個々の刺激をひとつのImageコンポーネントで描画すれば形状や位置の変化に対応しやすくなりますが、試行毎に個数が変化するというのも今までの実験にはなかったことなので、やはり対策を考えいないといけません。

続いて3.と4.ですが、これらはいずれも条件ファイルに関わる問題です。今までの章では、無作為に変化するパラメータは条件ファイルで値を設定してきました。条件ファイルを使う場合は、すべてのパラメータの「組み合わせ」を明示的に記述しなければいけません。今回の実験を今までの章のように条件ファイルで作成しようとすると、固視点の提示時間が3種類ありますので、12種類の刺激と掛け合わせて36条件の条件ファイルになります。この条件ファイルを使うと実現可能な試行数は36の倍数になりますが、240は36で割り切れませんので、この時点で全試行数を240試行にすることは不可能になってしまいました。固視点の提示時間を1.0秒と1.5秒の2種類に減らすと12種類の刺激との掛け合わせで24条件の条件ファイルとなり、240試行にすることが可能になります。しかし、今回の実験ではアイテムの位置もCの向きもすべて無作為なのです。どう工夫しても、これまでの章の条件ファイルと同様の考え方では、総試行数が240試行となる条件ファイルを作成することはできません。

さて困りましたが、本章でのアプローチは「きちんと各条件の試行回数などのバランスがとられてきて、繰り返し順序だけが無作為なもの」と「本当に無作為でいいもの」を切り分けて、「本当に無作為でいいもの」は実行時にBuilderに決めさせようというものです。今回の実験では、アイテム数が5、10、15個の条件が均等な回数実施されること、ターゲットありの条件となしの条件が均等な回数実施されること、Oがターゲットの条件とCがターゲットの条件が均等な回数実施されることの3点は死守したいと思いますので、これらは条件ファイルに任せます。 一方、アイテムが描画される位置、Cの角度、固視点の提示時間は「本当に無作為でいいもの」とします。無作為なので、実験を実施した後に確認すると均等になっていないかもしれませんが、無作為とはそもそもそういうものです。 前置きが長くなりましたが、そろそろ実験の作成に入りましょう。

11.2. 実験の作成

  • 準備作業
    • この章の実験のためのフォルダを作成して、その中にexp11.psyexpという名前で新しい実験を保存する。

  • 実験設定ダイアログ

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

    • 「入力」タブの [キーボードバックエンド] がPsychToolboxか確認する。

  • trialルーチン

    • Codeコンポーネントをひとつ配置して、 [名前]codeTrial にする。今はコードを入力しない。

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

      • 「基本」タブの [名前]fixpoint[終了]delay にする。 [形状] を十字にする。

      • 「レイアウト」タブの [サイズ [w, h] $](0.03,0.03) にする。

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

      • 「基本」タブの [名前]key_resp_trial にする。 [開始]delay に、 [終了] を空白にする。

      • 「基本」タブの [Routineを終了] がチェックされていることを確認し、 [検出するキー $]'right', 'left' とする。

      • 「データ」タブの [正答を記録] をチェックし、 [正答]$correctAns と入力する。

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

      • 「基本」タブの [名前]image00 にする。 [開始]delay に、 [終了] を空白にする。 [画像]imagefile[0] と入力し、「繰り返し毎に更新」設定する。

      • 「レイアウト」タブの [サイズ [w, h] $](0.03, 0.03) と入力する。 [位置 [x, y] $]pos[0] と入力し、「繰り返し毎に更新」に設定する。 [回転角度 $]ori[0] と入力し、「繰り返し毎に更新」に設定する。

    • image00をコピーし、 image01 の名前で貼り付けて以下のように設定する。
      • 「基本」タブの [画像]``imagefile[1]``にする。

      • 「レイアウト」タブの [位置 [x, y] $]pos[1][回転角度 $]ori[1] にする。

    ― 続けてコンポーネントの貼り付けを操作して(image00をコピーした状態になっているはず)、名前がimage14に達するまで1ずつ数値を増やしながら作業を繰り返す。 [画像][位置 [x, y] $][回転角度 $] の[ ]内の数値も1ずつ増やしていく。作業が終了したら、「実験内を検索...」を使ってimagefile、pos、oriを検索してインデックスが適切に変更されているか(これらのパラメータの[ ]の中の値がimage07であれば7、image11であれば11といった具合に一致してるか)確認する。

_images/copy-stims.png

図11.4 iamge00をコピーして名前の数値部分とパラメータのインデックスを1ずつ増やしながらimage14に達するまで繰り返す。終了したら「実験内を検索...」を使って入力ミスがないか確認するとよい。図右の例ではimage10に対するimagefileの[ ]内のインデックスが0のままになっており修正の必要がある。

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

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

    • Codeコンポーネントをひとつ配置して、 [名前]code_rest にする。今はコードを入力しない。

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

      • 「基本」タブの [名前]textRest にし、 [開始]0[終了] を空白にする。 [文字列]準備ができたらカーソルキーの左右どちらか一方を押してください と入力する。

      • [文字の高さ $]0.04 にする。

    • trialルーチンのkey_resp_trialをコピーして、restルーチンに key_resp_rest という名前で貼り付けて以下のように設定する。

      • 「データ」タブの [記録] を「なし」にする。 [正答を記録] のチェックを外す。

  • trialsループ(作成する)

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

    • [繰り返し回数 $]20 と入力する。

    • [条件]exp11cnd.xlsx と入力する。先にexp11cnd.xlsxを作成してから「選択…」ボタンをクリックして選択してもよい。

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

    • numItems、firstItem、otherItems、correctAnsという名前のパラメータを定義する。

    • numItemsは5、10、15の3種類、firstItemはo.png、c.pngの2種類、otherItemsもo.pngとc.pngの2種類の値をとる。これらの全ての組み合わせを入力する。パラメータ名を定義する行を除いて3×2×2=12行の条件ファイルとなる。

    • firstItemとotherItemsが同じ行のcorrectAnsの列にleftと入力する。firstItemとotherItemsが異なる行のcorrectAnsの列にrightと入力する。

  • o.pngおよびc.png(刺激画像ファイル)

    • 背景が透過したPNGファイルを作成し、o.pngという名前でexp11.psyexpと同じフォルダに保存する。o.pngには白色の円を描く。o.pngをc.pngという別名で保存し、円の中心右側に切れ目を入れて保存する(30×30pixで作成したものを http://s12600.net/psy/python/ppb/index.html#ppb-files からダウンロードできます)。

11.3. 特定の条件を満たさない時に実行されるルーチンを活用して休憩画面を設けよう

ではまず、80試行毎に表示される休憩画面の設定から始めましょう。 古いバージョンのPsychoPyではCodeコンポーネントを使って工夫しないといけなかったのですが、「 4.9:動作確認のために一部の動作をスキップしよう 」で紹介した「Routineの設定」を使って簡単に実現できるようになりました。

restルーチンを開いて、ルーチンペインの左上にある「Routineの設定」をクリックしてください。restルーチンのプロパティ設定ダイアログが開くので、「Flow」タブに [条件に合致する場合はスキップ... $] という項目があるのを確認してください。 ここに評価結果がTrueまたはFalseになる式を書くと、Trueのときにこのルーチンをスキップすることができます。 今回の実験では、80試行毎に休憩画面としてrestルーチンを表示させたいので、「 9.11:軌跡データを間引きしよう 」の剰余演算子を使ったテクニックを使って

trials.thisN % 80 != 0:

とするとtrial.thisNが0、80、160以外の時にTrueとなり目標を達成できます。trialループのデータ属性thisNは 表7.1 で出てきた「このループで実行済みの繰り返し回数」でしたね。thisNは0から始まって240回目の繰り返しでは239なので、240になることはない点に注意してください。 では、上記の式を [条件に合致する場合はスキップ... $] に入力してください。

式を入力すると 図11.5 のように != が ≠ に変換されてしまいますが(2024.2.5で確認)、表示だけの問題なので気にしないでください。個人的にはPythonの式が書かれるべき場所なのでこういった小細工なしで正しいPythonの式を表示してほしいのですが、まあ仕方ありません。

_images/skip-routine.png

図11.5 iamge00をコピーして名前の数値部分とパラメータのインデックスを1ずつ増やしながらimage14に達するまで繰り返す。終了したら「実験内を検索...」を使って入力ミスがないか確認するとよい。図右の例ではimage10に対するimagefileの[ ]内のインデックスが0のままになっており修正の必要がある。

なお、この方法だと0回目、つまりtrialsループに入って1回目の繰り返しにもrestルーチンが表示されます。しかし、実験の手続きによっては、最初はループに入る前に詳細な教示画面を出して80回目、160回目は休憩を促す簡素なメッセージにしたいといったケースも考えられます。その場合は論理演算を使って「trials.thisNを80で割った余りが0でないか、trials.thisNが0」といった条件式にすることもできますが、80回目と160回目の2回しかないのですから

trials.thisN not in (80, 160)

のように列挙してしまう方が楽かもしれません。

チェックリスト
  • 指定した条件に当てはまる時にルーチンの実行をスキップさせることができる。

11.4. Codeコンポーネントを使って無作為に固視点の提示時間を選択しよう

続いて配置済みのCodeコンポーネントにコードを入力していきましょう。以下の処理を実現する必要があります。

  • trialルーチンで使われている変数delayの値を決定する。固視点の [開始] が0で [終了] がdelayに設定され、image00からimage14の [開始] がdelayに設定されているので、固視点が出現したdelay秒後に固視点が消失して代わりにimage00からimage14が提示される。

  • ori[0]からori[14]の値を決定する。値は試行毎に0、90、180、270から無作為に選択する。

  • pos[0]からpos[14]の値を決定する。値は試行毎に 図11.3 に示したグリッドから重複がないように無作為に選ぶ。

  • imagefile[0]からimagefile[14]の値を決定する。この値の決め方については後述する。

これらの問題はいずれも「無作為に選択する」という点で共通しているので、同じ方法で解決できます。しかし、delayの決定以外は「アイテム数が5個、10個、15個と変化することにどう対応するか」という問題と併せて考えないといけないので、次節でまとめて考えることにしましょう。まずはdelayの問題を解決します。

無作為に値を選択するには、Builderが内部で用意している乱数関数( 表11.1 )を用います。使い方は非常に単純で、randint(0,5)と書けば0から4の整数の一様乱数からサンプルをひとつ得ることができます。引数high「未満」ですから5を含まない点に注意してください。同様に、normal(50, 10)と書けば平均値50、標準偏差10の正規乱数からサンプルをひとつ得ることができます。引数sizeは、randint(0, 5, size=3)のように使用します。この例の場合、戻り値は[0, 2, 1]といった具合に0から4の整数の一様乱数からサンプルを3つ並べたシーケンス型データが得られます。正確に書くとこの戻り値はnumpy.ndarrayクラスのインスタンスなのですが、numpy.ndarrayについて説明すると脱線が長くなるので「 11.9.1:numpy.ndarray型について 」を参照してください。

表11.1 Builderで利用できる乱数関数。いずれもnumpy.randomからimportされています。

random(size = None)

0.0以上1.0未満の一様乱数のサンプルを返す。sizeがNoneの時(初期値)にはひとつのサンプルを、自然数の場合にはsize個のサンプルを返す。

randint(low, high, size = None)

low以上high未満の範囲の整数の一様乱数のサンプルを返す。lowとhighは整数でなければならない。highがNoneの時には0以上low未満の範囲と見なされる。sizeの働きはrandom( )と同様。

normal(loc=0.0, scale=1.0, size = None)

正規分布に従う乱数のサンプルを返す。locは平均値、scaleは標準偏差に対応する。sizeの働きはrandom( )と同様。

shuffle(x)

リストなどの要素を変更可能なシーケンス型データの要素を無作為に並べ替える。戻り値はない。xの元の順序は失われてしまう点に注意。

randchoice(x)

リストなどの要素を変更可能なシーケンス型データの要素からひとつの値を無作為に取り出す。

さて、今回の実験のように、複数個の選択肢からひとつを無作為に選びだすという用途にはrandchoice( )が便利です。 delayの値は1.0、1.5、2.0の3通りなので、これを並べたシーケンスを引数として以下のようにrandchoice( )を呼ぶだけです。

delay = randchoice( (1.0, 1.5, 2.0) )

exp11.psyexpのtrialルーチンを開いて、codeTrialにこの式を入力しましょう。試行毎にdelayの値を変化させるのですから、入力すべき場所は [Routine開始時] です。

あっさり解決してしまったので、もう少し無作為選択について触れておきましょう。まず、randchoiceはシーケンスの要素にできるものなら何に対しても使用できます。以下の例では文字列を並べたタプルを実験開始時にあらかじめtasklistという変数に代入しておいて、ルーチンの開始前にrandchoice( )で選択を行っています。現在のPCの処理能力だとルーチン開始のたびにタプルを作成しても問題になることはないでしょうが、まあ繰り返しのたびに変化しないものは繰り返しが始まる前に一度処理するだけにする習慣をつけておくのは良いことだと思います。

# 「実験開始時」にタプルを初期化
tasklist = ('一致', '不一致')

# 「Routine開始時」に選択
tasktype = randchoice( tasklist )

今回のように数個しか候補がなければrandchoiceが便利なのですが、多数の数値の中からひとつを選択しないといけない場合、random( )やrandint( )を使った方がよい場合があります。 例として1.0、1.5、2.0をrandint( )で生成してみましょう。 randint(0, 3)とすれば戻り値として0、1、2の乱数が得られますから、戻り値に0.5を掛ければ0.0、0.5、1.0の乱数が得られます。ここへさらに1.0を加えると、1.0、1.5、2.0の乱数が得られます。 従って、以下のコードでdelayの値に1.0、1.5、2.0の中から無作為にひとつ選んで設定できます。

delay = randint(0, 3)*0.5 + 1.0

なお、web上でPythonの乱数について検索すると、randint(low, high)は「low以上high『以下』の値を返す」と書かれている資料がヒットするかもしれません。非常に紛らわしいのですが、このような資料で紹介されているrandint( )と、Builderが内部で参照している乱数関数のrandint( )とは別の関数です。「low以上high『以下』の値を返す」randint( )は、Pythonのrandomモジュールからimportされています。ですから、モジュール名を省略せずに書けばrandom.randint( )という関数です。一方、Builderが内部で準備しているrandint( )はnumpyというパッケージのサブモジュールnumpy.randomからimportされています。省略せずに書けばnumpy.random.randint( )です。気をつけてください。

チェックリスト
  • 試行毎にシーケンスからひとつの値を無作為に選択するコードを書くことができる。

  • low以上high未満の整数を範囲とする一様乱数のサンプルをひとつ得るコードを書くことができる。(low、highは整数)

11.5. Codeコンポーネントを使って無作為にアイテムの各パラメータを決めよう

delayの問題が解決したので、続いてアイテムの個数、種類、位置、回転角度を決める方法について考えましょう。一度にすべてのパラメータについて考えるのは大変なので、まずは「アイテムが15個の場合」に限定して考えます。

まず、簡単に解決できるのが回転角度の決定です。15個のImageコンポーネントの [回転角度 $] は、ori[0]、ori[1]、…、ori[14]というリストの値がすでに入力されています。ですから、oriという要素数15のリストを作成して、要素に0、90、180、270のいずれかの値を無作為に割り当てればよいだけです。どうせ毎試行oriの値は更新するので、最初にoriを作成する時には値は何を設定しても構いません。例えば以下のように0を15個並べたリストを作成してもよいでしょう。

ori = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

別にこのコードで全く問題はないのですが、もし要素が100個必要になった場合、100個も0を並べたリストを入力するのは面倒です。そのような時に便利な関数がrange( )です。range( )は1個から3個の整数を引数として取ることができます。引数が1個の場合は、0から引数より1小さい整数まで順番に取り出すことができる「rangeオブジェクト」というオブジェクトが得られます。rangeオブジェクトの詳細については「 11.9.2:range( )オブジェクトについて 」をご覧ください。range( )をlistオブジェクトを作る関数list( )に渡すと、0から引数より1小さい整数までを並べたリストが得られます。例えばlist(range(15))とした場合、以下のリストが戻り値として得られます。

[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14]

引数が2個(x, yとします)の場合は、xからy-1までの整数を取り出すrangeオブジェクトが得られます。例えばlist(range(10, 15))を実行すると以下のリストが得られます。

[10,11,12,13,14]

引数が3個の場合、整数が1ずつ増加するのではなく3個目の引数の値ずつ増加します。list(range(0,15,3))を実行すると、以下のように3ずつ増加する整数のリストが得られます。15は含まない点に注意してください。

[0,3,6,9,12]

3個目の引数が負の場合は、だんだん値が小さくなっていきます。list(range(15,0,-3)を実行すると、以下のように15から始まって3ずつ減少するリストが得られます。上の例と同様に0は含まない点に注意してください。

[15, 12, 9, 6, 3]

range( )が本領を発揮するのは、list( )ではなくfor文と組み合わせるときです。for文と組み合わせると、ブロックが繰り返される毎にrange( )から順番に値が取り出されるので、要素数が等しい複数のリストに対してまとめて処理を行うコードが簡単に書けます。まず、0が15個並んだリストを作成してみましょう。

ori = [ ]
for i in range(15):
    ori.append(0)

これで0が15個並んだリストが変数oriに格納されましたが、これだけでしたら先ほどのようにずらっと0を15個並べたほうが楽だと思われるかもしれませんね。ここへ、各アイテムの種類(OかCか)を格納する変数imagefileを準備する処理も組み込んでみましょう。imagefileはImageコンポーネントので使用されますので、その要素は画像ファイル名でなければいけません。こちらも試行毎に値を変更するのでとりあえずOとCのどちらを設定しておいても構いません。とりあえずOで初期化しておくことにしましょう。Oの刺激はo.pngと画像ファイルに対応していますので、'o.png'という文字列を15個並べたリストを作成する必要があります。oriを作成した時と同じ要領で考えると、以下のコードで実現できます。

ori = [ ]
imagefile = [ ]
for i in range(15):
    ori.append(0)
    imagefile.append('o.png')

刺激の位置を格納する変数posの準備もここへ組み込むことができます。要素は [位置 [x, y] $] で使用するので、要素数2のリスト[0, 0]で初期化しておきましょう。

ori = [ ]
imagefile = [ ]
pos = [ ]
for i in range(15):
    ori.append(0)
    imagefile.append('o.png')
    pos.append([0, 0])

以上でori、imagefile、posの準備は完了です。このコードは実験開始時に一回実行すればよいので、 [実験開始時] に入力しておきましょう。

変数の準備ができたので、続いて各試行の最初に無作為にこれらの変数の値を決定するコードを作成しましょう。まず、oriについてはdelayと同じ方法が使えます。Randint(0, 4)で0から3の整数の乱数を得て、90倍すれば0、90、180、270の乱数が得られますので、for文でori[0]からori[14]に代入すればいいでしょう。この方法では回転させる必要がないOも回転させてしまいますが、Oは回転させても見た目が同じなので実質的に問題とはなりません。以下のコードを [Routine開始時] に追加して下さい。

for i in range(15):
    ori[i] = 90*randint(0,4)

続いてimagefileの設定ですが、こちらは少し解説が必要です。すべてOの条件、すべてCの条件、Oの中にひとつだけCの条件、Cの中にひとつだけOの条件の4条件があるのでした。そして、条件ファイルを確認にはfirstItemとotherItemsというパラメータが定義されています。firstItemはimagefile[0]、otherItemsはimagefile[1]からimagefile[14]に設定することを想定しています。 図11.6 をご覧ください。firstItemとotherItemsがともにo.pngであれば「すべてO」の条件に、ともにc.pngであれば「すべてC」の条件になります。同様にfirstItemがc.pngでotherItemsがo.pngであれば「Oの中にひとつだけC」、firstItemgがo.pngでotherItemsがc.pngであれば「Cの中にひとつだけO」になります。「必ずimage00ターゲットになっても問題はないの?」と思われる方がおられるかも知れませんが、試行毎に位置を無作為に決定するので問題ありません。

_images/parameter-condition-correspondence.png

図11.6 firstItemとotherItemsのパラメータ値と刺激条件との対応。

先ほど [Routine開始時] に入力したoriを更新するfor文に、imagefileを更新するコードを追加しましょう。変数iの値が0の時にはimagefile[i]にfirstItemの値を設定し、iの値が0以外の時にはimagefile[i]にotherItemsの値を設定します。

for i in range(15):
    ori[i] = 90*randint(0,4)
    if i==0:
        imagefile[i] = firstItem
    else:
        imagefile[i] = otherItems

ori、imagefileの更新ができたので、あとはアイテムの位置に対応する変数posの更新です。posは「無作為に値を決定する」という点ではoriと同じですが、重要な違いがあります。oriはアイテム間で値が重複しても構いません。つまり、例えば同時に回転角度が90度のアイテムが複数個存在しても構いません。一方、posはアイテム間で値が重複するとアイテムが重なってしまいますので、値の重複は許されません。次の節では、posの値を決定する方法を考えます。

チェックリスト
  • range( )を用いて、0からn (n>0)までの整数を順番に取り出すrangeオブジェクトを作成することができる。

  • range( )を用いて、mからn (n>m)までの整数を順番に取り出すrangeオブジェクトを作成することができる。

  • range( )を用いて、mからnまで、sの間隔で整数を順番に取り出すrangeオブジェクトを作成することができる。ただしm、nは互いに異なる整数、sは非0の整数である。

11.6. 無作為に重複なく選択しよう

それでは改めて、posの値の決定方法を考えましょう。posの候補となる値は 図11.3 に示したグリッドの座標で、36個あります。これら36個の値の中から15個を重複なく無作為に選択しなければいけません。心理学実験においては、この例のように複数個の値を重複なく無作為に選択しなければいけないことがよくあります。このようなときは、「無作為に選択する」のではなく「無作為に並べ替える」という方法が有効です。

まず、36個の座標値をすべて並べたリストを作成してposlistという変数に格納しておきましょう。以下のようにfor文を重ねると簡単に作成できます。appendしている行の式については、グリッドの間隔が0.1で一番左下の座標が[-0.25, -0.25]だったことを思い出してください。この多重for文を実行したときにposlistに値が追加されていく様子を 図11.7 に示しましたので、多重for文の動作がイメージしにくい方は参考にしてください。この多重for文をcodeTrialの [実験開始時] に追加してください。

poslist = [ ]
for pos_y in range(6):
    for pos_x in range(6):
        poslist.append([0.1*pos_x-0.25, 0.1*pos_y-0.25])
_images/generate-coods-by-nested-for.png

図11.7 多重for文による座標値リストの作成。

続いて、作成したposlistの要素を無作為な順序に並び替えます。並び替えには 表11.1 で紹介したshuffle( )を用います。shuffle( )は引数として受け取ったリストの要素の順番を無作為に並び替えます。戻り値は返さないので、ただshuffle(poslist)と書けばposlistの要素を並び替えることができます(suffled_list = shuffle(original_list)と書くのはよくある間違い)。試行毎にアイテムの位置を変更したいので、 [Routine開始時] に記入してください。

さて、ここからがポイントです。poslistの要素はルーチンの開始時に無作為に並び替えられているのですから、poslistの先頭から順番に15個の要素を取りだせば、poslistの中から重複なしに無作為に15個の要素を取り出したことになります。従って、以下のコードでpos[0]からpos[14]に重複なく無作為に位置を割り当てることができるはずです。

for i in range(15):
    pos[i] = poslist[i]

for i in range(15):という繰り返しは先ほどoriとimagefileの値を設定する時にも使用したのですから、同じfor文にpos[i] = poslist[i]を挿入するだけでOKです。確認のため、delayの設定やshuffleも含めた [Routine開始時] 全体のコードを示しておきます。

delay = randchoice( (1.0, 1.5, 2.0) )
shuffle(poslist)
for i in range(15):
    ori[i] = 90*randint(0,4)
    pos[i] = poslist[i]
    if i==0:
        imagefile[i] = firstItem
    else:
        imagefile[i] = otherItems

これで「アイテム数が15個の場合」に限定した条件での実験が完成しました。一度exp11.psyexpを保存して実行してみましょう。常にアイテムが15個提示されてしまいますが、アイテムの位置や向きが試行毎に無作為に変化していることが確認できます。これで残りはアイテムの個数をnumItemsパラメータの値に従って変化させるだけです。

チェックリスト
  • リストの要素を無作為に並べ替えることができる。

  • m個の要素を持つリストから、n個の要素(m>n)を重複なく無作為に抽出することができる。

11.7. アイテムの個数を可変にしよう

いよいよ最終段階、numItemsの値に従って試行毎にアイテム数を変化させる問題に取り組みましょう。いろいろな方法が考えられるのですが、ここでは簡単に実現できる「スクリーン外にアイテムを配置する」という方法を紹介します。今まで自分で実験を作成していて、heightではスクリーン上端のY座標が0.5なのに「 [位置 [x, y] $] に[0.0, 1.0]などと指定して、刺激がスクリーンに描画されずに困ったことはないでしょうか。スクリーンの上下左右の限界からはみ出た位置を指定してもエラーにならないせいでこういった困ったことが生じるのですが、今回はこれがエラーにならない事を逆手に取ります。 [実験開始時] 入力済みのコードのうち、アイテムの位置を決定する処理だけを抜き出してみましょう。

for i in range(15):
    pos[i] = poslist[i]

ここへif文を追加して、iがnumItems未満の時は上記と同様の処理、iがnumItems以上の時はスクリーンの描画範囲を超えた位置を設定する処理を行うように変更します。 今回の刺激はサイズが(0.03, 0.03)で、単位がheightのためスクリーン上端のY座標が0.5ですから、[0.0, 1.0]であれば刺激全体がスクリーンの外になります。

for i in range(15):
    if i<numItems:
        pos[i] = poslist[i]
    else:
        pos[i] = [0.0, 1.0]

if文の条件式がi<numItems であって、i<=numItemsではないことに注意してください。Pythonにおいてリストのインデックスは0から数えますので、5個のアイテムを表示するときにはインデックス0、1、2、3、4の合計5個のアイテムにposlistの値を設定する必要があります。同様に、n個のアイテムを表示するためにはインデックス0からn-1までのアイテムにposlistの値を設定しなければいけません。if文の条件式がi<=numItemsだと、0からnumItemsまでのnumItems+1個のアイテムにposlistの値が設定されてしまいます。

なお、アイテムを描画しないようにさせるには、今回のようにスクリーンの描画範囲外の位置を指定するという方法の他にも、[不透明度 $] を0.0にして完全な透明にしてしまうという方法もあります。ただし、透明化する方法の場合は、あくまで描画されていないだけでPsychoPyにとってはその位置に刺激があると認識されますので、第9章 で紹介したcontains( )やoverlaps( )を使う時に注意する必要があります。刺激がそこに存在していないように見えるのに、マウスカーソルが「刺激の上に重なっている」と判定されてしまうなどの恐れがあるからです。もっとも、この問題ですら「実験参加者がスクリーン上のある領域にマウスカーソルを置いているか否か、参加者に悟られないように記録する」という用途にも使えますので、一概に不備だとは言えません。こういった一見不備に思える現象を積極的に利用することによって、Builderで実現できる実験の幅は飛躍的に広がります。ぜひ、いろいろと工夫していただきたいと思います。

さて、上記のコードをtrialルーチンの [実験開始時] に組み込んだ、最終版のコードを以下に記します。pos[i]への代入部分が変化したことを確認してください。

delay = randchoice( (1.0, 1.5, 2.0) )
shuffle(poslist)
for i in range(15):
    ori[i] = 90*randint(0,4)
    if i<numItems:
        pos[i] = poslist[i]
    else:
        pos[i] = [0.0, 1.0]
    if i==0:
        imagefile[i] = firstItem
    else:
        imagefile[i] = otherItems

exp11.psyexpに上記の変更を加えたら、exp11.psyexpを保存して実行してみましょう。今度は試行毎に無作為な順番にアイテム数が5個、10個、15個と変化することを確認してください。十数試行ほどスクリーンに描画されたアイテム数をメモしてEscapeキーを押して実験を中断し、描画されたアイテム数とtrial-by-trial記録ファイルに出力されたnumItemsの値が一致していることも確認しましょう。これで今回の目標はすべて達成できました。

最後に、後の分析でアイテム位置の情報が必要になった場合に備えて、アイテム位置を保持している変数poslistの値を実験記録ファイルに出力する処理を付け加えておきましょう。独自の変数の値を出力する方法についてはすでに 第7章 で解説しましたので、出力自体はもう皆さん解説なしでできると思います。ただ、poslistは要素数36のリストである一方、後の分析で実際に必要となる可能性がある要素は実際にスクリーン上に提示された刺激の位置に対応する要素のみです。言い換えると、各試行で先頭からnumItems個の要素のみが必要です。何の工夫もせずにposlistをaddData( )メソッドに渡してしまうと、36個全部が出力されてしまうため、分析時に不必要な値を除去しなければならず、非常に無駄です。必要な値だけを抜き出して出力するのが理想的です。

for文を用いると、リストの先頭からnumItems個の要素を取り出したリストを作成するのは簡単です。例えば以下のコードのようにすれば変数displayedPosに実際に提示に利用された位置をまとめることができるでしょう。

displayedPos = []
for i in range(numItems):
    displayedPos.append(poslist[i])

if文やfor文の利用はプログラミングの基本中の基本なので、こういったコードがぱっと頭に浮かぶようにしっかりとこれらの文に慣れて欲しいと思います。しかし、今回の用途に関してはPythonにスライスと呼ばれる非常に便利な機能がありますので、そちらもぜひ覚えて欲しいと思います。

スライスとは、リストやタプルなどのシーケンス型のデータから、連続する要素を抜き出す演算です。シーケンス型データが格納された変数varに対してvar[a : b]の書式で用い、インデックスaからインデックスbの間に含まれる要素を抜き出したリストを返します。aとbの間の記号は半角のコロンです。第9章 で用いたリストの例をもう一度使って解説しましょう。 図11.8 例1をご覧ください。[100, 200, 300, 400, 500, 600]というリストを格納した変数varがあります。正のインデックスは、先頭から順番にそれぞれの要素の「前」にあると考えます。var[1:4]と書くと、インデックス1からインデックス4までの間の要素を取り出すのですから、[200, 300, 400]が得られます。初心者の方によくある勘違いに、スライスを「a番目の要素からb番目の要素を抜き出す」と考えてしまうというものがあります。「var[1]が200、var[4]が500ですから、var[1:4]は[200, 300, 400, 500]じゃないの?」というのがこの勘違いの典型です。飽くまで4というインデックスは500の前にあり、var[1:4]というスライスは「インデックス1からインデックス4までの間の要素を抜き出す」のですから、500は含まれません。

_images/slice-operation.png

図11.8 スライスによるリスト要素の抽出。var[a:b]と書くと、変数varのインデックスaからインデックスbの間にある要素を抜き出します。aが省略されたときは先頭が、bが省略されたときは末尾が指定されたものとします。

リストから要素をひとつ取り出す時に負のインデックスを利用できたのと同様に、スライスでも負のインデックスを用いることができます( 図11.8 例2)。正と負のインデックスを混ぜて使うこともできます。ただし、var[a:b]のaの方がbよりもリストの前方でなければいけません。 図11.8 例3つめの例のように、aが省略された時には、先頭から抜き出されます。 図11.8 例4のようにbが省略された時には、末尾までを抜き出します。

このスライスを利用すれば、poslistから実験記録ファイルに出力すべき要素を抜き出したリストを簡単に作ることができます。poslistの先頭からnumItems個の要素を刺激提示に使ったのですから、poslist[:numItems]とすればよいだけです(コロンの前は省略している点に注意)。このリストを実際に実験記録ファイルに出力するコードを書くのは練習問題としましょう。

チェックリスト
  • ルーチンに配置された視覚刺激コンポーネントをスクリーン上に描画させないようにすることができる。

  • スライスを用いて、あるリストから連続する要素を抽出したリストを作り出すことができる。

  • リストの先頭から要素を抽出する場合のスライスの省略記法を用いることができる。

  • リストの末尾までの要素を抽出する場合のスライスの省略記法を用いることができる。

11.8. 練習問題:透明化によるアイテム数変更と無作為な位置の調整をおこなおう

exp11.psyexpを改造して、この章の解説で出てきた二つのテクニックを実際に試してみてください。さらに、特にアイテム数が15個の時に、アイテムが無作為に配置されているというよりは整然と並んでいるように見えてしまうことを防ぐために、アイテムの位置を無作為に上下にずらす処理も追加してください。

  • [不透明度$] を0.0にすることによってnumItems個のアイテムがスクリーンに描画されるようする。

  • アイテムの位置を実験記録ファイルに出力するコードを完成させる。

  • アイテムの位置を、変数posによって指定された位置から上下方向、左右方向ともに-0.01、-0.005、0.005または0.01ずらす。ずらす量は試行毎、アイテム毎、方向毎に無作為に決定する。

11.9. この章のトピックス

11.9.1. numpy.ndarray型について

これまでの章ではずっと、刺激の位置(座標値)や大きさといった二次元の量を指定するためにリストを使用してきました。本書の用途のように静的な位置を表現するだけならリストで十分なのですが、座標値に対する演算を行おうとするとリストは非常に不便です。例えば[5,3]という座標値をX軸方向に1、Y軸方向に2移動させたい場合、ベクトルの演算をご存知の方は直感的には[5,3]+[1,2]と書きたくなるでしょう。しかし、Pythonにおけるリストは数値以外にも文字列なども要素になり得ますので、[5,3]+[1,2]をベクトルの和と解釈することにすると要素に数値以外の値があったときに演算が定義できなくなってしまいます。そのようなわけで、かどうかはわかりませんが、Pythonはリスト同士に対する+演算子はリストの結合として解釈します。つまり、[5,3]+[1,2]=[5,3,1,2]です。同様に、リストに対する数値の積は、ベクトルとスカラーの積ではなく、ベクトルの繰り返しとして解釈されます。[5,3] * 4でしたら[5,3]を4回繰り返したリストである[5,3,5,3,5,3,5,3]が得られます。

これでは本格的なベクトル演算を行う時に不便で仕方がないので、PythonではNumPyというパッケージが用意されています。NumPyを導入すると、直感的なベクトル演算が可能となります。NumPyにおける演算の基本となるのがnumpy.ndarray型のオブジェクトです。Builderではリストなどのデータをnumpy.ndarrayに変換するnumpy.asarrayという関数がasarrayという名前で利用できるように準備されています。asarrayを使うと、先ほどのようなベクトル風の演算が可能になります。

asarray([5,3]) + asarray([1,2])  # 計算結果は array([6,5])
asarray([5,3]) * 4               # 計算結果は array([20,12])

これらの演算で得られた戻り値もnumpy.ndarray型のオブジェクトです。numpy.ndarray型オブジェクトは、リストと同じように[ ]演算子で要素を取り出したり、スライスを適用したり、len( )で要素数を求めたりすることができます。ですから、[ ]やlen( )に関しては今まで学んできたリストと全く同等に使えます。しかし、+演算子や*演算子を適用した時の働きがリストと異なります。違いを十分に理解できればasarray( )を使って積極的にnumpy.ndarrayの機能を活用していただければ良いのですが、区別に自信がない場合は使わない方がよいでしょう。

11.9.2. range( )オブジェクトについて

本文の説明では「range( )関数が返すrangeオブジェクトというものがわからない」という方が多いのではないかと思います。このrangeオブジェクトというのはPython3から導入されたもので、旧バージョンであるPython2のrange( )は数値を並べたリストを返していました。つまり、本文では0から14まで並べたリストを得るときにlist(range(15))としましたが、Python2の頃は単にrange(15)と書くだけでよかったのです。これがなぜPython3で変更されたのかをお話すれば、rangeオブジェクトというもののイメージがもう少しはっきりするのではないかと思います。

例として、for文とrange()を使って1000万回の繰り返しをする場合を考えてみましょう。通常の心理実験では1000万回も繰り返すことはなさそうですが、分野によっては普通にあり得る回数です。Python2のようにrange( )がリストを返すとすると、0から9999999までの1000万個の数値を並べたリストが作成されてfor文に渡されることになります。この巨大なリストはコンピュータのメモリ上に保持されますが、はっきり言って非効率的です。必要なのは今が何回目の繰り返しなのかという値だけなのに、1000万個分ものメモリ領域が食いつぶされてしまうからです。

_images/range-generator.png

図11.9 Python2と3のrange( )の違い。

この問題を解決するのがPython3のrangeオブジェクトです。rangeオブジェクトは 図11.9 右のように

  • 1ずつ増やす

  • 次は7を渡す

  • 10000000以上になる場合は終了

といったルールを保持していて、値を要求される度に値を生成します(厳密に言うとこれはrangeオブジェクトから作られるイテレータというオブジェクトの機能)。図11.9 右の例では、7を渡したら「1ずつ増やす」というルールに従って「次は 8 を渡す」と更新しておくわけですね。これなら繰り返し回数が1000万回だろうが1000億回だろうがメモリへの負担が変わりません。圧倒的に効率がよいです。

rangeオブジェクトの弱点は、一連の数値をバラバラな順番で取り出す必要がある場合です。いきなり「1852番目の数値が欲しいんだけど」と言われると、その数値がいくつであるのかをルールに従って延々と計算しなければいけません。このような場合はリストの方が優れています。Python3でリストが必要な場合は、本文で紹介したようにlist( )と組み合わせてlist(range(x))とします。