機械学習は、コンピュータ上での判断/予想をデータ/経験で学習/改善することだが、強化学習も機械学習の一方法. ある状況下 (state[t]) で何らかの行動 (action[t]) を取った場合に報酬 (reward[t]) が得られる世界に、エージェントを送り込む. このエージェントは周囲の状況を観察 (observation[t]) し、様々な行動を取ることで報酬を最大化することを目的とする. 取るべき行動はポリシー (policy[t]) により決定するが、そのポリシーを経験により最適化 (update[t]) するのが強化学習.

<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> Produced by OmniGraffle 7.9.4 2019-03-04 12:28:01 +0000 キャンバス 1 レイヤー 1 policy [t] State state [t] observation [t] action [t] Actor reward [t] update [t]

一連の学習過程はマルコフ決定過程 (Markov decision process) で以下の様に表される. ここで、R(Si-1,Ai-1,Si-1) は報酬 Ri-1 が、あるアクション Ai-1 で、状況が Si-1 から Si に変化する際に得られることを表している. 取るべき行動は、ポリシー π(Si-1) で決定される.

<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> Produced by OmniGraffle 7.9.4 2019-03-05 04:22:02 +0000 キャンバス 1 レイヤー 1 R i-1 S i-1 S i R i S i+1 R i+1 A i+1 A i A i-1 S i+2 𝝅 ( S i-1 ) R(S i-1 , A i-1 , S i )

強化学習用のツールである Gym を用いて強化学習の実際を見てみる. 経験によりポリシーを決定する仕組みを実装してみる. NChain-v0 というゲームは、あり得る状況が 5種類で、取り得る行動は各状況で 2種類. それぞれの状況下で取り得る行動を決定するポリシーを 5 x 2 の行列で表現する. ポリシーが初期状態の場合には、取るべき行動を一様乱数を用いて決定する. ポリシーがアップデートされて居る場合には、報酬が多かった行動を取る.

import gym
env = gym.make('NChain-v0')
nstate = env.observation_space.n
naction = env.action_space.n

def policy_table(env, nstate, naction, nplay=100):
  policy = np.zeros((nstate, naction))

  for _ in range(nplay):
    s0 = env.reset()
    done = False

    while not done:
      if np.sum(policy[s0, :]) == 0:
        action = np.random.randint(0, naction)
      else:
        action = np.argmax(policy[s0, :])
      s1, reward, done, info = env.step(action)
      policy[s0, action] += reward
      s0 = s1

  return policy

policy = policy_table(env, nstate, naction)

これにより決定されたポリシーは以下の様になった.

array([[15528.,     0.],
       [12118.,     0.],
       [    0., 39482.],
       [    0.,  7794.],
       [    0.,  4272.]])

次に目先の利益だけでは無く将来予想される報酬を考慮して現在の行動を決定するために Q-value (state-action values) を最適化するアルゴリズムを使用する.

\[Q_{k+1}(s,a) \leftarrow \Sigma_s T(s,a,s')[R(s,a,s')+\gamma\cdot\max_a Q_k(s',a')]\]

