C言語でEV3開発(26)
EV3で機械学習(1)
QLearningをC#に委譲してみた

2021年2月6日

どもです。
前回の投稿前回は、Q-LearningをEV3本体に実装してみたら、メモリが足りなくて動かせなかった、という内容を書きました。
今回は、この問題を解決すべく、Ev3側に実装していたQ-Learnig処理をPC側に委譲・計算させてみたので、その結果について書きます。

1.PCへの委譲内容

PC側へ委譲するQ-Learningは、状態の判定から行動の評価、および評価関数の一式です。
これらの処理に必要な情報をEV3から取得し、一連の処理(状態の判定~次の行動の決定)を行い、その後決定した行動に基づくパラメータの通知をPC側で行うようにします。
すなわち、一連の処理を記載すると、下記のようになります。

  1. 状態の判定に必要なパラメータ、状態情報を各種センサーから取得。
  2. 取得したセンサー情報を、PCへ通知する。
  3. PCは、取得したセンサー情報からQ-Learningを実施し、次の行動を決定する。
  4. PCは、決定した行動をEV3に通知する。

なお、取得するセンサー情報、状態情報は前回と同じく、下記のものを取得します。

  • モーターの出力値(出力目標) x2
  • 超音波センサーの距離値

2.PCとEv3の通信

PCとEv3の通信は、Bluetoothで行います。
なお、この通信処理については、EV3開発サポートツール(3)-Ev3Controllerで紹介したアプリケーション(GitHubで公開済み)で作成した機能を流用します。

3.PC側の実装

PC側のアプリケーションは、C#で実装します。
これは、先に記載したとおり、過去の記事で紹介したツールの機能を流用するためです。
また、C#で実装するQ-Learning処理については、EV3側で実装したコードをほとんど変更せずに流用しています。
ただし、Q-Learningでの報酬の与え方のみ変更しています。
なお、今回は前述のセンサー値/状態情報でQ-Learningがどのように学習、振る舞うかの確認を目的としているため、開発するアプリケーションは、コンソールアプリケーションとしています。

