File size: 9,833 Bytes
323c6ec | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | import os
import tensorflow as tf
from tf_agents.utils import common
from tf_agents.trajectories import trajectory
# Custom Modular Imports
from config import Config
from rewards import compute_rewards
from utilities import get_system_telemetry
class MultiAgentTrainer:
def __init__(self, config, tf_env, agent_1, agent_2):
self.config = config
self.tf_env = tf_env
self.agent_1 = agent_1
self.agent_2 = agent_2
log_dir = self.config.LOG_DIR
checkpoint_dir = self.config.CHECKPOINT_DIR
self.global_step = tf.Variable(0, name="global_step", trainable=False, dtype=tf.int64)
self.summary_writer = tf.summary.create_file_writer(log_dir)
self.checkpoint = tf.train.Checkpoint(
step=self.global_step,
agent_1_actor=self.agent_1._actor_net,
agent_1_critic=self.agent_1._value_net,
agent_2_actor=self.agent_2._actor_net,
agent_2_critic=self.agent_2._value_net
)
self.checkpoint_manager = tf.train.CheckpointManager(
self.checkpoint, directory=checkpoint_dir, max_to_keep=1000
)
if self.checkpoint_manager.latest_checkpoint:
self.checkpoint.restore(self.checkpoint_manager.latest_checkpoint)
print(f"π Checkpoint restored successfully from: {self.checkpoint_manager.latest_checkpoint}")
else:
print("π No checkpoints found. Initializing brand new weights.")
self.era_red_rewards = []
self.era_blue_rewards = []
def _evaluate_and_print_era(self, epoch_idx, current_step):
"""Analyzes historical trends over the last 1000 episodes, saves to file, and displays report."""
if not self.era_red_rewards or not self.era_blue_rewards:
return
avg_red = sum(self.era_red_rewards) / len(self.era_red_rewards)
avg_blue = sum(self.era_blue_rewards) / len(self.era_blue_rewards)
# 1. Terminal Output Presentation
print("\n" + "="*60)
print(f"π πΊοΈ ERA TRACKING REPORT | EPISODES {epoch_idx - 999} - {epoch_idx}")
print("="*60)
print(f"π΄ Red Agent Avg Reward: {avg_red:+.2f}")
print(f"π΅ Blue Agent Avg Reward: {avg_blue:+.2f}")
print("-"*60)
if avg_red > 200:
dominance = "π΄ RED DOMINANT (Attackers are breaching defenses with high margin)"
elif avg_blue > 200:
dominance = "π΅ BLUE DOMINANT (Defenders are shutting down and patching threats efficiently)"
else:
dominance = "βοΈ NASH EQUILIBRIUM / STALEMATE (Highly contested stalemate block)"
print(f"π Tactical Dominance Status: {dominance}")
if avg_blue >= 500:
security_status = "π VERIFIED SECURE (Exploits mitigated, network completely hardened)"
elif avg_blue >= -200:
security_status = "β οΈ CONDITIONALLY STABLE (Minor compromises occurring, but controlled)"
else:
security_status = "π¨ CRITICAL VULNERABILITY BREACH (Active Directory/Core compromised consistently)"
print(f"π‘οΈ Network Security State: {security_status}")
print("="*60 + "\n")
# 2. Append snapshot data securely to structured flat text asset
txt_path = os.path.join(self.config.LOG_DIR, "era_history.txt")
os.makedirs(self.config.LOG_DIR, exist_ok=True)
with open(txt_path, "a") as f:
f.write(f"EPISODE:{epoch_idx}|RED_REWARD:{avg_red:.2f}|BLUE_REWARD:{avg_blue:.2f}|STEP:{current_step}\n")
# Clear data structures for next epoch block tracking
self.era_red_rewards.clear()
self.era_blue_rewards.clear()
def train_epoch(self, epoch_idx):
"""Runs a complete training cycle: collects trajectories and applies gradient updates."""
time_step = self.tf_env.reset()
episode_reward_1 = 0.0
episode_reward_2 = 0.0
steps = 0
red_trajectory_buffer = []
blue_trajectory_buffer = []
with self.summary_writer.as_default():
while steps < self.config.MAX_STEPS_PER_EPISODE:
steps += 1
# Split nested structures for isolated calculation steps
red_time_step = time_step._replace(
observation=time_step.observation['red'],
reward=time_step.reward['red']
)
blue_time_step = time_step._replace(
observation=time_step.observation['blue'],
reward=time_step.reward['blue']
)
# Collect structural policy logit info
action_step_1 = self.agent_1.collect_policy.action(red_time_step)
action_step_2 = self.agent_2.collect_policy.action(blue_time_step)
combined_actions = {
'red': action_step_1.action,
'blue': action_step_2.action
}
next_time_step = self.tf_env.step(combined_actions)
#print(f"DEBUG | Red Action: {combined_actions['red']} | Info: {self.tf_env.pyenv.envs[0].get_current_info()}")
#print(f"DEBUG | Blue Action: {combined_actions['blue']} | Info: {self.tf_env.pyenv.envs[0].get_current_info()}")
# Process Environment Reward Signals
env_info = self.tf_env.pyenv.envs[0].get_current_info()
r1, r2 = compute_rewards(self.config.MODE, env_info)
#print(f"DEBUG | Red Reward: {r1} | Blue Reward: {r2}")
if hasattr(r1, 'numpy'): r1 = r1.numpy()
if hasattr(r2, 'numpy'): r2 = r2.numpy()
episode_reward_1 += float(r1)
episode_reward_2 += float(r2)
# Stretch shape arrays matching batch environments configuration
batch_size = tf.shape(time_step.step_type)[0]
broadcast_r1 = tf.fill([batch_size], tf.cast(r1, tf.float32))
broadcast_r2 = tf.fill([batch_size], tf.cast(r2, tf.float32))
next_red_time_step = next_time_step._replace(
observation=next_time_step.observation['red'],
reward=broadcast_r1
)
next_blue_time_step = next_time_step._replace(
observation=next_time_step.observation['blue'],
reward=broadcast_r2
)
# Construct Trajectories preserving distribution signatures
red_traj = trajectory.Trajectory(
step_type=red_time_step.step_type,
observation=red_time_step.observation,
action=action_step_1.action,
policy_info=action_step_1.info,
next_step_type=next_red_time_step.step_type,
reward=next_red_time_step.reward,
discount=next_red_time_step.discount
)
blue_traj = trajectory.Trajectory(
step_type=blue_time_step.step_type,
observation=blue_time_step.observation,
action=action_step_2.action,
policy_info=action_step_2.info,
next_step_type=next_blue_time_step.step_type,
reward=next_blue_time_step.reward,
discount=next_blue_time_step.discount
)
red_trajectory_buffer.append(red_traj)
blue_trajectory_buffer.append(blue_traj)
if steps % 100 == 0:
print(f"β³ Episode {epoch_idx:04d} | Simulating step: {steps}/{self.config.MAX_STEPS_PER_EPISODE}...", end="\r")
time_step = next_time_step
if time_step.is_last():
break
# Backpropagation Backpass Phase
red_experience = tf.nest.map_structure(lambda *args: tf.stack(args, axis=1), *red_trajectory_buffer)
blue_experience = tf.nest.map_structure(lambda *args: tf.stack(args, axis=1), *blue_trajectory_buffer)
red_loss_info = self.agent_1.train(experience=red_experience)
blue_loss_info = self.agent_2.train(experience=blue_experience)
self.global_step.assign_add(1)
current_step = self.global_step.numpy()
# Record metrics into rolling lists
self.era_red_rewards.append(episode_reward_1)
self.era_blue_rewards.append(episode_reward_2)
# TensorBoard metrics execution
tf.summary.scalar("Rewards/Red_Agent", float(episode_reward_1), step=current_step)
tf.summary.scalar("Rewards/Blue_Agent", float(episode_reward_2), step=current_step)
tf.summary.scalar("Losses/Red_Total_Loss", red_loss_info.loss, step=current_step)
tf.summary.scalar("Losses/Blue_Total_Loss", blue_loss_info.loss, step=current_step)
telemetry = get_system_telemetry()
for metric_name, metric_value in telemetry.items():
tf.summary.scalar(metric_name, metric_value, step=current_step)
# Trigger Evaluation Checkpoints and Write logs
if epoch_idx % 1000 == 0:
self._evaluate_and_print_era(epoch_idx, current_step)
if epoch_idx % self.config.CHECKPOINT_INTERVAL == 0:
save_path = self.checkpoint_manager.save(checkpoint_number=epoch_idx)
print(f"πΎ Checkpoint safely saved for episode {epoch_idx} at: {save_path}")
self.summary_writer.flush()
|