本小節介紹對代碼的分析。
一、程式架構分析
ActorNetwork.py 設定Actor網絡
CriticNetwork.py 設定Critic網絡
OU.py Ornstein-Uhlenbeck過程
ReplayBuffer.py 經驗回放
actormodel.json 儲存actor網絡模型的結構
actormodel.h5 儲存actor網絡權重
criticmodel.json 儲存critic網絡模型的結構
criticmodel.h5 儲存critic網絡權重
ddpg.py 主代碼
gym_torcs.py python與TORCS的接口
snakeoil3_gym.py 與TORCS伺服器通信的python腳本
二、ddpg.py檔案分析
1.基本參數BUFFER_SIZE = 100000 # 緩存大小,指網絡存儲能力
BATCH_SIZE = 32 # 批尺寸,指一次處理多少樣本
GAMMA = 0.99 # 折扣系數
TAU = 0.001 # 目标網絡超參數
LRA = 0.0001 # Actor網絡學習率
LRC = 0.001 # Critic網絡學習率
action_dim = 3 #加速、轉向、制動
state_dim = 29 #29個傳感器輸入
np.random.seed(1337) #随機數種子,如果使用相同的數字,則每次産生的随機數相同,這裡我的了解是定義了一個随機的初始值。
為什麼采用批資料處理(BATCH_SIZE = 32)?
①若采用全資料集(Full Batch Learning)
優點:由全資料集确定的方向能夠更好的代表樣本總體,進而更準确地朝向極值所在的方向;
缺點:資料集大的情況,一次性載入所有資料,不可行。
②若每次隻訓練一個樣闆,即batch_size=1,這叫做線上學習(online learning)。
缺點:每次修正方向以各自樣本的梯度方向修正,導緻波動較大、難以收斂。
③适中的batch_size,即批梯度下降法(Mini_batches learning)。
優點:相比全資料集處理方法,小批量處理需要更少的記憶體就可以訓練網絡,相比batch_size=1的情況,訓練網絡更新更快。
缺點:批次越小,估值越不準确,相比全資料集波動大。
2.Tensorflow限制GPU資源使用為了加快運作效率,Tensorflow在初始化時會嘗試配置設定所有的GPU資源給自己,這在多人使用的伺服器上工作就會導緻GPU占用,别人無法用GPU工作,tf提供了兩種控制GPU資源的方法,一種是讓Tensorflow在運作過程中動态申請顯存,一種是限制GPU的使用率。
(1)動态申請緩存(代碼中使用的方法)
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
sess = tf.Session(config=config)
(2)限制GPU使用率:
config = tf.ConfigProto()
config.gpu_options.per_process_gpu_memory_fraction = 0.4 #占40%記憶體
sess = tf.Session(config=config)
3.将Keras作為tensorflow的精簡接口 我們将使用Keras的
Dense
層(全連接配接層)來建構一個TensorFlow分類器,是以需要在Tensorflow中調用Keras。首先建立一個TensorFlow會話并注冊到Keras。這意味着Keras将使用我們注冊的會話來初始化它在内部建立的所有變量。
from keras import backend as K
K.set_session(sess)
4.加載權重和儲存權重 加載權重并儲存在actormodel.h5和criticmodel.h5中,通過json檔案儲存模型權重。
keras模型一般儲存為字尾名為.h5的檔案。同樣是.h5的檔案,save()和save_weight()的儲存效果是不一樣的。
save_weights()方法:
①隻儲存了模型權重而沒有儲存模型結構,節省記憶體空間;
②它儲存的資料不能用于繼續訓練模型;
③save_weights()儲存的權重通過load_weights()方法加載模型權重。 并且,在加載模型權重之前,必須把網絡結構定義好,并且模型的各個層的名稱必須與儲存模型權重時的各個層保持一緻。
save()方法:
①檔案包含模型的結構、模型的權重、訓練配置(損失函數、優化器等)和優化器的狀态,占用記憶體大,可以直接用可視化工具打開;
②它儲存的資料可以在上次中斷的地方繼續訓練;
③save()儲存的資料使用models.load_model()進行加載。
- 加載權重
print("Now we load the weight")
try:
actor.model.load_weights("actormodel.h5")
critic.model.load_weights("criticmodel.h5")
actor.target_model.load_weights("actormodel.h5")
critic.target_model.load_weights("criticmodel.h5")
print("Weight load successfully")
except:
print("Cannot find the weight")
- 儲存權重
if np.mod(i, 3) == 0: # 每三局更新一次權重
if (train_indicator): # train_indicator==1時,訓練模式,執行下面代碼
print("Now we save model")
actor.model.save_weights("actormodel.h5", overwrite=True) # 儲存權重
with open("actormodel.json", "w") as outfile:
json.dump(actor.model.to_json(), outfile)
critic.model.save_weights("criticmodel.h5", overwrite=True)
with open("criticmodel.json", "w") as outfile:
json.dump(critic.model.to_json(), outfile)
5.經驗回放 代碼使用經驗回放來儲存訓練中所有的階段(s,a,r,s')。當訓練神經網絡時,從其中随機小批量抽取階段資料,将大大提高系統的穩定性。
當我們target_q_values時,使用的是目标網絡的輸出。使用緩慢變化的目标網絡将減少Q值估計的振蕩,這極大地提高了學習的穩定性。
代碼如下:
buff.add(s_t, a_t[0], r_t, s_t1, done)
# 從存儲回放器中随機小批量抽取N個變換階段 (si, ai, ri, si+1)
batch = buff.getBatch(BATCH_SIZE)
states = np.asarray([e[0] for e in batch])
actions = np.asarray([e[1] for e in batch])
rewards = np.asarray([e[2] for e in batch])
new_states = np.asarray([e[3] for e in batch])
dones = np.asarray([e[4] for e in batch])
y_t = np.asarray([e[1] for e in batch])
target_q_values = critic.target_model.predict([new_states, actor.target_model.predict(new_states)]) #Still using tf
for k in range(len(batch)):
if dones[k]:
y_t[k] = rewards[k]
else:
y_t[k] = rewards[k] + GAMMA*target_q_values[k]
getBatch()是ReplayBuffer.py中的随機抽樣函數。
np.asarray()将輸入資料轉化為一個新的(copy)ndarray,ndarray對象是用于存放同類型元素的多元數組。
6
.神經網絡的訓練首先通過減少損失來更新critic network
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5CMxMTZkRjZ1MTNhBjYycTM4YjM2MGN3MjN4EjM2gjY48CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
然後使用采樣的政策梯度更新actor政策。
确定性政策
代碼的最後兩行更新target network:
if (train_indicator):
loss += critic.model.train_on_batch([states,actions], y_t)
a_for_grad = actor.model.predict(states)
grads = critic.gradients(states, a_for_grad)
actor.train(states, grads)
actor.target_train()
critic.target_train()
7. if _name_==”_main_”
if _name_==”_main_”相當于python模拟的程式入口,python本身并沒有規定這麼寫,這隻是一種編碼習慣。
三、gym_torcs.py檔案分析
Gym-TORCS是一個模仿Open-AI接口的TORCS的python封裝,用于在TORCS上測試增強學習算法。
1.基本參數terminal_judge_start = 100 # 如果在100次後沒有進展則終止
termination_limit_progress = 5 # [km/h], 目前速度低于此速度則終止
default_speed = 50 # 預設速度
2.初始化油門、視覺、齒輪變速 if throttle is False:
self.action_space = spaces.Box(low=-1.0, high=1.0, shape=(1,))
else:
self.action_space = spaces.Box(low=-1.0, high=1.0, shape=(2,))
if vision is False:
high = np.array([1., np.inf, np.inf, np.inf, 1., np.inf, 1., np.inf])
low = np.array([0., -np.inf, -np.inf, -np.inf, 0., -np.inf, 0., -np.inf])
self.observation_space = spaces.Box(low=low, high=high)
else:
high = np.array([1., np.inf, np.inf, np.inf, 1., np.inf, 1., np.inf, 255])
low = np.array([0., -np.inf, -np.inf, -np.inf, 0., -np.inf, 0., -np.inf, 0])
self.observation_space = spaces.Box(low=low, high=high)
3.自動換擋設定 # 通過Snakeoil自動換擋
if self.gear_change is True:
action_torcs['gear'] = this_action['gear']
else:
# 共設定六個檔位
action_torcs['gear'] = 1
if self.throttle:
if client.S.d['speedX'] > 50:
action_torcs['gear'] = 2
if client.S.d['speedX'] > 80:
action_torcs['gear'] = 3
if client.S.d['speedX'] > 110:
action_torcs['gear'] = 4
if client.S.d['speedX'] > 140:
action_torcs['gear'] = 5
if client.S.d['speedX'] > 170:
action_torcs['gear'] = 6
4.一步動态更新 #将動作傳給TORCS
client.respond_to_server()
# 獲得TORCS的回複
client.get_servers_input()
# 從TORCS獲得全部觀察
obs = client.S.d
# 從TORCS原始觀察向量獲得觀察
self.observation = self.make_observaton(obs)
5.獎勵函數 track = np.array(obs['track'])
trackPos = np.array(obs['trackPos'])
sp = np.array(obs['speedX'])
damage = np.array(obs['damage'])
rpm = np.array(obs['rpm'])
#基于速度和距離中心線位置的獎賞
progress = sp*np.cos(obs['angle']) - np.abs(sp*np.sin(obs['angle'])) - sp * np.abs(obs['trackPos'])
reward = progress
# 碰撞懲罰
if obs['damage'] - obs_pre['damage'] > 0:
reward = -1
6.終止條件 episode_terminate = False
# 汽車跑出賽道則終止
if (abs(track.any()) > 1 or abs(trackPos) > 1):
reward = -200
episode_terminate = True
client.R.d['meta'] = True
# 100步後沒有進展則終止
if self.terminal_judge_start < self.time_step: #如果目前步數小于100
if progress < self.termination_limit_progress: #如果目前速度小于5
print("No progress")
episode_terminate = True
client.R.d['meta'] = True
# 汽車往後走(加速度為負數)則終止
if np.cos(obs['angle']) < 0:
episode_terminate = True
client.R.d['meta'] = True
# 發送重置信号
if client.R.d['meta'] is True:
self.initial_run = False
client.respond_to_server()
7.傳感器輸入 傳感器輸入在此處修改,各個參數的含義在上一節的表格中有說明。
def make_observaton(self, raw_obs):
if self.vision is False:
names = ['focus',
'speedX', 'speedY', 'speedZ', 'angle', 'damage',
'opponents',
'rpm',
'track',
'trackPos',
'wheelSpinVel']
Observation = col.namedtuple('Observaion', names)
return Observation(focus=np.array(raw_obs['focus'], dtype=np.float32)/200.,
speedX=np.array(raw_obs['speedX'], dtype=np.float32)/300.0,
speedY=np.array(raw_obs['speedY'], dtype=np.float32)/300.0,
speedZ=np.array(raw_obs['speedZ'], dtype=np.float32)/300.0,
angle=np.array(raw_obs['angle'], dtype=np.float32)/3.1416,
damage=np.array(raw_obs['damage'], dtype=np.float32),
opponents=np.array(raw_obs['opponents'], dtype=np.float32)/200.,
rpm=np.array(raw_obs['rpm'], dtype=np.float32)/10000,
track=np.array(raw_obs['track'], dtype=np.float32)/200.,
trackPos=np.array(raw_obs['trackPos'], dtype=np.float32)/1.,
wheelSpinVel=np.array(raw_obs['wheelSpinVel'], dtype=np.float32))
else:
names = ['focus',
'speedX', 'speedY', 'speedZ', 'angle',
'opponents',
'rpm',
'track',
'trackPos',
'wheelSpinVel',
'img']
Observation = col.namedtuple('Observaion', names)
# RGB
image_rgb = self.obs_vision_to_image_rgb(raw_obs[names[8]])
return Observation(focus=np.array(raw_obs['focus'], dtype=np.float32)/200.,
speedX=np.array(raw_obs['speedX'], dtype=np.float32)/self.default_speed,
speedY=np.array(raw_obs['speedY'], dtype=np.float32)/self.default_speed,
speedZ=np.array(raw_obs['speedZ'], dtype=np.float32)/self.default_speed,
opponents=np.array(raw_obs['opponents'], dtype=np.float32)/200.,
rpm=np.array(raw_obs['rpm'], dtype=np.float32),
track=np.array(raw_obs['track'], dtype=np.float32)/200.,
trackPos=np.array(raw_obs['trackPos'], dtype=np.float32)/1.,
wheelSpinVel=np.array(raw_obs['wheelSpinVel'], dtype=np.float32),
img=image_rgb)
四、ActorNetwork.py分析
代碼中使用了2個隐藏層,分别有300和600個神經元。輸出包括3個連續動作。
(1)Steering:使用tanh激活函數(輸出-1表示最大右轉彎,+1表示最大左轉彎);
(2)Acceleration:使用
sigmoid
激活函數(輸出0代表不加速,1表示全加速);
(3)Brake:使用
sigmoid
激活函數(輸出0表示不制動,1表示緊急制動)。
代碼使用Keras函數Merge來合并三個輸出層(在keras2.2.0的版本中Merge已經取消)。
def create_actor_network(self, state_size,action_dim):
print("Now we build the model")
S = Input(shape=[state_size])
h0 = Dense(HIDDEN1_UNITS, activation='relu')(S)
h1 = Dense(HIDDEN2_UNITS, activation='relu')(h0)
Steering = Dense(1,activation='tanh',init=lambda shape, name: normal(shape, scale=1e-4, name=name))(h1)
Acceleration = Dense(1,activation='sigmoid',init=lambda shape, name: normal(shape, scale=1e-4, name=name))(h1)
Brake = Dense(1,activation='sigmoid',init=lambda shape, name: normal(shape, scale=1e-4, name=name))(h1)
V = merge([Steering,Acceleration,Brake],mode='concat')
model = Model(input=S,output=V)
return model, model.trainable_weights, S
權重更新的代碼如下,代碼中使用了tf.gradients(),其中self.model.output對self.weights求導,self.action_gradient對model.trainable_weights中的每個元素的求導權重重。
#初始化
self.model , self.weights, self.state = self.create_actor_network(state_size, action_size)
self.target_model, self.target_weights, self.target_state = self.create_actor_network(state_size, action_size)
#權重更新
self.action_gradient = tf.placeholder(tf.float32,[None, action_size])
self.params_grad = tf.gradients(self.model.output, self.weights, -self.action_gradient) #梯度計算
grads = zip(self.params_grad, self.weights)
self.optimize = tf.train.AdamOptimizer(LEARNING_RATE).apply_gradients(grads)
self.sess.run(tf.initialize_all_variables())
def train(self, states, action_grads):
self.sess.run(self.optimize, feed_dict={
self.state: states,
self.action_gradient: action_grads
})
#實作目标網絡,用來計算目标值
def target_train(self):
actor_weights = self.model.get_weights()
actor_target_weights = self.target_model.get_weights()
for i in xrange(len(actor_weights)):
actor_target_weights[i] = self.TAU * actor_weights[i] + (1 - self.TAU)* actor_target_weights[i]
self.target_model.set_weights(actor_target_weights)
五、CriticNetwork.py分析
Critic網絡模型的建構與DQN網絡非常相似。代碼使用2個具有300和600個神經元的隐含層。Critic網絡将狀态和動作都作為輸入。根據DDPG的論文,直到Q網絡的第二個隐藏層才包含這些動作。同樣使用Keras中的Merge函數将動作和隐含層合并在一起。
def create_critic_network(self, state_size,action_dim):
print("Now we build the model")
S = Input(shape=[state_size])
A = Input(shape=[action_dim],name='action2')
w1 = Dense(HIDDEN1_UNITS, activation='relu')(S)
a1 = Dense(HIDDEN2_UNITS, activation='linear')(A)
h1 = Dense(HIDDEN2_UNITS, activation='linear')(w1)
h2 = merge([h1,a1],mode='sum')
h3 = Dense(HIDDEN2_UNITS, activation='relu')(h2)
V = Dense(action_dim,activation='linear')(h3)
model = Model(input=[S,A],output=V)
adam = Adam(lr=self.LEARNING_RATE)
model.compile(loss='mse', optimizer=adam)
return model, A, S
六、OU.py分析
OU.py檔案寫的是Ornstein-Uhlenbeck過程,它是具有均值回歸特性的随機過程。
公式:
θ意味着變量多快恢複到均值;μ表示均值;σ是過程的波動程度。
Ornstein-Uhlenbeck過程是一種非常常見的方法,可以随機模拟利率,外彙和商品價格。
下表是代碼中使用的建議值:
最重要的參數是加速度的μ,想讓汽車具有一定的初始速度,而不要陷在局部最小值(汽車一直踩刹車而不踩油門)。可以随意更改參數來看看AI在不同組合下的行為。
代碼如下:
import random
import numpy as np
class OU(object):
def function(self, x, mu, theta, sigma):
return theta * (mu - x) + sigma * np.random.randn(1)