feat(ml): WAVE 29 DQN Codebase Cleanup & Refactoring Campaign
BREAKING CHANGES: - Removed orphaned dqn.rs monolithic trainer (4,975 lines) - Removed orphaned dqn_ensemble.rs module (816 lines) - Removed orphaned tft.rs and tft_complete_int8_integration_test.rs - TFT trainer split into modular directory structure DQN Module Refactoring: - Split trainers/dqn.rs into modular structure (config.rs, statistics.rs, trainer.rs) - Fixed hyperopt 39D search space (continuous params only) - Boolean flags (use_dueling, use_double_dqn, use_per, use_noisy_nets) are now FIXED architectural decisions - use_distributional defaults to false (Candle BUG #36 - scatter_add gradient issues) Clean Module Structure: - ml/src/trainers/dqn/ directory with proper mod.rs exports - ml/src/trainers/tft/ directory with config.rs, types.rs, model.rs, trainer.rs, tests.rs - All P0 features validated: TD-error clamping, batch diversity, LR scheduler, priority staleness Documentation: - Added comprehensive docs in docs/codebase-cleanup/ - ADR-001 for DQN refactoring decisions - Rainbow DQN component matrix and quick reference guides Build Status: Compiles with zero errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
286
scripts/add_staleness_tracking.py
Normal file
286
scripts/add_staleness_tracking.py
Normal file
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Add priority staleness tracking to PER buffer"""
|
||||
|
||||
import re
|
||||
|
||||
def add_staleness_tracking(filepath):
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. Add field to struct
|
||||
struct_pattern = r'(pub struct PrioritizedReplayBuffer \{[^}]+priorities: Arc<Mutex<SegmentTree>>,)'
|
||||
struct_replacement = r'\1\n priority_update_steps: Arc<RwLock<Vec<u64>>>,'
|
||||
content = re.sub(struct_pattern, struct_replacement, content)
|
||||
|
||||
# 2. Initialize in new()
|
||||
new_pattern = r'(priorities: Arc::new\(Mutex::new\(SegmentTree::new\(config\.capacity\)\)\),)'
|
||||
new_replacement = r'\1\n priority_update_steps: Arc::new(RwLock::new(vec![0; config.capacity])),'
|
||||
content = re.sub(new_pattern, new_replacement, content)
|
||||
|
||||
# 3. Update push() - add current_step variable
|
||||
push_pattern1 = r'(let index = self\.position\.fetch_add\(1, Ordering::AcqRel\) % self\.config\.capacity;)'
|
||||
push_replacement1 = r'\1\n let current_step = self.training_step.load(Ordering::Acquire) as u64;'
|
||||
content = re.sub(push_pattern1, push_replacement1, content)
|
||||
|
||||
# 3b. Update push() - initialize priority update step
|
||||
push_pattern2 = r'(\{[\s\n]+let mut tree = self\.priorities\.lock\(\);[\s\n]+tree\.update\(index, priority\)\?;[\s\n]+\})'
|
||||
push_replacement2 = r'''\1
|
||||
|
||||
// Initialize priority update step
|
||||
{
|
||||
let mut update_steps = self.priority_update_steps.write();
|
||||
if index < update_steps.len() {
|
||||
update_steps[index] = current_step;
|
||||
}
|
||||
}'''
|
||||
content = re.sub(push_pattern2, push_replacement2, content)
|
||||
|
||||
# 4. Add staleness decay to sample() at the beginning
|
||||
sample_pattern = r'(pub fn sample\(\s+&self,\s+batch_size: usize,\s+\) -> Result<\(Vec<Experience>, Vec<f32>, Vec<usize>\), MLError> \{)'
|
||||
sample_replacement = r'''\1
|
||||
// Apply staleness decay before sampling
|
||||
// Default: max_age = 10000 steps, decay_factor = 0.9
|
||||
self.apply_staleness_decay(
|
||||
self.training_step.load(Ordering::Acquire) as u64,
|
||||
10000,
|
||||
0.9,
|
||||
)?;
|
||||
'''
|
||||
content = re.sub(sample_pattern, sample_replacement, content)
|
||||
|
||||
# 5. Update update_priorities() - add current_step
|
||||
update_pattern1 = r'(let mut max_priority = f32::from_bits\(self\.max_priority\.load\(Ordering::Acquire\) as u32\);)'
|
||||
update_replacement1 = r'\1\n let current_step = self.training_step.load(Ordering::Acquire) as u64;'
|
||||
content = re.sub(update_pattern1, update_replacement1, content)
|
||||
|
||||
# 5b. Update update_priorities() - track update steps
|
||||
update_pattern2 = r'(self\.max_priority[\s\n]+\.store\(max_priority\.to_bits\(\) as u64, Ordering::Release\);)'
|
||||
update_replacement2 = r'''\1
|
||||
|
||||
// Update priority update steps
|
||||
{
|
||||
let mut update_steps = self.priority_update_steps.write();
|
||||
for &idx in indices {
|
||||
if idx < update_steps.len() {
|
||||
update_steps[idx] = current_step;
|
||||
}
|
||||
}
|
||||
}'''
|
||||
content = re.sub(update_pattern2, update_replacement2, content)
|
||||
|
||||
# 6. Add apply_staleness_decay method before clear()
|
||||
clear_pattern = r'( /// Reset buffer \(clear all experiences\)[\s\n]+ pub fn clear\(&self\) \{)'
|
||||
staleness_method = r''' /// Apply staleness decay to old priorities
|
||||
pub fn apply_staleness_decay(
|
||||
&self,
|
||||
current_step: u64,
|
||||
max_age: u64,
|
||||
decay_factor: f64,
|
||||
) -> Result<(), MLError> {
|
||||
let size = self.size.load(Ordering::Acquire);
|
||||
if size == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let update_steps = self.priority_update_steps.read();
|
||||
let mut tree = self.priorities.lock();
|
||||
|
||||
for idx in 0..size {
|
||||
if idx >= update_steps.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let last_update = update_steps[idx];
|
||||
let age = current_step.saturating_sub(last_update);
|
||||
|
||||
if age > max_age {
|
||||
let current_priority = tree.get_priority(idx);
|
||||
if current_priority > 0.0 {
|
||||
let decay = decay_factor.powi((age / max_age) as i32) as f32;
|
||||
let new_priority = (current_priority * decay).max(self.config.min_priority);
|
||||
tree.update(idx, new_priority)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
\1'''
|
||||
content = re.sub(clear_pattern, staleness_method, content)
|
||||
|
||||
# 7. Update clear() to reset update steps
|
||||
clear_update_pattern = r'(\{[\s\n]+let mut tree = self\.priorities\.lock\(\);[\s\n]+for i in 0\.\.self\.config\.capacity \{[\s\n]+let _ = tree\.update\(i, 0\.0\);[\s\n]+\}[\s\n]+\})'
|
||||
clear_update_replacement = r'''\1
|
||||
|
||||
{
|
||||
let mut update_steps = self.priority_update_steps.write();
|
||||
for step in update_steps.iter_mut() {
|
||||
*step = 0;
|
||||
}
|
||||
}'''
|
||||
content = re.sub(clear_update_pattern, clear_update_replacement, content)
|
||||
|
||||
# 8. Add tests before the closing brace of mod tests
|
||||
test_pattern = r'( \}\n\}\n)$'
|
||||
tests_addition = r'''
|
||||
#[test]
|
||||
fn test_priority_staleness_decay() {
|
||||
let config = PrioritizedReplayConfig {
|
||||
capacity: 100,
|
||||
initial_priority: 1.0,
|
||||
min_priority: 1e-6,
|
||||
..Default::default()
|
||||
};
|
||||
let buffer = PrioritizedReplayBuffer::new(config)
|
||||
.expect("Failed to create prioritized replay buffer in test");
|
||||
|
||||
// Push 10 experiences at step 0
|
||||
buffer.set_training_step(0);
|
||||
for _ in 0..10 {
|
||||
buffer
|
||||
.push(create_test_experience())
|
||||
.expect("Failed to push experience in test");
|
||||
}
|
||||
|
||||
// Get initial priorities
|
||||
let tree = buffer.priorities.lock();
|
||||
let initial_priorities: Vec<f32> = (0..10).map(|i| tree.get_priority(i)).collect();
|
||||
drop(tree);
|
||||
|
||||
// Simulate 15000 steps passing without priority updates
|
||||
buffer.set_training_step(15000);
|
||||
|
||||
// Apply staleness decay (max_age=10000, decay_factor=0.9)
|
||||
buffer
|
||||
.apply_staleness_decay(15000, 10000, 0.9)
|
||||
.expect("Failed to apply staleness decay in test");
|
||||
|
||||
// Check that priorities have decayed
|
||||
let tree = buffer.priorities.lock();
|
||||
for i in 0..10 {
|
||||
let current_priority = tree.get_priority(i);
|
||||
let initial_priority = initial_priorities[i];
|
||||
|
||||
// Age = 15000 steps, max_age = 10000
|
||||
// Decay should be: decay_factor^(age/max_age) = 0.9^1
|
||||
let expected_decay = 0.9_f64.powi((15000 / 10000) as i32) as f32;
|
||||
let expected_priority = (initial_priority * expected_decay).max(1e-6);
|
||||
|
||||
// Allow small floating point tolerance
|
||||
assert!(
|
||||
(current_priority - expected_priority).abs() < 0.01,
|
||||
"Priority at index {} should have decayed from {} to ~{}, got {}",
|
||||
i,
|
||||
initial_priority,
|
||||
expected_priority,
|
||||
current_priority
|
||||
);
|
||||
|
||||
// Verify priority was reduced
|
||||
assert!(
|
||||
current_priority < initial_priority,
|
||||
"Priority at index {} should be reduced after staleness decay",
|
||||
i
|
||||
);
|
||||
}
|
||||
drop(tree);
|
||||
|
||||
// Test that recently updated priorities don't decay
|
||||
buffer.set_training_step(16000);
|
||||
|
||||
// Update some priorities
|
||||
let indices = vec![0, 1, 2];
|
||||
let new_priorities = vec![5.0, 6.0, 7.0];
|
||||
buffer
|
||||
.update_priorities(&indices, &new_priorities)
|
||||
.expect("Failed to update priorities in test");
|
||||
|
||||
// Move forward only 5000 steps (below max_age threshold)
|
||||
buffer.set_training_step(21000);
|
||||
buffer
|
||||
.apply_staleness_decay(21000, 10000, 0.9)
|
||||
.expect("Failed to apply staleness decay in test");
|
||||
|
||||
// Recently updated priorities should not decay significantly
|
||||
let tree = buffer.priorities.lock();
|
||||
for &idx in &indices {
|
||||
let priority = tree.get_priority(idx);
|
||||
// Age = 5000, which is < max_age (10000), so no decay should occur
|
||||
assert!(
|
||||
priority > 4.5,
|
||||
"Recently updated priority at index {} should not decay significantly, got {}",
|
||||
idx,
|
||||
priority
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_staleness_tracking_on_push() {
|
||||
let config = PrioritizedReplayConfig {
|
||||
capacity: 100,
|
||||
..Default::default()
|
||||
};
|
||||
let buffer = PrioritizedReplayBuffer::new(config)
|
||||
.expect("Failed to create prioritized replay buffer in test");
|
||||
|
||||
// Push experiences at different training steps
|
||||
buffer.set_training_step(100);
|
||||
buffer
|
||||
.push(create_test_experience())
|
||||
.expect("Failed to push experience in test");
|
||||
|
||||
buffer.set_training_step(200);
|
||||
buffer
|
||||
.push(create_test_experience())
|
||||
.expect("Failed to push experience in test");
|
||||
|
||||
// Verify update steps were recorded
|
||||
let update_steps = buffer.priority_update_steps.read();
|
||||
assert_eq!(update_steps[0], 100, "First experience should have update step 100");
|
||||
assert_eq!(update_steps[1], 200, "Second experience should have update step 200");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_staleness_tracking_on_update() {
|
||||
let config = PrioritizedReplayConfig {
|
||||
capacity: 100,
|
||||
..Default::default()
|
||||
};
|
||||
let buffer = PrioritizedReplayBuffer::new(config)
|
||||
.expect("Failed to create prioritized replay buffer in test");
|
||||
|
||||
// Push experiences
|
||||
buffer.set_training_step(100);
|
||||
for _ in 0..10 {
|
||||
buffer
|
||||
.push(create_test_experience())
|
||||
.expect("Failed to push experience in test");
|
||||
}
|
||||
|
||||
// Update priorities at a later step
|
||||
buffer.set_training_step(500);
|
||||
let indices = vec![0, 1, 2];
|
||||
let priorities = vec![2.0, 3.0, 4.0];
|
||||
buffer
|
||||
.update_priorities(&indices, &priorities)
|
||||
.expect("Failed to update priorities in test");
|
||||
|
||||
// Verify update steps were updated
|
||||
let update_steps = buffer.priority_update_steps.read();
|
||||
assert_eq!(update_steps[0], 500, "Updated experience should have new update step");
|
||||
assert_eq!(update_steps[1], 500, "Updated experience should have new update step");
|
||||
assert_eq!(update_steps[2], 500, "Updated experience should have new update step");
|
||||
assert_eq!(update_steps[3], 100, "Non-updated experience should retain old update step");
|
||||
}
|
||||
\1'''
|
||||
content = re.sub(test_pattern, tests_addition, content)
|
||||
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"Successfully updated {filepath}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
add_staleness_tracking("/home/jgrusewski/Work/foxhunt/ml/src/dqn/prioritized_replay.rs")
|
||||
50
scripts/analyze_dead_code.sh
Executable file
50
scripts/analyze_dead_code.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# Dead Code Analysis Script for Foxhunt Workspace
|
||||
|
||||
echo "=== FOXHUNT DEAD CODE ANALYSIS ==="
|
||||
echo ""
|
||||
|
||||
# 1. Find public items that might be unused
|
||||
echo "1. PUBLIC API SURFACE ANALYSIS"
|
||||
echo "-------------------------------"
|
||||
find . -name "*.rs" -type f -not -path "./target/*" -exec grep -l "^pub fn\|^pub struct\|^pub enum\|^pub trait" {} \; | wc -l
|
||||
echo " files with public items found"
|
||||
echo ""
|
||||
|
||||
# 2. Find large commented code blocks
|
||||
echo "2. LARGE COMMENTED CODE BLOCKS (>10 lines)"
|
||||
echo "-------------------------------------------"
|
||||
find . -name "*.rs" -type f -not -path "./target/*" -print0 | while IFS= read -r -d '' file; do
|
||||
awk '/^[[:space:]]*\/\// {count++} !/^[[:space:]]*\/\// {if (count >= 10) print FILENAME":"NR-count"-"NR-1":"count" lines"; count=0} END {if (count >= 10) print FILENAME":"NR-count"-"NR":"count" lines"}' "$file"
|
||||
done | head -20
|
||||
echo ""
|
||||
|
||||
# 3. Find empty or nearly empty modules
|
||||
echo "3. EMPTY OR MINIMAL MODULES"
|
||||
echo "----------------------------"
|
||||
find . -name "*.rs" -type f -not -path "./target/*" -exec sh -c 'lines=$(wc -l < "$1"); if [ "$lines" -lt 10 ]; then echo "$1: $lines lines"; fi' _ {} \; | head -20
|
||||
echo ""
|
||||
|
||||
# 4. Find test modules with no tests
|
||||
echo "4. TEST MODULES WITHOUT TESTS"
|
||||
echo "------------------------------"
|
||||
find . -name "*.rs" -type f -not -path "./target/*" -exec grep -l "#\[cfg(test)\]" {} \; | while read file; do
|
||||
if ! grep -q "#\[test\]" "$file"; then
|
||||
echo "$file - has #[cfg(test)] but no #[test] functions"
|
||||
fi
|
||||
done | head -10
|
||||
echo ""
|
||||
|
||||
# 5. Find feature-gated code
|
||||
echo "5. FEATURE-GATED CODE"
|
||||
echo "----------------------"
|
||||
find . -name "*.rs" -type f -not -path "./target/*" -exec grep -l "#\[cfg(feature" {} \; | head -10
|
||||
echo ""
|
||||
|
||||
# 6. Analyze Cargo.toml dependencies
|
||||
echo "6. DEPENDENCY ANALYSIS PER CRATE"
|
||||
echo "---------------------------------"
|
||||
find . -name "Cargo.toml" -not -path "./target/*" | head -5
|
||||
echo ""
|
||||
|
||||
echo "=== ANALYSIS COMPLETE ==="
|
||||
154
scripts/check_unused_deps.py
Executable file
154
scripts/check_unused_deps.py
Executable file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Analyze Rust workspace for unused dependencies.
|
||||
Checks if dependencies in Cargo.toml are actually used in source code.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
|
||||
def parse_cargo_toml(path):
|
||||
"""Parse dependencies from Cargo.toml"""
|
||||
deps = []
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Extract dependencies section
|
||||
in_deps = False
|
||||
in_dev_deps = False
|
||||
for line in content.split('\n'):
|
||||
if line.strip().startswith('[dependencies]'):
|
||||
in_deps = True
|
||||
in_dev_deps = False
|
||||
continue
|
||||
elif line.strip().startswith('[dev-dependencies]'):
|
||||
in_deps = False
|
||||
in_dev_deps = True
|
||||
continue
|
||||
elif line.strip().startswith('['):
|
||||
in_deps = False
|
||||
in_dev_deps = False
|
||||
continue
|
||||
|
||||
if in_deps and line.strip() and not line.strip().startswith('#'):
|
||||
# Extract dependency name
|
||||
match = re.match(r'^([a-zA-Z0-9_-]+)\s*=', line)
|
||||
if match:
|
||||
dep_name = match.group(1).replace('-', '_')
|
||||
deps.append((dep_name, 'normal'))
|
||||
elif in_dev_deps and line.strip() and not line.strip().startswith('#'):
|
||||
match = re.match(r'^([a-zA-Z0-9_-]+)\s*=', line)
|
||||
if match:
|
||||
dep_name = match.group(1).replace('-', '_')
|
||||
deps.append((dep_name, 'dev'))
|
||||
except Exception as e:
|
||||
print(f"Error parsing {path}: {e}")
|
||||
|
||||
return deps
|
||||
|
||||
def check_dep_usage(crate_path, dep_name):
|
||||
"""Check if dependency is used in Rust source files"""
|
||||
src_path = os.path.join(crate_path, 'src')
|
||||
tests_path = os.path.join(crate_path, 'tests')
|
||||
benches_path = os.path.join(crate_path, 'benches')
|
||||
examples_path = os.path.join(crate_path, 'examples')
|
||||
|
||||
patterns = [
|
||||
f'use {dep_name}',
|
||||
f'use {dep_name}::',
|
||||
f'extern crate {dep_name}',
|
||||
f'{dep_name}::',
|
||||
]
|
||||
|
||||
for base_path in [src_path, tests_path, benches_path, examples_path]:
|
||||
if not os.path.exists(base_path):
|
||||
continue
|
||||
|
||||
for root, dirs, files in os.walk(base_path):
|
||||
# Skip target directory
|
||||
if 'target' in dirs:
|
||||
dirs.remove('target')
|
||||
|
||||
for file in files:
|
||||
if not file.endswith('.rs'):
|
||||
continue
|
||||
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
for pattern in patterns:
|
||||
if pattern in content:
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
def analyze_workspace():
|
||||
"""Analyze all crates in workspace"""
|
||||
workspace_root = '/home/jgrusewski/Work/foxhunt'
|
||||
|
||||
# Find all Cargo.toml files (excluding target directories)
|
||||
cargo_tomls = []
|
||||
for root, dirs, files in os.walk(workspace_root):
|
||||
# Skip target and vendor directories
|
||||
if 'target' in dirs:
|
||||
dirs.remove('target')
|
||||
if '.git' in dirs:
|
||||
dirs.remove('.git')
|
||||
|
||||
if 'Cargo.toml' in files:
|
||||
cargo_path = os.path.join(root, 'Cargo.toml')
|
||||
# Skip if it's a workspace-only Cargo.toml
|
||||
with open(cargo_path, 'r') as f:
|
||||
content = f.read()
|
||||
if '[package]' in content:
|
||||
cargo_tomls.append(cargo_path)
|
||||
|
||||
results = {}
|
||||
|
||||
for cargo_toml in cargo_tomls[:15]: # Limit to first 15 for performance
|
||||
crate_path = os.path.dirname(cargo_toml)
|
||||
crate_name = os.path.basename(crate_path)
|
||||
|
||||
print(f"\nAnalyzing {crate_name}...")
|
||||
|
||||
deps = parse_cargo_toml(cargo_toml)
|
||||
unused = []
|
||||
|
||||
for dep_name, dep_type in deps:
|
||||
# Skip workspace crates
|
||||
if dep_name in ['trading_engine', 'risk', 'data', 'backtesting',
|
||||
'common', 'storage', 'ml', 'config', 'database',
|
||||
'market_data', 'tli', 'risk_data', 'trading_data',
|
||||
'adaptive_strategy', 'model_loader']:
|
||||
continue
|
||||
|
||||
# Check if dependency is used
|
||||
if not check_dep_usage(crate_path, dep_name):
|
||||
unused.append((dep_name, dep_type))
|
||||
|
||||
if unused:
|
||||
results[crate_name] = unused
|
||||
|
||||
return results
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=== UNUSED DEPENDENCY ANALYSIS ===\n")
|
||||
results = analyze_workspace()
|
||||
|
||||
if not results:
|
||||
print("\n✅ No obvious unused dependencies found!")
|
||||
else:
|
||||
print("\n⚠️ POTENTIALLY UNUSED DEPENDENCIES:\n")
|
||||
for crate, unused in results.items():
|
||||
print(f"\n{crate}:")
|
||||
for dep, dep_type in unused:
|
||||
print(f" - {dep} ({dep_type})")
|
||||
|
||||
print("\n=== ANALYSIS COMPLETE ===")
|
||||
35
scripts/run_wave26_p1_12_tests.sh
Executable file
35
scripts/run_wave26_p1_12_tests.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# WAVE 26 P1.12: Polyak Soft Updates Test Runner
|
||||
# Run this script to execute the comprehensive TDD test suite
|
||||
|
||||
set -e
|
||||
|
||||
echo "============================================="
|
||||
echo "WAVE 26 P1.12: Polyak Soft Updates Tests"
|
||||
echo "============================================="
|
||||
echo ""
|
||||
|
||||
echo "Running target_update module unit tests..."
|
||||
cargo test --package ml --lib dqn::target_update::tests --no-fail-fast
|
||||
|
||||
echo ""
|
||||
echo "Running comprehensive TDD tests..."
|
||||
cargo test --package ml --lib dqn::tests::target_update_comprehensive_tests --no-fail-fast -- --nocapture
|
||||
|
||||
echo ""
|
||||
echo "============================================="
|
||||
echo "✅ All tests passed!"
|
||||
echo "============================================="
|
||||
echo ""
|
||||
echo "Test Coverage:"
|
||||
echo " - 13 comprehensive TDD tests"
|
||||
echo " - Formula verification"
|
||||
echo " - Boundary conditions"
|
||||
echo " - Convergence rates"
|
||||
echo " - Network divergence"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Run hyperopt with 30D search space (includes tau)"
|
||||
echo " 2. Monitor divergence during training"
|
||||
echo " 3. Compare tau values: 0.0001, 0.001, 0.005, 0.01"
|
||||
echo ""
|
||||
117
scripts/test_dropout_scheduler.sh
Executable file
117
scripts/test_dropout_scheduler.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
# Standalone test for DropoutScheduler logic verification
|
||||
|
||||
cat > /tmp/test_dropout.rs << 'EOF'
|
||||
/// Adaptive dropout scheduler that decreases dropout rate over training
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DropoutScheduler {
|
||||
initial_rate: f64,
|
||||
final_rate: f64,
|
||||
decay_steps: usize,
|
||||
current_step: usize,
|
||||
}
|
||||
|
||||
impl DropoutScheduler {
|
||||
pub fn new(initial_rate: f64, final_rate: f64, decay_steps: usize) -> Self {
|
||||
Self {
|
||||
initial_rate,
|
||||
final_rate,
|
||||
decay_steps,
|
||||
current_step: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_rate(&self) -> f64 {
|
||||
if self.decay_steps == 0 {
|
||||
return self.final_rate;
|
||||
}
|
||||
|
||||
let progress = (self.current_step as f64 / self.decay_steps as f64).min(1.0);
|
||||
self.initial_rate * (1.0 - progress) + self.final_rate * progress
|
||||
}
|
||||
|
||||
pub fn step(&mut self, steps: usize) {
|
||||
self.current_step = self.current_step.saturating_add(steps);
|
||||
}
|
||||
|
||||
pub fn current_step(&self) -> usize {
|
||||
self.current_step
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("Testing DropoutScheduler...\n");
|
||||
|
||||
// Test 1: Basic creation
|
||||
let scheduler = DropoutScheduler::new(0.5, 0.1, 10000);
|
||||
println!("✓ Test 1: Creation - initial rate: {}", scheduler.get_rate());
|
||||
assert!((scheduler.get_rate() - 0.5).abs() < 1e-6);
|
||||
|
||||
// Test 2: Linear decay
|
||||
let mut scheduler = DropoutScheduler::new(0.5, 0.1, 10000);
|
||||
|
||||
scheduler.step(2500); // 25%
|
||||
let rate_25 = scheduler.get_rate();
|
||||
println!("✓ Test 2: 25% progress - rate: {:.6} (expected: 0.4)", rate_25);
|
||||
assert!((rate_25 - 0.4).abs() < 1e-6);
|
||||
|
||||
scheduler.step(2500); // 50%
|
||||
let rate_50 = scheduler.get_rate();
|
||||
println!("✓ Test 3: 50% progress - rate: {:.6} (expected: 0.3)", rate_50);
|
||||
assert!((rate_50 - 0.3).abs() < 1e-6);
|
||||
|
||||
scheduler.step(2500); // 75%
|
||||
let rate_75 = scheduler.get_rate();
|
||||
println!("✓ Test 4: 75% progress - rate: {:.6} (expected: 0.2)", rate_75);
|
||||
assert!((rate_75 - 0.2).abs() < 1e-6);
|
||||
|
||||
scheduler.step(2500); // 100%
|
||||
println!("✓ Test 5: 100% progress - rate: {:.6} (expected: 0.1)", scheduler.get_rate());
|
||||
assert!((scheduler.get_rate() - 0.1).abs() < 1e-6);
|
||||
|
||||
// Test 3: Beyond decay steps
|
||||
scheduler.step(5000); // 150%
|
||||
println!("✓ Test 6: Beyond decay - rate stays at: {:.6}", scheduler.get_rate());
|
||||
assert!((scheduler.get_rate() - 0.1).abs() < 1e-6);
|
||||
|
||||
// Test 4: Step tracking
|
||||
let mut scheduler = DropoutScheduler::new(0.8, 0.2, 1000);
|
||||
assert_eq!(scheduler.current_step(), 0);
|
||||
scheduler.step(100);
|
||||
assert_eq!(scheduler.current_step(), 100);
|
||||
println!("✓ Test 7: Step tracking works correctly");
|
||||
|
||||
// Test 5: Zero decay steps edge case
|
||||
let scheduler = DropoutScheduler::new(0.5, 0.1, 0);
|
||||
println!("✓ Test 8: Zero decay steps - immediately at final: {:.6}", scheduler.get_rate());
|
||||
assert!((scheduler.get_rate() - 0.1).abs() < 1e-6);
|
||||
|
||||
// Test 6: Constant rate
|
||||
let mut scheduler = DropoutScheduler::new(0.3, 0.3, 1000);
|
||||
scheduler.step(500);
|
||||
println!("✓ Test 9: Constant rate - stays at: {:.6}", scheduler.get_rate());
|
||||
assert!((scheduler.get_rate() - 0.3).abs() < 1e-6);
|
||||
|
||||
// Test 7: Realistic schedule
|
||||
let mut scheduler = DropoutScheduler::new(0.5, 0.05, 100_000);
|
||||
|
||||
scheduler.step(10_000); // Early training
|
||||
let early = scheduler.get_rate();
|
||||
println!("✓ Test 10: Realistic early training (10k/100k) - rate: {:.6}", early);
|
||||
assert!(early > 0.4);
|
||||
|
||||
scheduler.step(40_000); // Mid training (total 50k)
|
||||
let mid = scheduler.get_rate();
|
||||
println!("✓ Test 11: Realistic mid training (50k/100k) - rate: {:.6}", mid);
|
||||
assert!((mid - 0.275).abs() < 0.01);
|
||||
|
||||
scheduler.step(50_000); // Late training (total 100k)
|
||||
let late = scheduler.get_rate();
|
||||
println!("✓ Test 12: Realistic late training (100k/100k) - rate: {:.6}", late);
|
||||
assert!((late - 0.05).abs() < 1e-6);
|
||||
|
||||
println!("\n✅ All tests passed!");
|
||||
}
|
||||
EOF
|
||||
|
||||
rustc /tmp/test_dropout.rs -o /tmp/test_dropout && /tmp/test_dropout
|
||||
Reference in New Issue
Block a user