forge/trinity/ann.py (216 lines of code) (raw):
from pdb import set_trace as T
import numpy as np
import torch
from torch import nn
from torch.nn import functional as F
from torch.distributions import Categorical
from forge.blade.action.tree import ActionTree
from forge.blade.action.v2 import ActionV2
from forge.blade.lib.enums import Neon
from forge.blade.lib import enums
from forge.ethyr import torch as torchlib
from forge.blade import entity
def classify(logits):
if len(logits.shape) == 1:
logits = logits.view(1, -1)
distribution = Categorical(1e-3+F.softmax(logits, dim=1))
atn = distribution.sample()
return atn
####### Network Modules
class ConstDiscrete(nn.Module):
def __init__(self, config, h, nattn):
super().__init__()
self.fc1 = torch.nn.Linear(h, nattn)
self.config = config
def forward(self, env, ent, action, stim):
leaves = action.args(env, ent, self.config)
x = self.fc1(stim)
xIdx = classify(x)
leaf = leaves[int(xIdx)]
return leaf, x, xIdx
class VariableDiscrete(nn.Module):
def __init__(self, config, xdim, h):
super().__init__()
self.attn = AttnCat(xdim, h)
self.config = config
#Arguments: stim, action/argument embedding
def forward(self, env, ent, action, key, vals):
leaves = action.args(env, ent, self.config)
x = self.attn(key, vals)
xIdx = classify(x)
leaf = leaves[int(xIdx)]
return leaf, x, xIdx
class AttnCat(nn.Module):
def __init__(self, xdim, h):
super().__init__()
#self.fc1 = torch.nn.Linear(xdim, h)
#self.fc2 = torch.nn.Linear(h, 1)
self.fc = torch.nn.Linear(xdim, 1)
self.h = h
def forward(self, x, args):
n = args.shape[0]
x = x.expand(n, self.h)
xargs = torch.cat((x, args), dim=1)
x = self.fc(xargs)
#x = F.relu(self.fc1(xargs))
#x = self.fc2(x)
return x.view(1, -1)
####### End network modules
class ValNet(nn.Module):
def __init__(self, config):
super().__init__()
self.fc = torch.nn.Linear(config.HIDDEN, 1)
self.envNet = Env(config)
def forward(self, conv, flat, ent):
stim = self.envNet(conv, flat, ent)
x = self.fc(stim)
x = x.view(1, -1)
return x
class Ent(nn.Module):
def __init__(self, entDim, h):
super().__init__()
self.ent = torch.nn.Linear(entDim, h)
def forward(self, ents):
ents = self.ent(ents)
ents, _ = torch.max(ents, 0)
return ents
class Env(nn.Module):
def __init__(self, config):
super().__init__()
h = config.HIDDEN
entDim = 11 # + 225
self.fc1 = torch.nn.Linear(3*h, h)
self.embed = torch.nn.Embedding(7, 7)
self.conv = torch.nn.Linear(1800, h)
self.flat = torch.nn.Linear(entDim, h)
self.ents = Ent(entDim, h)
def forward(self, conv, flat, ents):
tiles, nents = conv[0], conv[1]
nents = nents.view(-1)
tiles = self.embed(tiles.view(-1).long()).view(-1)
conv = torch.cat((tiles, nents))
conv = self.conv(conv)
ents = self.ents(ents)
flat = self.flat(flat)
x = torch.cat((conv, flat, ents)).view(1, -1)
x = self.fc1(x)
#Removed relu (easier training, lower policy cap)
#x = torch.nn.functional.relu(self.fc1(x))
return x
class MoveNet(nn.Module):
def __init__(self, config):
super().__init__()
self.moveNet = ConstDiscrete(config, config.HIDDEN, 5)
self.envNet = Env(config)
def forward(self, env, ent, action, s):
stim = self.envNet(s.conv, s.flat, s.ents)
action, arg, argIdx = self.moveNet(env, ent, action, stim)
return action, (arg, argIdx)
#Network that selects an attack style
class StyleAttackNet(nn.Module):
def __init__(self, config):
super().__init__()
self.config, h = config, config.HIDDEN
self.h = h
self.envNet = Env(config)
self.targNet = ConstDiscrete(config, h, 3)
def target(self, ent, arguments):
if len(arguments) == 1:
return arguments[0]
arguments = [e for e in arguments if e.entID != ent.entID]
arguments = sorted(arguments, key=lambda a: a.health.val)
return arguments[0]
def forward(self, env, ent, action, s):
stim = self.envNet(s.conv, s.flat, s.ents)
action, atn, atnIdx = self.targNet(env, ent, action, stim)
#Hardcoded targeting
arguments = action.args(env, ent, self.config)
argument = self.target(ent, arguments)
attkOuts = [(atn, atnIdx)]
return action, [argument], attkOuts
#Network that selects an attack and target (In progress,
#for learned targeting experiments)
class AttackNet(nn.Module):
def __init__(self, config):
super().__init__()
self.config, h = config, config.HIDDEN
entDim = 11
self.styleEmbed = torch.nn.Embedding(3, h)
self.targEmbed = Ent(entDim, h)
self.h = h
self.envNet = Env(config)
self.styleNet = ConstDiscrete(config, h, 3)
self.targNet = VariableDiscrete(config, 3*h, h)
def forward(self, env, ent, action, s):
stim = self.envNet(s.conv, s.flat, s.ents)
action, atn, atnIdx = self.styleNet(env, ent, action, stim)
#Embed targets
targets = action.args(env, ent, self.config)
targets = torch.tensor([e.stim for e in targets]).float()
targets = self.targEmbed(targets).unsqueeze(0)
nTargs = len(targets)
atns = self.styleEmbed(atnIdx).expand(nTargs, self.h)
vals = torch.cat((atns, targets), 1)
argument, arg, argIdx = self.targNet(
env, ent, action, stim, vals)
attkOuts = ((atn, atnIdx), (arg, argIdx))
return action, [argument], attkOuts
class ANN(nn.Module):
def __init__(self, config):
super().__init__()
self.valNet = ValNet(config)
self.config = config
self.moveNet = MoveNet(config)
self.attackNet = (StyleAttackNet(config) if
config.AUTO_TARGET else AttackNet(config))
def forward(self, ent, env):
s = torchlib.Stim(ent, env, self.config)
val = self.valNet(s.conv, s.flat, s.ents)
actions = ActionTree(env, ent, ActionV2).actions()
_, move, attk = actions
#Actions
moveArg, moveOuts = self.moveNet(
env, ent, move, s)
attk, attkArg, attkOuts = self.attackNet(
env, ent, attk, s)
action = (move, attk)
arguments = (moveArg, attkArg)
outs = (moveOuts, *attkOuts)
return action, arguments, outs, val
#Messy hooks for visualizers
def visDeps(self):
from forge.blade.core import realm
from forge.blade.core.tile import Tile
colorInd = int(12*np.random.rand())
color = Neon.color12()[colorInd]
color = (colorInd, color)
ent = realm.Desciple(-1, self.config, color).server
targ = realm.Desciple(-1, self.config, color).server
sz = 15
tiles = np.zeros((sz, sz), dtype=object)
for r in range(sz):
for c in range(sz):
tiles[r, c] = Tile(enums.Grass, r, c, 1, None)
targ.pos = (7, 7)
tiles[7, 7].addEnt(0, targ)
posList, vals = [], []
for r in range(sz):
for c in range(sz):
ent.pos = (r, c)
tiles[r, c].addEnt(1, ent)
s = torchlib.Stim(ent, tiles, self.config)
conv, flat, ents = s.conv, s.flat, s.ents
val = self.valNet(conv, s.flat, s.ents)
vals.append(float(val))
tiles[r, c].delEnt(1)
posList.append((r, c))
vals = list(zip(posList, vals))
return vals
def visVals(self, food='max', water='max'):
from forge.blade.core import realm
posList, vals = [], []
R, C = self.world.shape
for r in range(self.config.BORDER, R-self.config.BORDER):
for c in range(self.config.BORDER, C-self.config.BORDER):
colorInd = int(12*np.random.rand())
color = Neon.color12()[colorInd]
color = (colorInd, color)
ent = entity.Player(-1, color, self.config)
ent._pos = (r, c)
if food != 'max':
ent._food = food
if water != 'max':
ent._water = water
posList.append(ent.pos)
self.world.env.tiles[r, c].addEnt(ent.entID, ent)
stim = self.world.env.stim(ent.pos, self.config.STIM)
s = torchlib.Stim(ent, stim, self.config)
val = self.valNet(s.conv, s.flat, s.ents).detach()
self.world.env.tiles[r, c].delEnt(ent.entID)
vals.append(float(val))
vals = list(zip(posList, vals))
return vals