Files
changqingai e074c62837 v2.0
2021-04-25 16:12:40 +08:00

219 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Tencent is pleased to support the open source community by making GameAISDK available.
This source code file is licensed under the GNU General Public License Version 3.
For full details, please refer to the file "LICENSE.txt" which is provided as part of this source code package.
Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
"""
from __future__ import division
from collections import namedtuple
import numpy as np
import torch
Transition = namedtuple('Transition', ('timestep', 'state', 'action', 'reward', 'nonterminal'))
blank_trans = Transition(0, torch.zeros(84, 84, dtype=torch.uint8), None, 0, False)
# Segment tree data structure where parent node values are sum/max of children node values
class SegmentTree:
def __init__(self, size):
self.index = 0
self.size = size # 40000
self.full = False # Used to track actual capacity
self.sum_tree = np.zeros((2 * size - 1,),
dtype=np.float32) # Initialise fixed size tree with all (priority) zeros
self.data = np.array([None] * size) # Wrap-around cyclic buffer
self.max = 1 # Initial max value to return (1 = 1^ω)
# Propagates value up tree given a tree index
def _propagate(self, index, value):
parent = (index - 1) // 2
left, right = 2 * parent + 1, 2 * parent + 2
self.sum_tree[parent] = self.sum_tree[left] + self.sum_tree[right]
if parent != 0:
self._propagate(parent, value)
# Updates value given a tree index
def update(self, index, value):
self.sum_tree[index] = value # Set new value
self._propagate(index, value) # Propagate value
self.max = max(value, self.max)
def append(self, data, value):
# 开始的值设置为index
self.data[self.index] = data # Store data in underlying data structure
self.update(self.index + self.size - 1, value) # Update tree
self.index = (self.index + 1) % self.size # Update index
self.full = self.full or self.index == 0 # Save when capacity reached
self.max = max(value, self.max)
# Searches for the location of a value in sum tree
def _retrieve(self, index, value):
left, right = 2 * index + 1, 2 * index + 2
if left >= len(self.sum_tree):
return index
if value <= self.sum_tree[left]:
return self._retrieve(left, value)
return self._retrieve(right, value - self.sum_tree[left])
# Searches for a value in sum tree and returns value, data index and tree index
def find(self, value):
index = self._retrieve(0, value) # Search for index of item from root
data_index = index - self.size + 1
return (self.sum_tree[index], data_index, index) # Return value, data index, tree index
# Returns data given a data index
def get(self, data_index):
return self.data[data_index % self.size]
def total(self):
return self.sum_tree[0]
class ReplayMemory:
def __init__(self, args, capacity):
self.device = args.device
self.capacity = capacity # 40000
self.history = args.history_length # 4
self.discount = args.discount # 0.99
self.n = args.multi_step # 3
# 0.4 Initial importance sampling weight β, annealed to 1 over course of training
self.priority_weight = args.priority_weight
self.priority_exponent = args.priority_exponent # 0.5
self.t = 0 # Internal episode timestep counter
self.transitions = SegmentTree(
capacity) # Store transitions in a wrap-around cyclic buffer within a sum tree for querying priorities
# Adds state and action at time t, reward and terminal at time t + 1
def append(self, state, action, reward, terminal):
state = state[-1].mul(255).to(dtype=torch.uint8,
device=torch.device('cpu')) # Only store last frame and discretise to save memory
self.transitions.append(Transition(self.t, state, action, reward, not terminal),
self.transitions.max) # Store new transition with maximum priority
self.t = 0 if terminal else self.t + 1 # Start new episodes with t = 0
# Returns a transition with blank states where appropriate
def _get_transition(self, idx):
transition = np.array([None] * (self.history + self.n)) # 数组为7的列表
transition[self.history - 1] = self.transitions.get(idx)
for t in range(self.history - 2, -1, -1): # e.g. 2 1 0
if transition[t + 1].timestep == 0:
transition[t] = blank_trans # If future frame has timestep 0
else:
transition[t] = self.transitions.get(idx - self.history + 1 + t)
for t in range(self.history, self.history + self.n): # e.g. 4 5 6
if transition[t - 1].nonterminal:
transition[t] = self.transitions.get(idx - self.history + 1 + t)
else:
transition[t] = blank_trans # If prev (next) frame is terminal
return transition
# Returns a valid sample from a segment
def _get_sample_from_segment(self, segment, i):
valid = False
while not valid:
# 从片段中随机获取一个值
sample = np.random.uniform(i * segment,
(i + 1) * segment) # Uniformly sample an element from within a segment
# value, 数组索引,数的索引值
prob, idx, tree_idx = self.transitions.find(
sample) # Retrieve sample from tree with un-normalised probability
# Resample if transition straddled current index or probablity 0
if (self.transitions.index - idx) % self.capacity > self.n and (
idx - self.transitions.index) % self.capacity >= self.history and prob != 0:
valid = True # Note that conditions are valid but extra conservative around buffer index 0
# Retrieve all required transition data (from t - h to t + n)
transition = self._get_transition(idx) # 供7个数
# Create un-discretised state and nth next state
# 历史数据的图片值和将来数据的图片值
state = torch.stack([trans.state for trans in transition[:self.history]]).to(device=self.device).to(
dtype=torch.float32).div_(255)
next_state = torch.stack([trans.state for trans in transition[self.n:self.n + self.history]]).to(
device=self.device).to(dtype=torch.float32).div_(255)
# Discrete action to be used as index, 获取当前操作
action = torch.tensor([transition[self.history - 1].action], dtype=torch.int64, device=self.device)
r = torch.tensor([sum(self.discount ** n * transition[self.history + n - 1].reward for n in range(self.n))],
dtype=torch.float32, device=self.device)
# Mask for non-terminal nth next states
nonterminal = torch.tensor([transition[self.history + self.n - 1].nonterminal], dtype=torch.float32,
device=self.device)
return prob, idx, tree_idx, state, action, r, next_state, nonterminal
# 数据采样
def sample(self, batch_size):
# 样本总数,位置0保存样本总数,其节点的值为样本总数,这个是summaryTree特性决定的
# Retrieve sum of all priorities (used to create a normalised probability distribution)
p_total = self.transitions.total()
# 片段
segment = p_total / batch_size # Batch size number of segments, based on sum over all probabilities
# 获取的样本数组,从每个片段获取一个值
batch = [self._get_sample_from_segment(segment, i) for i in range(batch_size)] # Get batch of valid samples
# [value, 数组索引,数索引, 当前状态, 操作, reward, 下个状态, 是否终止] 列表
probs, _, tree_idxs, states, actions, returns, next_states, nonterminals = zip(*batch)
states, next_states, = torch.stack(states), torch.stack(next_states)
actions, returns, nonterminals = torch.cat(actions), torch.cat(returns), torch.stack(nonterminals)
probs = np.array(probs, dtype=np.float32) / p_total # Calculate normalised probabilities 概率值数组
capacity = self.capacity if self.transitions.full else self.transitions.index
weights = (capacity * probs) ** -self.priority_weight # Compute importance-sampling weights w
# 归一化
weights = torch.tensor(weights / weights.max(), dtype=torch.float32,
device=self.device) # Normalise by max importance-sampling weight from batch
return tree_idxs, states, actions, returns, next_states, nonterminals, weights
def update_priorities(self, idxs, priorities):
priorities = np.power(priorities, self.priority_exponent) # 用损失函数作为优先级
# 遍历id和其损失函数值
[self.transitions.update(idx, priority) for idx, priority in zip(idxs, priorities)]
# Set up internal state for iterator
def __iter__(self):
self.current_idx = 0
return self
# Return valid states for validation
def __next__(self):
if self.current_idx == self.capacity:
raise StopIteration
# Create stack of states
state_stack = [None] * self.history
state_stack[-1] = self.transitions.data[self.current_idx].state
prev_timestep = self.transitions.data[self.current_idx].timestep
for t in reversed(range(self.history - 1)):
if prev_timestep == 0:
state_stack[t] = blank_trans.state # If future frame has timestep 0
else:
state_stack[t] = self.transitions.data[self.current_idx + t - self.history + 1].state
prev_timestep -= 1
# Agent will turn into batch
state = torch.stack(state_stack, 0).to(dtype=torch.float32, device=self.device).div_(255)
self.current_idx += 1
return state
next = __next__ # Alias __next__ for Python 2 compatibility