- 追加された行はこの色です。
- 削除された行はこの色です。
*はじめに [#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 クラスの子クラスとします。
*Q学習エージェント qlagent.py [#m72d7cc6]
Q学習エージェントを表す QLearningAgent クラスには、Q学習のアルゴリズムだけを実装します。
*強化学習エージェント rlagent.py [#qdcfcadd]
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]
強化学習エージェントを表す ReinforcementLearningAgent クラスには、主に行動選択のメソッドを実装します。
これを、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
学習するためのメソッドは抽象クラスとし、子クラスである QLearningAgent に実装します。
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)
}}
*強化学習エージェント rlagent.py [#qdcfcadd]
#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() # 学習する
}}