Unityで行動分析 ～その4：反応率分化強化スケジュール編～

# 1. はじめに
　[ 準備編 ](https://qiita.com/TeRa_YUKI/items/b9433da24af552d6b3e3)で作成した *Operandum1* の Script を編集して、時間スケジュールを作成します。基本的なことは準備編で一通り解説しているので、本記事では *Operandum1* の Script の解説のみとなります。また、今回も「オペランダムへの反応」→「得点上昇」と「強化オペランダムへの反応」→「得点上昇」の2つの場合を考慮して解説したいと思います。

# 2. 反応率分化強化スケジュールとは
　反応率分化強化スケジュールは、反応形成において反応を変容する手続きです。本記事では反応間時間 ( IRT ) を分化強化の対象とする反応型の分化強化スケジュールとして、**低反応率分化強化スケジュール** ( Differential reinforcement of low rate; **DRL** )と**高反応率分化強化スケジュール** ( Differential reinforcement of high rate; **DRH** )の2つを解説します。

## 2.1. DRLとは
　DRLとは、「IRTがある値を超えた後の最初の反応に強化子が随伴する(坂上・井上, 2018, pp175)」強化スケジュールです。もしその値以内で反応した場合は、再びその時点からIRTが計測されます。無反応での一定時間の経過が強化の必要条件となっています。
　Unityで作成する場合は、弁別刺激点灯時に限り反応はいつでも受けつけるけれど、スケジュール値 ( *x sec* ) を超過してから反応しなければ強化子を得られない ( あるいは *Ramp* が点灯しない ) ようにすれば良いです。また、スケジュール値 ( *x sec* ) 経過中に反応してしまうと時間を0にリセットするよう設定します。

## 2.2. DRHとは
　DRHとは、DRLの逆にあたる強化スケジュールで(坂上・井上, 2018)、スケジュール値よりも短い時間での反応に強化子が随伴する強化スケジュールです(Ono & Iwabuchi, 1997)。もしスケジュール値以上で反応した場合は、再びその時点からIRTが計測されます。
　Unityで作成する場合は、弁別刺激点灯時に限り反応はいつでも受けつけるけれど、反応がスケジュール値 ( *x sec* ) 未満でなければ強化子を得られない ( あるいは *Ramp* が点灯しない ) ようにすれば良いです。また、スケジュール値 ( *x sec* ) 以降に反応してしまうと時間を0にリセットするよう設定します。

# 3. DRL
## 3.1. 「オペランダムへの反応」→「得点上昇」
　Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の *Operandum1* の Script にはなかったコードです。

```C#:Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Operandum1_Script : MonoBehaviour
{
    int Point = 1;
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public Text CountText;
    public AudioClip PointSE;
    AudioSource audioSource;

    //New
    float time;
    public float DRL;
    public AudioClip Operandum1SE;


    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }


    // New
    void Update()
    {
        if (Sd1_on.activeSelf)
        {
            time += Time.deltaTime;
            if (Input.GetKeyDown(KeyCode.F))
            {
                audioSource.PlayOneShot(Operandum1SE);
                if (time > DRL)
                {
                    audioSource.PlayOneShot(PointSE);
                    CountText.text = "Point : " + Point.ToString();
                    Point += 1;
                    time = 0;
                }
                else
                {
                    time = 0;
                }
            }
        }

        if (Sd1_off.activeSelf)
        {
            time = 0;
        }
    }
}
```
　このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。

**変数の宣言**

* float time; ... float型の変数 time を宣言
* public float DRL; ... public な float型の変数 DRL を宣言<br>
 → Editor上ではDRLのスケジュール値を入れてください
* public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言<br>
 → Editor上では *Operandum1* に反応したときに鳴るSEを入れてください

**Update()**

* **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
    * 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
    * **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
        * 効果音( Operandum1SE )が鳴る
        * **「if (time > DRL)」**の処理 ... timeがDRLのスケジュール値を超過したときの処理
            * 効果音( PointSE )が鳴る
            * 得点が1点上昇( Point += 1; )
            * time を0にリセット<br>
             → 得点が上昇すると時間がリセットされ、もう一度時間を計測しはじめる<br>
             → 弁別刺激が点灯している間、DRLが走り続ける
        * **「else」**の処理 ... timeがDRLのスケジュール値を超過したとき以外の処理
            * time を0にリセット<br>
             → オペランダムに反応すると時間がリセットされ、もう一度時間を計測しはじめる

## 3.2. 「強化オペランダムへの反応」→「得点上昇」
　Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の *Operandum1* の Script にはなかったコードです。

```C#:Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Operandum1_Script : MonoBehaviour
{
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public GameObject Ramp_off;
    public GameObject Ramp_on;
    public AudioClip Operandum1SE;
    AudioSource audioSource;
    GameObject Ramp;
    Ramp_Script Ramp_Script;

    //New
    float time;
    public float DRL;


    void ResetTime()
    {
        time = 0;
    }


    void Start()
    {
        audioSource = GetComponent<AudioSource>();
        Ramp = GameObject.Find("Ramp");
        Ramp_Script = Ramp.GetComponent<Ramp_Script>();
    }

    
    void Update()
    {
        // New_1
        if (Sd1_on.activeSelf)
        {
            time += Time.deltaTime;
            if (Input.GetKeyDown(KeyCode.F))
            {
                audioSource.PlayOneShot(Operandum1SE);

                // New_2
                if (time > DRL)
                {
                    Ramp_off.SetActive(false);
                    Ramp_on.SetActive(true);
                    Invoke("ResetTime", Ramp_Script.ReinforceableTime);
                }

                else
                {
                    time = 0;
                }
            }
        }

        if (Sd1_off.activeSelf)
        {
            time = 0;
            Ramp_off.SetActive(true);
            Ramp_on.SetActive(false);
        }
    }
}
```
　このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。

**変数の宣言**

* float time; ... float型の変数 time を宣言
* public float DRL; ... public な float型の変数 DRL を宣言<br>
 → Editor上ではDRLのスケジュール値を入れてください

**Update()**

* New_1
    * **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
        * 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
        * **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
            * 効果音( Operandum1SE )が鳴る
* New_2
    * **「if (time > DRL)」**の処理 ... timeがDRLのスケジュール値を超過したときの処理
        * Ramp_off を非アクティブ化、Ramp_on をアクティブ化<br>
         → 疑似的に強化可能ランプの点灯を表現
        * Invoke("ResetTime", Ramp_Script.ReinforceableTime)<br>
         → 強化可能ランプ点灯時から強化可能時間が経過すると、time を0にリセット
        * **「else」**の処理 ... timeがDRLのスケジュール値を超過したとき以外の処理
            * time を0にリセット<br>
             → オペランダムに反応すると時間がリセットされ、もう一度時間を計測しはじめる

# 4. DRH
## 4.1. 「オペランダムへの反応」→「得点上昇」
　Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の *Operandum1* の Script にはなかったコードです。

```C#:Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Operandum1_Script : MonoBehaviour
{
    int Point = 1;
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public Text CountText;
    public AudioClip PointSE;
    AudioSource audioSource;

    //New
    float time;
    public float DRH;
    public AudioClip Operandum1SE;


    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }


    // New
    void Update()
    {
        if (Sd1_on.activeSelf)
        {
            time += Time.deltaTime;
            if (Input.GetKeyDown(KeyCode.F))
            {
                audioSource.PlayOneShot(Operandum1SE);
                if (time < DRH)
                {
                    audioSource.PlayOneShot(PointSE);
                    CountText.text = "Point : " + Point.ToString();
                    Point += 1;
                    time = 0;
                }
                else
                {
                    time = 0;
                }
            }
        }

        if (Sd1_off.activeSelf)
        {
            time = 0;
        }
    }
}
```
　このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。

**変数の宣言**

* float time; ... float型の変数 time を宣言
* public float DRH; ... public な float型の変数 DRH を宣言<br>
 → Editor上ではDRHのスケジュール値を入れてください
* public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言<br>
 → Editor上では *Operandum1* に反応したときに鳴るSEを入れてください

**Update()**

* **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
    * 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
    * **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
        * 効果音( Operandum1SE )が鳴る
        * **「if (time < DRH)」**の処理 ... timeがDRHのスケジュール値未満だったときの処理
            * 効果音( PointSE )が鳴る
            * 得点が1点上昇( Point += 1; )
            * time を0にリセット<br>
             → 得点が上昇すると時間がリセットされ、もう一度時間を計測しはじめる<br>
             → 弁別刺激が点灯している間、DRHが走り続ける
        * **「else」**の処理 ... timeがDRHのスケジュール値未満だったとき以外の処理
            * time を0にリセット<br>
             → オペランダムに反応すると時間がリセットされ、もう一度時間を計測しはじめる

## 4.2. 「強化オペランダムへの反応」→「得点上昇」
　Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の *Operandum1* の Script にはなかったコードです。

```C#:Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Operandum1_Script : MonoBehaviour
{
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public GameObject Ramp_off;
    public GameObject Ramp_on;
    public AudioClip Operandum1SE;
    AudioSource audioSource;
    GameObject Ramp;
    Ramp_Script Ramp_Script;

    //New
    float time;
    public float DRH;


    void ResetTime()
    {
        time = 0;
    }


    void Start()
    {
        audioSource = GetComponent<AudioSource>();
        Ramp = GameObject.Find("Ramp");
        Ramp_Script = Ramp.GetComponent<Ramp_Script>();
    }

    
    void Update()
    {
        // New_1
        if (Sd1_on.activeSelf)
        {
            time += Time.deltaTime;
            if (Input.GetKeyDown(KeyCode.F))
            {
                audioSource.PlayOneShot(Operandum1SE);

                // New_2
                if (time < DRH)
                {
                    Ramp_off.SetActive(false);
                    Ramp_on.SetActive(true);
                    Invoke("ResetTime", Ramp_Script.ReinforceableTime);
                }

                else
                {
                    time = 0;
                }
            }
        }

        if (Sd1_off.activeSelf)
        {
            time = 0;
            Ramp_off.SetActive(true);
            Ramp_on.SetActive(false);
        }
    }
}
```
　このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。

**変数の宣言**

* float time; ... float型の変数 time を宣言
* public float DRH; ... public な float型の変数 DRH を宣言<br>
 → Editor上ではDRHのスケジュール値を入れてください

**Update()**

* New_1
    * **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
        * 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
        * **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
            * 効果音( Operandum1SE )が鳴る
* New_2
    * **「if (time ＜ DRH)」**の処理 ... timeがDRHのスケジュール値未満だったときの処理
        * Ramp_off を非アクティブ化、Ramp_on をアクティブ化<br>
         → 疑似的に強化可能ランプの点灯を表現
        * Invoke("ResetTime", Ramp_Script.ReinforceableTime)<br>
         → 強化可能ランプ点灯時から強化可能時間が経過すると、time を0にリセット
        * **「else」**の処理 ... timeがDRHのスケジュール値未満だったとき以外の処理
            * time を0にリセット<br>
             → オペランダムに反応すると時間がリセットされ、もう一度時間を計測しはじめる

## 4.3. *x sec* 間に *n* 回
　「2.2. DRHとは」では、DRHはスケジュール値よりも短い時間での反応に強化子が随伴する強化スケジュールと書きましたが、*x sec* 間に *n* 回反応すると強化子が随伴する場合もあるようです(e.g., 小野, 1994)。
　Unityで作成する場合は、弁別刺激点灯時に限り反応はいつでも受けつけるけれど、スケジュール値 ( *x sec* ) 未満かつ *n* 回反応しなければ強化子を得られない ( あるいは *Ramp* が点灯しない ) ようにすれば良いです。また、スケジュール値 ( *x sec* ) 以降に反応したり *n* 回に到達していない場合は、時間を0にリセットするよう設定します。


### 4.3.1. 「オペランダムへの反応」→「得点上昇」

```C#:Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Operandum1_Script : MonoBehaviour
{
    int Point = 1;
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public Text CountText;
    public AudioClip PointSE;
    AudioSource audioSource;

    //New
    float time;
    int Counter = 0;
    public float DRHTime;
    public float DRHTimes;
    public AudioClip Operandum1SE;


    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }


    // New
    void Update()
    {
        if (Sd1_on.activeSelf)
        {
            time += Time.deltaTime;
            if (Input.GetKeyDown(KeyCode.F))
            {
                audioSource.PlayOneShot(Operandum1SE);
                if (time < DRHTime)
                {
                    if (Counter == DRHTimes)
                    {
                        audioSource.PlayOneShot(PointSE);
                        CountText.text = "Point : " + Point.ToString();
                        Point += 1;
                        time = 0;
                        Counter = 0;
                    } 

                }

                else
                {
                    time = 0;
                    Counter = 0;
                }
            }
        }

        if (Sd1_off.activeSelf)
        {
            time = 0;
            Counter = 0;
        }
    }
}
```



### 4.3.2. 「強化オペランダムへの反応」→「得点上昇」

``` C#:Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Operandum1_Script : MonoBehaviour
{
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public GameObject Ramp_off;
    public GameObject Ramp_on;
    public AudioClip Operandum1SE;
    AudioSource audioSource;
    GameObject Ramp;
    Ramp_Script Ramp_Script;

    //New
    float time;
    public float DRHTime;
    public float DRHTimes;
    int Counter = 0;


    void ResetTime()
    {
        time = 0;
        Counter = 0;
    }


    void Start()
    {
        audioSource = GetComponent<AudioSource>();
        Ramp = GameObject.Find("Ramp");
        Ramp_Script = Ramp.GetComponent<Ramp_Script>();
    }


    void Update()
    {
        // New_1
        if (Sd1_on.activeSelf)
        {
            time += Time.deltaTime;
            Debug.Log(time);
            if (Input.GetKeyDown(KeyCode.X))
            {
                audioSource.PlayOneShot(Operandum1SE);
                Counter += 1;

                // New_2
                if (time < DRHTime)
                {
                    if (Counter == DRHTimes)
                    {
                        Ramp_off.SetActive(false);
                        Ramp_on.SetActive(true);
                        Invoke("ResetTime", Ramp_Script.ReinforceableTime);
                    } 
                }
                else
                {
                    time = 0;
                    Counter = 0;
                }
            }
        }

        if (Sd1_off.activeSelf)
        {
            time = 0;
            Counter = 0;
            Ramp_off.SetActive(true);
            Ramp_on.SetActive(false);
        }
    }
}
```


# 5. 最後に
　「反応率分化強化スケジュール」の中の、「低反応率分化強化(Differential Reinforcement of Low rates)」と「低反応率分化強化(Differential Reinforcement of High rates)」をUnityで作る方法の解説を行いました。コードや用語等で間違っている点があれば、ご指摘いただけると幸いです。

# 参考URL

・NumPy, randomで様々な種類の乱数の配列を生成
https://note.nkmk.me/python-numpy-random/

・Unity で CSV ファイルを読み込む方法
https://note.com/macgyverthink/n/n83943f3bad60

・【Unity】C#の基本構文『for』
http://kimama-up.net/unity-for/


# 引用文献
小野 浩一 (1994). 迷信行動と言語. 駒澤社会学研究, *26*, 59-84.

Ono K, & Iwabuchi K. (1997). Effects of histories of differential reinforcement of response rate on variable-interval responding. *Journal of the Experimental Analysis of Behavior*. *67*(3), 311–322.

坂上 貴之・井上 雅彦 (2018). 行動分析学──行動の科学的理解をめざして── 有斐閣