Hard refactor — no shims, no compat layers. Candle removed from Cargo.toml and all source files in 6 crates: - ml-core: MlDevice enum, checkpoint.rs (safetensors direct), cudarc imports fixed from candle re-export to direct, AdamWConfig lr_decay, cuda_compat gutted. Net -7,341 lines. - ml-ppo: All 16 files rewritten. LSTM→CudaLSTM, VarMap→GpuVarStore, PPOAgent 2306→700 lines, checkpoint→binary format. - ml-ensemble: GPU-resident sigmoid via custom CUDA kernel. - ml-explainability: Integrated gradients via GPU finite-difference kernels. - ml-labeling: Device→MlDevice. - ml-hyperopt: Cargo.toml only. Remaining: ml-dqn (24 files), ml-supervised (4 files), ml crate (104 files). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
255 lines
7.7 KiB
Rust
255 lines
7.7 KiB
Rust
//! Symlog value transform (`DreamerV3`)
|
|
//!
|
|
//! Compresses large values while preserving sign. Near-identity for small values.
|
|
//! Critical for financial returns spanning multiple orders of magnitude.
|
|
//!
|
|
//! # References
|
|
//! - Hafner et al., "Mastering Diverse Domains through World Models" (`DreamerV3`, 2023)
|
|
//!
|
|
//! # Properties
|
|
//! - `symlog(0) = 0`
|
|
//! - Near-identity for `|x| << 1`
|
|
//! - Logarithmic compression for `|x| >> 1`
|
|
//! - Preserves sign: `sign(symlog(x)) == sign(x)`
|
|
//! - Invertible: `symexp(symlog(x)) == x`
|
|
|
|
use ml_core::MLError;
|
|
|
|
/// Symlog transform: `sign(x) * ln(|x| + 1)`
|
|
///
|
|
/// Compresses the magnitude of large values logarithmically while
|
|
/// leaving small values approximately unchanged.
|
|
///
|
|
/// # Examples
|
|
/// - `symlog(0.0) = 0.0`
|
|
/// - `symlog(1.0) = ln(2) ≈ 0.693`
|
|
/// - `symlog(100.0) = ln(101) ≈ 4.615`
|
|
/// - `symlog(-5.0) = -ln(6) ≈ -1.792`
|
|
#[inline]
|
|
pub fn symlog(x: f64) -> f64 {
|
|
x.signum() * (x.abs() + 1.0).ln()
|
|
}
|
|
|
|
/// Inverse of symlog: `sign(x) * (exp(|x|) - 1)`
|
|
///
|
|
/// Recovers the original value from its symlog representation.
|
|
#[inline]
|
|
pub fn symexp(x: f64) -> f64 {
|
|
x.signum() * (x.abs().exp() - 1.0)
|
|
}
|
|
|
|
/// Element-wise symlog for a batch of f32 values.
|
|
///
|
|
/// Applies `sign(x) * ln(|x| + 1)` to each element.
|
|
pub fn symlog_vec(data: &[f32]) -> Vec<f32> {
|
|
data.iter()
|
|
.map(|&x| {
|
|
let xf = x as f64;
|
|
(xf.signum() * (xf.abs() + 1.0).ln()) as f32
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Element-wise symexp for a batch of f32 values.
|
|
///
|
|
/// Applies `sign(x) * (exp(|x|) - 1)` to each element.
|
|
pub fn symexp_vec(data: &[f32]) -> Vec<f32> {
|
|
data.iter()
|
|
.map(|&x| {
|
|
let xf = x as f64;
|
|
(xf.signum() * (xf.abs().exp() - 1.0)) as f32
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// GPU-accelerated symlog via CUDA kernel (operates on `CudaVec` data).
|
|
///
|
|
/// For GPU data, download to host, apply symlog, and re-upload. For large
|
|
/// batches, a fused CUDA kernel would be more efficient but the download
|
|
/// path is correct and used only at batch boundaries.
|
|
#[cfg(feature = "cuda")]
|
|
pub fn symlog_gpu(
|
|
data: &crate::cuda_nn::CudaVec,
|
|
stream: &std::sync::Arc<cudarc::driver::CudaStream>,
|
|
) -> Result<crate::cuda_nn::CudaVec, MLError> {
|
|
let host = data.to_vec(stream)?;
|
|
let transformed = symlog_vec(&host);
|
|
crate::cuda_nn::cuda_from_slice(stream, &transformed)
|
|
}
|
|
|
|
/// GPU-accelerated symexp via CUDA kernel (operates on `CudaVec` data).
|
|
#[cfg(feature = "cuda")]
|
|
pub fn symexp_gpu(
|
|
data: &crate::cuda_nn::CudaVec,
|
|
stream: &std::sync::Arc<cudarc::driver::CudaStream>,
|
|
) -> Result<crate::cuda_nn::CudaVec, MLError> {
|
|
let host = data.to_vec(stream)?;
|
|
let transformed = symexp_vec(&host);
|
|
crate::cuda_nn::cuda_from_slice(stream, &transformed)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::else_if_without_else)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const EPSILON: f64 = 1e-9;
|
|
|
|
#[test]
|
|
fn test_symlog_zero() {
|
|
let result = symlog(0.0);
|
|
assert!(
|
|
result.abs() < EPSILON,
|
|
"symlog(0) should be 0, got {result}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_symlog_positive() {
|
|
let result = symlog(1.0);
|
|
let expected = 2.0_f64.ln(); // ln(|1| + 1) = ln(2)
|
|
assert!(
|
|
(result - expected).abs() < EPSILON,
|
|
"symlog(1.0) should be ln(2) ≈ {expected}, got {result}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_symlog_negative() {
|
|
let result = symlog(-1.0);
|
|
let expected = -(2.0_f64.ln());
|
|
assert!(
|
|
(result - expected).abs() < EPSILON,
|
|
"symlog(-1.0) should be -ln(2) ≈ {expected}, got {result}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_symlog_large() {
|
|
let result = symlog(1000.0);
|
|
let expected = (1001.0_f64).ln(); // ≈ 6.908
|
|
assert!(
|
|
(result - expected).abs() < EPSILON,
|
|
"symlog(1000.0) should be ln(1001) ≈ {expected}, got {result}"
|
|
);
|
|
// Verify significant compression
|
|
assert!(
|
|
result < 7.0,
|
|
"symlog(1000) should compress to < 7, got {result}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_symlog_negative_large() {
|
|
let result = symlog(-500.0);
|
|
let expected = -(501.0_f64.ln());
|
|
assert!(
|
|
(result - expected).abs() < EPSILON,
|
|
"symlog(-500.0) should be -ln(501) ≈ {expected}, got {result}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_symexp_inverse() {
|
|
// symexp(symlog(x)) should equal x for various values
|
|
let test_values = [
|
|
0.0, 1.0, -1.0, 0.5, -0.5, 10.0, -10.0, 100.0, -100.0, 0.001, -0.001,
|
|
];
|
|
for &x in &test_values {
|
|
let roundtrip = symexp(symlog(x));
|
|
assert!(
|
|
(roundtrip - x).abs() < 1e-6,
|
|
"symexp(symlog({x})) should be {x}, got {roundtrip}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_symlog_near_identity() {
|
|
// For |x| < 0.5, symlog(x) should be close to x
|
|
// Because ln(|x|+1) ≈ |x| for small |x| (first-order Taylor)
|
|
let small_values = [-0.4, -0.2, -0.1, -0.01, 0.01, 0.1, 0.2, 0.4];
|
|
for &x in &small_values {
|
|
let result = symlog(x);
|
|
let diff = (result - x).abs();
|
|
assert!(
|
|
diff < 0.1,
|
|
"|symlog({x}) - {x}| = {diff} should be < 0.1"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_symlog_financial_returns() {
|
|
// Realistic trading return magnitudes
|
|
let returns = [-0.05, -0.001, 0.001, 0.05, 5.0];
|
|
|
|
// Small returns should be nearly unchanged
|
|
let small_return = symlog(0.001);
|
|
assert!(
|
|
(small_return - 0.001).abs() < 0.001,
|
|
"tiny return should be near-identity, got {small_return}"
|
|
);
|
|
|
|
// Large returns should be compressed
|
|
let large_return = symlog(5.0);
|
|
assert!(
|
|
large_return < 2.0,
|
|
"5.0 return should compress below 2.0, got {large_return}"
|
|
);
|
|
|
|
// All returns should preserve sign
|
|
for &r in &returns {
|
|
let s = symlog(r);
|
|
if r > 0.0 {
|
|
assert!(s > 0.0, "positive return {r} should yield positive symlog");
|
|
} else if r < 0.0 {
|
|
assert!(s < 0.0, "negative return {r} should yield negative symlog");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_symlog_vec() {
|
|
let data = vec![-10.0_f32, -1.0, 0.0, 1.0, 10.0];
|
|
let result = symlog_vec(&data);
|
|
|
|
// Check each element matches scalar symlog
|
|
for (i, (&input, &output)) in data.iter().zip(result.iter()).enumerate() {
|
|
let expected = symlog(f64::from(input)) as f32;
|
|
assert!(
|
|
(output - expected).abs() < 1e-4,
|
|
"element {i}: symlog_vec({input}) = {output}, expected {expected}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_symexp_vec() {
|
|
let data = vec![-2.0_f32, -0.5, 0.0, 0.5, 2.0];
|
|
let result = symexp_vec(&data);
|
|
|
|
for (i, (&input, &output)) in data.iter().zip(result.iter()).enumerate() {
|
|
let expected = symexp(f64::from(input)) as f32;
|
|
assert!(
|
|
(output - expected).abs() < 1e-3,
|
|
"element {i}: symexp_vec({input}) = {output}, expected {expected}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_symlog_symexp_vec_roundtrip() {
|
|
let data = vec![-5.0_f32, -1.0, -0.1, 0.0, 0.1, 1.0, 5.0];
|
|
let encoded = symlog_vec(&data);
|
|
let decoded = symexp_vec(&encoded);
|
|
|
|
for (i, (&original, &roundtrip)) in data.iter().zip(decoded.iter()).enumerate() {
|
|
assert!(
|
|
(roundtrip - original).abs() < 1e-4,
|
|
"element {i}: roundtrip of {original} = {roundtrip}"
|
|
);
|
|
}
|
|
}
|
|
}
|