1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PsychoPyのTrialHandlerとExperimentHandler

Last updated at Posted at 2020-03-05

Builderを使っているとほとんど意識することはないと思いますが、Coderを使いたいときTrialHandlerってどうやって使うの? TrialHandler2というのもあるけど、どう違うの? え?さらにExperimentHandlerてのもあるんだけど・・・となりがちです。私だけかもしれませんが。

ということで、同じ悩みを抱えているかたへの解説です。

この例では、次の4種類の視覚刺激を呈示することにします。

スクリーンショット 2020-03-04 16.47.11.png

上の4つの中からひとつが呈示され、それに対して f または j のキーを押すという課題を考えましょう。

こういったケースでは、色の要因、形の要因の影響を見たいことが一般的です。色の要因としては赤と青の2水準があり、形の要因としては三角形と四角形の2水準があります。2 x 2 で4条件を設けることになりますが、PsychoPyには便利な関数があります。

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

print(conditions)をしてみると分かりますが、conditionsは次のような内容です。専門用語では、辞書型を要素として持つリストになります。各要素が実験条件を表しています。関数ひとつで、ぱっとこれを作ってくれるので便利ですね。ちなみにimportConditionsを使えばエクセルファイルに記述した実験条件を読み込むことができます。

conditionsの中身
[
{'stim_color': 'red', 'stim_shape': 'triangle'}, 
{'stim_color': 'blue', 'stim_shape': 'triangle'}, 
{'stim_color': 'red', 'stim_shape': 'rectangle'}, 
{'stim_color': 'blue', 'stim_shape': 'rectangle'}
]

TrialHandler

TrialHandlerの動作を確認するためのコードです。こちらのコードはBuilderではなく、Coderで記述します。

from psychopy import data, visual, core
from psychopy.hardware import keyboard

kb = keyboard.Keyboard()

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

stim_vertices = { # 三角形と四角形の座標
    'triangle': [(-0.5, 0), (0, 0.5), (0.5,0)],
    'rectangle': [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]
    }
    
trials = data.TrialHandler(conditions, 2) # 各条件を2回繰り返す

win = visual.Window()

thisTrial = trials.next() # 1試行目

win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
stim.draw()
win.flip()

key_loop = True
while key_loop:
    ptb_keys = kb.getKeys(['f', 'j'], waitRelease=False) # f,jキーのみ入力可能
    if len(ptb_keys):        
        trials.addData('RT', ptb_keys[0].rt) # 反応時間の記録
        trials.addData('choice', ptb_keys[0].name) # どのキーを押されたかの記録
        key_loop = False
        
# 以下はプログラムの書き方としてはスマートではありませんが、TrialHandlerの動作確認のため、
# 1試行目と同じ作業を繰り返しています。
thisTrial = trials.next() # 2試行目

win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
stim.draw()
win.flip()

key_loop = True
while key_loop:
    ptb_keys = kb.getKeys(['f', 'j'], waitRelease=False)
    if len(ptb_keys):        
        trials.addData('RT', ptb_keys[0].rt)
        trials.addData('choice', ptb_keys[0].name)
        key_loop = False


trials.saveAsWideText("results.csv", appendFile=False)

動作確認のため、ちょっと変則的な使い方になっています。全試行は、4条件 x 2繰り返し = 8試行ですが、2試行のみを実施します。

本題からそれますが、推奨されているPsychtoolbox由来のキーボードを使っています。取得された反応時間がpsychopy.event.getKeys()より正確になるらしいです。

kb = keyboard.Keyboard()

また反応時間を正確に取得するため、次の2行を加えています。

win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

callOnFlipで指定された関数は、次のFlipのタイミングで実行されます。上の例では、刺激を呈示した瞬間にクロックをリセットして、反応時間の計測を開始しています。またすでに押されていたキーがあった場合には、その情報をクリアするようにしています。

さて、TrialHandlerのパラメーター(引数)はたくさんあるのですが、今回は必要最小限にしています。第1パラメーターで実験条件のリストを指定します。第2パラメーターで繰り返し数を指定します。

