NIRS Data Augmentation in nirs4all
Developer guidelines (no-learning augmentations)
This document describes how to implement and integrate purely algorithmic NIRS augmentations into nirs4all.
All operators are designed as sklearn-style transformers and must be safe to use in NIRS pipelines (no model training inside the transform).
1. General design rules
1.1 Input / output conventions
Input spectra:
Xasnp.ndarrayorArrayLikeof shape(n_samples, n_wavelengths).Optional wavelength axis:
lambda_axis(1D array of shape(n_wavelengths,)), passed via:__init__(..., lambda_axis: Optional[np.ndarray] = None), ora config object / dataset wrapper (depending on
nirs4all’s existing patterns).
Output: same shape as input,
(n_samples, n_wavelengths).No change of sample order; no change of dtype beyond standard float conversions.
1.2 Base class and randomness
All augmenters should:
Inherit from:
class BaseAugmenter(BaseEstimator, TransformerMixin): def __init__(self, random_state: Optional[int | np.random.Generator] = None, ...): ...
Use a local RNG:
self._rng = np.random.default_rng(self.random_state)
Be stateless w.r.t. the data (no data-dependent fit, except for mixup where we just cache
n_samples).
1.3 Training-only behavior
Most augmentations should be used during training only. We recommend a common wrapper (if not already present):
class TrainOnlyWrapper(BaseEstimator, TransformerMixin):
def __init__(self, augmenter, apply_during: str = "fit"):
# apply_during in {"fit", "always"}
self.augmenter = augmenter
self.apply_during = apply_during
def fit(self, X, y=None):
self.augmenter.fit(X, y)
return self
def transform(self, X, y=None, *, is_training: bool = False):
if self.apply_during == "fit" and not is_training:
return X
return self.augmenter.transform(X, y)
Pipelines can then pass is_training=True from the training loop.
2. Augmentation families and suggested classes
Below: one class per family, with a minimal required behavior and recommended parameters.
2.1 Additive / multiplicative noise
2.1.2 Multiplicative Noise (gain jitter)
Class name: MultiplicativeNoise
Effect:
X_aug = (1 + ε) * X with ε ~ N(0, sigma_gain²).
Constructor:
class MultiplicativeNoise(BaseAugmenter):
def __init__(
self,
sigma_gain: float = 0.01,
per_sample: bool = True,
random_state: Optional[int | np.random.Generator] = None,
):
...
Notes:
per_sample=True: draw one gain per sample.Optionally add
per_wavelength(gain per (sample, wavelength) with small σ).
2.2 Baseline shifts and drifts
2.2.1 Linear Baseline Drift
Class name: LinearBaselineDrift
Effect:
X_aug[i, :] = X[i, :] + a_i + b_i * (λ - λ₀)
Constructor:
class LinearBaselineDrift(BaseAugmenter):
def __init__(
self,
offset_range: tuple[float, float] = (-0.02, 0.02),
slope_range: tuple[float, float] = (-0.0005, 0.0005),
lambda_axis: Optional[np.ndarray] = None,
random_state: Optional[int | np.random.Generator] = None,
):
...
Notes:
If
lambda_axisisNone, use indices [0..n_wavelengths-1] as surrogate λ.a_iandb_idrawn independently per sample.
2.2.2 Polynomial Baseline Drift
Class name: PolynomialBaselineDrift
Effect: Add a low-frequency polynomial to each spectrum.
class PolynomialBaselineDrift(BaseAugmenter):
def __init__(
self,
degree: int = 2,
coeff_ranges: dict[int, tuple[float, float]] | None = None,
lambda_axis: Optional[np.ndarray] = None,
random_state: Optional[int | np.random.Generator] = None,
):
...
Notes:
coeff_rangesmaps polynomial degree →(min, max)coefficient.If
coeff_rangesisNone, define default ranges internally, scaled to typical NIRS amplitude.
2.3 Wavelength axis distortions
All these transforms warp λ then resample to the original λ grid.
Use a robust interpolation method (e.g. np.interp for 1D).
2.3.1 Global Wavelength Shift
Class name: WavelengthShift
Effect:
Shift λ by δλ, then interpolate back on original axis.
class WavelengthShift(BaseAugmenter):
def __init__(
self,
shift_range: tuple[float, float] = (-2.0, 2.0), # nm
lambda_axis: Optional[np.ndarray] = None,
random_state: Optional[int | np.random.Generator] = None,
):
...
2.3.2 Global Stretch / Compression
Class name: WavelengthStretch
Effect: λ’ = λ₀ + (1 + α) * (λ - λ₀), α small; then interpolation.
class WavelengthStretch(BaseAugmenter):
def __init__(
self,
stretch_range: tuple[float, float] = (-0.005, 0.005), # relative
lambda_axis: Optional[np.ndarray] = None,
random_state: Optional[int | np.random.Generator] = None,
):
...
2.3.3 Local Nonlinear Warp
Class name: LocalWavelengthWarp
Effect: Apply a smooth monotone warp using random control points.
class LocalWavelengthWarp(BaseAugmenter):
def __init__(
self,
n_control_points: int = 5,
max_shift_nm: float = 1.0,
lambda_axis: Optional[np.ndarray] = None,
random_state: Optional[int | np.random.Generator] = None,
):
...
Implementation idea:
Define control points on λ-axis.
Sample small shifts for each control point in
[-max_shift_nm, max_shift_nm].Fit a monotone spline and apply warp per sample.
2.4 Magnitude warping
2.4.1 Smooth Gain Function
Class name: SmoothMagnitudeWarp
Effect:
Multiply by a smooth gain curve f(λ) with f(λ) ≈ 1.
class SmoothMagnitudeWarp(BaseAugmenter):
def __init__(
self,
n_control_points: int = 5,
gain_range: tuple[float, float] = (0.95, 1.05),
lambda_axis: Optional[np.ndarray] = None,
random_state: Optional[int | np.random.Generator] = None,
):
...
Implementation idea:
Sample gain values at control points uniformly in
gain_range.Interpolate by spline or linear interpolation across λ.
X_aug = X * f(λ).
2.4.2 Band-Specific Perturbation
Class name: BandPerturbation
Effect: Multiply or offset intensity within specific λ bands (e.g. water bands).
class BandPerturbation(BaseAugmenter):
def __init__(
self,
bands: list[tuple[float, float]], # list of (λ_min, λ_max)
gain_range: tuple[float, float] = (0.9, 1.1),
offset_range: tuple[float, float] = (-0.01, 0.01),
lambda_axis: Optional[np.ndarray] = None,
random_state: Optional[int | np.random.Generator] = None,
):
...
Notes:
For each sample, select a subset of bands and apply random gain/offset.
2.5 Resolution / smoothing jitter
2.5.1 Gaussian Smoothing Jitter
Class name: GaussianSmoothingJitter
Effect: Convolve each spectrum with a Gaussian of random σ within a range.
class GaussianSmoothingJitter(BaseAugmenter):
def __init__(
self,
sigma_range: tuple[float, float] = (0.5, 1.5),
kernel_size: int = 7,
random_state: Optional[int | np.random.Generator] = None,
):
...
Notes:
Use a normalized 1D Gaussian kernel; handle boundary with reflection or nearest padding.
2.5.2 Unsharp Mask (Mild Sharpening)
Class name: UnsharpSpectralMask
Effect:
X_aug = X + k * (X - smooth(X)), small k.
class UnsharpSpectralMask(BaseAugmenter):
def __init__(
self,
amount_range: tuple[float, float] = (0.0, 0.2),
smoothing_sigma: float = 1.0,
kernel_size: int = 7,
random_state: Optional[int | np.random.Generator] = None,
):
...
2.6 Spectral masking and dropout
2.6.1 Band Masking
Class name: BandMasking
Effect: Randomly mask short contiguous bands (set to 0 or interpolate).
class BandMasking(BaseAugmenter):
def __init__(
self,
max_mask_width: int = 10,
n_bands_range: tuple[int, int] = (0, 2),
mode: str = "zero", # or "interp"
random_state: Optional[int | np.random.Generator] = None,
):
...
Notes:
Draw
kbands per sample inn_bands_range.For
mode="interp", linearly interpolate between boundaries of each band.
2.6.2 Channel Dropout
Class name: ChannelDropout
Effect: Drop individual wavelengths.
class ChannelDropout(BaseAugmenter):
def __init__(
self,
dropout_prob: float = 0.01,
mode: str = "zero", # or "interp"
random_state: Optional[int | np.random.Generator] = None,
):
...
2.7 Rare structured artefacts
2.7.1 Spike Noise
Class name: SpikeNoise
Effect: Add a small number of narrow spikes.
class SpikeNoise(BaseAugmenter):
def __init__(
self,
n_spikes_range: tuple[int, int] = (0, 3),
amplitude_range: tuple[float, float] = (0.05, 0.2),
width_range: tuple[int, int] = (1, 3),
random_state: Optional[int | np.random.Generator] = None,
):
...
Notes:
Spikes can be positive or negative; consider symmetric amplitude range.
Optionally smooth a bit to avoid delta-like artefacts.
2.7.2 Local Saturation / Clipping
Class name: LocalClipping
Effect: Clip segments locally to mimic saturation.
class LocalClipping(BaseAugmenter):
def __init__(
self,
clip_prob: float = 0.1,
n_segments_range: tuple[int, int] = (0, 2),
segment_width_range: tuple[int, int] = (1, 5),
clip_mode: str = "max", # "max", "min", or "both"
random_state: Optional[int | np.random.Generator] = None,
):
...
2.8 Sample combinations (mixup-style, no learning)
2.8.1 Global Mixup
Class name: MixupAugmenter
Effect:
Combine pairs of samples (x_i, y_i) and (x_j, y_j):
x_aug = λ_mix * x_i + (1 - λ_mix) * x_jy_aug = λ_mix * y_i + (1 - λ_mix) * y_j(regression)
class MixupAugmenter(BaseEstimator, TransformerMixin):
def __init__(
self,
alpha: float = 0.2,
random_state: Optional[int | np.random.Generator] = None,
):
...
Notes:
Use Beta(α, α) to sample
λ_mix.This transformer modifies both X and y;
transformmust return(X_aug, y_aug)or usefit_resample-style API if you already have one.
2.8.2 Local Mixup (nearest-neighbor)
Class name: LocalMixupAugmenter
Same as MixupAugmenter but restricts pairs to neighbors in spectral space.
class LocalMixupAugmenter(MixupAugmenter):
def __init__(
self,
alpha: float = 0.2,
n_neighbors: int = 10,
distance_metric: str = "euclidean",
random_state: Optional[int | np.random.Generator] = None,
):
...
Implementation note:
Precompute nearest neighbors in fit, then sample j among neighbors of i in transform.
2.9 Scattering-based simulation
2.9.1 MSC-Style Scatter Simulation
Class name: ScatterSimulationMSC
Effect:
Simulate scatter variation by perturbing a, b in x ≈ a + b * x_ref.
class ScatterSimulationMSC(BaseAugmenter):
def __init__(
self,
offset_range: tuple[float, float] = (-0.02, 0.02),
scale_range: tuple[float, float] = (0.95, 1.05),
reference_mode: str = "self", # "self", "global_mean", or "provided"
reference_spectrum: Optional[np.ndarray] = None,
random_state: Optional[int | np.random.Generator] = None,
):
...
Notes:
For
reference_mode="self": treatxitself as reference and applyx_aug = a + b * x.For
reference_mode="global_mean": compute mean spectrum infit.For
reference_mode="provided": use external reference.
3. Implementation details and utilities
3.1 Utility functions
Create a small internal module for common operations:
1D convolution with reflection padding:
def conv1d_reflect(x: np.ndarray, kernel: np.ndarray) -> np.ndarray: ...
Gaussian kernel factory:
def gaussian_kernel(size: int, sigma: float) -> np.ndarray: ...
Safe interpolation:
def interpolate_to_axis( x: np.ndarray, lambda_src: np.ndarray, lambda_dst: np.ndarray, fill_value: float = "edge", ) -> np.ndarray: ...
3.2 Parameter validation
Each augmenter should validate ranges in __init__:
Ensure min ≤ max for ranges.
Ensure widths and kernel sizes are positive integers.
Raise clear
ValueErrorwith short messages (for config debugging).
4. Integration into nirs4all pipelines
Expose all classes in a dedicated module, e.g.
nirs4all.augmentation.spectral.Provide factory functions or configuration keys to instantiate from JSON/YAML:
- type: GaussianAdditiveNoise params: sigma: 0.02 smoothing_kernel_width: 7 - type: WavelengthShift params: shift_range: [-1.0, 1.0]
Ensure compatibility with:
Existing dataset abstraction (
SpectraDataset/SpectroDataset).Any
TrainOnlyWrapperor pipeline-level mechanism that passesis_training=Trueonly during training.
5. Testing guidelines
For each augmenter:
Shape invariance:
X_aug.shape == X.shape.Determinism: fixed
random_state⇒ identical outputs.Amplitude sanity: output values stay in reasonable bounds for typical NIRS ranges.
No NaN / inf: unless explicitly requested (should not be).
Lambda-axis usage: when
lambda_axisis provided, warps must be consistent with nm values.
Once implemented, add small end-to-end tests combining several augmenters in a pipeline to ensure they compose correctly.