From 1cbcfb159809ba1775f5b0a8fc4ab7533fd6f25e Mon Sep 17 00:00:00 2001 From: rical730 Date: Fri, 21 Aug 2020 15:24:18 +0800 Subject: [PATCH] add torch coma (#216) * add torch coma * add Apache License comment * update readme * update readme for installing sc2 on windows * update readme * add new line at the end of shell file * update readme * update readme of coma * fix model_path * self.algorithm to self.alg Co-authored-by: Bo Zhou <2466956298@qq.com> --- benchmark/torch/coma/.benchmark/3m_result.png | Bin 0 -> 68789 bytes benchmark/torch/coma/README.md | 59 ++++ benchmark/torch/coma/coma_config.py | 47 +++ benchmark/torch/coma/sc2_agent.py | 237 ++++++++++++++ benchmark/torch/coma/sc2_model.py | 102 ++++++ benchmark/torch/coma/starcraft2/Dockerfile | 38 +++ .../torch/coma/starcraft2/build_docker.sh | 7 + .../torch/coma/starcraft2/install_sc2.sh | 54 ++++ benchmark/torch/coma/train.py | 225 ++++++++++++++ parl/algorithms/__init__.py | 5 - parl/algorithms/torch/__init__.py | 1 + parl/algorithms/torch/coma.py | 290 ++++++++++++++++++ 12 files changed, 1060 insertions(+), 5 deletions(-) create mode 100644 benchmark/torch/coma/.benchmark/3m_result.png create mode 100644 benchmark/torch/coma/README.md create mode 100644 benchmark/torch/coma/coma_config.py create mode 100644 benchmark/torch/coma/sc2_agent.py create mode 100644 benchmark/torch/coma/sc2_model.py create mode 100644 benchmark/torch/coma/starcraft2/Dockerfile create mode 100644 benchmark/torch/coma/starcraft2/build_docker.sh create mode 100644 benchmark/torch/coma/starcraft2/install_sc2.sh create mode 100644 benchmark/torch/coma/train.py create mode 100644 parl/algorithms/torch/coma.py diff --git a/benchmark/torch/coma/.benchmark/3m_result.png b/benchmark/torch/coma/.benchmark/3m_result.png new file mode 100644 index 0000000000000000000000000000000000000000..3d5343bc1920a0e96ded07ff45f05b79ecd979a3 GIT binary patch literal 68789 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�y_8{#guCEh+1a-iy-t)W7_)=b7YStfS`u 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 SRyV*WI4N + + + + +## StarCraft II Installation +The environment is based on the full game of StarCraft II (version >= 3.16.1). To install the game, follow the commands bellow, or check more detail in [SMAC](https://github.com/oxwhirl/smac#installing-starcraft-ii). MacOS/Windows users are required to run this folder in Docker, as the starcraft environment does not support these two systems. + +### Linux +```shell +$ cd starcraft2 +$ SC2PATH=~ bash install_sc2.sh +``` +### MacOS (use Docker) +```shell +$ cd starcraft2 +$ bash build_docker.sh # build Dockerfile +$ bash install_sc2.sh # download startcraft II and maps +``` +### Windows (use Docker) +- Step 1: Build docker images, `cd starcraft2 && bash build_docker.sh` +- Step 2: Download a [Starcraft II package](https://github.com/Blizzard/s2client-proto#linux-packages), unzip to folder `starcraft2/StarCraftII` (password: `iagreetotheeula`) +- Step 3: Download [Map](https://github.com/oxwhirl/smac/releases/download/v0.1-beta1/SMAC_Maps.zip), unzip to folder `starcraft2/StarCraftII/Maps/SMAC_Maps` + + +## How to use +### Dependencies +- python3.5+ +- parl +- torch +- [SMAC](https://github.com/oxwhirl/smac) + +### Start Training +#### Linux +```shell +$ python3 train.py +``` +#### MacOS/Windows (use Docker) +```shell +$ cd coma +$ NV_GPU=$your_gpu_id docker run --name $your_container_name --user $(id -u):$(id -g) -v `pwd`:/parl -t parl-starcraft2:1.0 python3 train.py +``` +*or you can operate docker interactively by `docker run --name $your_container_name -it -v $your_host_path:/parl -t parl-starcraft2:1.0 /bin/bash`* + + + +### Reference +- [StarCraft](https://github.com/starry-sky6688/StarCraft) +- [pymarl](https://github.com/oxwhirl/pymarl) diff --git a/benchmark/torch/coma/coma_config.py b/benchmark/torch/coma/coma_config.py new file mode 100644 index 0000000..25d6971 --- /dev/null +++ b/benchmark/torch/coma/coma_config.py @@ -0,0 +1,47 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# arguments of coma +config = { + # ========== Environment ========== + 'difficulty': '3', # The difficulty of the game + 'map': '3m', # The map of the game + 'env_seed': None, # Environment random seed + 'replay_dir': '', # Save the replay, not available in Ubuntu + + # ========== Learn ========== + 'gamma': 0.99, + 'grad_norm_clip': 10, # Prevent gradient explosion + 'td_lambda': 0.8, # Lambda of td-lambda return + 'actor_lr': 1e-4, + 'critic_lr': 1e-3, + 'target_update_cycle': 200, # How often to update the target_net + + # ========== Epsilon-greedy ========== + 'epsilon': 0.5, + 'anneal_epsilon': 0.00064, + 'min_epsilon': 0.02, + # 'epsilon_anneal_scale' : 'epoch', + + # ========== Other ========== + 'n_epoch': 5000, # The number of the epoch to train the agent + 'n_episodes': 5, # The number of the episodes in one epoch + 'test_episode_n': 20, # The Number of the epochs to evaluate the agent + 'threshold': 19, # The threshold to judge whether win + 'test_cycle': 5, # How often to evaluate (every 'test_cycle' epcho) + 'save_cycle': 1000, # How often to save the model + 'model_dir': './model', # The model directory of the policy + 'test': False, # Evaluate model and quit (no training) + 'restore': False # restore model or not +} diff --git a/benchmark/torch/coma/sc2_agent.py b/benchmark/torch/coma/sc2_agent.py new file mode 100644 index 0000000..1eb9bcd --- /dev/null +++ b/benchmark/torch/coma/sc2_agent.py @@ -0,0 +1,237 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import parl +import torch +from torch.distributions import Categorical + +device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + + +class Agents(parl.Agent): + def __init__(self, algorithm, config): + self.n_actions = config['n_actions'] + self.n_agents = config['n_agents'] + self.state_shape = config['state_shape'] + self.obs_shape = config['obs_shape'] + + self.config = config + self.train_steps = 0 + self.rnn_h = None + super(Agents, self).__init__(algorithm) + print('Init all agents') + + def init_hidden(self): + """ function: init a hidden tensor for every agent at the begging of every episode + self.rnn_h: rnn hidden state, shape (n_agents, hidden_size) + """ + self.rnn_h = self.alg.init_hidden(1)[0] + + def predict(self, obs, rnn_h_in): + """input: + obs: obs + last_action + agent_id, shape: (1, obs_shape + n_actions + n_agents) + rnn_h_in: rnn's hidden input + output: + prob: output of actor, shape: (1, n_actions) + rnn_h_out: rnn's hidden output + """ + obs = np.expand_dims(obs, 0) + obs = torch.tensor(obs, dtype=torch.float32).to(device) + prob, rnn_h_out = self.alg.predict(obs, rnn_h_in) + return prob, rnn_h_out + + def sample(self, + obs, + last_action, + agent_id, + avail_actions, + epsilon, + test=False): + """input: + obs (array): agent i's obs + last_action (int): agent i's last action + agent_id (int): agent index + avail_actions (one_hot): available actions + epsilon (float): e_greed discount + test (bool): train or test + output: + action: int + prob: probability of every action, float, 0 ~ 1 + """ + obs = obs.copy() + # make obs: obs + agent's last action(one_hot) + agent's id(one_hot) + last_act_one_hot = np.zeros(self.n_actions) + last_act_one_hot[last_action] = 1. + id_one_hot = np.zeros(self.n_agents) + id_one_hot[agent_id] = 1. + obs = np.hstack((obs, last_act_one_hot)) + obs = np.hstack((obs, id_one_hot)) + + # predict action prob + prob, self.rnn_h[agent_id] = self.predict(obs, self.rnn_h[agent_id]) + + # add noise + avail_actions = torch.tensor( + avail_actions, dtype=torch.float32).unsqueeze(0).to( + device) # shape: (1, n_actions) + action_num = avail_actions.sum() # how many actions are available + prob = ((1 - epsilon) * prob + + torch.ones_like(prob) * epsilon / action_num) + prob[avail_actions == 0] = 0.0 # set avail action + + # choose action + if epsilon == 0 or test: + action = torch.argmax(prob) + else: + action = Categorical(prob).sample().long() + return action.cpu() + + def _get_actor_inputs(self, batch): + """ o(t), u(t-1)_a, agent_id + """ + obs = batch['o'] + u_onehot = batch['u_onehot'] + u_onehot_last = np.zeros_like(u_onehot) + u_onehot_last[:, 1:] = u_onehot[:, :-1] + ep_num = batch['o'].shape[0] + tr_num = batch['o'].shape[1] + + actor_inputs = [] + for agent_id in range(self.n_agents): + obs_a = obs[:, :, agent_id] + u_a_onehot_last = u_onehot_last[:, :, agent_id] + id_onehot = np.zeros((ep_num, tr_num, self.n_agents)) + id_onehot[:, :, agent_id] = 1. + # actor inputs: obs + agent's last action(one_hot) + agent's id(one_hot) + a_inputs = np.concatenate((obs_a, u_a_onehot_last, id_onehot), + axis=2) + # a_inpits shape (ep_num, tr_num, actor_input_dim) + actor_inputs.append(a_inputs) + + actor_inputs = np.stack( + actor_inputs, + axis=2) # shape (ep_num, tr_num, n_agents, actor_input_dim) + return actor_inputs + + def _get_critic_inputs(self, batch): + """ o(t)_a, s(t), u(t)_-a, u(t-1), agent_id + """ + ep_num = batch['o'].shape[0] + tr_num = batch['o'].shape[1] + + # o, o_next, state, state_next + o = batch['o'] # shape (ep_num, tr_num, n_agents, obs_shape) + o_next = np.zeros_like(o) + o_next[:, :-1] = o[:, 1:] + s = batch['s'] # shape (ep_num, tr_num, state_shape) + s_next = np.zeros_like(s) + s_next[:, :-1] = s[:, 1:] + # u_onehot, u_onehot_last shape (ep_num, tr_num, n_agents, n_actions) + u_onehot = batch['u_onehot'] + u_onehot_next = np.zeros_like(u_onehot) + u_onehot_next[:, :-1] = u_onehot[:, 1:] + u_onehot_last = np.zeros_like(u_onehot) + u_onehot_last[:, 1:] = u_onehot[:, :-1] + + critic_inputs = [] + critic_inputs_next = [] + for agent_id in range(self.n_agents): + # get o(t)_a, s(t) + o_a = o[:, :, agent_id] # shape (ep_num, tr_num, obs_shape) + o_a_next = o_next[:, :, agent_id] + s_a = s # shape (ep_num, tr_num, state_shape) + s_a_next = s_next + # get u(t-1) shape (ep_num, tr_num, n_agents * n_actions) + u_all_onehot = u_onehot.reshape((ep_num, tr_num, + self.n_agents * self.n_actions)) + u_all_onehot_next = u_onehot_next.reshape( + (ep_num, tr_num, self.n_agents * self.n_actions)) + u_all_onehot_last = u_onehot_last.reshape( + (ep_num, tr_num, self.n_agents * self.n_actions)) + # get u(t)_-a, set 0 to mask action, shape (ep_num, tr_num, n_agents * n_actions) + u_not_a_onehot = u_all_onehot.copy() + u_not_a_onehot_next = u_all_onehot_next.copy() + m_s = agent_id * self.n_actions # mask start flag + m_e = (agent_id + 1) * self.n_actions # mask end flag + u_not_a_onehot[:, :, m_s:m_e] = 0 + u_not_a_onehot_next[:, :, m_s:m_e] = 0 + # get id onehot, shape (ep_num, tr_num, n_agents) + id_onehot = np.zeros((ep_num, tr_num, self.n_agents)) + id_onehot[:, :, agent_id] = 1. + + # input: o, s, u_-a, u_last, agent_id + # input_next: o_next, s_next, u_-a_next, u, agent_id + # shape (ep_num, tr_num, critic_input_dim) + c_inputs = np.concatenate( + (o_a, s_a, u_not_a_onehot, u_all_onehot_last, id_onehot), + axis=2) + c_inputs_next = np.concatenate( + (o_a_next, s_a_next, u_not_a_onehot_next, u_all_onehot, + id_onehot), + axis=2) + critic_inputs.append(c_inputs) + critic_inputs_next.append(c_inputs_next) + critic_inputs = np.stack(critic_inputs, axis=2) + critic_inputs_next = np.stack(critic_inputs_next, axis=2) + # shape (ep_num, tr_num, n_agents, critic_input_dim) + return critic_inputs, critic_inputs_next + + def _get_avail_transitions_num(self, isover_batch): + """ input: + isover_batch: shape (ep_num, tr_num, 1) + output: + max_tr_num: max avail transitions number in all episodes + """ + ep_num = isover_batch.shape[0] + max_tr_num = 0 + for ep_id in range(ep_num): + for tr_id in range(self.config['episode_limit']): + if isover_batch[ep_id, tr_id, 0] == 1: + if tr_id + 1 >= max_tr_num: + max_tr_num = tr_id + 1 + break + return max_tr_num + + def learn(self, batch, epsilon=None): + """ input: + batch: dict(o, s, u, r, u_onehot, avail_u, padded, isover) + epsilon: e-greedy discount + """ + # different episode has different avail transition length + tr_num = self._get_avail_transitions_num(batch['isover']) + for key in batch.keys(): + # cut batch data's episode length + batch[key] = batch[key][:, :tr_num] + + # get actor input and critic input + batch['actor_inputs'] = self._get_actor_inputs(batch) + batch['critic_inputs'], batch[ + 'critic_inputs_next'] = self._get_critic_inputs(batch) + + # change batch data to torch tensor + for key in batch.keys(): + if key == 'u': + batch[key] = torch.tensor( + batch[key], dtype=torch.long).to(device) + else: + batch[key] = torch.tensor( + batch[key], dtype=torch.float32).to(device) + + self.alg.learn(batch, epsilon) + + if self.train_steps > 0 and self.train_steps % self.config[ + 'target_update_cycle'] == 0: + self.alg.sync_target() + self.train_steps += 1 diff --git a/benchmark/torch/coma/sc2_model.py b/benchmark/torch/coma/sc2_model.py new file mode 100644 index 0000000..7894363 --- /dev/null +++ b/benchmark/torch/coma/sc2_model.py @@ -0,0 +1,102 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch.nn as nn +import torch.nn.functional as F +import numpy as np +import parl + + +class ComaModel(parl.Model): + def __init__(self, config): + super(ComaModel, self).__init__() + self.n_actions = config['n_actions'] + self.n_agents = config['n_agents'] + self.state_shape = config['state_shape'] + self.obs_shape = config['obs_shape'] + + actor_input_dim = self._get_actor_input_dim() + critic_input_dim = self._get_critic_input_dim() + + self.actor_model = ActorModel(actor_input_dim, self.n_actions) + self.critic_model = CriticModel(critic_input_dim, self.n_actions) + + def policy(self, obs, hidden_state): + return self.actor_model.policy(obs, hidden_state) + + def value(self, inputs): + return self.critic_model.value(inputs) + + def get_actor_params(self): + return self.actor_model.parameters() + + def get_critic_params(self): + return self.critic_model.parameters() + + def _get_actor_input_dim(self): + input_shape = self.obs_shape # obs: 30 in 3m map + input_shape += self.n_actions # agent's last action (one_hot): 9 in 3m map + input_shape += self.n_agents # agent's one_hot id: 3 in 3m map + return input_shape # 30 + 9 + 3 = 42 + + def _get_critic_input_dim(self): + input_shape = self.state_shape # state: 48 in 3m map + input_shape += self.obs_shape # obs: 30 in 3m map + input_shape += self.n_agents # agent_id: 3 in 3m map + input_shape += self.n_actions * self.n_agents * 2 # all agents' action and last_action (one-hot): 54 in 3m map + return input_shape # 48 + 30+ 3 = 135 + + +# all agents share one actor network +class ActorModel(parl.Model): + def __init__(self, input_shape, act_dim): + """ input : obs, include the agent's id and last action, shape: (batch, obs_shape + n_action + n_agents) + output: one agent's q(obs, act) + """ + super(ActorModel, self).__init__() + self.hid_size = 64 + + self.fc1 = nn.Linear(input_shape, self.hid_size) + self.rnn = nn.GRUCell(self.hid_size, self.hid_size) + self.fc2 = nn.Linear(self.hid_size, act_dim) + + def init_hidden(self): + # new hidden states + return self.fc1.weight.new(1, self.hid_size).zero_() + + def policy(self, obs, h0): + x = F.relu(self.fc1(obs)) + h1 = h0.reshape(-1, self.hid_size) + h2 = self.rnn(x, h1) + policy = self.fc2(h2) + return policy, h2 + + +class CriticModel(parl.Model): + def __init__(self, input_shape, act_dim): + """ inputs: [ s(t), o(t)_a, u(t)_a, agent_a, u(t-1) ], shape: (Batch, input_shape) + output: Q, shape: (Batch, n_actions) + Batch = ep_num * n_agents + """ + super(CriticModel, self).__init__() + hid_size = 128 + self.fc1 = nn.Linear(input_shape, hid_size) + self.fc2 = nn.Linear(hid_size, hid_size) + self.fc3 = nn.Linear(hid_size, act_dim) + + def value(self, inputs): + hid1 = F.relu(self.fc1(inputs)) + hid2 = F.relu(self.fc2(hid1)) + Q = self.fc3(hid2) + return Q diff --git a/benchmark/torch/coma/starcraft2/Dockerfile b/benchmark/torch/coma/starcraft2/Dockerfile new file mode 100644 index 0000000..176e894 --- /dev/null +++ b/benchmark/torch/coma/starcraft2/Dockerfile @@ -0,0 +1,38 @@ +FROM nvidia/cuda:9.2-cudnn7-devel-ubuntu16.04 +MAINTAINER Tabish Rashid + +# CUDA includes +ENV CUDA_PATH /usr/local/cuda +ENV CUDA_INCLUDE_PATH /usr/local/cuda/include +ENV CUDA_LIBRARY_PATH /usr/local/cuda/lib64 + +# Ubuntu Packages +RUN apt-get update -y && apt-get install software-properties-common -y && \ + add-apt-repository -y multiverse && apt-get update -y && apt-get upgrade -y && \ + apt-get install -y apt-utils nano vim git man build-essential wget sudo && \ + rm -rf /var/lib/apt/lists/* + +# Install python3 pip3 +RUN apt-get update +RUN apt-get -y install python3 +RUN apt-get -y install python3-pip +RUN pip3 install --upgrade pip + +#### ------------------------------------------------------------------- +#### install parl +#### ------------------------------------------------------------------- +RUN pip3 install parl + +#### ------------------------------------------------------------------- +#### install SMAC +#### ------------------------------------------------------------------- +RUN pip3 install git+https://github.com/oxwhirl/smac.git + +#### ------------------------------------------------------------------- +#### install pytorch +#### ------------------------------------------------------------------- +RUN pip3 install torch + + +ENV SC2PATH /parl/starcraft2/StarCraftII +WORKDIR /parl diff --git a/benchmark/torch/coma/starcraft2/build_docker.sh b/benchmark/torch/coma/starcraft2/build_docker.sh new file mode 100644 index 0000000..ad7a742 --- /dev/null +++ b/benchmark/torch/coma/starcraft2/build_docker.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +#### ------------------------------------------------------------------- +#### build docker image +#### ------------------------------------------------------------------- +echo 'Building Dockerfile with image name parl-starcraft2:1.0' +docker build -t parl-starcraft2:1.0 . diff --git a/benchmark/torch/coma/starcraft2/install_sc2.sh b/benchmark/torch/coma/starcraft2/install_sc2.sh new file mode 100644 index 0000000..75e1599 --- /dev/null +++ b/benchmark/torch/coma/starcraft2/install_sc2.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +#### ------------------------------------------------------------------- +#### Install StarCraft II +#### ------------------------------------------------------------------- + +if [ -z "$SC2PATH" ]; then + SC2PATH=`pwd`'/StarCraftII' +else + SC2PATH=$SC2PATH'/StarCraftII' +fi + +export SC2PATH=$SC2PATH +echo 'SC2PATH is set to '$SC2PATH + +if [ ! -d $SC2PATH ]; then + echo 'StarCraftII is not installed. Installing now ...' + wget http://blzdistsc2-a.akamaihd.net/Linux/SC2.4.6.2.69232.zip + unzip -P iagreetotheeula SC2.4.6.2.69232.zip + rm -f SC2.4.6.2.69232.zip + echo 'Finished installing StarCraftII' +else + echo 'StarCraftII is already installed.' +fi + +if [ -f $SC2PATH/Libs/libstdc++.so* ]; then + echo 'Successfully installing StarCraft II' +else + echo 'Fail to install StarCraft II !' + exit 1 +fi + + +#### ------------------------------------------------------------------- +#### Add the custom maps +#### ------------------------------------------------------------------- + +echo 'Adding SMAC maps.' +MAP_DIR="$SC2PATH/Maps" +echo 'MAP_DIR is set to '$MAP_DIR +mkdir -p $MAP_DIR + +wget https://github.com/oxwhirl/smac/releases/download/v0.1-beta1/SMAC_Maps.zip +unzip SMAC_Maps.zip +mv SMAC_Maps $MAP_DIR +rm -f SMAC_Maps.zip +cp $MAP_DIR/SMAC_Maps/3m.SC2Map ./ + +if [ -f $MAP_DIR/SMAC_Maps/3m.SC2Map ]; then + echo 'Successfully adding custom maps' +else + echo 'Fail to add maps !' + exit 1 +fi diff --git a/benchmark/torch/coma/train.py b/benchmark/torch/coma/train.py new file mode 100644 index 0000000..59f4f71 --- /dev/null +++ b/benchmark/torch/coma/train.py @@ -0,0 +1,225 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from smac.env import StarCraft2Env +import numpy as np +import os +from sc2_model import ComaModel +from sc2_agent import Agents +from parl.algorithms import COMA +from parl.utils import tensorboard + + +def run_episode(env, agents, config, test=False): + o, u, r, s, avail_u, u_onehot, isover, padded = [], [], [], [], [], [], [], [] + env.reset() + done = False + step = 0 + ep_reward = 0 + last_act = [0 for _ in range(config['n_agents'])] + agents.init_hidden() # init rnn h0 for all agents + + while not done: + obs = env.get_obs() + state = env.get_state() + acts, avail_acts, acts_onehot = [], [], [] + + for agent_id in range(config['n_agents']): + avail_act = env.get_avail_agent_actions(agent_id) + + # action + epsilon = 0 if test else config['epsilon'] + act = agents.sample(obs[agent_id], last_act[agent_id], agent_id, + avail_act, epsilon, test) + last_act[agent_id] = act + + # action one-hot + act_onehot = np.zeros(config['n_actions']) + act_onehot[act] = 1 + acts.append(act) + acts_onehot.append(act_onehot) + avail_acts.append(avail_act) + + # step + reward, done, _ = env.step(acts) + + if step == config['episode_limit'] - 1: + done = 1 + + o.append(obs) + s.append(state) + u.append(np.reshape(acts, [config['n_agents'], 1])) + u_onehot.append(acts_onehot) + avail_u.append(avail_acts) + r.append([reward]) + isover.append([done]) + padded.append([0.]) # 0: no padded, 1: padded + + ep_reward += reward + step += 1 + + # fill trainsition len to episode_limit + for _ in range(step, config['episode_limit']): + # shape: (config['episode_limit'], n_agents, shape) + o.append(np.zeros((config['n_agents'], config['obs_shape']))) + s.append(np.zeros(config['state_shape'])) + u.append(np.zeros([config['n_agents'], 1])) + u_onehot.append(np.zeros((config['n_agents'], config['n_actions']))) + avail_u.append(np.zeros((config['n_agents'], config['n_actions']))) + # shape: (config['episode_limit'], 1) + r.append([0.]) + padded.append([1.]) + isover.append([1.]) + + ep_data = dict( + o=o.copy(), + s=s.copy(), + u=u.copy(), + r=r.copy(), + avail_u=avail_u.copy(), + u_onehot=u_onehot.copy(), + padded=padded.copy(), + isover=isover.copy()) + + # add an additional dimension at axis 0 for each item + for key in ep_data.keys(): + # each items shape: (1, trainsition_num, n_agents, own_shape) + ep_data[key] = np.array([ep_data[key]]) + + return ep_data, ep_reward + + +def run(env, agents, config): + win_rates = [] + episode_rewards = [] + train_steps = 0 + for epoch in range(config['n_epoch']): + print('train epoch {}'.format(epoch)) + # decay epsilon at the begging of each epoch + if config['epsilon'] > config['min_epsilon']: + config['epsilon'] -= config['anneal_epsilon'] + + # run n episode(s) + ep_data_list = [] + for _ in range(config['n_episodes']): + ep_data, _ = run_episode(env, agents, config, test=False) + ep_data_list.append(ep_data) + # each item in ep_batch shape: (episode_num, trainsition_num, n_agents, item_shape) + ep_batch = ep_data_list[0] + ep_data_list.pop(0) + for ep_data in ep_data_list: + for key in ep_batch.keys(): + ep_batch[key] = np.concatenate((ep_batch[key], ep_data[key]), + axis=0) + + # learn + agents.learn(ep_batch, config['epsilon']) + train_steps += 1 + + # save model + if train_steps > 0 and train_steps % config['save_cycle'] == 0: + model_path = config['model_dir'] + '/coma_' + str( + train_steps) + '.ckpt' + agents.save(save_path=model_path) + print('save model: ', model_path) + + # test + if epoch % config['test_cycle'] == 0: + win_rate, ep_mean_reward = test(env, agents, config) + # print('win_rate is ', win_rate) + win_rates.append(win_rate) + episode_rewards.append(ep_mean_reward) + tensorboard.add_scalar('win_rate', win_rates[-1], len(win_rates)) + tensorboard.add_scalar('episode_rewards', episode_rewards[-1], + len(episode_rewards)) + print('win_rate', win_rates, len(win_rates)) + print('episode_rewards', episode_rewards, len(episode_rewards)) + + +def test(env, agents, config): + win_number = 0 + episode_rewards = 0 + for ep_id in range(config['test_episode_n']): + _, ep_reward = run_episode(env, agents, config, test=True) + episode_rewards += ep_reward + if ep_reward > config['threshold']: + win_number += 1 + return win_number / config['test_episode_n'], episode_rewards / config[ + 'test_episode_n'] + + +def test_by_sparse_reward(agents, config): + env = StarCraft2Env( + map_name=config['map'], + difficulty=config['difficulty'], + seed=config['env_seed'], + replay_dir=config['replay_dir'], + reward_sparse=True, # Receive 1/-1 reward for winning/loosing an episode + reward_scale=False) + win_number = 0 + for ep_id in range(config['test_episode_n']): + _, ep_reward = run_episode(env, agents, config, test=True) + result = 'win' if ep_reward > 0 else 'defeat' + print('Episode {}: {}'.format(ep_id, result)) + if ep_reward > 0: + win_number += 1 + env.close() + win_rate = win_number / config['test_episode_n'] + print('The win rate of coma is {}'.format(win_rate)) + return win_rate + + +def main(config): + env = StarCraft2Env( + map_name=config['map'], + seed=config['env_seed'], + difficulty=config['difficulty'], + replay_dir=config['replay_dir']) + env_info = env.get_env_info() + + config['n_actions'] = env_info['n_actions'] + config['n_agents'] = env_info['n_agents'] + config['state_shape'] = env_info['state_shape'] + config['obs_shape'] = env_info['obs_shape'] + config['episode_limit'] = env_info['episode_limit'] + + model = ComaModel(config=config) + algorithm = COMA( + model, + n_actions=config['n_actions'], + n_agents=config['n_agents'], + grad_norm_clip=config['grad_norm_clip'], + actor_lr=config['actor_lr'], + critic_lr=config['critic_lr'], + gamma=config['gamma'], + td_lambda=config['td_lambda']) + agents = Agents(algorithm, config) + + # restore model here + model_file = config['model_dir'] + '/coma.ckpt' + if config['restore'] and os.path.isfile(model_file): + agents.restore(model_file) + print('model loaded: ', model_file) + + if config['test']: + test_by_sparse_reward(agents, config) + else: + run(env, agents, config) + + env.close() + + +if __name__ == '__main__': + from coma_config import config + main(config) diff --git a/parl/algorithms/__init__.py b/parl/algorithms/__init__.py index 8565455..20c3d3d 100644 --- a/parl/algorithms/__init__.py +++ b/parl/algorithms/__init__.py @@ -13,13 +13,8 @@ # limitations under the License. from parl.utils.utils import _HAS_FLUID, _HAS_TORCH -from parl.utils import logger if _HAS_FLUID: from parl.algorithms.fluid import * elif _HAS_TORCH: from parl.algorithms.torch import * -else: - logger.warning( - "No deep learning framework was found, but it's ok for parallel computation." - ) diff --git a/parl/algorithms/torch/__init__.py b/parl/algorithms/torch/__init__.py index 9de7afb..86d0386 100644 --- a/parl/algorithms/torch/__init__.py +++ b/parl/algorithms/torch/__init__.py @@ -16,5 +16,6 @@ from parl.algorithms.torch.ddqn import * from parl.algorithms.torch.dqn import * from parl.algorithms.torch.a2c import * from parl.algorithms.torch.td3 import * +from parl.algorithms.torch.coma import * from parl.algorithms.torch.ppo import * from parl.algorithms.torch.policy_gradient import * diff --git a/parl/algorithms/torch/coma.py b/parl/algorithms/torch/coma.py new file mode 100644 index 0000000..f8cc6bf --- /dev/null +++ b/parl/algorithms/torch/coma.py @@ -0,0 +1,290 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import os +from copy import deepcopy +import parl +import numpy as np + +device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + +__all__ = ['COMA'] + + +class COMA(parl.Algorithm): + def __init__(self, + model, + n_actions, + n_agents, + grad_norm_clip=None, + actor_lr=None, + critic_lr=None, + gamma=None, + td_lambda=None): + """ COMA algorithm + + Args: + model (parl.Model): forward network of actor and critic. + n_actions (int): action dim for each agent + n_agents (int): agents number + grad_norm_clip (int or float): gradient clip, prevent gradient explosion + actor_lr (float): actor network learning rate + critic_lr (float): critic network learning rate + gamma (float): discounted factor for reward computation + td_lambda (float): lambda of td-lambda return + """ + assert isinstance(n_actions, int) + assert isinstance(n_agents, int) + assert isinstance(grad_norm_clip, int) or isinstance( + grad_norm_clip, float) + assert isinstance(actor_lr, float) + assert isinstance(critic_lr, float) + assert isinstance(gamma, float) + assert isinstance(td_lambda, float) + + self.n_actions = n_actions + self.n_agents = n_agents + self.grad_norm_clip = grad_norm_clip + self.actor_lr = actor_lr + self.critic_lr = critic_lr + self.gamma = gamma + self.td_lambda = td_lambda + + self.model = model.to(device) + self.target_model = deepcopy(model).to(device) + + self.sync_target() + + self.actor_parameters = list(self.model.get_actor_params()) + self.critic_parameters = list(self.model.get_critic_params()) + + self.critic_optimizer = torch.optim.RMSprop( + self.critic_parameters, lr=self.critic_lr) + self.actor_optimizer = torch.optim.RMSprop( + self.actor_parameters, lr=self.actor_lr) + + self.train_rnn_h = None + + def init_hidden(self, ep_num): + """ function: init a hidden tensor for every agent + input: + ep_num: How many episodes are included in a batch of data + output: + rnn_h: rnn hidden state, shape (ep_num, n_agents, hidden_size) + """ + assert hasattr(self.model.actor_model, 'init_hidden'), \ + "actor must have rnn structure and has method 'init_hidden' to make hidden states" + rnn_h = self.model.actor_model.init_hidden().unsqueeze(0).expand( + ep_num, self.n_agents, -1) + return rnn_h + + def predict(self, obs, rnn_h_in): + """input: + obs: obs + last_action + agent_id, shape: (1, obs_shape + n_actions + n_agents) + rnn_h_in: rnn's hidden input + output: + prob: output of actor, shape: (1, n_actions) + rnn_h_out: rnn's hidden output + """ + with torch.no_grad(): + policy_logits, rnn_h_out = self.model.policy( + obs, rnn_h_in) # input obs shape [1, 42] + prob = torch.nn.functional.softmax( + policy_logits, dim=-1) # shape [1, 9] + return prob, rnn_h_out + + def _get_critic_output(self, batch): + """ input: + batch: dict(o, s, u, r, u_onehot, avail_u, padded, isover, actor_inputs, critic_inputs) + output: + q_evals and q_targets: shape (ep_num, tr_num, n_agents, n_actions) + """ + ep_num = batch['r'].shape[0] + tr_num = batch['r'].shape[1] + critic_inputs = batch['critic_inputs'] + critic_inputs_next = batch['critic_inputs_next'] + + critic_inputs = critic_inputs.reshape((ep_num * tr_num * self.n_agents, + -1)) + critic_inputs_next = critic_inputs.reshape( + (ep_num * tr_num * self.n_agents, -1)) + + q_evals = self.model.value(critic_inputs) + q_targets = self.model.value(critic_inputs_next) + + q_evals = q_evals.reshape((ep_num, tr_num, self.n_agents, -1)) + q_targets = q_targets.reshape((ep_num, tr_num, self.n_agents, -1)) + return q_evals, q_targets + + def _get_actor_output(self, batch, epsilon): + """ input: + batch: dict(o, s, u, r, u_onehot, avail_u, padded, isover, actor_inputs, critic_inputs) + epsilon: noise discount factor + output: + action_prob: probability of actions, shape (ep_num, tr_num, n_agents, n_actions) + """ + ep_num = batch['r'].shape[0] + tr_num = batch['r'].shape[1] + avail_actions = batch['avail_u'] + actor_inputs = batch['actor_inputs'] + action_prob = [] + for tr_id in range(tr_num): + inputs = actor_inputs[:, + tr_id] # shape (ep_num, n_agents, actor_input_dim) + inputs = inputs.reshape( + (-1, inputs.shape[-1])) # shape (-1, actor_input_dim) + policy_logits, self.train_rnn_h = self.model.policy( + inputs, self.train_rnn_h) + # policy_logits shape from (-1, n_actions) to (ep_num, n_agents, n_actions) + policy_logits = policy_logits.view(ep_num, self.n_agents, -1) + prob = torch.nn.functional.softmax(policy_logits, dim=-1) + action_prob.append(prob) + + action_prob = torch.stack( + action_prob, + dim=1).to(device) # shape: (ep_num, tr_num, n_agents, n_actions) + action_num = avail_actions.sum() # how many actions are available + action_prob = ((1 - epsilon) * action_prob + + torch.ones_like(action_prob) * epsilon / action_num) + action_prob[avail_actions == 0] = 0.0 # set avail action + + action_prob = action_prob / action_prob.sum( + dim=-1, keepdim=True) # in case action_prob.sum != 1 + action_prob[avail_actions == 0] = 0.0 + action_prob = action_prob.to(device) + return action_prob + + def _cal_td_target(self, batch, q_targets): # compute TD(lambda) + """ input: + batch: dict(o, s, u, r, u_onehot, avail_u, padded, isover, actor_inputs, critic_inputs) + q_targets: Q value of target critic network, shape (ep_num, tr_num, n_agents) + output: + lambda_return: TD lambda return, shape (ep_num, tr_num, n_agents) + """ + ep_num = batch['r'].shape[0] + tr_num = batch['r'].shape[1] + mask = (1 - batch['padded'].float()).repeat(1, 1, + self.n_agents).to(device) + isover = (1 - batch['isover'].float()).repeat(1, 1, self.n_agents).to( + device) # used for setting last transition's q_target to 0 + # reshape reward: from (ep_num, tr_num, 1) to (ep_num, tr_num, n_agents) + r = batch['r'].repeat((1, 1, self.n_agents)).to(device) + # compute n_step_return + n_step_return = torch.zeros((ep_num, tr_num, self.n_agents, + tr_num)).to(device) + for tr_id in range(tr_num - 1, -1, -1): + n_step_return[:, tr_id, :, 0] = ( + r[:, tr_id] + self.gamma * q_targets[:, tr_id] * + isover[:, tr_id]) * mask[:, tr_id] + for n in range(1, tr_num - tr_id): + n_step_return[:, tr_id, :, n] = ( + r[:, tr_id] + self.gamma * + n_step_return[:, tr_id + 1, :, n - 1]) * mask[:, tr_id] + + lambda_return = torch.zeros((ep_num, tr_num, self.n_agents)).to(device) + for tr_id in range(tr_num): + returns = torch.zeros((ep_num, self.n_agents)).to(device) + for n in range(1, tr_num - tr_id): + returns += pow(self.td_lambda, + n - 1) * n_step_return[:, tr_id, :, n - 1] + lambda_return[:, tr_id] = (1 - self.td_lambda) * returns + \ + pow(self.td_lambda, tr_num - tr_id - 1) * \ + n_step_return[:, tr_id, :, tr_num - tr_id - 1] + return lambda_return + + def _critic_learn(self, batch): + """ input: + batch: dict(o, s, u, r, u_onehot, avail_u, padded, isover, actor_inputs, critic_inputs) + output: + q_values: Q value of eval critic network, shape (ep_num, tr_num, n_agents, n_actions) + """ + u = batch['u'] # shape (ep_num, tr_num, agent, n_actions) + u_next = torch.zeros_like(u, dtype=torch.long) + u_next[:, :-1] = u[:, 1:] + mask = (1 - batch['padded'].float()).repeat(1, 1, + self.n_agents).to(device) + + # get q value for every agent and every action, shape (ep_num, tr_num, n_agents, n_actions) + q_evals, q_next_target = self._get_critic_output(batch) + q_values = q_evals.clone() # used for function return + + # get q valur for every agent + q_evals = torch.gather(q_evals, dim=3, index=u).squeeze(3) + q_next_target = torch.gather( + q_next_target, dim=3, index=u_next).squeeze(3) + + targets = self._cal_td_target(batch, q_next_target) + + td_error = targets.detach() - q_evals + masked_td_error = mask * td_error # mask padded data + + loss = (masked_td_error** + 2).sum() / mask.sum() # mask.sum: avail transition num + + self.critic_optimizer.zero_grad() + loss.backward() + torch.nn.utils.clip_grad_norm_(self.critic_parameters, + self.grad_norm_clip) + self.critic_optimizer.step() + return q_values + + def _actor_learn(self, batch, epsilon, q_values): + """ input: + batch: dict(o, s, u, r, u_onehot, avail_u, padded, isover, actor_inputs, critic_inputs) + epsilon (float): e-greedy discount + q_values: Q value of eval critic network, shape (ep_num, tr_num, n_agents, n_actions) + """ + action_prob = self._get_actor_output(batch, epsilon) # prob of u + + # mask: used to compute TD-error, filling data should not affect learning + u = batch['u'] + mask = (1 - batch['padded'].float()).repeat(1, 1, self.n_agents).to( + device) # shape (ep_num, tr_num, 3) + + q_taken = torch.gather(q_values, dim=3, index=u).squeeze(3) # Q(u_a) + pi_taken = torch.gather( + action_prob, dim=3, + index=u).squeeze(3) # prob of act that agent a choosen + pi_taken[mask == 0] = 1.0 # prevent log overflow + log_pi_taken = torch.log(pi_taken) + + # advantage + baseline = (q_values * action_prob).sum( + dim=3, keepdim=True).squeeze(3).detach() + advantage = (q_taken - baseline).detach() + loss = -((advantage * log_pi_taken) * mask).sum() / mask.sum() + self.actor_optimizer.zero_grad() + loss.backward() + torch.nn.utils.clip_grad_norm_(self.actor_parameters, + self.grad_norm_clip) + self.actor_optimizer.step() + + def learn(self, batch, epsilon): + """ input: + batch: dict(o, s, u, r, u_onehot, avail_u, padded, isover, actor_inputs, critic_inputs) + epsilon (float): e-greedy discount + """ + ep_num = batch['r'].shape[0] + self.train_rnn_h = self.init_hidden(ep_num) + self.train_rnn_h = self.train_rnn_h.to(device) + + q_values = self._critic_learn(batch) + self._actor_learn(batch, epsilon, q_values) + + def sync_target(self, decay=0): + for param, target_param in zip(self.model.parameters(), + self.target_model.parameters()): + target_param.data.copy_((1 - decay) * param.data + + decay * target_param.data) -- GitLab