ここで、 \(T(s,a,s')\) は 行動 \(a\) を取った際の状態 \(s\) から 状態 \(s'\) への遷移確率. \(R(s,a,s')\) は、その際に得られる報酬. \(\gamma\) は将来に期待される報酬の割引率. ある状況下 s で取るべき行動は Q-value を最大化する行動、\(\pi^*(s) = \mathrm{argmax}_aQ^*(s,a)\). 対応するコードは以下の様になる.

def policy_table_qlearning(env, nstate, naction, nplay=100):
  ## initialise policy table
  policy = np.zeros((nstate, naction))

  y = 0.95  # discount factor
  lr = 0.8  # learning rate

  for _ in range(nplay):
    s0 = env.reset()
    done = False

    while not done:
      ## decide action to take
      if np.sum(policy[s0, :]) == 0:
        action = np.random.randint(0, naction)
      else:
        action = np.argmax(policy[s0, :])

      ## update policy table, considering future reward
      s1, reward, done, info = env.step(action)
      policy[s0, action] += reward + lr*(y*np.max(policy[s1, :]) - policy[s0, action])
              
      s0 = s1

  return policy

これにより決定されたポリシーは以下の様になった.

array([[21.05875701,  0.        ],
       [ 0.        , 22.22714818],
       [ 0.        , 22.796456  ],
       [ 0.        , 22.82361894],
       [ 0.        , 22.19951512]])

上記二つのアルゴリズム共に Si で最初に取る行動は一様乱数で決定される. 得られた報酬でポリシーを更新するが、一度報酬が得られた行動をその後ずっと踏襲する為に、Si で取る行動は一種類に限定される. 初めに乱数で決定された行動が、最善の行動かどうかは分からないので、このアルゴリズムによる一般的な最適化は不可能となる.

次に、エージェントに好奇心を与えて、ある状況下でこれまでとは違う行動も取るようにしてみる. ε-greedy policy と呼ばれる好奇心を与えたエージェントの学習アルゴリズムは以下の様になる. ここで用いられている、decay はエージェントに飽きっぽさを与える. 何度も同じ事を繰り返していると面倒くさくなり此までの経験で学習したことを盲目的に適用するようになる.

def policy_table_egreedy_qlearning(env, nstate, naction, nplay=100):
  ## initialise policy table
  policy = np.zeros((nstate, naction))

  y = 0.95      # discount factor
  lr = 0.8      # learning rate
  eps = 0.5     # epsilon
  decay = 0.999 # decay factor

  for _ in range(nplay):
    s0 = env.reset()
    done = False
    eps *= decay

    while not done:
      ## decide action to take
      if (np.random.random() < eps) or (np.sum(policy[s0, :]) == 0):
        action = np.random.randint(0, naction)
      else:
        action = np.argmax(policy[s0, :])

      ## update policy table, considering future reward
      s1, reward, done, info = env.step(action)
      policy[s0, action] += reward + lr*(y*np.max(policy[s1, :]) - policy[s0, action])
              
      s0 = s1

  return policy

これにより決定されたポリシーは以下の様になった. si で、取りうるべき行動全てが取られている. この方法で学習回数を増やせば最適なポリシーが得られる.

array([[79.04993073, 78.91555113],
       [82.93251651, 82.45432332],
       [86.74169249, 83.22698797],
       [82.09109268, 89.6518662 ],
       [97.24225729, 89.64617297]])

上記3つのアルゴリズムを対戦させてみる. それぞれのアルゴリズムが一度学習する毎に対戦し、計百回の学習と対戦を行う.

def run_game(policy, env):
  s0 = env.reset()
  sum_reward = 0
  done = False
  while not done:
    action = np.argmax(policy[s0, :])
    s1, reward, done, info = env.step(action)
    sum_reward += reward
  return sum_reward


def compare_methods(env, nstate, naction, nplay=100):
  winner = np.zeros((3,))

  for _ in range(nplay):
    print('inf> loop = {}'.format(_))
    policy_rl = policy_table(env, nstate, naction)
    policy_qrl = policy_table_qlearning(env, nstate, naction)
    policy_egqrl = policy_table_egreedy_qlearning(env, nstate, naction)
    rl = run_game(policy_rl, env)
    qrl = run_game(policy_qrl, env)
    egqrl = run_game(policy_egqrl, env)
    w = np.argmax(np.array([rl, qrl, egqrl]))
    print('inf> winner = {}'.format(w))
    winner[w] += 1
  return winner

compare_algorithms(env, nstate, naction)

結果は、期待通り ε-greedy Q-Learning が 44% と最多勝率となった. (全てが同等の強さであれば勝率はそれぞれ 33% と予想される.)

array([29., 27., 44.])

Deep Q-Learning

Q-value を最適化する際に用いる多層ニューラルネットワークを Deep Q-Network (DQN) と呼ぶ. また、DQN を用いた Q-Learning を Deep Q-Learning と呼ぶ. ε-greedy Q-Learning を多層ニューラルネットワークを用いて実装してみる.

def deep_q_learning(env, nstate, naction, nplay=100):
  ## build deep Q-network
  from keras.models import Sequential
  from keras.layers import InputLayer, Dense
  from keras.callbacks import CSVLogger

  path_log = 'rl_gym-log.csv'
  callbacks = [
    CSVLogger(filename=path_log, append=True),
    ]
  if os.path.isfile(path_log): os.remove(path_log)

  model = Sequential()
  model.add(InputLayer(batch_input_shape=(1, nstate)))
  model.add(Dense(10, activation='sigmoid'))
  model.add(Dense(naction, activation='linear'))
  model.compile(loss='mse', optimizer='adam', metrics=['mae'])


  y = 0.95      # discount factor
  eps = 0.5     # epsilon
  decay = 0.999 # decay factor

  sum_rewards = []
  for ii in range(nplay):
    s0 = env.reset()
    eps *= decay
    done = False
    if ii % 10 == 0:
      print("loop {} of {}".format(ii+1, nplay))

    sum_reward = 0.
    while not done:
      ## decide action to take
      if (np.random.random() < eps):
        action = np.random.randint(0, naction)
      else:
        action = np.argmax(model.predict(np.identity(nstate)[s0:s0+1]))

      s1, reward, done, info = env.step(action)

      ## update deep Q-network
      target = reward + y * np.max(model.predict(np.identity(nstate)[s1:s1+1]))
      target_vec = model.predict(np.identity(nstate)[s0:s0 + 1])[0]
      target_vec[action] = target 
      model.fit(np.identity(nstate)[s0:s0+1], target_vec.reshape(-1, naction),
                callbacks=callbacks, epochs=1, verbose=0)
      s0 = s1
      sum_reward += reward
    sum_rewards.append(sum_reward)

  # construct policy table
  policy = np.zeros((nstate, naction))
  for ii in range(nstate):
    policy[ii] = model.predict(np.identity(nstate)[ii:ii+1])

  return policy, sum_rewards

学習回数毎の報酬の推移. 徐々に報酬が最大化されていくのが見て取れる.

<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

1000回の学習で得られたポリシーは以下の通り.

array([[61.61214447, 60.6944046 ],
       [65.25592041, 61.80910492],
       [69.88792419, 63.04613495],
       [75.7440033 , 64.34770203],
       [84.44594574, 66.54193115]])

全ての状態に於いて行動 0 を取ることが報酬を最大化する事が分かる.

def print_policy(policy):
  for ii in range(policy.shape[0]):
    print('state={} action={}'.format(ii, np.argmax(policy[ii, :])))

print_policy(policy_3)
state=0 action=0
state=1 action=0
state=2 action=0
state=3 action=0
state=4 action=0

今回使用したコードはこちら


補遺

2019-12-31
TensorFlow >= v2 では tf.random.set_seed(seed)、keras.callbacks.callbacks.History[‘accuracy’]、keras.callbacks.callbacks.History[‘val_accuracy’] を使用する.