P5

2AFC課題

どのような課題か

画面の左右に2つの円が提示され、どちらの円の方が明るいかを解答する課題です。


まずは実際にプログラムを動かしてみましょう。
下記のリンク先のプログラムコードをコピーして、Processingのエディタに貼付けて下さい。

プログラムコード

実行する前に、このプログラムを保存しておいて下さい。保存場所やファイル名は何でもかまいません。

プログラムを実行すると、まず画面下部に英文が表示されます。
その画面でエンターキー(もしくはリターンキー)を押すと、試行が始まります。
注視点のあと2つの円が提示され、どちらの方が明るいかをキーボードの左キーもしくは右キーで解答します。
被験者が解答キーを押すか、もしくは一定時間が経過すると次の試行に移ります。

全試行(デフォルトでは3試行)が終了すると、画面に終了メッセージが表示されます。
プログラムのフォルダの中に data という名前のフォルダがあり、その中に result.txt というファイルがあります。
行ごとに各試行の反応データが記録されており、1つ目の値が押されたキー、2つ目の値が反応時間[msec]です。
キーの値は、左キーが押された場合は1、右キーの場合は2、無回答の場合は0としています。

この実験プログラムの意義

この実験課題は、心理実験の課題としてはさほど意味のある課題ではありません。輝度の弁別閾の計測課題に近いですが、提示される円の輝度は毎試行でランダムとしているので、あまり統制された実験とはなっていません。

このプログラムは Processing による心理実験プログラミングの基本手法の解説を目的として作成しました。
刺激を提示し、被験者の反応を取得し、次の試行のためにまた画面を切り替える。
こうしたプログラムの流れは、ほぼ全ての知覚課題や認知課題に共通する基本的なものだと思います。
この実験課題は、この基本的な流れを含み、かつ、それ以外の要素は可能な限り簡略化したものとなっています。ですので、様々なタイプの実験を作成する上での基礎となるプログラミング手法をこのプログラムを通して説明できると思います。

では、順を追ってこのプログラムコードについて解説して行きます。

変数の宣言と初期化

コードの冒頭で、プログラムで使用する変数を宣言しています。

// Experimental settings
boolean isDebug; //開発中か本番実験モードか
int numTrial; //全部で何試行を行なうか
int fixationDuration; //注視点の提示時間
int judgementDuration; //解答の制限時間
color backgroundColor; //画面の背景色
color textColor; //文字の色

// Variables
int currentState; //状態管理用の変数で、現在の状態を保持(意味は後述します)
int currentTrial; //実験が現在何試行目を実行中かを保持
int baseTime; //RT(反応時間)計測に使用するための変数(意味は後述)
int[] response; //被験者の押したキーを記録するための変数
int[] RT; //RTを記録するための変数
PFont font; //フォント用の変数
color diskColorLeft; //左の円の色
color diskColorRight; //右の円の色

エディタ内で日本語は使用できません

上記のコード内ではコメントに日本語も使用していますが、Processingのエディタでは日本語は使用できません。
日本語をエディタにコピペしても、おそらく文字化けすると思います。
ただし、日本語を使用可能にする方法もあります。ここでは解説しないので必要な場合はやり方をググって下さい。

次に、setup関数(初期化関数)内で、上記の変数たちに初期値を与えます。

void setup(){
  size( 800, 600 ); //横800px、縦600pxで画面を作成する
  frameRate( 60 ); //フレームレートを60fpsに設定する
  
  //実験条件の設定に関する変数
  isDebug = false; //実験本番時はfalseにする. trueにするとデバッグ情報を表示する
  numTrial = 3; //試行数を3に
  fixationDuration = 1000; //注視点の提示時間を1000msとする
  judgementDuration = 4000; //解答の制限時間を4000msとする
  backgroundColor = 0; // 背景色を黒とする
  textColor = 255; // 文字色を白とする
  
  //プログラム実行のための変数
  currentState = 0; //状態0から開始する(意味は後述)
  currentTrial = 0; //初期値を0とする
  baseTime = 0; //とりあえず初期値0を与える
  response = new int[ numTrial ]; //試行数と同じサイズの配列とする
  RT = new int[ numTrial ]; //試行数と同じサイズの配列とする
  font = createFont( "Georgia", 24 ); //書体としてGeorgiaを用い、大きさを24とする
  textFont( font ); //その書体をテキスト描画用に用いる
}

画面サイズを 800 x 600 としていますが、実際の実験の場合はフルスクリーンで行なうのが普通だと思います。フルスクリーンでプログラムを実行したい場合にはショートカットキーを利用すると良いでしょう。
Ctrl + Shift + R (Macの場合は command + shift + R) でフルスクリーンでプログラムが実行されます。通常通り、エスケープキーを押せばプログラムは終了します。

変数の意味についてはコメントに書いてあるので説明は不要でしょう。currentState や baseTime など一部の変数についてはあとで意味を説明します。

response と RT は被験者の反応を保存しておくための変数です。response には被験者が押したキー(0or1or2)が、RT には反応時間が、それぞれ代入されることになります。どちらの変数も試行数分のサイズが必要なので、配列サイズとして numTrial の値を用いています。

メインループ関数

続いて、draw関数(メインループ関数)は以下のようです。

void draw() {
  background( backgroundColor );
  
  if( currentState == 0 ){
    titlePhase();
  } else if( currentState == 1 ){
    fixationPhase();
  } else if( currentState == 2 ){
    responsePhase();
  } else if( currentState == 3 ){
    endPhase();
  }

  if( isDebug ){ drawDebugInfo(); }
}

分量は短いですが、これがこの実験プログラムの骨格であり、その全体像となります。

始めに background 関数で画面の背景色を指定しています。引数の backgroundColor には 0 を設定しているので、画面の背景は黒色になります。

次に if 文があり、currentState の値に応じて4種類の関数のうちのどれかが実行されます。currentState という変数はプログラムの現在の状態を表現しており、この状態に応じてどの関数が実行されるかが操作されています。初期状態では currentState の値は 0 であるため、titlePhase 関数が実行されることになります。

最後の行はデバッグ情報を表示させるためのコードで、プログラムの本筋とはあまり関係ありません(後述)。

なお、titlePhase, fixationPhase, responsePhase, endPhase という関数はどれもこの実験プログラム内で定義された自前の関数であり、Processing が持つ固有の関数ではありません。

currentStateの挙動とプログラム動作の大まかな説明

プログラムを実行すると最初に画面に文字が表示されたと思いますが、あれをやっているのが titlePhase 関数になります。この画面(状態)の時にエンターキーが押されると currentState の値が 1 となり、fixationPhase 関数が実行されるようになります。fixationPhase 関数はその名の通り、注視点を表示するためのコードが記述された関数です。

 1秒間が経過すると currentState の値は 2 となります。注視点の提示は終了し、responsePhase 関数が実行されるようになります。この関数には2つの円を提示したりするためのコードが記述されています。

 被験者が反応する、あるいは制限時間が経過すると currentState の値は再度 1 に戻り、注視点が表示されます。試行数の分だけ注視点の提示と円の提示が繰り返され(その都度 currentState の値は 1 と 2 を行き帰します)、全試行が終了すると currentState の値は 3 となり、endPhase 関数(実験終了後のサンキューメッセージを表示する関数)が実行されるようになります。

このように、currentState の値によりいくつかの状態を行き来することでプログラムの実行が制御されています。このしくみが、Processing を使った実験プログラムの作成において最もキモとなる部分ではないかと思います。そこで、この点についてもう少し詳しく説明をします。

状態遷移という考え方

プログラムを、いくつかの状態が遷移して行くものとして捉える考え方について説明します。

