二、 卷積網(wǎng)絡(luò)和訓(xùn)練
接上回 處理環(huán)境圖片禀倔。
python幾處值得關(guān)注的用法(連接)
示例用卷積網(wǎng)絡(luò)來(lái)訓(xùn)練動(dòng)作輸出:
def conv2d_size_out(size, kernel_size = 5, stride = 2):
return (size - (kernel_size - 1) - 1) // stride + 1
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)
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)
# Called with either one element to determine next action, or a batch
# during optimization. Returns tensor([[left0exp,right0exp]...]).
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))
還是比較直白的:
- Conv 3通道 16通道
- Conv 16通道 32通道
- Conv 32通道 32通道
- Linear 512節(jié)點(diǎn) 2節(jié)點(diǎn)
為何第2層最后轉(zhuǎn)為512節(jié)點(diǎn),用到了卷積形狀計(jì)算公式:
conv 為某維度上卷積后的尺寸恋腕,X為卷積前的尺寸歼捏。
(W - kernel_size + 2 * padding ) // stride + 1
示例中的Conv層沒(méi)有padding处嫌,所以公式變?yōu)椋?/p>
(size - kernel_size) // stride + 1
但不知為何示例代碼將 - kernel_size 寫(xiě)為 - (kernel_size - 1) - 1啥寇。因?yàn)閮烧咄耆嗟龋?/p>
def conv2d_size_out(size, kernel_size = 5, stride = 2):
return (size - (kernel_size - 1) - 1) // stride + 1
這只是某個(gè)維度的一次卷積變化偎球,所以一張圖,完整的尺寸應(yīng)該是2個(gè)維度的乘積辑甜,再經(jīng)過(guò)3層變化甜橱,乘上第三層通道數(shù),就是最終全連接層的大姓淮痢:。代碼寫(xiě)作:
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
這個(gè)網(wǎng)絡(luò)的輸出為動(dòng)作值难裆,動(dòng)作值為0或1子檀,但0/1代表的是枚舉類型,并不是值類型乃戈,也就是說(shuō)褂痰,動(dòng)作0并不意味著沒(méi)有,動(dòng)作1也不意味著1和0之間的某種數(shù)值度量關(guān)系症虑,0和1純粹是枚舉缩歪,所以輸出數(shù)為2個(gè),而不是1個(gè)谍憔。應(yīng)為將圖像縮放到40 x 90匪蝙,所以網(wǎng)絡(luò)的參數(shù)就是(40, 90习贫,2)逛球。試一下這個(gè)網(wǎng)絡(luò):
net = DQN(40, 90, 2).to(device) scr = get_screen() net(scr)
tensor([[-1.0281, 0.0997]], device='cuda:0', grad_fn=<AddmmBackward>)
OK,返回兩個(gè)值苫昌。
行動(dòng)決策采用 epsilon greedy policy颤绕,就是有一定的比例,選擇隨機(jī)行為(否則按照網(wǎng)絡(luò)預(yù)測(cè)的最佳行為行事)祟身。這個(gè)比例從0.9逐漸降到0.05奥务,按EXP曲線遞減:
EPS_START = 0.9 # 概率從0.9開(kāi)始
EPS_END = 0.05 # 下降到 0.05
EPS_DECAY = 200 # 越小下降越快
steps_done = 0 # 執(zhí)行了多少步
隨機(jī)行為是強(qiáng)化學(xué)習(xí)的靈魂,沒(méi)有隨機(jī)行動(dòng)袜硫,就沒(méi)有探索氯葬,沒(méi)有探索就沒(méi)有持續(xù)的成長(zhǎng)。select_action() 的作用就是 選擇網(wǎng)絡(luò)輸出的2個(gè)值中的最大值()或 隨機(jī)數(shù)
def select_action(state):
global steps_done
sample = random.random() #[0, 1)
#epsilon greedy policy婉陷。EPS_END 加上額外部分溢谤,steps_done 越小瞻凤,額外部分越接近0.9
eps_threshold = EPS_END + (EPS_START - EPS_END) * math.exp(-1. * steps_done / EPS_DECAY)
steps_done += 1
if sample > eps_threshold:
with torch.no_grad():
#選擇使用網(wǎng)絡(luò)來(lái)做決定。max返回 0:最大值和 1:索引
return policy_net(state).max(1)[1].view(1, 1)
else:
#選擇一個(gè)隨機(jī)數(shù) 0 或 1
return torch.tensor([[random.randrange(n_actions)]], device=device, dtype=torch.long)
通常網(wǎng)絡(luò)做枚舉輸出世杀,是需要用到CrossEntropy的阀参。(關(guān)于CrossEntropy的文章),示例代碼在使用網(wǎng)絡(luò)時(shí)瞻坝,簡(jiǎn)單判斷了一下蛛壳,誰(shuí)大就取誰(shuí)的索引,所以就相當(dāng)于做了一個(gè)CrossEntropy所刀。
pytorch 的 tensor.max() 返回所有維度的最大值及其索引衙荐,但如果指定了維度,就會(huì)返回namedtuple浮创,包含各維度最大值及索引 (values=..., indices=...) 忧吟。
max(1)[1] 只取了索引值,也可以用 max(1).indices斩披。view(1,1) 把數(shù)值做成[[1]] 的二維數(shù)組形式溜族。為何返回一個(gè)二維 [[1]] ? 這是因?yàn)楹竺嬉阉械膕tate用torch.cat() 合成batch(cat()說(shuō)明連接)。
return policy_net(state).max(1)[1].view(1, 1)
# return 0 if value[0] > value[1] else 1
示例中垦沉,訓(xùn)練是用兩次屏幕截圖的差別來(lái)訓(xùn)練網(wǎng)絡(luò):
for t in count():
# 1. 獲取屏幕 1
last_screen = get_screen()
# 2. 選擇行為煌抒、步進(jìn)
action = select_action(state)
_, reward, done, _ = env.step(action)
# 3. 獲取屏幕 2
current_screen = get_screen()
# 4. 計(jì)算差別 2-1
state = current_screen - last_screen
# 5. 優(yōu)化網(wǎng)絡(luò)
optimize_model()
當(dāng)前狀態(tài)及兩次狀態(tài)的差,如下所示厕倍,
- 上邊兩個(gè)分別是step0和step1原圖
- 中間灰色圖是差值部分寡壮,藍(lán)色是少去的部分,棕色是多出的部分
- 下面兩圖是原始圖覆蓋差值圖讹弯,step0將完全復(fù)原為step1况既,step1則多出部分顏色加強(qiáng)
可以看出,差值是step0到step1的變化组民。
以下是關(guān)鍵訓(xùn)練循環(huán)代碼坏挠,邏輯是一樣的。只是有一處需要注意邪乍,在循環(huán)的時(shí)候降狠,會(huì)將(state, action, next_state, reward)這四個(gè)值,保存起來(lái)庇楞,循環(huán)存放在一個(gè)叫memory的列表里榜配,湊夠批次后,才會(huì)用數(shù)據(jù)訓(xùn)練網(wǎng)絡(luò)吕晌,否則optimize_model()直接返回蛋褥。
num_episodes = 50
TARGET_UPDATE = 10
for i_episode in range(num_episodes):
env.reset()
last_screen = get_screen()
current_screen = get_screen()
state = current_screen - last_screen
# [0, 無(wú)限) 直到 done
for t in count():
action = select_action(state)
_, reward, done, _ = env.step(action.item())
reward = torch.tensor([reward], device=device)
last_screen = current_screen
current_screen = get_screen()
next_state = None if done else current_screen - last_screen
// 保存 state, action, next_state, reward 到列表 memory
state = next_state
optimize_model()
if done:
break
關(guān)于optimize_model(),大致過(guò)程是這樣的:
- 從memory列表里選取n個(gè) (state, action, next_state, reward)
- 用net獲取state的(net輸出為2個(gè)值)睛驳,再用action選出結(jié)果
- 用net獲取next_state獲取烙心,取最大值 膜廊。如果state沒(méi)有對(duì)應(yīng)的next_state,則
- 用公式算出期望y: (常量 )
- 用smooth_l1_loss計(jì)算誤差
- 用RMSprop 反向傳導(dǎo)優(yōu)化網(wǎng)絡(luò)
期望y的計(jì)算方法很簡(jiǎn)單淫茵,就是把next_state的net結(jié)果爪瓜,直接乘一個(gè)0.9然后加上獎(jiǎng)勵(lì)。如果有 next_state匙瘪,就是1铆铆,如果next_state為None,獎(jiǎng)勵(lì)是0丹喻。因此薄货,沒(méi)有明天的state,期望y最小碍论。
這里的關(guān)鍵是如何求期望y谅猾,用了Q learning:Q Learning解釋
也就是遺忘率為1的Q learning求值函數(shù)。為何遺忘率是1呢鳍悠?我的想法是税娜,在NN optimize的時(shí)候,本身就是有一個(gè)learning rate的贼涩,就相當(dāng)于
,所以 Q Learning 公式中的
前面的部分就省掉了薯蝎。
示例使用的gamma 為0.99遥倦,效果并不好,幾乎不會(huì)學(xué)習(xí)占锯。我改為0.7后袒哥,訓(xùn)練120次達(dá)到57步,總的來(lái)說(shuō)消略,就小車(chē)環(huán)境而言堡称,示例中的卷積網(wǎng)絡(luò),效果比128節(jié)點(diǎn)的全連接層網(wǎng)絡(luò)差太多艺演。128節(jié)點(diǎn)的全連接層網(wǎng)絡(luò)却紧,訓(xùn)練幾十次就可以達(dá)到滿分200步。
這是訓(xùn)練中持續(xù)時(shí)長(zhǎng)統(tǒng)計(jì)胎撤,橙色為平均值晓殊,最高也就是50多,感覺(jué)示例代碼的效果并不是很好伤提。OpenAI官方的要求是巫俺,連續(xù)跑100次平均持續(xù)時(shí)長(zhǎng)為195。這是改為0.7后的訓(xùn)練結(jié)果肿男。