DonkeyCar simulatorで強化学習(その1)

DonkeyCar simulatorで強化学習のサンプルを実行してみる(DDQN編)

概要

DonkeyCar3シミュレーターで強化学習してみるのマネをしてDonkeyCarシミュレータライブラリの中にあるサンプルのddqn.pyを実行してみる。
参考:
DQNについてはここが分かりやすかったかな。
【深層強化学習,入門】Deep Q Network(DQN)の解説とPythonで実装 〜図を使って説明〜
【深層強化学習】Double Deep Q Network(DDQN)

準備

python仮想環境の準備

# 作業ディレクトリ作成
mkdir -p /work2/donkey_sim
cd /work2/donkey_sim

# 仮想環境構築
pyenv virtualenv 3.8.11 donkey_sim
pyenv local donkey_sim 
pip install --upgrade pip setuptools wheel

# 必要なライブラリのインストール
pip install opencv-python
pip install tensorflow

# 現時点での最新リリース v21.7.24 をcheckoutしておく
git clone https://github.com/tawnkramer/gym-donkeycar -b v21.07.24

# gym-donkeycarライブラリのインストール
pip install -e gym-donkeycar

[!NOTE] gym-donkeycar は -e (–editable) オプション付きでインストールしているので、編集も可能。 git cloneした先を参照しているので、インストール後もgym-donkeycarを削除しちゃダメ。

githubから直接インストールする場合は以下。
この場合、パッケージは通常のディレクトリにインストールされる。

pip install git+https://github.com/tawnkramer/gym-donkeycar@v21.07.24

でも、以下でサンプルプログラム使うので、git cloneもしないとダメ。

シミュレータのダウンロード&インストール

以下のページから実行するプラットフォームに合わせてDonkeySimXXXX.zip(XXXXはプラットフォーム名)をダウンロードし、
適当なディレクトリに展開しておきます。
(Linux/Macの場合は実行属性付けるのを忘れずに)
https://github.com/tawnkramer/gym-donkeycar/releases

マシンスペックがそれほど高くない場合は別マシンで実行してリモート接続するのがおススメ。
SSH接続で実行する場合はリモート必須。

patchをあてる

以下のパッチファイルを使用してサンプルプログラムにパッチをあてます。
内容は、

rl_sample.patch

diff --git a/examples/reinforcement_learning/ddqn.py b/examples/reinforcement_learning/ddqn.py
index 87c74f0..5c32f49 100755
--- a/examples/reinforcement_learning/ddqn.py
+++ b/examples/reinforcement_learning/ddqn.py
@@ -21,6 +21,8 @@ from tensorflow.keras.layers import Activation, Conv2D, Dense, Flatten
 from tensorflow.keras.models import Sequential
 from tensorflow.keras.optimizers import Adam
 
+import gym_donkeycar
+
 EPISODES = 10000
 img_rows, img_cols = 80, 80
 # Convert image into Black and white
@@ -121,6 +123,9 @@ class DQNAgent:
         if self.epsilon > self.epsilon_min:
             self.epsilon -= (self.initial_epsilon - self.epsilon_min) / self.explore
 
+    def set_epsilon(self, epsilon):
+        self.epsilon = epsilon
+
     def train_replay(self):
         if len(self.memory) < self.train_start:
             return