実装は、下記の通りです。
全てのコードは記載できないので、Q-Learningを行うクラスのみを記載します。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace Ev3ConsoleDeepLearning.DeepLearning
{
    /// <summary>
    /// Class for machine learning, Q-Learning algorithm.
    /// </summary>
    public class QLearning
    {
        public enum ACTION {
            ACTION_SPEED_UP = 0,
            ACTION_SLOW_DOWN,
            ACTION_TURN_RIGHT,
            ACTION_TURN_LEFT,
            ACTION_SPEED_KEEP,
            ACTION_NUM
        };

        protected const int STATE_MOTOR = 81;   //-41 to 41
        protected const int STATE_DIST = 251;   //  0 to 250
        protected const int Q_VALUE_MAX = 10000;// 0 to 100(LSB:0.01)
        protected const int Q_VALUE_MIN = 0;    // minimum value.
        protected const Int16 ALPHA = 10;       //Learning coefficient, LSB : 0.01.
        protected const Int16 GANNMA = 90;      //Discount rate, LSB : 0.01. 

        protected Int16[ , , , ] qvalues;//4 dimensional array.
        protected Random rnd;

        /// <summary>
        /// Initialize values.
        /// </summary>
        public void Init()
        {
            this.rnd = new Random(10000);

            this.InitQValues();
        }

        /// <summary>
        /// Initialize q-values.
        /// </summary>
        protected void InitQValues()
        {
            qvalues = new Int16[STATE_MOTOR, STATE_MOTOR, STATE_DIST, (int)ACTION.ACTION_NUM];
            for (int i = 0; i < STATE_MOTOR; i++)
            {
                for (int j = 0; j < STATE_MOTOR; j++)
                {
                    for (int k = 0; k < STATE_DIST; k++)
                    {
                        for (int l = 0; l < (int)ACTION.ACTION_NUM; l++)
                        {
                            qvalues[i, j, k, l] = (Int16)rnd.Next(Q_VALUE_MIN, Q_VALUE_MAX);

                            if ((0 == i) || (0 == j))
                            {
                                qvalues[i, j, k, (int)ACTION.ACTION_SLOW_DOWN] = 0;
                            }
                            if (((STATE_MOTOR - 1) == i) || ((STATE_MOTOR - 1) == j))
                            {
                                qvalues[i, j, k, (int)ACTION.ACTION_SPEED_UP] = 0;
                                qvalues[i, j, k, (int)ACTION.ACTION_SPEED_KEEP] = 0;
                                qvalues[i, j, k, (int)ACTION.ACTION_TURN_LEFT] = 0;
                                qvalues[i, j, k, (int)ACTION.ACTION_TURN_RIGHT] = 0;
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Select action based on current state decided by motor output power and distance read
        /// from ultrasonic sensor.
        /// </summary>
        /// <param name="MotorState1">Motor output power.</param>
        /// <param name="MotorState2">Motor output power.</param>
        /// <param name="Distance">Distance read from ultrasonic sensor.</param>
        /// <returns>
        /// Action selected (decided) base on "Q" values specified by motor output
        /// power and distance.
        /// </returns>
        protected const Int16 EPSILON = 3000;
        public ACTION SelectAction(int MotorState1, int MotorState2, int Distance)
        {
            ACTION SelAction = ACTION.ACTION_NUM;

            if (this.Random100() < EPSILON)
            {
                do
                {
                    SelAction = this.SelectActionByRandom();
                } while (0 == this.qvalues[MotorState1, MotorState2, Distance, (int)SelAction]);
            }
            else
            {
                SelAction = this.SelectActionByQ(MotorState1, MotorState2, Distance);
            }

            return SelAction;
        }

        /// <summary>
        /// Select action based on Q-Learned value, qvalues, specified by motor output powers and
        /// distance read from ultrasonic sensor.
        /// </summary>
        /// <param name="MotorState1">motor output power</param>
        /// <param name="MotorState2">motor output power</param>
        /// <param name="Distance">distance read from ultrasonic sensor</param>
        /// <returns>selected action</returns>
        public ACTION SelectActionByQ(int MotorState1, int MotorState2, int Distance)
        {
            ACTION SelAction = ACTION.ACTION_NUM;
            int QMax = 0;
            int ActionIndex = 0;

            for (ActionIndex = 0; ActionIndex < (int)ACTION.ACTION_NUM; ActionIndex++)
            {
                if (QMax < this.qvalues[MotorState1, MotorState2, Distance, ActionIndex])
                {
                    QMax = this.qvalues[MotorState1, MotorState2, Distance, ActionIndex];
                    SelAction = (ACTION)ActionIndex;
                }
            }

            return SelAction;
        }

        /// <summary>
        /// Returns action randomly.
        /// </summary>
        /// <returns></returns>
        ACTION SelectActionByRandom()
        {
            return (ACTION)(this.rnd.Next() % (int)ACTION.ACTION_NUM);
        }

        /// <summary>
        /// Select next state based on current motor output, distance, and action
        /// based on these values.
        /// </summary>
        /// <param name="MotorState1">Current motor output.</param>
        /// <param name="MotorState2">Current motor output.</param>
        /// <param name="Distance">Distance read from ultrasonic sensor.</param>
        /// <param name="CurAction">Current action </param>
        /// <param name="NextMotorState1">Referenceto next motor output.</param>
        /// <param name="NextMotorState2">Referenceto next motor output.</param>
        public void NextState(int MotorState1, int MotorState2, int Distance, ACTION CurAction, 
            ref int NextMotorState1, ref int NextMotorState2)
        {
            switch (CurAction)
            {
                case ACTION.ACTION_SPEED_UP:
                    NextMotorState1 = MotorState1 + 1;
                    NextMotorState2 = MotorState2 + 1;
                    break;

                case ACTION.ACTION_SLOW_DOWN:
                    NextMotorState1 = MotorState1 - 1;
                    NextMotorState2 = MotorState2 - 1;
                    break;

                case ACTION.ACTION_TURN_LEFT:
                    NextMotorState1 = MotorState1 + 1;
                    NextMotorState2 = MotorState2;
                    break;

                case ACTION.ACTION_TURN_RIGHT:
                    NextMotorState1 = MotorState1;
                    NextMotorState2 = MotorState2 + 1;
                    break;

                case ACTION.ACTION_SPEED_KEEP:
                default:
                    NextMotorState1 = MotorState1;
                    NextMotorState2 = MotorState2;
                    break;
            }

            if (NextMotorState1 <= 0) { NextMotorState1 = 0; }
            if (NextMotorState2 <= 0) { NextMotorState2 = 0; }
            if (STATE_MOTOR <= NextMotorState1) { NextMotorState1 = STATE_MOTOR - 1; }
            if (STATE_MOTOR <= NextMotorState2) { NextMotorState2 = STATE_MOTOR - 1; }
        }

        /// <summary>
        /// Update Q-Learning value, qvalues.
        /// </summary>
        /// <param name="MotorState1">Current motor output.</param>
        /// <param name="MotorState2">Current motor output.</param>
        /// <param name="Distance">Distance read from ultrasonic sensor.</param>
        /// <param name="CurAction">Current action </param>
        /// <param name="NextMotorState1">Next motor output.</param>
        /// <param name="NextMotorState2">Next motor output.</param>
        /// <param name="IsPenalty"></param>
        public void UpdateQValues(int MotorState1, int MotorState2, int Distance, ACTION CurAct,
            int NextMotorState1, int NextMotorState2, bool IsPenalty)
        {
            //ACTION NextAct;
            Int16 Reward = 0;   //LSB:0.01
            //Int16 qvalue = 0;
            //Int16 NextQValue = 0;
            Int16 AlphaItem = 0;
            //Int16 GannmaItem = 0;

            if (!IsPenalty)
            {
                Reward = -100; //LSB:0.01 - PHY = -1
            }
            else
            {
                Reward = (Int16)(((Int32)(ALPHA * ((Int32)1000 - ((Int32)(Distance * 1000) / STATE_DIST)))) / 1000);
            }

            AlphaItem = (Int16)(((Int32)(ALPHA * (Reward - this.qvalues[MotorState1, MotorState2, Distance, (int)CurAct]))) / 100);
            this.qvalues[MotorState1, MotorState2, Distance, (int)CurAct] += AlphaItem;
            if (Q_VALUE_MAX <= this.qvalues[MotorState1, MotorState2, Distance, (int)CurAct])
            {
                this.qvalues[MotorState1, MotorState2, Distance, (int)CurAct] = Q_VALUE_MAX;
            }
            if (this.qvalues[MotorState1, MotorState2, Distance, (int)CurAct] <= Q_VALUE_MIN)
            {
                this.qvalues[MotorState1, MotorState2, Distance, (int)CurAct] = Q_VALUE_MIN;
            }
        }

        /// <summary>
        /// Create random value from 0 to 100, LSB is 0.01.
        /// </summary>
        /// <returns>Rando value</returns>
        protected Int16 Random100()
        {
            return (Int16)this.rnd.Next(Q_VALUE_MIN, Q_VALUE_MAX);
        }
    }
}

4.動かした結果

今回は、都合により位置を固定しています。
すなわち、距離センサーの値が変わらない環境で、検証を行いました。
その実行結果が、下記のグラフです。
Ev3ConsoleDeepLearning
グレーの線が距離センサー値、青とオレンジがモーターの動作値と動作目標値、黄色が動作指示値(学習結果)になります。
動作開始時点では、「0」付近でうろうろしていますが、次第に動作目標値、動作指示値が増加しています。
また、突然センサー距離値が小さくなっている箇所がありますが、そのタイミングでは目標値が小さくなり、それにあわせて指示値も減少しています。
その後、距離が正常(?)に戻れば、またモーター指示値が増加し始めています。
すなわち、前方に障害物があればモーターの動作指示値を下げ、前方に障害物が無ければモーターの動作指示値を上げています。
これらの結果から、「期待する学習が行われている」ということが確認できます。

4.結論

前回、今回と、Ev3での強化学習/Q-Learningの実施例を書いてきました。
Ev3購入時に一緒についてくるセンサーだけでも、強化学習が可能であることを示すことができました。
今後は、センサー情報を増やす、学習方法を増やすことで学習結果がどのように変化するかを見てみたいです。

5.公開しています

今回作成したプログラムの全ては、GitHubにて公開しています。

ではっ!