first commit

This commit is contained in:
2026-06-11 03:33:14 +08:00
commit 5f555bf342
599 changed files with 142347 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
"""
Data Augmentation Example
Demonstrates how to create a custom data augmentation function
following the architecture design pattern.
"""
import torch
from typing import Dict
from src.data_module.augmentation import register_augmentation
@register_augmentation("time_shift")
def time_shift(signal: torch.Tensor, max_shift: int = 10) -> torch.Tensor:
"""Randomly shift signal in time.
Args:
signal: Input signal tensor of shape (channels, time_steps)
max_shift: Maximum number of steps to shift
Returns:
Shifted signal tensor
"""
shift = torch.randint(-max_shift, max_shift + 1, (1,)).item()
return torch.roll(signal, shifts=shift, dims=-1)
@register_augmentation("amplitude_scale")
def amplitude_scale(
signal: torch.Tensor,
min_scale: float = 0.8,
max_scale: float = 1.2
) -> torch.Tensor:
"""Randomly scale signal amplitude.
Args:
signal: Input signal tensor of shape (channels, time_steps)
min_scale: Minimum scaling factor
max_scale: Maximum scaling factor
Returns:
Scaled signal tensor
"""
scale = torch.empty(1).uniform_(min_scale, max_scale).item()
return signal * scale
@register_augmentation("gaussian_noise")
def add_gaussian_noise(
signal: torch.Tensor,
mean: float = 0.0,
std: float = 0.1
) -> torch.Tensor:
"""Add Gaussian noise to signal.
Args:
signal: Input signal tensor of shape (channels, time_steps)
mean: Mean of Gaussian noise
std: Standard deviation of Gaussian noise
Returns:
Signal with added noise
"""
noise = torch.randn_like(signal) * std + mean
return signal + noise
# Example: Composed augmentation
@register_augmentation("composed")
def composed_augmentation(signal: torch.Tensor, cfg) -> torch.Tensor:
"""Apply multiple augmentations in sequence.
Args:
signal: Input signal tensor
cfg: Configuration object with augmentation parameters
Returns:
Augmented signal tensor
"""
# Apply each augmentation based on config
if cfg.augmentation.time_shift:
signal = time_shift(signal, cfg.augmentation.max_shift)
if cfg.augmentation.amplitude_scale:
signal = amplitude_scale(
signal,
cfg.augmentation.min_scale,
cfg.augmentation.max_scale
)
if cfg.augmentation.gaussian_noise:
signal = add_gaussian_noise(
signal,
cfg.augmentation.noise_mean,
cfg.augmentation.noise_std
)
return signal
# Usage in dataset class
class AugmentedDataset:
"""Example dataset with augmentation support."""
def __init__(self, cfg):
self.cfg = cfg
self.augmentation_fn = AugmentationFactory(cfg.augmentation.name)
def __getitem__(self, idx: int) -> Dict[str, torch.Tensor]:
# Load signal
signal = self.load_signal(idx)
# Apply augmentation (training mode only)
if self.training and self.augmentation_fn:
signal = self.augmentation_fn(signal, self.cfg)
return {"signal": signal, "label": self.labels[idx]}

View File