@@ -196,15 +201,17 @@ def run_ddqn(args):
     run a DDQN training session, or test it's result, with the donkey simulator
     """
 
-    # only needed if TF==1.13.1
-    config = tf.ConfigProto()
-    config.gpu_options.allow_growth = True
-    sess = tf.Session(config=config)
-    K.set_session(sess)
+    tf_ver = tf.__version__.split('.')
+    if (tf_ver[0] == 1 and tf_ver[1] >= 13) :
+        # only needed if TF==1.13.1
+        config = tf.ConfigProto()
+        config.gpu_options.allow_growth = True
+        sess = tf.Session(config=config)
+        K.set_session(sess)
 
     conf = {
         "exe_path": args.sim,
-        "host": "127.0.0.1",
+        "host": args.host,
         "port": args.port,
         "body_style": "donkey",
         "body_rgb": (128, 128, 128),
@@ -237,6 +244,9 @@ def run_ddqn(args):
     try:
         agent = DQNAgent(state_size, action_space, train=not args.test)
 
+        if args.epsilon > 0 :
+            agent.set_epsilon(args.epsilon)
+
         throttle = args.throttle  # Set throttle as constant value
 
         episodes = []
@@ -350,6 +360,7 @@ if __name__ == "__main__":
         default="manual",
         help="path to unity simulator. maybe be left at manual if you would like to start the sim on your own.",
     )
+    parser.add_argument("--host", type=str, default="127.0.0.1", help="simulator address")
     parser.add_argument("--model", type=str, default="rl_driver.h5", help="path to model")
     parser.add_argument("--test", action="store_true", help="agent uses learned model to navigate env")
     parser.add_argument("--port", type=int, default=9091, help="port to use for websockets")
@@ -357,6 +368,7 @@ if __name__ == "__main__":
     parser.add_argument(
         "--env_name", type=str, default="donkey-warehouse-v0", help="name of donkey sim environment", choices=env_list
     )
+    parser.add_argument("--epsilon", type=float, default=0.0, help="initial epsilon value")
 
     args = parser.parse_args()

以下のコマンドを実行します。

cd gym-donkeycar/
patch -p1 < rl_sample.patch 

とりあえず学習

とりあえず学習しないと話にならないので学習します。
強化学習は教師データが要らないので、準備がラクチン… でも学習には時間がかかる…
DonkeyCar シミュレータをリモートマシンで実行する場合、
リモートマシンでDonkeyCar シミュレータを実行しておき、以下のコマンドを実行します。 --simオプションにremoteを、実行マシンのIPアドレスを--hostオプションに指定します。

cd examples/reinforcement_learning
python ddqn.py --sim=remote --host=192.168.78.200

[!NOTE] ローカルマシンでシミュレータを実行する場合は
--simオプションにDonkeyCar シミュレータ起動コマンドをフルパスで指定します。
このとき、--hostオプションは不要です。
シミュレータをあらかじめ起動しておく必要はなく、自動的に起動されます。

エピソード毎に学習結果が rl_driver.h5に保存されるので、任意のタイミングでCTRL-Cで中断できます。
次回学習を再開する場合は、ログとして表示されているepsilon: 0.XXXXXXXの部分の最後の値を覚えておいてください。
このプログラムではε値は0.02を下回ると固定されるので、ある程度学習が進んだ状態では0.02だと思っても問題ないでしょう。

学習を再開する場合は以下のように上記コマンドに--epsilonオプションを追加して実行します。
(0.XXXXXXXの部分は上で覚えておいた値。ピッタリ同じでなくて大体で可)

python ddqn.py --sim=remote --host=192.168.78.200 --epsilon=0.XXXXXXX

学習結果でテストしてみる。

学習のときのコマンドに--testを指定するだけです。
--epsilonオプションは指定しません。

python ddqn.py --sim=remote --host=192.168.78.200 --test

うまく学習が進んでいれば、コースアウトすることなく周回してくれるハズ。
学習時と同様、コースアウトすると自動的にスタート位置に戻って再スタートします。
適当にCTRL-Cで止めてください。

ソースを読んでみる

以下はソースを読んだ時のメモです。
書いてみたけど、自分で読んでも なにが何だか分からない…😢

冒頭部分

この辺はお約束なので。

"""
file: ddqn.py
author: Felix Yu
date: 2018-09-12
original: https://github.com/flyyufelix/donkey_rl/blob/master/donkey_rl/src/ddqn.py
"""
import argparse
import os
import random
import signal
import sys
import uuid
from collections import deque

import cv2
import gym
import numpy as np
import tensorflow as tf
from tensorflow.keras import backend as K
from tensorflow.keras.layers import Activation, Conv2D, Dense, Flatten
from tensorflow.keras.models import Sequential
from tensorflow.keras.optimizers import Adam

冒頭部分その2

gym_donkeyをimportしないとDonkeyCarシミュレータと接続できないので。
なぜかオリジナルでは入ってなかった。
if __name__ == "__main__":付けといた方が良いかもしれんが、とりあえずそのままimportしておく。

import gym_donkeycar

パラメータの設定

意味は以下の通り。

変数 意味
EPISODES 学習回数
img_rows 入力に使用する画像サイズ(Y)
img_cols 入力に使用する画像サイズ(X)
img_channels 入力に過去何フレーム分のデータを使用するか
EPISODES = 10000
img_rows, img_cols = 80, 80
# Convert image into Black and white
img_channels = 4  # We stack 4 frames

強化学習エージェントクラス

強化学習のエージェントを定義したクラスです。

class DQNAgent:

コンストラクタ

クラス変数

変数 意味
t 実行カウンタ(ステータス表示用のみ使用)
max_Q Q値の最大値(ステータス表示用のみ使用)
train 学習モード/テストモード(--testオプションで指定)
state_size モデルの入力層のサイズ。現状未使用
action_space シミュレータの現在のステアリング/スロットル設定値取得用
action_size 未使用。たぶん、ステアリング角を何分割するかの定義(15)にすべきだと思う
discount_factor 割引率(γ値) (固定値)
learning_rate 学習率 (固定値)
epsilon 現在の探索率(ε値)
initial_epsilon 探索率の最大値 最小率と共に探索率の変更率を計算する(固定値)
epsilon_min 探索率の最小値 学習時の探索率をこれより小さくしない(固定値)
explore 探索率を最小値にするまでの回数(固定値)
batch_size バッチサイズ (固定値)
train_start 学習開始タイミング(最初は学習を行わない)(固定値)
memory Experience Buffer
model メインモデル
target_model ターゲットモデル(double DQNなので)
    def __init__(self, state_size, action_space, train=True):
        self.t = 0
        self.max_Q = 0
        self.train = train

        # Get size of state and action
        self.state_size = state_size
        self.action_space = action_space
        self.action_size = action_space

        # These are hyper parameters for the DQN
        self.discount_factor = 0.99
        self.learning_rate = 1e-4
        if self.train:
            self.epsilon = 1.0
            self.initial_epsilon = 1.0
        else:
            self.epsilon = 1e-6
            self.initial_epsilon = 1e-6
        self.epsilon_min = 0.02
        self.batch_size = 64
        self.train_start = 100
        self.explore = 10000

        # Create replay memory using deque
        self.memory = deque(maxlen=10000)

        # Create main model and target model
        self.model = self.build_model()
        self.target_model = self.build_model()

        # Copy the model to target model
        # --> initialize the target model so that the parameters of model & target model to be same
        self.update_target_model()

モデルの生成

そんなに複雑なモデルではないみたい。

    def build_model(self):
        model = Sequential()
        model.add(
            Conv2D(24, (5, 5), strides=(2, 2), padding="same", input_shape=(img_rows, img_cols, img_channels))
        )  # 80*80*4
        model.add(Activation("relu"))
        model.add(Conv2D(32, (5, 5), strides=(2, 2), padding="same"))
        model.add(Activation("relu"))
        model.add(Conv2D(64, (5, 5), strides=(2, 2), padding="same"))
        model.add(Activation("relu"))
        model.add(Conv2D(64, (3, 3), strides=(2, 2), padding="same"))
        model.add(Activation("relu"))
        model.add(Conv2D(64, (3, 3), strides=(1, 1), padding="same"))
        model.add(Activation("relu"))
        model.add(Flatten())
        model.add(Dense(512))
        model.add(Activation("relu"))

        # 15 categorical bins for Steering angles
        model.add(Dense(15, activation="linear"))

        adam = Adam(lr=self.learning_rate)
        model.compile(loss="mse", optimizer=adam)

        return model

RGB→グレースケール変換処理

シミュレータの出力はRGB画像、モデルの入力はグレースケール画像なので、その変換を行うための関数。
cv2.dst = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY) で良い気もするが…

    def rgb2gray(self, rgb):
        """
        take a numpy rgb image return a new single channel image converted to greyscale
        """
        return np.dot(rgb[..., :3], [0.299, 0.587, 0.114])

入力画像前処理

シミュレータの出力画像をモデルの入力データに変換する処理。
RGBからグレースケールに変換し、リサイズを行う。

    def process_image(self, obs):
        obs = self.rgb2gray(obs)
        obs = cv2.resize(obs, (img_rows, img_cols))
        return obs

ターゲットモデルのパラメータ更新

メインモデルのパラメータをターゲットモデルにコピーする

    def update_target_model(self):
        self.target_model.set_weights(self.model.get_weights())

現在の環境での次の行動を取得する

乱数を発生し、ε値以下だったら環境が生成したランダム値(self.action_space.sample()[0])を返す。
それ以外はメインモデルで予測した結果を返す。
その際、モデルの出力結果そのままではなく、どのステアリング位置に当たるかの量子化を行って返す。
(得られるのはステアリング情報だけで、スロットル情報は固定値)

    # Get action from model using epsilon-greedy policy
    def get_action(self, s_t):
        if np.random.rand() <= self.epsilon:
            return self.action_space.sample()[0]
        else:
            # print("Return Max Q Prediction")
            q_value = self.model.predict(s_t)

            # Convert q array to steering value
            return linear_unbin(q_value[0])

状態等の保存

現在の状態(state)、行動(action)、報酬(reward)、行動後の状態(next_state)、 終了フラグ(done)をExperience Bufferに保存する。
(Experience Buffer は Experience Replayに使用するためのデータを保存しておくところ)
memorycollections.dque()で作成しているので、指定サイズを超えたときは古いデータから順に削除される。

    def replay_memory(self, state, action, reward, next_state, done):
        self.memory.append((state, action, reward, next_state, done))

ε値の更新

現在のε値が最小値より大きかったら一定比率で小さくしていく。
最小値以下になっていたらそのまま。

    def update_epsilon(self):
        if self.epsilon > self.epsilon_min:
            self.epsilon -= (self.initial_epsilon - self.epsilon_min) / self.explore

ε値の初期設定

--epsilonオプション追加したので、指定値でε値を変更する処理を追加。

    def set_epsilon(self, epsilon):
        self.epsilon = epsilon

学習

Experience Bufferから任意の経験を取り出し、Q Networkをミニバッチ学習(Experience Replay)

記憶したデータ数がself.train_startに達するまでは何もしない。
バッチ学習に使用するデータをExperience Bufferから取り出し、
それぞれの配列にバラす(state_t,action_t, reward_t, state_t1, terminal)。
state_tstate_t1np.concatenate()でndarrayにまとめておく。
11行目のself.model.predict(state_t)self.max_Qの取得にしか使用されておらず、

self.max_Qはステータス表示にしか使用されてなく、無駄な計算なので、削除するのが良いと思われる(無駄な計算なので)。
その場合、targetsの初期化はtargets = np.zeros((batch_size, 15))で行う。
(self.max_Qを参照しているところも削除。あるいはget_action()で戻り値として返すのも手か。)

state_t1を入力としてメインモデルをターゲットモデルを使用して得られた出力から出力期待値を取得し、
学習を行うself.model.train_on_batch(state_t, targets)
この辺は 【深層強化学習】Double Deep Q Network(DDQN) のソースとかを見ると分かったような分からないような気になれるかも…

    def train_replay(self):
        if len(self.memory) < self.train_start:
            return

        batch_size = min(self.batch_size, len(self.memory))
        minibatch = random.sample(self.memory, batch_size)

        state_t, action_t, reward_t, state_t1, terminal = zip(*minibatch)
        state_t = np.concatenate(state_t)
        state_t1 = np.concatenate(state_t1)
        targets = self.model.predict(state_t)
        self.max_Q = np.max(targets[0])
        target_val = self.model.predict(state_t1)
        target_val_ = self.target_model.predict(state_t1)
        for i in range(batch_size):
            if terminal[i]:
                targets[i][action_t[i]] = reward_t[i]
            else:
                a = np.argmax(target_val[i])
                targets[i][action_t[i]] = reward_t[i] + self.discount_factor * (target_val_[i][a])

        self.model.train_on_batch(state_t, targets)

モデルのロード

モデルの読み込み先はメインモデル。
このあと、ターゲットモデルへコピーするので、ここではターゲットモデルは触らない。

    def load_model(self, name):
        self.model.load_weights(name)

モデルの保存

メインモデルをファイルに保存する。

    # Save the model which is under training
    def save_model(self, name):
        self.model.save_weights(name)

ステアリング角→モデル出力形式変換

ステアリング角(-1~1)をモデル出力形式(要素数15の配列のどれか1つに1が入る)に変換する。

def linear_bin(a):
    """
    Convert a value to a categorical array.

    Parameters
    ----------
    a : int or float
        A value between -1 and 1

    Returns
    -------
    list of int
        A list of length 15 with one item set to 1, which represents the linear value, and all other items set to 0.
    """
    a = a + 1
    b = round(a / (2 / 14))
    arr = np.zeros(15)
    arr[int(b)] = 1
    return arr

モデル出力形式→ステアリング角変換

モデル出力のうち、最大値を持つindexに相当するステアリング角を取得する。

def linear_unbin(arr):
    """
    Convert a categorical array to value.

    See Also
    --------
    linear_bin
    """
    if not len(arr) == 15:
        raise ValueError("Illegal array length, must be 15")
    b = np.argmax(arr)
    a = b * (2 / 14) - 1
    return a

メインルーチン

def run_ddqn(args):
    """
    run a DDQN training session, or test it's result, with the donkey simulator
    """

Tensorflow 1.13.1でのおまじない

Tensorflow 2 を使用したかったので、処理不要。
コメントアウトすれば良いのだけれど、なんとなくバージョンで分けてみた。
1.14以降では要るのかな?要ると思って書いてみた。

    tf_ver = tf.__version__.split('.')
    if (tf_ver[0] == 1 and tf_ver[1] >= 13) :
        # only needed if TF==1.13.1
        config = tf.ConfigProto()
        config.gpu_options.allow_growth = True
        sess = tf.Session(config=config)
        K.set_session(sess)

シミュレータ環境の構築

body_style には donkeybarecar01cybertruckf1が使用できるらしい。
body_rgb で 色を指定できるらしい。
car_name でシミュレータに表示される名前を指定。複数の車を走らせるときに見分けられるみたい。
font_sizeで名前のフォントサイズを指定。

    conf = {
        "exe_path": args.sim,
        "host": args.host,
        "port": args.port,
        "body_style": "donkey",
        "body_rgb": (128, 128, 128),
        "car_name": "me",
        "font_size": 100,
        "racer_name": "DDQN",
        "country": "USA",
        "bio": "Learning to drive w DDQN RL",
        "guid": str(uuid.uuid4()),
        "max_cte": 10,
    }

    # Construct gym environment. Starts the simulator if path is given.
    env = gym.make(args.env_name, conf=conf)

プログラム終了時のフックルーチンの定義と登録

プログラム終了時にシミュレータの終了処理を行うようにフックルーチンを登録する。

    # not working on windows...
    def signal_handler(signal, frame):
        print("catching ctrl+c")
        env.unwrapped.close()
        sys.exit(0)

    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGABRT, signal_handler)

パラメータ用変数の定義

    # Get size of state and action from environment
    state_size = (img_rows, img_cols, img_channels)
    action_space = env.action_space  # Steering and Throttle

エージェントの生成

    try:
        agent = DQNAgent(state_size, action_space, train=not args.test)

ε値の設定

オプションでε値が指定されていたら設定する。

        if args.epsilon > 0 :
            agent.set_epsilon(args.epsilon)

スロットル値の設定

スロットルの設定は固定値(コマンドラインオプションで設定)

        throttle = args.throttle  # Set throttle as constant value

        episodes = []

モデルのロード

モデルファイルがあれば読み込む。

        if os.path.exists(args.model):
            print("load the saved model")
            agent.load_model(args.model)

学習ループ

        for e in range(EPISODES):

            print("Episode: ", e)

スタート位置へ移動

obs ← スタート時のカメラ画像
x_tobs をモデルの入力形式に合わせて変換(グレースケール化&リサイズ)
s_tx_tを4枚分コピー(入力画像は過去4枚分を使用するので)(ちゃんとimg_channels参照して欲しいけど)

            done = False
            obs = env.reset()

            episode_len = 0

            x_t = agent.process_image(obs)

            s_t = np.stack((x_t, x_t, x_t, x_t), axis=2)
            # In Keras, need to reshape
            s_t = s_t.reshape(1, s_t.shape[0], s_t.shape[1], s_t.shape[2])  # 1*80*80*4

エピソードループ

終了フラグがセットされるまでループ

            while not done:
現在の状態から行動を予測し、シミュレータで実行

steering ← 予測結果
env.step()でシミュレータステップ実行
x_t1 ←ステップ実行後のカメラ画像
s_t1 ← 現在の入力データの一番古いものを削除し、今回の画像を追加

                # Get action for the current state and go one step in environment
                steering = agent.get_action(s_t)
                action = [steering, throttle]
                next_obs, reward, done, info = env.step(action)

                x_t1 = agent.process_image(next_obs)

                x_t1 = x_t1.reshape(1, x_t1.shape[0], x_t1.shape[1], 1)  # 1x80x80x1
                s_t1 = np.append(x_t1, s_t[:, :, :, :3], axis=3)  # 1x80x80x4
Experience Bufferに現在の状態を保存

ε値の更新も

                # Save the sample <s, a, r, s'> to the replay memory
                agent.replay_memory(s_t, np.argmax(linear_bin(steering)), reward, s_t1, done)
                agent.update_epsilon()
学習実行
                if agent.train:
                    agent.train_replay()
ループ更新処理とステータス表示
                s_t = s_t1
                agent.t = agent.t + 1
                episode_len = episode_len + 1
                if agent.t % 30 == 0:
                    print(
                        "EPISODE",
                        e,
                        "TIMESTEP",
                        agent.t,
                        "/ ACTION",
                        action,
                        "/ REWARD",
                        reward,
                        "/ EPISODE LENGTH",
                        episode_len,
                        "/ Q_MAX ",
                        agent.max_Q,
                    )
ループ更新処理とステータス表示

agent.update_target_model()でターゲットモデルの更新 episodes.append(e)はデバッグ用?

                if done:

                    # Every episode update the target model to be same with model
                    agent.update_target_model()

                    episodes.append(e)

                    # Save model for each episode
                    if agent.train:
                        agent.save_model(args.model)

                    print(
                        "episode:",
                        e,
                        "  memory length:",
                        len(agent.memory),
                        "  epsilon:",
                        agent.epsilon,
                        " episode length:",
                        episode_len,
                    )

エピソードループと学習ループの終わり

キーボード割り込み例外と終了処理

    except KeyboardInterrupt:
        print("stopping run...")
    finally:
        env.unwrapped.close()

コマンドライン解析処理まわり

コマンドライン解析処理とメインルーチンへのジャンプ

if __name__ == "__main__":

    # Initialize the donkey environment
    # where env_name one of:
    env_list = [
        "donkey-warehouse-v0",
        "donkey-generated-roads-v0",
        "donkey-avc-sparkfun-v0",
        "donkey-generated-track-v0",
        "donkey-roboracingleague-track-v0",
        "donkey-waveshare-v0",
        "donkey-minimonaco-track-v0",
        "donkey-warren-track-v0",
        "donkey-thunderhill-track-v0",
        "donkey-circuit-launch-track-v0",
    ]

    parser = argparse.ArgumentParser(description="ddqn")
    parser.add_argument(
        "--sim",
        type=str,
        default="manual",
        help="path to unity simulator. maybe be left at manual if you would like to start the sim on your own.",
    )
    parser.add_argument("--host", type=str, default="127.0.0.1", help="simulator address")
    parser.add_argument("--model", type=str, default="rl_driver.h5", help="path to model")
    parser.add_argument("--test", action="store_true", help="agent uses learned model to navigate env")
    parser.add_argument("--port", type=int, default=9091, help="port to use for websockets")
    parser.add_argument("--throttle", type=float, default=0.3, help="constant throttle for driving")
    parser.add_argument(
        "--env_name", type=str, default="donkey-warehouse-v0", help="name of donkey sim environment", choices=env_list
    )
    parser.add_argument("--epsilon", type=float, default=0.0, help="initial epsilon value")

    args = parser.parse_args()

    run_ddqn(args)

でもって、こんな改造をするとちょびっと計算量が減る

シミュレータに表示される車を変更してるのはご愛敬😅

--- ddqn.py.old	2021-12-02 06:25:02.149997073 +0900
+++ ddqn.py	2021-12-03 07:14:49.346378878 +0900
@@ -98,9 +98,10 @@
         return np.dot(rgb[..., :3], [0.299, 0.587, 0.114])
 
     def process_image(self, obs):
-        obs = self.rgb2gray(obs)
-        obs = cv2.resize(obs, (img_rows, img_cols))
-        return obs
+        # obs1 = self.rgb2gray(obs)
+        obs1 = cv2.dst = cv2.cvtColor(obs, cv2.COLOR_RGB2GRAY)
+        obs2 = cv2.resize(obs1, (img_rows, img_cols))
+        return obs2
 
     def update_target_model(self):
         self.target_model.set_weights(self.model.get_weights())
@@ -108,13 +109,17 @@
     # Get action from model using epsilon-greedy policy
     def get_action(self, s_t):
         if np.random.rand() <= self.epsilon:
-            return self.action_space.sample()[0]
+            return self.action_space.sample()[0], 0
         else:
             # print("Return Max Q Prediction")
             q_value = self.model.predict(s_t)
 
+            max_q = np.amax(q_value[0])
+            if self.max_Q < max_q :
+                self.max_Q = max_q
+
             # Convert q array to steering value
-            return linear_unbin(q_value[0])
+            return linear_unbin(q_value[0]), max_q
 
     def replay_memory(self, state, action, reward, next_state, done):
         self.memory.append((state, action, reward, next_state, done))
@@ -136,16 +141,16 @@
         state_t, action_t, reward_t, state_t1, terminal = zip(*minibatch)
         state_t = np.concatenate(state_t)
         state_t1 = np.concatenate(state_t1)
-        targets = self.model.predict(state_t)
-        self.max_Q = np.max(targets[0])
-        target_val = self.model.predict(state_t1)
-        target_val_ = self.target_model.predict(state_t1)
+
+        targets = np.zeros((batch_size, 15))
+        q_val = self.model.predict(state_t1)
+        target_q_val = self.target_model.predict(state_t1)
         for i in range(batch_size):
             if terminal[i]:
                 targets[i][action_t[i]] = reward_t[i]
             else:
-                a = np.argmax(target_val[i])
-                targets[i][action_t[i]] = reward_t[i] + self.discount_factor * (target_val_[i][a])
+                a = np.argmax(q_val[i])
+                targets[i][action_t[i]] = reward_t[i] + self.discount_factor * (target_q_val[i][a])
 
         self.model.train_on_batch(state_t, targets)
 
@@ -213,8 +218,8 @@
         "exe_path": args.sim,
         "host": args.host,
         "port": args.port,
-        "body_style": "donkey",
-        "body_rgb": (128, 128, 128),
+        "body_style": "f1",
+        "body_rgb": (255, 128, 128),
         "car_name": "me",
         "font_size": 100,
         "racer_name": "DDQN",
@@ -273,7 +278,7 @@
             while not done:
 
                 # Get action for the current state and go one step in environment
-                steering = agent.get_action(s_t)
+                steering, max_Q = agent.get_action(s_t)
                 action = [steering, throttle]
                 next_obs, reward, done, info = env.step(action)
 
@@ -305,7 +310,7 @@
                         "/ EPISODE LENGTH",
                         episode_len,
                         "/ Q_MAX ",
-                        agent.max_Q,
+                        max_Q,
                     )
 
                 if done: