強化学習/Pythonで強化学習する のバックアップソース(No.2)

*はじめに [#l524e399]

ここでは、Pythonで強化学習を行います。

例題として、Sutton & Barto の『エージェント・アプローチ人工知能』で用いられている [math]4 \times 3[/math] の迷路を使います。

強化学習アルゴリズムはQ学習、行動選択は[math]\epsilon[/math]-グリーディーです。

次の環境で確認しました。
-macOS Sierra 10.12.5
-Python 3.5.1


*準備 [#t9a5a07c]

NumPyを使いますので、インストールします。

まず、pip3をインストールし、最新版にします。
#geshi(sh){{
$ sudo easy_install pip
$ sudo pip3 install --upgrade pip
}}

次に、pip3でNumPyをインストールします。
#geshi(sh){{
$ sudo pip3 install numpy
}}



*クラス構造 [#we2a4a0c]

強化学習の枠組みでは、学習エージェントが環境から状態を観測し、それに基づいて行動を選択し、実行します。
すると、環境の状態が変化し、次の状態と報酬が観測できます。
これを繰り返します。

そこで、強化学習エージェントを表す ReinforcementLearningAgent と環境を表す Environment クラスを用意します。

また、Q学習エージェントを表す QLearningAgent を用意し、ReinforcementLearningAgent クラスのの子クラスとします。

同様に、迷路を表す Maze クラスを用意し、Environment クラスの子クラスとします。



*強化学習エージェント rlagent.py [#qdcfcadd]

強化学習エージェントを表す ReinforcementLearningAgent クラスには、主に行動選択のメソッドを実装します。

学習するためのメソッドは抽象クラスとし、子クラスである QLearningAgent に実装します。

#geshi(python){{
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# rlagent.py
#
# Copyright 2017 Tohgoroh Matsui All Rights Reserved.
#
from abc import ABCMeta, abstractmethod
import environment as env
import numpy as np


class ReinforcementLearningAgent(metaclass=ABCMeta):
  @abstractmethod
  def learn():
    pass


  # 一様ランダム選択
  def uniformly(self):
    a = np.random.randint(self.env.actions)     # 0からaction-1までの整数をランダムに生成する
    return a                                    # 生成した行動を返す


  # グリーディー選択
  #   s:  状態
  def greedy(self, s):
    maxQ     = float('-inf')                    # 最大のQ値
    maxA     = -1                               # 最大のQ値を持つ行動
    for a in range(self.env.actions):           # すべての行動 a について繰り返し:
      if self.aQ[s, a] > maxQ:                    # Q値がこれまでの最大のQ値よりも大きいなら:
        maxQ     =  self.aQ[s, a]                 # 最大のQ値を更新する
        maxA     =  a                             # 最大のQ値を持つ行動を更新する
    # 最大のQ値を持つ行動が複数ある場合、その中から一様ランダムに選択する
    aMaxId = np.where(self.aQ[s] == maxQ)[0]    # Q値がmaxQである要素のインデックスの配列
    if len(aMaxId) > 1:                         # Q値がmaxQである要素が複数あるなら
      r = np.random.randint(len(aMaxId))          # 0から要素数-1までの整数をランダムに生成する
      maxA = aMaxId[r]                            # Q値がmaxQである要素のインデックスから行動を選択する
    return maxA                                 # 最大のQ値を持つ行動を返す


  # ε-グリーディー選択
  #   aQ: Q値
  #   s:  状態
  def epsilonGreedy(self, s):
    r = np.random.rand()          # 乱数を生成する
    if r < self.epsilon:          # 確率εで:
      a = self.uniformly()          # 一様ランダムに行動を選択する
    else:                         # 確率1-εで:
      a = self.greedy(s)            # グリーディーに行動を選択する
    return a                      # 選択した行動を返す


  # 環境をセットする
  #   env: 環境
  def setEnvironment(self, env):
    self.env = env


  # コンストラクター
  def __init__(self, seed=0, discountRate=0.9, stepSize = 0.1, epsilon=0.1, episodes=10000):
    np.random.seed(seed)
    self.discountRate = discountRate    # 割引率 γ
    self.stepSize     = stepSize        # ステップ・サイズ α
    self.epsilon      = epsilon         # ε-グリーディー選択のε
    self.episodes     = episodes        # 最大エピソード数
    self.aQ           = None            # Q値
}}



*Q学習エージェント qlagent.py [#m72d7cc6]

Q学習エージェントを表す QLearningAgent クラスには、Q学習のアルゴリズムだけを実装します。

Q学習のアルゴリズムは、次のようなものです。
+行動価値 [math]Q(s, a)[/math] を初期化
+エピソードごとに繰り返し:
++状態 [math]s[/math] を初期化
++[math]s[/math] が終端状態になるまで繰り返し:
+++[math]Q[/math]から導かれる行動選択確率に基づいて [math]s[/math] から行動 [math]a[/math] を選択
+++[math]a[/math] を実行し、次の状態 [math]s'[/math] と報酬 [math]r[/math] を観測する
+++[math]Q(s, a) \leftarrow Q(s, a) + \alpha \left[ r + \gamma * \max_{a'}Q(s', a') - Q(s, a)\right][/math]
+++[math]s \leftarrow s'[/math]

これを、Pythonで実装すると、次のようになります。
#geshi(python){{
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# qlagent.py
#
# Copyright 2017 Tohgoroh Matsui All Rights Reserved.
#
import rlagent
import environment as env
import numpy as np


class QLearningAgent(rlagent.ReinforcementLearningAgent):
  # 学習する
  def learn(self):
    gamma = self.discountRate   # 割引率
    alpha = self.stepSize       # ステップ・サイズ

    episodes = 0
    self.aQ = np.zeros([self.env.states, self.env.actions])       # Q値をゼロに初期化する
    while episodes < self.episodes:
      steps = 0
      s = self.env.initState()                                                  # 状態 s を初期化する
      while not self.env.isTerminal(s):                                         # 状態 s が終端状態になるまで繰り返す:
        a = self.epsilonGreedy(s)                                                 # Q値と状態 s から行動 a を選択する
        s_, r = self.env.takeAction(s, a)                                         # 行動 a を実行し、次の状態 s_ と報酬 r を観測する
        # self.env.printLog(steps, s, a, r)                                       # ログを出力する
        a_ = self.greedy(s_)                                                      # 次の状態 s_ でQ値が最大の行動を調べる
        self.aQ[s, a] += alpha * (r + gamma * self.aQ[s_, a_] - self.aQ[s, a])    # Q値を更新する
        s = s_                                                                    # 状態を更新する
        steps += 1
      episodes += 1
    print(self.aQ)


  # コンストラクター
  def __init__(self, seed=0, discountRate=0.9, stepSize=0.1, epsilon=0.1, episodes=10000):
    super().__init__(seed, discountRate, stepSize, epsilon, episodes)
}}




*環境 environment.py [#y715bf52]

環境を表す Environment クラスは、ほとんどのメソッドが実際の環境を表す子クラスが実装すべき抽象メソッドとしています。

#geshi(python){{
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# environment.py
#
# Copyright 2017 Tohgoroh Matsui All Rights Reserved.
#
from abc import ABCMeta, abstractmethod

class Environment(metaclass=ABCMeta):
  @abstractmethod
  def initState(self):
    pass

  @abstractmethod
  def isTerminal(self, s):
    pass

  @abstractmethod
  def getState(self):
    pass

  @abstractmethod
  def getReward(self, s):
    pass

  @abstractmethod
  def takeAction(self, s, a):
    pass

  @abstractmethod
  def strState(self, s):
    pass

  @abstractmethod
  def strAction(self, a):
    pass


  # ログを出力する
  def printLog(self, steps, s, a, r):
    print('%d: %s, %s, %f' % (steps, self.strState(s), self.strAction(a), r))
}}