@@ -0,0 +1,131 @@
# Hydra Configuration Example
# This demonstrates the config structure for training pipeline
# Run with: python train.py --config-name=config_example
defaults:
- training: default
- dataset: brain_decoder
- model: transformer
- override hydra/launcher: submitit_local
# Project settings
project_name: brain_decoder
experiment_name: transformer_baseline
# Random seed
seed: 42
# Device settings
device: cuda
num_workers: 4
pin_memory: true
# Training configuration
training:
epochs: 100
batch_size: 32
learning_rate: 0.001
weight_decay: 0.0001
gradient_clip: 1.0
early_stopping:
patience: 10
min_delta: 0.001
# Optimizer settings
optimizer: adamw
optimizer_params:
betas: [0.9, 0.999]
eps: 1.0e-08
# Scheduler settings
scheduler: cosine
scheduler_params:
warmup_epochs: 10
min_lr: 1.0e-06
# Checkpoint settings
checkpoint:
save_every: 5
save_best: true
monitor: val_loss
mode: min
# Dataset configuration
dataset:
name: brain_decoder
task: movement_classification
target_size:
movement_classification: 5
reconstruction: [64, 64]
# Data paths
data_dir: ${dir.data_dir}/processed
train_split: train
val_split: val
test_split: test
# Data loading
num_channels: 64
sampling_rate: 1000
sequence_length: 1000
# Augmentation
augmentation:
name: composed
time_shift: true
amplitude_scale: true
gaussian_noise: true
max_shift: 10
min_scale: 0.8
max_scale: 1.2
noise_mean: 0.0
noise_std: 0.1
# Model configuration
model:
name: Transformer
hidden_dim: 256
num_heads: 8
num_layers: 6
dropout: 0.1
activation: gelu
# Architecture specific
encoder:
input_dim: 64
embedding_dim: 256
positional_encoding: true
decoder:
output_dim: ${dataset.target_size.${dataset.task}}
pooling: avg
# Logging configuration
logging:
logger: wandb
log_every: 10
log_grads: false
# TensorBoard
tensorboard: true
histogram: true
# W&B
wandb:
project: ${project_name}
entity: null
tags: ["baseline", "transformer"]
# Output directories
dir:
data_dir: ./data
output_dir: ./outputs
log_dir: ${dir.output_dir}/logs
checkpoint_dir: ${dir.output_dir}/checkpoints
figure_dir: ${dir.output_dir}/figures
table_dir: ${dir.output_dir}/tables
# Debug settings
debug: false
fast_dev_run: false

View File

@@ -0,0 +1,50 @@
"""
Example: Creating a Custom Dataset
This example shows how to add a new dataset following the project architecture.
"""
from torch.utils.data import Dataset
from typing import Dict
import torch
from src.data_module.dataset import register_dataset
@register_dataset("time_series")
class TimeSeriesDataset(Dataset):
"""
Time series dataset for sequence modeling.
Args:
sequences: List of time series sequences
seq_length: Fixed sequence length (pad or truncate if needed)
"""
def __init__(self, sequences: list, seq_length: int = 100):
self.sequences = sequences
self.seq_length = seq_length
def __len__(self) -> int:
return len(self.sequences)
def __getitem__(self, i: int) -> Dict[str, torch.Tensor]:
sequence = self.sequences[i]
# Pad or truncate to fixed length
if len(sequence) < self.seq_length:
padding = torch.zeros(self.seq_length - len(sequence))
sequence = torch.cat([sequence, padding])
else:
sequence = sequence[:self.seq_length]
return {
"input": sequence,
"label": sequence, # For autoencoder, etc.
"length": torch.tensor(min(len(self.sequences[i]), self.seq_length))
}
# Usage in training:
# from src.data_module.dataset import DatasetFactory
# dataset = DatasetFactory("time_series")(sequences=training_data, seq_length=128)
# dataloader = DataLoader(dataset, batch_size=32, shuffle=True)

View File

@@ -0,0 +1,217 @@
"""
Example: Creating a Custom Model
This example shows how to add a new model following the project architecture.
IMPORTANT: Models use a config-driven pattern where __init__ only accepts cfg.
Key Requirements:
- Use @register_model('ModelName') decorator
- __init__ accepts ONLY cfg parameter
- All hyperparameters come from cfg (cfg.model.*, cfg.dataset.*, etc.)
- forward() returns dict: {"loss": loss, "labels": labels, "logits": logits}
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Dict, Optional
# Import the register_model decorator
# Location may vary: src.model_module.brain_decoder or src.model_module.model
from src.model_module.brain_decoder import register_model
@register_model('SimpleMLP')
class SimpleMLP(nn.Module):
"""
Simple Multi-Layer Perceptron for classification tasks.
Config structure ( Hydra YAML ):
model:
input_dim: 100
hidden_dim: 256
output_dim: 10
num_layers: 3
dropout: 0.1
dataset:
task: classification # Used to get target_size
target_size:
classification: 10
"""
def __init__(self, cfg):
super().__init__()
# Store config
self.cfg = cfg
# Get task info from config
self.task = cfg.dataset.task
# Build model - ALL parameters from cfg
self.input_dim = cfg.model.input_dim
self.hidden_dim = cfg.model.get('hidden_dim', 256)
self.output_dim = cfg.dataset.target_size[cfg.dataset.task]
self.num_layers = cfg.model.get('num_layers', 3)
self.dropout = cfg.model.get('dropout', 0.1)
# Build layers
layers = []
in_dim = self.input_dim
for i in range(self.num_layers):
layers.extend([
nn.Linear(in_dim, self.hidden_dim),
nn.ReLU(),
nn.Dropout(self.dropout)
])
in_dim = self.hidden_dim
# Output layer
layers.append(nn.Linear(self.hidden_dim, self.output_dim))
self.network = nn.Sequential(*layers)
# Loss function
self.loss_fn = nn.CrossEntropyLoss()
def forward(
self,
x: torch.Tensor,
labels: Optional[torch.Tensor] = None,
**kwargs
) -> Dict[str, Optional[torch.Tensor]]:
"""
Forward pass.
Args:
x: Input tensor of shape (batch_size, input_dim)
labels: Ground truth labels (optional, for training)
Returns:
Dictionary with:
- loss: Computed loss (None if labels not provided)
- labels: Ground truth labels
- logits: Model predictions
"""
logits = self.network(x)
loss = None
if labels is not None:
# Convert labels to long type if needed
if labels.dtype != torch.long:
labels = labels.long()
loss = self.loss_fn(logits, labels)
return {
"loss": loss,
"labels": labels,
"logits": logits
}
# ============================================
# Example with Training/Inference Modes
# ============================================
@register_model('SimpleMLPWithModes')
class SimpleMLPWithModes(nn.Module):
"""
MLP with separate training and inference logic.
Shows how to handle different modes using self.training.
"""
def __init__(self, cfg):
super().__init__()
self.cfg = cfg
self.task = cfg.dataset.task
self.input_dim = cfg.model.input_dim
self.hidden_dim = cfg.model.get('hidden_dim', 256)
self.output_dim = cfg.dataset.target_size[cfg.dataset.task]
self.fc_in = nn.Linear(self.input_dim, self.hidden_dim)
self.ln = nn.LayerNorm(self.hidden_dim)
self.fc_out = nn.Linear(self.hidden_dim, self.output_dim)
self.loss_fn = nn.CrossEntropyLoss()
# Test-time augmentation config
self.tta_times = cfg.model.get('tta_times', 1)
def forward(
self,
x: torch.Tensor,
labels: Optional[torch.Tensor] = None,
**kwargs
) -> Dict[str, Optional[torch.Tensor]]:
"""
Forward pass with training/inference modes.
"""
if self.training:
# Training mode
x = x.float()
x = self.fc_in(x)
x = self.ln(x)
x = F.relu(x)
logits = self.fc_out(x)
loss = None
if labels is not None:
if labels.dtype != torch.long:
labels = labels.long()
loss = self.loss_fn(logits, labels)
return {
"loss": loss,
"labels": labels,
"logits": logits
}
else:
# Inference mode with TTA
all_logits = []
with torch.no_grad():
x = x.float()
for _ in range(self.tta_times):
x_aug = x.clone()
# Apply TTA transformations here if needed
x_aug = self.fc_in(x_aug)
x_aug = self.ln(x_aug)
x_aug = F.relu(x_aug)
logits = self.fc_out(x_aug)
all_logits.append(logits)
# Average predictions
avg_logits = torch.mean(torch.stack(all_logits), dim=0)
loss = None
if labels is not None:
if labels.dtype != torch.long:
labels = labels.long()
loss = self.loss_fn(avg_logits, labels)
return {
"loss": loss,
"labels": labels,
"logits": avg_logits
}
# ============================================
# Config Example (Hydra YAML)
# ============================================
"""
# run/conf/model/simple_mlp.yaml
model:
name: SimpleMLP
input_dim: 100
hidden_dim: 256
output_dim: 10
num_layers: 3
dropout: 0.1
tta_times: 1
# Then in training pipeline:
# from src.model_module.brain_decoder import ModelFactory
# model = ModelFactory(cfg.model.name)(cfg)
"""

View File

@@ -0,0 +1,189 @@
#!/bin/bash
###############################################################################
# Training Pipeline Script
#
# This script demonstrates the standard training pipeline execution pattern.
# It handles environment setup, configuration, and execution with proper
# error handling and logging.
#
# Usage:
# ./run/pipeline/training/train.sh --config-name=config_example
###############################################################################
set -e # Exit on error
set -o pipefail # Exit on pipe failure
# Script configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
EXPERIMENT_NAME="baseline_experiment"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
###############################################################################
# Helper Functions
###############################################################################
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
cleanup() {
log_info "Cleaning up..."
# Add cleanup logic here (e.g., kill background processes)
}
trap cleanup EXIT
###############################################################################
# Environment Setup
###############################################################################
setup_environment() {
log_info "Setting up environment..."
# Activate virtual environment if it exists
if [ -f "${PROJECT_ROOT}/.venv/bin/activate" ]; then
source "${PROJECT_ROOT}/.venv/bin/activate"
log_info "Activated virtual environment"
fi
# Check required commands
command -v python >/dev/null 2>&1 || { log_error "Python not found"; exit 1; }
# Set Python path
export PYTHONPATH="${PROJECT_ROOT}/src:${PYTHONPATH}"
log_info "PYTHONPATH set to: ${PYTHONPATH}"
}
###############################################################################
# Configuration
###############################################################################
parse_arguments() {
# Default values
CONFIG="default"
GPUS=0
SEED=42
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--config-name|-c)
CONFIG="$2"
shift 2
;;
--gpus|-g)
GPUS="$2"
shift 2
;;
--seed|-s)
SEED="$2"
shift 2
;;
*)
log_warn "Unknown argument: $1"
shift
;;
esac
done
log_info "Configuration: ${CONFIG}"
log_info "GPUs: ${GPUS}"
log_info "Seed: ${SEED}"
}
###############################################################################
# Main Training Function
###############################################################################
run_training() {
log_info "Starting training..."
# Output directory for this run
OUTPUT_DIR="${PROJECT_ROOT}/outputs/${EXPERIMENT_NAME}/${TIMESTAMP}"
mkdir -p "${OUTPUT_DIR}"
log_info "Output directory: ${OUTPUT_DIR}"
# Training command with Hydra
python "${PROJECT_ROOT}/train.py" \
--config-name="${CONFIG}" \
seed=${SEED} \
dir.output_dir="${OUTPUT_DIR}" \
training.device=cuda \
hydra.output_dir="${OUTPUT_DIR}/hydra" \
hydra.run.dir="${OUTPUT_DIR}/hydra" || {
log_error "Training failed!"
exit 1
}
log_info "Training completed successfully!"
}
###############################################################################
# Post-Processing
###############################################################################
post_process() {
log_info "Post-processing results..."
# Copy logs to output directory
if [ -f "${OUTPUT_DIR}/hydra/*.log" ]; then
cp "${OUTPUT_DIR}/hydra/"*.log "${OUTPUT_DIR}/"
fi
# Generate summary
log_info "Run summary:"
log_info " Config: ${CONFIG}"
log_info " Seed: ${SEED}"
log_info " Output: ${OUTPUT_DIR}"
# Print path to best checkpoint
BEST_CHECKPOINT=$(find "${OUTPUT_DIR}" -name "best*.pt" | head -n 1)
if [ -n "${BEST_CHECKPOINT}" ]; then
log_info " Best checkpoint: ${BEST_CHECKPOINT}"
fi
}
###############################################################################
# Main Execution
###############################################################################
main() {
log_info "=========================================="
log_info "Training Pipeline"
log_info "=========================================="
# Setup
setup_environment
# Parse arguments
parse_arguments "$@"
# Run training
run_training
# Post-process
post_process
log_info "=========================================="
log_info "Pipeline completed successfully!"
log_info "=========================================="
}
# Run main function
main "$@"