一般に、実験プログラムの流れはいくつかの画面(状態)が切り替わって行くことで進行します。今回の実験の場合、実験全体の流れを以下の4つの状態に分割することができます。

0.初期画面
 1.注視点
 2.刺激提示画面
 3.終了画面

これらの状態がどのように切り替わるかを図示すると以下のようになります。

各ボックスがそれぞれの状態で、矢印に添えられた条件が成立した場合に、状態は遷移します。
1.注視点と2.刺激提示はペアで1試行を構成しており、全試行が終わるまでこの2つの状態間を繰り返し行き来します。最終試行の刺激提示後、状態は終了画面へと遷移し、実験は終了します。

それぞれの状態においてなされなければならない処理が、各ボックス内に書かれている関数にそれぞれ記述されています。titlePhase 関数には画面にメッセージ文を表示させる処理が、fixationPhase 関数には注視点を表示する処理が、それぞれ記述されているという具合です。

状態の制御のために currentState という変数を用いています。再度 draw 関数を見てみます。

void draw() {
  background( backgroundColor );
  
  if( currentState == 0 ){
    titlePhase();
  } else if( currentState == 1 ){
    fixationPhase();
  } else if( currentState == 2 ){
    responsePhase();
  } else if( currentState == 3 ){
    endPhase();
  }
}

currentState の値が上図内の状態の番号に対応しており、この値が例えば 0、つまり初期画面の時には titlePhase 関数が実行されるようになっているのがわかると思います。他の状態の場合にもそれに対応した関数が実行されます。そして、キー押しや時間経過などの出来事が起こった時にこの currentState の値が適切に変更されるようにすれば、上の図のような状態遷移を実現することができます。

例えば初期画面にいる時にエンターキーが押されたら、currentState の値を 1 に変更します。すると if 文の条件分岐で注視点画面に対応する fixationPhase 関数が実行されるようになります。つまり、上図における初期画面から注視点画面への遷移が行なわれたことになります。状態の遷移とはつまり、現在実行すべき関数を切り替える(スイッチする)ということを意味します。

状態の切替という手法をより実感的に理解するために、下記ボックスの「変数情報の表示」も試みて下さい。

変数情報の表示

setup 関数内で isDebug の値を true にすると、currentState 等の値の様子が画面左上に表示されるようになります。drawDebugInfo という関数を作って、これらの変数の値をリアルタイムに監視できるようにしています(この関数は isDebug の値が true の場合のみ実行されます)。currentState や currentTrial といった変数の挙動がわかりやすいのではないでしょうか。画面が変わるごとに currentState の値が変わり(実際にはこの値が変わることで画面が切り替わっているわけですが)、一試行終わるごとに currentTrial の値が増えて行く様子が観察出来ます。

ところで、プログラムの開発時にはこのように主要な状態変数の値を画面内に表示させておくようにすると、動作の不具合が生じている場合に原因の箇所がわかりやすくて便利です。例えば、currentStateの値が 2 の時にプログラムが停止してしまう場合、この状態の時に実行されているのは responsePhase 関数なので、バグはこの関数内にあるのだろうと推測することができます。

各状態ごとの関数

それでは、各状態ごとの関数についてそれぞれ順に解説します。

titlePhase 関数(タイトル画面表示時の関数)

titlePhase 関数の内容は以下のようです。

void titlePhase(){
  fill( textColor );
  text( "Press Enter button to start experiment.", 100, height * 0.8 );
}

文字色をfill関数で指定し、text関数でテキストを表示しています。

fixationPhase 関数(注視点提示時の関数)

fixationPhase 関数の内容は以下のようです。

void fixationPhase(){
  // draw fixation cross
  stroke( 200 ); // define gray scale color (0 to 255) of lines
  strokeWeight( 3 );
  line( width/2 - 10, height/2, width/2 + 10, height/2 ); // orizontal line
  line( width/2, height/2 - 10, width/2, height/2 + 10 ); // vertical line
  
  // check elapsed time to transit state
  int elapsedTime = millis() - baseTime;
  if( elapsedTime > fixationDuration ){
    transitState();
  }
}