*迷路 maze.py [#se58b5a5]
#geshi(python){{
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# maze.py
#
# Copyright 2017 Tohgoroh Matsui All Rights Reserved.
#
import environment as env
import numpy as np

class Maze(env.Environment):
  # 壁
  walls = np.array([[0, 0, 0, 0],
                    [0, 1, 0, 0],
                    [0, 0, 0, 0]]).T

  # 終端状態
  terminals = np.array([[0, 0, 0, 1],
                        [0, 0, 0, 1],
                        [0, 0, 0, 0]]).T

  # 報酬
  rewards = np.array([[-0.02, -0.02, -0.02,  1.  ],
                      [-0.02,     0, -0.02, -1.  ],
                      [-0.02, -0.02, -0.02, -0.02]]).T


  # 状態を初期化する
  def initState(self):
    s = self.coordinate2state(0, 2)
    self.state = s
    return s


  # 終端状態ならTrue、そうでないならFalseを返す
  #   s: 状態
  def isTerminal(self, s):
    x, y = self.state2coodinate(s)
    if self.terminals[x, y] == 1:
      return True
    else:
      return False


  # 北に進む
  #   s: 状態
  def north(self, s):
    x_, y_ = self.state2coodinate(s)
    if y_ > 0 and self.walls[x_, y_ - 1] == 0:
      y_ -= 1
    s_ = self.coordinate2state(x_, y_)
    return s_


  # 東に進む
  #   s: 状態
  def east(self, s):
    x_, y_ = self.state2coodinate(s)
    if x_ < 3 and self.walls[x_ + 1, y_] == 0:
      x_ += 1
    s_ = self.coordinate2state(x_, y_)
    return s_


  # 西に進む
  #   s: 状態
  def west(self, s):
    x_, y_ = self.state2coodinate(s)
    if x_ > 0 and self.walls[x_ - 1, y_] == 0:
      x_ -= 1
    s_ = self.coordinate2state(x_, y_)
    return s_


  # 南に進む
  #   s: 状態
  def south(self, s):
    x_, y_ = self.state2coodinate(s)
    if y_ < 2 and self.walls[x_, y_ + 1] == 0:
      y_ += 1
    s_ = self.coordinate2state(x_, y_)
    return s_


  # 座標から状態に変換する
  #   x: x座標
  #   y: y座標
  def coordinate2state(self, x, y):
    s = y * self.height + x
    return s


  # 状態から座標に変換する
  #   s: 状態
  def state2coodinate(self, s):
    x = s % self.height
    y = s // self.height
    return x, y


  # 状態を返す
  def getState():
    return self.state


  # 報酬を返す
  #   s: 状態
  def getReward(self, s):
    x, y = self.state2coodinate(s)
    r = self.rewards[x, y]
    return r


  # 行動する
  #   s: 状態
  #   a: 行動
  def takeAction(self, s, a):
    act = self.lActions[a]        # 行動
    s_ = act(s)                   # 行動を実行する
    r  = self.getReward(s_)       # 報酬を観測する
    return s_, r                  # 次の状態と報酬を返す


  # 状態を文字列で表して返す
  #   s: 状態
  def strState(self, s):
    x, y = self.state2coodinate(s)
    return '(%d, %d)' % (x, y)


  # 行動を文字列で表して返す
  #   a: 行動
  def strAction(self, a):
    return self.lActions[a].__name__


  # コンストラクター
  def __init__(self):
    super().__init__()
    self.width    = len(self.walls[0])                              # 迷路の幅
    self.height   = len(self.walls)                                 # 迷路の高さ
    self.states   = self.width * self.height                        # 状態の数
    self.lActions = [self.north, self.east, self.west, self.south]  # 行動のリスト
    self.actions  = len(self.lActions)                              # 行動の数
    self.initState()                                                # 状態を初期化する
}}


*実行用ファイル mazeql.py [#ab065b92]
#geshi(python){{
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# mazeql.py
#
# Copyright 2017 Tohgoroh Matsui All Rights Reserved.
#
import maze
import qlagent

# mainモジュール
if __name__ == '__main__':
  env   = maze.Maze()                   # 環境
  agent = qlagent.QLearningAgent()      # Q学習エージェント
  agent.setEnvironment(env)             # 環境をエージェントにセットする
  agent.learn()                         # 学習する
}}
トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS