Skip to main content

thrust_rl/train/ppo/
stats.rs

1//! Training statistics for PPO
2//!
3//! This module defines structures for tracking and aggregating
4//! training metrics during PPO training.
5
6use std::ops::AddAssign;
7
8/// Training statistics for a PPO update
9///
10/// Tracks various metrics from a single training step including
11/// losses, KL divergence, and other diagnostic information.
12#[derive(Debug, Clone, Default)]
13pub struct TrainingStats {
14    /// Policy loss
15    pub policy_loss: f64,
16
17    /// Value function loss
18    pub value_loss: f64,
19
20    /// Entropy bonus
21    pub entropy: f64,
22
23    /// Total loss (weighted sum of policy, value, entropy, and aux losses)
24    pub total_loss: f64,
25
26    /// Auxiliary loss (caller-supplied --- e.g. representation regularizer)
27    pub aux_loss: f64,
28
29    /// Fraction of clipped policy updates
30    pub clip_fraction: f64,
31
32    /// Approximate KL divergence between old and new policies
33    pub approx_kl: f64,
34
35    /// Explained variance of value function predictions
36    pub explained_var: f64,
37
38    /// Number of gradient updates performed
39    pub num_updates: usize,
40}
41
42impl TrainingStats {
43    /// Create zero-initialized statistics
44    pub fn zeros() -> Self {
45        Self::default()
46    }
47
48    /// Create statistics from scalar values.
49    ///
50    /// `aux_loss` defaults to 0.0; set with [`Self::with_aux_loss`] when an
51    /// auxiliary loss term is present.
52    pub fn new(
53        policy_loss: f64,
54        value_loss: f64,
55        entropy: f64,
56        total_loss: f64,
57        clip_fraction: f64,
58        approx_kl: f64,
59        explained_var: f64,
60    ) -> Self {
61        Self {
62            policy_loss,
63            value_loss,
64            entropy,
65            total_loss,
66            aux_loss: 0.0,
67            clip_fraction,
68            approx_kl,
69            explained_var,
70            num_updates: 1,
71        }
72    }
73
74    /// Builder-style setter for the auxiliary loss field.
75    pub fn with_aux_loss(mut self, aux_loss: f64) -> Self {
76        self.aux_loss = aux_loss;
77        self
78    }
79
80    /// Add another statistics instance to this one
81    pub fn add(&mut self, other: &TrainingStats) {
82        self.policy_loss += other.policy_loss;
83        self.value_loss += other.value_loss;
84        self.entropy += other.entropy;
85        self.total_loss += other.total_loss;
86        self.aux_loss += other.aux_loss;
87        self.clip_fraction += other.clip_fraction;
88        self.approx_kl += other.approx_kl;
89        self.explained_var += other.explained_var;
90        self.num_updates += other.num_updates;
91    }
92
93    /// Compute average statistics across multiple updates
94    pub fn average(&self) -> Self {
95        let scale = self.num_updates as f64;
96        if scale == 0.0 {
97            return Self::zeros();
98        }
99
100        Self {
101            policy_loss: self.policy_loss / scale,
102            value_loss: self.value_loss / scale,
103            entropy: self.entropy / scale,
104            total_loss: self.total_loss / scale,
105            aux_loss: self.aux_loss / scale,
106            clip_fraction: self.clip_fraction / scale,
107            approx_kl: self.approx_kl / scale,
108            explained_var: self.explained_var / scale,
109            num_updates: 1,
110        }
111    }
112}
113
114impl AddAssign<&TrainingStats> for TrainingStats {
115    fn add_assign(&mut self, other: &TrainingStats) {
116        self.add(other);
117    }
118}
119
120/// Aggregated training statistics across multiple training steps
121///
122/// Provides summary statistics and trends for monitoring training progress.
123#[derive(Debug, Clone)]
124pub struct AggregatedStats {
125    /// Statistics from the current training step
126    pub current: TrainingStats,
127
128    /// Running average of recent statistics
129    pub running_avg: TrainingStats,
130
131    /// Best policy loss achieved so far
132    pub best_policy_loss: f64,
133
134    /// Best value loss achieved so far
135    pub best_value_loss: f64,
136
137    /// Total number of training steps
138    pub total_steps: usize,
139
140    /// Learning rate (for logging)
141    pub learning_rate: f64,
142}
143
144impl AggregatedStats {
145    /// Create new aggregated statistics
146    pub fn new(learning_rate: f64) -> Self {
147        Self {
148            current: TrainingStats::zeros(),
149            running_avg: TrainingStats::zeros(),
150            best_policy_loss: f64::INFINITY,
151            best_value_loss: f64::INFINITY,
152            total_steps: 0,
153            learning_rate,
154        }
155    }
156
157    /// Update with new training statistics
158    pub fn update(&mut self, stats: TrainingStats) {
159        self.current = stats.clone();
160        self.total_steps += 1;
161
162        // Update running average (exponential moving average with alpha=0.1)
163        let alpha = 0.1;
164        self.running_avg.policy_loss =
165            alpha * stats.policy_loss + (1.0 - alpha) * self.running_avg.policy_loss;
166        self.running_avg.value_loss =
167            alpha * stats.value_loss + (1.0 - alpha) * self.running_avg.value_loss;
168        self.running_avg.entropy = alpha * stats.entropy + (1.0 - alpha) * self.running_avg.entropy;
169        self.running_avg.total_loss =
170            alpha * stats.total_loss + (1.0 - alpha) * self.running_avg.total_loss;
171        self.running_avg.clip_fraction =
172            alpha * stats.clip_fraction + (1.0 - alpha) * self.running_avg.clip_fraction;
173        self.running_avg.approx_kl =
174            alpha * stats.approx_kl + (1.0 - alpha) * self.running_avg.approx_kl;
175        self.running_avg.explained_var =
176            alpha * stats.explained_var + (1.0 - alpha) * self.running_avg.explained_var;
177
178        // Update best losses
179        if stats.policy_loss < self.best_policy_loss {
180            self.best_policy_loss = stats.policy_loss;
181        }
182        if stats.value_loss < self.best_value_loss {
183            self.best_value_loss = stats.value_loss;
184        }
185    }
186}