最初の4行は中心点を表示するためのコードです。
線の色と太さを指定した後、line関数を用いて縦線と横線を引き、十字の注視クロスを描画しています。

後半部分は状態を遷移させるための処理です。
注視点の画面が表示されてからの経過時間を計算し elapsedTime という変数に入れます。
これが fixationDuration の値(1000msです)より大きければ、状態を遷移させます。
状態遷移の処理は transitState という関数内に記述しています。

responsePhase 関数(円の提示時の関数)

responsePhase 関数の内容は以下のようです。

void responsePhase(){
  // draw message
  fill( textColor );
  text( "Which circle is brighter?", 250, 100 );
  
  // draw stimuli
  noStroke();
  fill( diskColorLeft );
  ellipse( width/2 - 200, height/2, 100, 100 );
  fill( diskColorRight );
  ellipse( width/2 + 200, height/2, 100, 100 );
  
  // check elapsed time to transit state
  int elapsedTime = millis() - baseTime;
  if( elapsedTime > judgementDuration ){
    transitState();
  }
}

準備中

endPhase 関数(実験終了時の関数)

endPhase 関数の内容は以下のようです。

void endPhase(){
  fill( textColor );
  text( "Thank you for your time!", 200, height * 0.8 );
}

準備中

その他の関数

次に、上記以外の残りの関数について説明します。

transitState 関数(状態を切り替えるための関数)

transitState 関数の内容は以下のようです。

void transitState(){
  if( currentState == 1 ){
    currentState = 2;
    baseTime = millis();
    diskColorLeft = (int)random( 255 );
    diskColorRight = (int)random( 255 );
  } else {
    if( currentTrial == numTrial - 1 ){
      // if all the trials have done, save data and transit to state 3.
      saveData();
      currentState = 3;
    } else {
      // move on to next trial
      currentTrial++;
      currentState = 1;
      baseTime = millis();
    }
  }
}

準備中

keyPressed 関数(キー押しされた際に実行される関数)

keyPressed 関数の内容は以下のようです。

void keyPressed() {
  if ( key == ENTER || key == RETURN ){
    if( currentState == 0 ){
      currentState = 1;
      baseTime = millis();
    }
  } else if ( keyCode == LEFT ) {
    if( currentState == 2 ){
      // record performance
      response[ currentTrial ] = 1;
      RT[ currentTrial ] = millis() - baseTime;
      // transit state
      transitState();
    }
  } else if ( keyCode == RIGHT ) {
    if( currentState == 2 ){
      // record performance
      response[ currentTrial ] = 2;
      RT[ currentTrial ] = millis() - baseTime;
      // transit state
      transitState();
    }
  }
}

準備中

saveData 関数(被験者の反応データをファイルに保存するための関数)

saveData 関数の内容は以下のようです。

void saveData(){
  String fileName = "data/result.txt";
  String[] dataStrings = new String[ numTrial ];
  for( int i=0; i < numTrial; i++ ){
    dataStrings[ i ] = nf( response[i], 1 ) + "\t" + nf( RT[i], 4 );
  }
  saveStrings( fileName, dataStrings );
}

準備中

まとめ

頭の整理のため、実験コード全体を大部分の関数の中身を省略して書いてみました。

void setup(){}
void draw() {
  background( backgroundColor );
  
  if( currentState == 0 ){
    titlePhase();
  } else if( currentState == 1 ){
    fixationPhase();
  } else if( currentState == 2 ){
    responsePhase();
  } else if( currentState == 3 ){
    endPhase();
  }
}

void titlePhase(){}
void fixationPhase(){}
void responsePhase(){}
void endPhase(){}

void transitState(){}
void keyPressed() {}
void saveData(){}

準備中

コメント

Copied title and URL