雑ではあるが、ここではメモ程度の説明とします。

このお題では、アルゴリズムへの入力は角度なので-π ~ π の連続値、出力は時間間隔なので、0 ~ t_max の連続値である。

強化学習の基本であるQ-leaningアルゴリズムを使いたいのだが、Q-learning は離散値に対して離散値を出力する仕様となっている。

そこで、入出力は離散化して扱う。それぞれを何分割するかはパラメータであり、チューニングが必要であるが、少ないと学習が荒すぎて目標の行動が獲得できないかもしれないし、あまり多くすると学習がなかなか進まなくなる。

動画でのQ-learning は、角度を21分割、出力も21分割で行っていて、次に説明する 21 x 21 の Q-table (配列変数)を使用していた。

Q-table 配列変数Q[is, ia] の is は角度を変換した 0~20 が入り、ia は0~t_max を変換した0~20が入る。
それぞれのQ[is, ia] は、角度is の時に ia を出力した場合に得られる将来の報酬量の近似値がはいる。
式で書くとQは、

Q[is, ia] = reward(t) + gamma * reward(t+1) + gamma**2 * reward(t+2) + …

となるように逐次更新されていく。

もし、Q[is, ia] がすべてのis, ia で正しく求まれば、正しい行動は、optimal_a = argmax(Q[is, :]) となる。

しかし、学習初期においては、Qは正しくはなく、適度のノイズをもって行動を選択することが重要である。ここでの行動選択には、epsilon greedy 法を使い、epsilon は 0.1 からスタート、振り子が立つたびに0.5を掛けて小さくしていった。

行動選択を行う関数は以下のような感じ。

def action(s, epsilon):
    istate = digitize_state(s) # 角度sを 0~21の数値に変換
    q = self.Q[istate, :]
    if np.random.rand() < epsilon: # epsilon 以下の確率でランダムに行動を選ぶ
        ia = np.random.randint(self.action_n)
        is_random = True
    else:
        ia = np.argmax(q) # そうでなかったら最もQ値の高い行動を選ぶ
        is_random = False
    a = a_val(ia) # 0~20 までのia を 0~max_t に変換
    return a

Q-table を正しい値に近づけていくために、reward を受け取るたびに Q-table のupdate を以下のコードで行う。
Q-learning のtable のupdateに使う学習定数 alpha は、これも0.1からスタートし、振り子が立つたびに、0.5を掛けて小さくしていった。gamma は 0.9 で固定。

Q-table をupdate する関数は以下のような感じ。

import numpy as np

def update(self, s, a, r, s_next, alpha, gamma, done):
    js = digitize_state(s) # 0~20の整数に変換
    ia = digitize_action(a) # 0~20の整数に変換
    q0 = Q[js, ia] # q_table
    if done is False: # terminal ではないとき
        js_next = self.digitize_state(s_next)
        qmax= np.max(self.Q[js_next, :])
        q0 = (1 - alpha) * q0 + alpha * (r + gamma * qmax)
    else: # terminal のとき
        q0 = (1- alpha) * q0 + alpha * r
    self.Q[js, ia] = q0