trials = data.TrialHandler(conditions, 2) # 各条件を2回繰り返す

TrialHandlerはすべての実験条件を管理していて、trials.next()によって現在の試行を参照します。上のコードは2試行だけが実施されますが、trials.next()が2回呼び出されていることを確認してください。各試行に入る直前で呼び出します。しかし、後述しますが、trials.next()を手動で呼び出すことはまれです。

ptb_keys = kb.getKeys(['f', 'j'], waitRelease=False)

で、現在のキー押し状況を確認しています。fまたはjキーの入力のみを受け付けています。またキーから指を離すことを待ちません。

fまたはjキーが押されていたら、反応時間とどのキーを押していたかの情報を保存します。押されているキーの情報は複数存在する可能性があるため(つまり、同時に複数のキーを押している)、その先頭(0番目)のみを受け付けるようにしています。

trials.addData('RT', ptb_keys[0].rt)
trials.addData('choice', ptb_keys[0].name)

trialsTrialHandlerによって作成されていることを思い出してください。つまりTrialHandlerはすべての実験条件を管理し、かつ、データの保存まで請け負っているわけです。

trials.saveAsWideText("results.csv", appendFile=False)

実験結果をresults.csvに保存します。上書きをしないようにしています。

では実験結果を見てみましょう。たぶん、こんな感じになっていると思います。

スクリーンショット 2020-03-05 11.12.27.png

反応時間やどのキーを押したかは2試行分しか記録されていませんが、それ以外の情報については8試行すべてのデータが保存されていますね。

TrialHandler2

次にTrialHandler2の説明です。

上のコードをそのまま使います。ただし、次の行のみ、TrialHandlerからTrialHandler2に変更してください。

TrialHandler2に変更
trials = data.TrialHandler2(conditions, 2) # 各条件を2回繰り返す

実験結果(results.csv)を見てみましょう。

スクリーンショット 2020-03-05 11.16.30.png

TrialHandlerを使ったときとの違いが分かるでしょうか?

そうです、3試行目から8試行目までのデータがまったくないですね。これがTrialHandlerとTrialHandler2の違いです。

うん、違いは分かった! でもこの違いがどんな役に立つの? と思いますよね。実は正直なところ、私もよく分かっていないのですが、私の理解では、事前に割り当てたすべての条件を出力すると結果が見にくくなるケースがあり、そういうときには TrialHandler2を使うようです。ですので、基本的にはTrialHandlerを使って、余計な内容が出力されていると感じたときにTrialHandler2を試すということでよいと思います。

TrialHanlder を for文と組み合わせる

先に「trials.next()を手動で呼び出すことはまれです」と書きました。
ふつうは以下のようにします。なお、TrialHandler2からTrialHandlerに戻しています。

from psychopy import data, visual, core
from psychopy.hardware import keyboard

kb = keyboard.Keyboard()

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

stim_vertices = { # 三角形と四角形の座標
    'triangle': [(-0.5, 0), (0, 0.5), (0.5,0)],
    'rectangle': [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]
    }
    
trials = data.TrialHandler(conditions, 2) # 各条件を2回繰り返す

win = visual.Window()

for thisTrial in trials:
    win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
    win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

    stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
    stim.draw()
    win.flip()

    key_loop = True
    while key_loop:
        ptb_keys = kb.getKeys(['f', 'j'], waitRelease=False)
        if len(ptb_keys):        
            trials.addData('RT', ptb_keys[0].rt)
            trials.addData('choice', ptb_keys[0].name)
            key_loop = False
        
trials.saveAsWideText("results.csv", appendFile=False)

for文を使って、trialsの各条件を参照します。自動でtrials.next()が呼び出されていると思ってかまいません。

ExperimentHandler

上のfor文を使ったサンプルにExperimentHandlerを加えてみましょう。変更は4箇所だけです。ExperimentHandlerのパラメーター(引数)もたくさんありますが、今回は必要最小限にしています。

from psychopy import data, visual, core
from psychopy.hardware import keyboard

kb = keyboard.Keyboard()

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

stim_vertices = { # 三角形と四角形の座標
    'triangle': [(-0.5, 0), (0, 0.5), (0.5,0)],
    'rectangle': [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]
    }
    
trials = data.TrialHandler(conditions, 2) # 各条件を2回繰り返す

win = visual.Window()

exp = data.ExperimentHandler(name='testExp', dataFileName='exp_results') # 変更(追加)1
exp.addLoop(trials) # 変更(追加)2

for thisTrial in trials:
    win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
    win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

    stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
    stim.draw()
    win.flip()

    key_loop = True
    while key_loop:
        ptb_keys = kb.getKeys(['f', 'j'], waitRelease=False)
        if len(ptb_keys):        
            trials.addData('RT', ptb_keys[0].rt)
            trials.addData('choice', ptb_keys[0].name)
            key_loop = False
            
    exp.nextEntry()    # 変更(追加)3

# trials.saveAsWideText("results.csv", appendFile=False) 変更4 不要なのでコメントアウト

exp.nextEntry()を呼び出すタイミングに注意が必要です。この関数は現在の試行のデータの保存が終わったあとに呼び出す必要があります。trials.next()と違って、こちらは明記しなくてはなりません。

さて、以上の例からも分かるように、実はExperimentHandlerは実験に必須ではありません。TrialHandlerだけでも実験はできます。ExperimentHandlerを使うメリットは、実験結果のファイル出力を明記しなくても自動で保存される点です。

なーんだ、と思ったかもしれませんが、メリットはもうひとつあります。ていうか、こちらのメリットのほうが重要。

上の実験プログラムに練習試行を加えたいとします。そして練習の結果と、実験の結果を同一のファイルに保存したいとします。こういうケースでExperimentHandlerは効果を発揮します。

from psychopy import data, visual, core
from psychopy.hardware import keyboard

kb = keyboard.Keyboard()

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

stim_vertices = { # 三角形と四角形の座標
    'triangle': [(-0.5, 0), (0, 0.5), (0.5,0)],
    'rectangle': [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]
    }

win = visual.Window()

exp = data.ExperimentHandler(name='testExp', dataFileName='exp_results')

for block in ['practice', 'experiment']:
    if block == 'practice': # 練習試行
        trials = data.TrialHandler(conditions, 1, name='practice') # 練習では各条件を1回ずつ
        instruction = visual.TextStim(win, text="練習を始めます")
    else: # 本試行
        trials = data.TrialHandler(conditions, 2, name='exp') # 実験では各条件を2回ずつ
        instruction = visual.TextStim(win, text="実験を始めます")
        
    exp.addLoop(trials)

    instruction.draw()
    win.flip()
    core.wait(2)

    for thisTrial in trials:
        win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
        win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

        stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
        stim.draw()
        win.flip()

        key_loop = True
        while key_loop:
            ptb_keys = kb.getKeys(['f', 'j'], waitRelease=False)
            if len(ptb_keys):        
                trials.addData('RT', ptb_keys[0].rt)
                trials.addData('choice', ptb_keys[0].name)
                key_loop = False

        exp.nextEntry()

練習では各条件を1回ずつ、実験(本試行)では各条件を2回ずつ呈示しています。同一ファイルに実験の結果が保存されることを確認してください。なお、Coderのデモにある、実験制御 => experimentHandler.py では階段法を使用していて参考になります。

ExperimentHandlerは、ひとつの実験内で複数のループを使いたいときに役に立ちます。

ひとつの試行内で複数のキー入力を許容する

最後に、いっぷう変わったTrialHandlerの使い方を説明しておきます。

いままでの例では、各刺激に対して1回だけ反応するというものでした。これを少し変更して、好きなだけキーを押すような実験を考えてみましょう。なおすべてのキー押しを保存するものとします。エンターキーを押したら、次の試行に移ることとしましょう。

例えば、次のコードで希望通りの動作になります。

from psychopy import data, visual, core
from psychopy.hardware import keyboard

kb = keyboard.Keyboard()

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

stim_vertices = { # 三角形と四角形の座標
    'triangle': [(-0.5, 0), (0, 0.5), (0.5,0)],
    'rectangle': [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]
    }
    
trials = data.TrialHandler(conditions, 2) 

win = visual.Window()

exp = data.ExperimentHandler(name='testExp', dataFileName='exp_results')
exp.addLoop(trials)

instruction = visual.TextStim(win, text="複数のキーを押してください。\nエンターキーで次の試行に移ります。")
instruction.draw()
win.flip()
core.wait(2)

for thisTrial in trials:
    win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
    win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

    stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
    stim.draw()
    win.flip()
    

    responses_times=[]
    responses_keys = []
    
    while True:
        ptb_keys = kb.getKeys(waitRelease=False) # すべてのキーを受け付ける
        if len(ptb_keys):   
            if 'return' in ptb_keys:
                break
            responses_times.append(ptb_keys[0].rt)
            responses_keys.append(ptb_keys[0].name)

    trials.addData('RT', responses_times)
    trials.addData('choice', responses_keys)
    exp.nextEntry()

実験結果として、例えば以下のようなものが得られます。

スクリーンショット 2020-03-05 16.26.00.png

悪くはないんですが、反応時間(G列)と反応キー(H列)を見ると、なんか分析しにくいですよね。できれば各行をひとつの反応に対応させたいと思うかもしれません。

ということで、以下のコードです。

from psychopy import data, visual, core
from psychopy.hardware import keyboard

kb = keyboard.Keyboard()

conditions = data.createFactorialTrialList(
    {'stim_color': ['red', 'blue'], 'stim_shape': ['triangle', 'rectangle']})

stim_vertices = { # 三角形と四角形の座標
    'triangle': [(-0.5, 0), (0, 0.5), (0.5,0)],
    'rectangle': [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]
    }

exp = data.ExperimentHandler(name='testExp', dataFileName='exp_results')

trials = data.TrialHandler(conditions, 2) 

# ポイント1. 反応を記録するためのTrialHandlerを作成します。
# ここでは実験条件をあえて指定しません。繰り返し数はキーボードが押される回数を見越して多めに設定します。
responses = data.TrialHandler([], 100) 

# ポイント2. responsesとtrialsの両方をExperimentHandlerに加えます。
# 加える順番はどちらが先でも問題はありませんが、出力される結果が微妙に異なります。
# responsesを先に加えたほうが、thisRepN, thisTrialN, thisIndexなどが適切に保存されます
exp.addLoop(responses) 
exp.addLoop(trials)

win = visual.Window()

instruction = visual.TextStim(win, text="複数のキーを押してください。\nエンターキーで次の試行に移ります。")
instruction.draw()
win.flip()
core.wait(2)

for thisTrial in trials: # trialsについてはここでnext()される
    win.callOnFlip(kb.clock.reset) # 刺激を呈示した瞬間に反応時間の計測を開始する
    win.callOnFlip(kb.clearEvents, eventType='keyboard') # すでに押されていたキー情報をクリア

    stim = visual.ShapeStim(win, fillColor = thisTrial['stim_color'], vertices = stim_vertices[thisTrial['stim_shape']])
    stim.draw()
    win.flip()

    while True:
        ptb_keys = kb.getKeys(waitRelease=False) # すべてのキーを受け付ける
        if len(ptb_keys):   
            if 'return' in ptb_keys: # returnキーが入力されたら、その試行を終了
                break
            responses.next() # ポイント3. responsesについては、addDataをする前に手動でnext()
            responses.addData('RT', ptb_keys[0].rt)
            responses.addData('choice', ptb_keys[0].name)
            exp.nextEntry() # ポイント4. ExperimentHandlerの参照点をひとつ進める

実験条件を管理するTrialHandler (trials)とデータを保存するTrialHandler (responses)を分けているところがポイントです。trialsについてはfor文によって自動でnext()されますが、responsesについては手動でnext()しないといけません。ちょっと分かりにくいですね・・・

結果は次のようになります。

スクリーンショット 2020-03-09 10.36.48.png

各行が、各反応に対応するので、分析がしやすくなりました。.thisNを見ると試行番号が分かります。試行によっては複数行にまたがる(3回反応していたら3行)になる点に注意してください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?