最近在自學DQN,主要參考了Pytorch 上的這個DQN教程。本文首先從直觀上介紹一下DQN是什么,然后參考教程給出CartPole 這個例子的介紹,利用這個例子本文將會對DQN進行一個講解,最后會羅列一下在學教程的過程中學到的一些編程的騷操作。
1. 什么是DQN?
簡單來說,DQN是將強化學習中經典算法QLearning 和深度學習的技術相結合,是QLearning 在“新時代”的發展哈哈哈。本小節將會從強化學習開始講起,然后介紹一下經典的Qlearning 算法,最后會介紹DQN的基本理念和核心要點。
1.1 什么是強化學習(簡單的背景知識小補充)?
強化學習,是機器學習的一種,主要用來解決需要連續決策的情況(sequential decision making),比如操作一個機器人走迷宮需要不斷告訴機器人每一步的操作應該是什么。所以如果能夠把某一個task, map成一個連續決策的問題,那么這個task就可以采用強化學習的方法來解決。
通常,我們把強化學習的方法拆解成兩個部分:Agent和Environment。我們認為Agent通過和Environment 進行交互獲得一些關于environment的有價值的信息,從而能夠做出更好的決策。交互的越多,獲得信息越多,就能做出更好的選擇,這個過程就是learning。
下面我們呢,用一些稍微正式一點的符號來解釋一下強化學習。我們認為一個強化學習的過程應該包括:
- 初始時,environment 處于某一個狀態
,然后這個狀態體現為觀察值
(很多時候,我們并不知道環境處于什么樣的狀態,只能從一些觀察值推理出環境的狀態信息) 。
- 在觀察到觀察值
之后,agent 做出一個判斷認為環境處于狀態
(不一定是
,誰知道這個agent行不行呢,它能不能推斷出正確的狀態呢)。然后這個agent根據某一個自己的policy,結合判斷出來的狀態,執行某一個對自己最有利的action。
- 在agent執行完action之后,就改造了environment!使得environment 發生了狀態的改變,怎么改呢?是根據一個叫做transition function的東西進行轉換的 。這個函數給定一個初始狀態和轉換后的狀態,以及執行的action, 該函數可以給出進行這樣轉換的概率。
在完成這樣的狀態轉換之后,環境會給于agent一個對應的rewards。具體是什么樣的reward,是取決于reward函數的。給定一個初始狀態和轉換后的狀態以及執行的action,該函數可以給出進行該action的Reward。
-
重復上面步驟直到終止狀態。
Screen Shot 2022-10-28 at 16.56.17.png
這就是強化學習稍微正式一點的過程解釋。在實際的運用中,我們通常會對這個過程在簡化一點點,簡化成馬爾科夫決策過程(Markov Decision Process)。其實簡化的地方很少,就是我們認為不存在觀察值這么一說,就是我們直接能觀察到狀態(老厲害了我們),其他過程都不會發生變化。具體可以參考下面這張圖。
有了上面的解釋,我們就能夠把一個實際的問題,轉換強化學習的問題。那么現在我們想要知道,強化學習又是怎么學習的呢, 我們怎么一步一步的提高這個強化學習的方法的呢?
首先,我們回顧一下剛剛講的那個強化學習的過程(沒錯我就是要回顧,明明剛剛講過?明明上上一段講的內容?哎對,我就是要再講一下,怎么滴)。我記得我初學的時候看到這個過程,就很困惑。什么?這不是一切都給你了,狀態怎么轉移你知道了,reward怎么計算你也知道了,agent如何做決策你也知道了,那不就計算一下就能知道每一步的決策應該是什么了嗎?如果你也有類似的疑問,那答案就是我們其實并不是知道所有的這些函數的,是需要我們自己從數據中或者從和環境的不斷交互中獲取到充分的信息,不斷的學習才能得到的,比如transition function。
強化學習的應用場景其實很多,有的應用場景呢是知道transition function 的,有的是不知道。知道transition function的呢,我們發明了一類強化學習的方法來解決這類問題,這些強化學習方法就叫model-based reinforcement learning. 這一類方法呢主要是非深度學習的方法,比如動態規劃(dynamic programming)。另外那些不知道transition function的應用場景呢,我們需要從數據中顯式或者隱式的學習到這個transition function,我們同樣也有一類專門的強化學習方法用來解決這類問題,這些強化學習的方法被稱為model-free reinforcement learning。這一類方法呢又分成了value-based methods 和policy based methods。 本文的重點DQN 就屬于典型的value-based 的方法。
value based 方法其實是非常大的一個類別,上面這張圖主要目的是為了告訴大家DQN在整個領域里的什么位置,所以并沒有羅列其他的value-based methods。 那么,什么是value-based methods呢。我們都已經知道了,強化學習的目的就是為了能夠更好的連續決策,那么我們直觀的想一想,我們如何去做這樣的決策。我們決策的依據應該是我們總是希望能夠做出有利于自己的決策,有利可圖的決策,或者利益(長遠利益)最大的決策。決策這個詞也有點寬泛,就是我們想要采用利益最大的action。所以,咚咚咚咚咚(drum roll),哎!我們如果給每個action在每個狀態都打個分,按照他們的價值排個序,不是就可以了,就選價值最大的action就行了!
那么,問題來了,我們想要怎么打分?什么樣的action的價值比較高?當然是能帶來的長遠利益比較多的action價值比較高啦(也沒有那么絕對哈哈,還是有些情況是只考慮短期利益的,不過不在這里討論那么全啦,這只是一個DQN的教程,沒辦法把所有的東西都顧及到呢)!所以我們想要用未來所有reward的期望作為action的價值!那么我們怎么求這個所有未來reward呢?
最簡單的方法是Monte carlo算法,用模擬代替計算,不過這個并不是本文的重點,就不詳細介紹啦。還有一種方法呢,是Q learning。Q learning 是 Deep Q learning 的前身。 不過呢, Q learning 并沒有試圖用未來所有的reward作為action的價值,而是做了一個近似,求了一個 。 假如說給了一個當前的狀態
,我們認為在這個狀態下,采取action
,狀態轉移到
的價值
可以用下面這個公式計算
好嚇人的公式!不過直觀解釋一下其實挺簡單的。前面說了,這個公式是近似求了一個未來所有的reward的和。怎么求的呢。對于任何一個狀態我們采用action
之后這個狀態就會發生轉移,并且產生一些reward,這些reward呢是我們受益的一部分,但并不是全部。狀態轉移到了
之后呢。我們的想法是,如果我們能給這個狀態也打個分,給它評個價值,用來表示以后所有的收益和不就行了?那么怎么表示呢?我們的直觀理解是,在狀態
我們能采取好多好多action,這每個action都有一定價值(
),我們是不是可以把這些action的價值求個和,或者求個平均,或者取個最大值作為這個狀態的價值?答案是肯定的。而在Q learning 中,我們采用的是最大值,而不是平均值或者求和,如果用平均值或者求和也是可以做的,只不過是另外一種算法,有另外的特性,并不是本文需要介紹的內容。有些人可能要問了,你說的這個狀態的價值還是要用action的價值來表示,好像哪里不對啊。我們不就是要求action的價值嗎,你這用另外一個action的價值來求這個action的價值?耍我呢?沒錯!不是沒錯耍你呢,而是沒錯我們就是要用一個action的價值來求另外一個action的價值,這是一種類似于迭代的求法。我們把它叫做Bootstrapping。有很多的強化學習的方法都用到這種bootstrapping。但是這個講起來或者證明起來都有點困難,有點浪費篇幅,所以我們就不在這里說明了,你只需要知道,這個確實是可以使用的。它的理論依據是Belleman optimization equation。
另外啊,有個小問題,可能有些人要問,為什么右邊不是直接reward加未來reward的估算啊,右邊這是什么玩意。其實這個東西變個性你可能就看得懂啦。看下面這個公式,新的其實是
倍的老
加上
倍的reward和未來reward的和。這是在干啥?這是在控制學習的速度呀同志們!我們不想讓新來的那么耀武揚威的,老同志們的臉往哪兒放?我不允許,只能讓新來的按照跟老同志商量著來,不管是三七分還是八二分,總要讓老同志發揮點作用啊你說對不對。
總結一下,有了這個公式,我們就能給每個狀態的每個action計算一個,所有的狀態和所有的action求出來的
就組成了一直張表格,這個表格呢就叫Q table。
這就是Q learning 的主要內容啦, 那么這還有一個問題就是,如果狀態很多呢,多到沒有什么表格能表示的下呢,至少計算機里是沒辦法保存的。為了解決這個問題 ,我們很自然的就想到了,神經網絡,深度學習。深度學習在背表格方面那可是太專業了。所以說, deep q learning 簡單來說就是用深度學習的神經網絡背了一張巨大無比的表格q table。復雜來說,DQN就是存了一張大表,然后加上一些騷操作,比如雙網絡,比如ReplayMemory。
2. 舉個栗子 (CartPole task)
為了能夠更好的說明DQN,我們參考了pytorch那篇官方教程中給出的例子Cartpole problem。這個任務其實挺簡單,就是一個小車中間差了一個木棍,我們可以操控小車往左還是往右,我們想通過我們的操作讓這個小車多活一會,也就是木棍能夠更長時間都保持平衡不至于臉著地。
按照強化學習的術語來總結一下這個task呢就是:
- State: 每一個時刻,小車的[速度,角速度,位置,角度]組成了一個向量,每一組值就是一個狀態。這是環境的狀態,但是我們在模型訓練的時候,討了個巧,用這一幀的圖像減去上一幀的圖像來表示變換,我們把這個變換作為我們的狀態,也是作為我們模型的輸入。我們之所以能討這個巧,是因為,我們這樣做得到的狀態其實是包含了我們剛剛那四個信息的,甚至還包含了更多的信息。
- Action: 我們的agent就是小車咯,它能進行的操作其實就兩個,向左和向右。
- Reward function: 我們認為小車每多活一秒都是勝利!所以每一步成功的沒死都應該得到+1的獎勵,死了就沒得獎勵+0。
- Transition function: 這不知道!因為是model-free 嘛,我們沒有對環境建模哦。
3. DQN 細節&代碼講解(Pytorch 官方教程代碼)
本文呢,對pytorch 官網的教程,按照作者自己的習慣進行了簡單的重構。官方教程為了方便大家能夠在google colab里直接執行,把所有的東西都寫到了一個文件里。本文呢,把這個教程分成了三個文件:
- preprocessing.py。主要用來處理輸入數據,把圖片處理成我們想要的格式。順便定義了ReplayMemory這個類,用于存儲以前的數據。
- model.py。 主要用來定義DQN 模型。
- train.py。主要用來定義訓練的過程。
我們就按照訓練過程為主線,來講解一下整個代碼,順便介紹一下DQN的細節。DQN主要包括這樣幾個步驟:
- 準備數據。
- 準備模型。我們有兩個模型,一個叫policy network,一個叫target network。我們前面一直在說我們使用深度學習模型來背那個巨大的q table。其實現實要比這個稍微復雜一點點。
- 首先是我們怎么背?我們的做法是設計這樣一個深度學習模型,它的輸入是狀態向量,輸出是對應的各個action的q value。當然也可以有別的設計,比如吧狀態和action同時作為輸入,輸出也是qvalue等,但是效果不如這個好啦。
- 其次,我們都知道深度學習是要計算一個loss值,并且進行梯度下降才能學習的。我們現在能夠通過以state作為輸入,計算出每個action的價值了,那我們怎么知道這個action的價值是不是對的?我們需要有一個golden standard啊?這個golden standard就相當于分類問題里面的標簽一樣,有它我們才能計算損失值。那么這個golden standard從哪里來呢?從它自己來!記得我們的belleman optimization function嗎?我們可以迭代的求啊!記得這個公式嗎
我們完全可以用公式右邊作為一個golden standard,不過捏,首先那個控制學習速度的東西我們用不著了,因為我們梯度下降的時候也會控制學習率呢,不要重復搞啦就。其次,如果我們每一個輸入都用這種方式計算golden standard,那我們的模型可是要瘋了!!你的數據樣本的隨機性那么大,你的golden standard自己也不穩定,那我還要從你這兩個東西里學到最佳參數??是不是難為人?是不是不給臉?能不能懂事點穩定點?答案是能,所以在DQN中的做法就是我們用了兩個一模一樣的模型。一個叫policy network,我們就正常的每一步從輸入計算輸出,還有一個模型叫target network,這個網絡和剛剛那個網絡結構一模一樣,但是他非常特別,它的參數不是學習來的,是我們從policy network中復制過來的,但是我們是每隔一段時間復制一次,比如50個batch復制一次。并且我們是使用這個target network來計算我們的golden standard的。是不是很迷惑?這樣做的好處是什么?其實就是我們剛剛說的,我們的golden standard就可以變得很穩定了啊,它50個batch才會更新一次target network好歹讓我們的policy network可以慢慢的學一學你在變。
- 開始訓練
- 觀察state,根據policy選擇action,然后執行action。在這個教程中,我們采用的是epsilon-greedy 的policy,其中epsilon值還是在衰減的。這個Policy的選擇是自由的,你當然可以換成別的啦。
action = epsilon_greedy_policy(state, policy_network, n_actions, eps_start, eps_end, eps_decay, steps_done)
steps_done += 1
_, reward, done, _, _ = env.step(action.item())
- action 執行了之后,環境的狀態將會發生改變。我們將原狀態,轉移后的狀態,reward,還有action的信息放在一起看做是一個訓練數據,然后把這個訓練數據存入到ReplayMemory。ReplayMemory其實就是個列表,存了很多這樣的數據,我們每次都是從這個memory里取的以前的數據,一個batch一個batch的取,這樣能夠降低一點我們數據的隨機性,讓我們的訓練能夠更加的穩定。
```python
reward = torch.tensor([reward])
last_screen = current_screen
current_screen = get_screen(env)
if not done:
next_state = current_screen - last_screen
else:
next_state = None
memory.push(state, action, next_state, reward)
- 從memory中取出一個batch的數據。
```python
if len(memory) > batch_size:
batch_data = memory.sample(batch_size)
batch_data = Transition(*zip(*batch_data))
state_batch =torch.cat(batch_data.state)
reward_batch =torch.cat(batch_data.reward)
action_batch = torch.cat(batch_data.action)
next_state_batch = torch.cat([s for s in batch_data.next_state if s is not None])
non_final_mask = torch.tensor(tuple(map(lambda s: s is not None, batch_data.next_state)), dtype=torch.bool)
``
- 用取出的batch數據作為policy network 輸入,計算各個action 的價值。利用target network 來計算一個golden standard,再計算huberloss,進行梯度下降
state_action_values = policy_network(state_batch).gather(1, action_batch)
next_state_values = torch.zeros(batch_size)
next_state_values[non_final_mask] = target_network(next_state_batch).max(1)[0].detach()
expected_state_action_values = next_state_values * gamma + reward_batch
loss = criterion(state_action_values, expected_state_action_values.unsqueeze(1))
optimizer.zero_grad()
loss.backward()
for param in policy_network.parameters():
param.grad.data.clamp_(-1, 1)
optimizer.step()
- 如果有必要(隔了50個batch了)就更新一下target network。
if episode_i % target_update == 0:
target_network.load_state_dict(policy_network.state_dict())
4. 教程中學到的一些編程騷操作
- Namedtuple 很好用哦,可以按照名字來存取數據,又可以輕松的得到對應的列表。
- Huber loss能夠更好的處理outlier,不像MSE一樣受到outlier影響那么大。
- Count()函數可以自增計數
- pytorch的unfold 函數很好玩,可以把一個Tensor一折一折的打開。在本文里用來畫了個小圖。
- clamp_()函數可以將梯度限制在某一個范圍內。
- slice()函數可以用來生成slicing 列表,slicing列表可以用來挑選元素。
5. 最后貼上完整的代碼
preprocessing.py
"""
Author: xuqh
Created on 2022/10/21
"""
import random
from collections import namedtuple, deque
import numpy as np
import torch
import torchvision.transforms as T
from PIL import Image
import gym
import matplotlib.pyplot as plt
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
resize = T.Compose([
T.ToPILImage(),
T.Resize(40, interpolation=Image.CUBIC),
T.ToTensor()
])
def get_cart_location(env):
"""find the location of the cart"""
screen = env.render()
_, screen_width, _ = screen.shape
world_width = env.x_threshold * 2
scale = screen_width / world_width
return int(env.state[0] * scale + screen_width / 2.0) # middle of the cart
def get_screen(env):
"""get an image of the environment"""
screen = env.render().transpose((2, 0, 1)) # [channel, height, width]
# get only the cart image (lower half) _, screen_height, screen_width = screen.shape
screen = screen[:, int(screen_height * 0.4):int(screen_height * 0.8)] # top left corner is the origin
view_width = int(screen_width * 0.6)
cart_location = get_cart_location(env)
if cart_location < view_width // 2:
slice_range = slice(view_width) # only select first half, what if it is close to the center
elif cart_location > (screen_width - view_width // 2):
slice_range = slice(-view_width, None) # only select second half
else:
slice_range = slice(cart_location - view_width // 2, cart_location + view_width // 2) # select center half
screen = screen[:, :, slice_range]
screen = np.ascontiguousarray(screen, dtype=np.float32) / 255
screen = torch.from_numpy(screen)
return resize(screen).unsqueeze(0) # add a batch dimensionò
Transition = namedtuple("Transition", ("state", "action", "next_state", "reward"))
class ReplayMemory(object):
def __init__(self, capacity):
self.memory = deque([], maxlen=capacity)
def push(self, *args):
self.memory.append(Transition(*args))
def sample(self, batch_size):
return random.sample(self.memory, batch_size)
def __len__(self):
return len(self.memory)
if __name__ == '__main__':
env = gym.make("CartPole-v0", render_mode="rgb_array").unwrapped
env.reset()
# plt.figure()
# plt.imshow(get_screen(env).cpu().squeeze(0).permute(1, 2, 0).numpy(), interpolation="none") # plt.title("Example extracted screen") # plt.show() print(get_screen(env))
print(get_screen(env))
print(get_screen(env))
model.py
"""
Author: xuqh
Created on 2022/10/21
"""
import torch.nn as nn
import torch.nn.functional as F
class DQN(nn.Module):
def __init__(self, h, w, outputs):
super(DQN, self).__init__()
self.conv1 = nn.Conv2d(3, 16, kernel_size=5, stride=2)
self.bn1 = nn.BatchNorm2d(16)
self.conv2 = nn.Conv2d(16, 32, kernel_size=5, stride=2)
self.bn2 = nn.BatchNorm2d(32)
self.conv3 = nn.Conv2d(32, 32, kernel_size=5, stride=2)
self.bn3 = nn.BatchNorm2d(32)
conv2d_size_out = lambda size, kernel_size=5, stride=2: (size - kernel_size) // stride + 1
convw = conv2d_size_out(conv2d_size_out(conv2d_size_out(w)))
convh = conv2d_size_out(conv2d_size_out(conv2d_size_out(h)))
linear_input_size = convw * convh * 32
self.head = nn.Linear(linear_input_size, outputs)
def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x = F.relu(self.bn2(self.conv2(x)))
x = F.relu(self.bn3(self.conv3(x)))
return self.head(x.view(x.size(0), -1))```
**train.py**
```python
"""
Author: xuqh
Created on 2022/10/21
"""
import math
import random
from itertools import count
import gym
import torch
from torch import optim, nn
from DQN.model import DQN
from DQN.preprocessing import ReplayMemory, get_screen, Transition
import matplotlib.pyplot as plt
def epsilon_greedy_policy(state, policy_network, n_actions, eps_start, eps_end, eps_decay, steps_done):
eps = eps_end + (eps_start - eps_end) * math.exp(-1.0 * steps_done / eps_decay)
if random.random() > eps:
action = policy_network(state).max(1)[1].view(1, 1) # [N, n_action,1]
else:
action = torch.tensor([[random.randrange(n_actions)]], dtype=torch.long)
return action
def plot_duration(episode_durations):
"""plot duration for each episode and average duration"""
plt.figure()
plt.clf()
duration_t = torch.tensor(episode_durations, dtype=torch.float)
plt.title("Training")
plt.xlabel("Episode")
plt.ylabel("Duration")
plt.plot(episode_durations)
# plot average duration as well
if len(episode_durations)>100:
means = duration_t.unfold(0, 100, 1).mean(1).view(-1)
means = torch.cat((torch.zeros(99), means))
plt.plot(means.numpy())
plt.pause(0.001)
if __name__ == '__main__':
# Initialization
n_episodes = 5000
gamma = 0.999
batch_size = 128
eps_start = 0.9
eps_end = 0.05
eps_decay = 200
target_update = 10
memory_capacity = 10000
steps_done = 0
# prepare environment & data
env = gym.make("CartPole-v0", render_mode="rgb_array").unwrapped
env.reset()
memory = ReplayMemory(memory_capacity)
init_screen = get_screen(env)
_, _, screen_height, screen_width = init_screen.shape
n_actions = env.action_space.n
# prepare model
policy_network = DQN(screen_height, screen_width, n_actions)
target_network = DQN(screen_height, screen_width, n_actions)
target_network.load_state_dict(policy_network.state_dict())
optimizer = optim.RMSprop(policy_network.parameters())
criterion = nn.SmoothL1Loss()
# start training
episode_durations = []
for episode_i in range(n_episodes):
env.reset()
last_screen = get_screen(env)
current_screen = get_screen(env)
state = current_screen - last_screen # 0 since the env did not change
for t in count(): # 有點東西
# take actions and get new transitions action = epsilon_greedy_policy(state, policy_network, n_actions, eps_start, eps_end, eps_decay, steps_done)
steps_done += 1
_, reward, done, _, _ = env.step(action.item())
reward = torch.tensor([reward])
last_screen = current_screen
current_screen = get_screen(env)
if not done:
next_state = current_screen - last_screen
else:
next_state = None
memory.push(state, action, next_state, reward)
state = next_state # move to next state, allowing environment to continue
# acquire data from memory if len(memory) > batch_size:
batch_data = memory.sample(batch_size)
batch_data = Transition(*zip(*batch_data))
state_batch =torch.cat(batch_data.state)
reward_batch =torch.cat(batch_data.reward)
action_batch = torch.cat(batch_data.action)
next_state_batch = torch.cat([s for s in batch_data.next_state if s is not None])
non_final_mask = torch.tensor(tuple(map(lambda s: s is not None, batch_data.next_state)), dtype=torch.bool)
# train model
state_action_values = policy_network(state_batch).gather(1, action_batch)
next_state_values = torch.zeros(batch_size)
next_state_values[non_final_mask] = target_network(next_state_batch).max(1)[0].detach()
expected_state_action_values = next_state_values * gamma + reward_batch
loss = criterion(state_action_values, expected_state_action_values.unsqueeze(1))
optimizer.zero_grad()
loss.backward()
for param in policy_network.parameters():
param.grad.data.clamp_(-1, 1)
optimizer.step()
# update target network
if episode_i % target_update == 0:
target_network.load_state_dict(policy_network.state_dict())
if done:
break
episode_durations.append(t)
plot_duration(episode_durations)