Skip to content

Commit d70b641

Browse files
ooplesclaude
andauthored
fix(facade): quantization trained-state + rank-3 tensor training + interface-completeness guard (#1887)
* fix(facade): unblock quantization trained-state, rank-3 tensor training, and …IAiModelBuilder.ConfigureMemoryManagement Three blockers that prevented consumers from using the facade optimization profile on real forecasting pipelines: 1. IAiModelBuilder<T,TInput,TOutput> was missing ConfigureMemoryManagement — it existed only on the concrete AiModelBuilder, so callers holding the interface (the normal facade surface) couldn't enable gradient checkpointing. Added to the interface (concrete already implements it). 2. Quantization dropped trained state. Quantizers rebuild the model from its parameter vector via IParameterizable.WithParameters, which for families that keep trained internals OUTSIDE GetParameters() (gradient-boosted trees' _trees/_binThresholds, etc.) produces a fresh UNTRAINED instance — so Predict threw "Model must be trained before making predictions" (surfaced when the JIT path then traced it). ApplyQuantizationIfConfigured now re-seats the quantized parameters onto a DeepCopy of the trained source (in-place SetParameters), preserving those internals across all quantizer strategies. 3. Rank-3 tensor forecasting models crashed the supervised build. The optimizer's "non-flat neural input" guard (which skips column-subset feature selection) keyed off the first layer's declared input rank, so Dense-embedding forecasters like PatchTST/iTransformer were misclassified as columnar and reached GetColumnVectors on a rank-3 [batch, seq, features] tensor → ArgumentException "Number of indices must match the tensor's rank." The guard now also falls back to the ACTUAL input rank (XTrain is a rank>2 Tensor), and GetColumnVectors rejects rank!=2 with a clear message. Matrix and rank-2 tensor paths untouched. Regression tests cover all three (FacadeOptimizationBlockerTests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(facade): guard IAiModelBuilder completeness + close 6 more interface gaps Adds a reflection-based test that FAILS if any fluent method on the concrete AiModelBuilder (one returning IAiModelBuilder for chaining) is not reachable through IAiModelBuilder or one of its documented derived capability interfaces (e.g. IWeightStreamingCapableBuilder). This permanently catches the class of gap that hid ConfigureMemoryManagement — a facade method callers couldn't invoke once a chain returned the interface. Running it surfaced six more genuinely-missing methods, now declared on the interface (the concrete already implemented them): ConfigureProfiling, ConfigureInterpretability, ConfigurePreprocessing(PreprocessingPipeline<T,TInput,TInput>), ConfigureAugmentation(AugmentationConfig<T,TInput>), ConfigureAutoML(IAutoMLModel<T,TInput,TOutput>), ConfigureDataLoader(IDataLoader<ImageView<T>, PixelBatch<T>>). ConfigureWeightStreaming stays on IWeightStreamingCapableBuilder by design (an opt-in-via-cast surface kept off IAiModelBuilder for binary compat); the guard treats derived-interface methods as reachable, so it is not flagged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(facade): address review — no state-losing quant fallback, keep interface non-breaking Resolves the four review findings on this PR: - Quantization: on trained-state preservation failure, return no quantized model (skip quantization, keep the original TRAINED model) instead of falling back to the quantizer's untrained rebuild that would crash at predict. - Do NOT add the seven fluent methods to IAiModelBuilder — that reverses the documented compat policy (adding abstract members breaks external implementers; the advanced ConfigureAutoML overload is explicitly noted as concrete-only). They stay public on the concrete builder; a NOTE records why. - Completeness guard: accept a derived capability interface only if AiModelBuilder actually IMPLEMENTS it, and pin the intentional concrete-only set in an explicit allowlist. A new fluent method now forces a conscious choice (interface vs allowlist) rather than silently disappearing from the surface. - Blocker test: assert the fluent call returns the SAME builder instance (Assert.Same), not merely non-null. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(facade): re-throw cancellation/OOM on quant preservation; log via observability Address review: the quantization state-preservation catch now filters to EXPECTED failures — OperationCanceledException and OutOfMemoryException propagate instead of being turned into a successful-looking unquantized build — and routes its warning through the configured _accelerationLogger rather than Console.WriteLine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(facade): all fluent Configure methods on IAiModelBuilder; guard enforces it Reverses the concrete-only approach per maintainer direction: every fluent AiModelBuilder method (one returning IAiModelBuilder for chaining) is now declared on IAiModelBuilder in the standard style — ConfigureMemoryManagement, ConfigureProfiling, ConfigureInterpretability, the PreprocessingPipeline overload of ConfigurePreprocessing, the typed ConfigureAugmentation, the advanced ConfigureAutoML(IAutoMLModel), the NeRF ConfigureDataLoader, and ConfigureWeightStreaming. Breaking changes for external implementers are accepted; the facade surface must be complete and uniform. Removes the compat shims: the 'intentionally concrete-only' NOTEs, the IntentionallyConcreteOnly allowlist, and the separate IWeightStreamingCapableBuilder capability interface (WeightStreaming now lives on IAiModelBuilder like everything else). The completeness test now fails if ANY fluent concrete method is missing from IAiModelBuilder — no allowlist, no shortcuts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(timeseries): deepAR no longer collapses to a constant on short prediction rows DeepARModel.PredictDistribution replaced the caller's window with a FIXED slice of the training series whenever the row was shorter than LookbackWindow — identical for every row — so the LSTM ran on the same input each call and the whole test block came out as one constant value (verified: DeepAR predicted a single value even on a trivially-predictable sine series, while DLinear on the same data varied). Left-pad the caller's own window instead (preserving its real values, especially the most-recent one that drives the residual skip); never overwrite it with training data. Regression test covers the short-row / cross-sectional case; all 58 DeepAR tests pass. NOTE (follow-up): TrainCore trains autoregressively on the target series y and ignores x. That is consistent with DeepAR's univariate paradigm but mismatches the x-rows-as-windows contract DLinear/NBEATS use, so a caller passing a feature matrix gets varying-but-covariate-blind forecasts. Reconciling that contract (covariate support, or documenting DeepAR as univariate-on-y) is a separate design change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(regression): numerically condition MultipleRegression against near-collinear designs Normal equations square the condition number, so a near-collinear design yields a severely ill-conditioned X'X. Cholesky still SUCCEEDS on it (positive-definite, just ill-conditioned) and returns absurd blown-up coefficients — huge but FINITE, so the NaN/Infinity guard in SolveSystem never trips — producing ~1e10 predictions on normal-scale data. Add a tiny Tikhonov diagonal-loading scaled to the matrix's own diagonal (1e-9 * mean-diagonal): negligible for well-conditioned problems (below solver noise; all 46 existing MultipleRegression tests unchanged) yet enough to bound the blow-up. Regression test drives a near-duplicate-column design. This is the upstream root fix for the ~1e10 spikes the Ooples RL-observation path was clamping defensively. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a2dd369 commit d70b641

11 files changed

Lines changed: 476 additions & 47 deletions

File tree

src/AiModelBuilder.Internals.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1879,6 +1879,33 @@ private static int TryInjectMonteCarloDropoutLayers(
18791879
var quantizedModel = quantizer.Quantize(model, internalConfig);
18801880
var quantizedParameters = InterfaceGuard.Parameterizable(quantizedModel).GetParameters();
18811881

1882+
// Preserve trained state through quantization. Quantizers rebuild the model from its parameter vector
1883+
// (IParameterizable.WithParameters), which for model families that keep trained state OUTSIDE
1884+
// GetParameters() — e.g. gradient-boosted trees' tree ensembles and bin thresholds — drops those
1885+
// internals, leaving the quantized model UNTRAINED so its Predict throws "Model must be trained before
1886+
// making predictions" (surfaced when a downstream consumer such as the JIT path then traces it). Re-seat
1887+
// the quantized parameters onto a full DeepCopy of the trained source, which keeps those internals, and
1888+
// apply the parameters in-place (SetParameters) rather than rebuilding.
1889+
try
1890+
{
1891+
var trainedQuantized = model.DeepCopy();
1892+
InterfaceGuard.Parameterizable(trainedQuantized).SetParameters(quantizedParameters);
1893+
quantizedModel = trainedQuantized;
1894+
}
1895+
catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException)
1896+
{
1897+
// Preserving trained state failed for an EXPECTED reason (e.g. a model that can't round-trip via
1898+
// DeepCopy/SetParameters). Do NOT fall back to the quantizer's rebuilt model — for families that keep
1899+
// trained internals outside the parameter vector it is UNTRAINED and would throw at predict time (the
1900+
// exact defect this path fixes). Skip quantization so the caller keeps the original trained,
1901+
// unquantized model rather than a landmine. Cancellation and out-of-memory are NOT expected here and
1902+
// propagate so they aren't silently turned into a successful-looking build.
1903+
_accelerationLogger?.Invoke(
1904+
$"[AiDotNet] Quantization skipped: could not preserve trained state ({ex.GetType().Name}: {ex.Message}); " +
1905+
"keeping the original trained model.");
1906+
return (null, null);
1907+
}
1908+
18821909
// Calculate actual quantized size based on bit width
18831910
// For sub-byte quantization (4-bit), we need to account for packing
18841911
long quantizedSizeBytes = ((long)quantizedParameters.Length * internalConfig.EffectiveBitWidth + 7) / 8;

src/AiModelBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ namespace AiDotNet;
132132
/// modelRegistry.TransitionStage(modelVersion.ModelId, modelVersion.Version, ModelStage.Production);
133133
/// </code>
134134
/// </remarks>
135-
public partial class AiModelBuilder<T, TInput, TOutput> : IAiModelBuilder<T, TInput, TOutput>, IWeightStreamingCapableBuilder<T, TInput, TOutput>, AiDotNet.Configuration.IConfiguredView<T, TInput, TOutput>
135+
public partial class AiModelBuilder<T, TInput, TOutput> : IAiModelBuilder<T, TInput, TOutput>, AiDotNet.Configuration.IConfiguredView<T, TInput, TOutput>
136136
{
137137
private static IEngine Engine => AiDotNetEngine.Current;
138138

src/Helpers/ModelHelper.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,15 @@ public static List<Vector<T>> GetColumnVectors(TInput input, int[] indices)
228228
}
229229
else if (input is Tensor<T> tensor)
230230
{
231-
if (tensor.Shape.Length < 2)
231+
if (tensor.Shape.Length != 2)
232232
{
233-
throw new ArgumentException("Tensor must have at least 2 dimensions to extract columns");
233+
// Column extraction indexes axis 1 with a 2-index accessor, so it is only defined for a rank-2
234+
// tensor. A rank>2 (e.g. [batch, seq, features]) input reaching here means a "non-flat" model
235+
// was misclassified as columnar upstream — fail with a clear message instead of the cryptic
236+
// "Number of indices must match the tensor's rank" from the tensor accessor.
237+
throw new ArgumentException(
238+
$"Column extraction requires a rank-2 tensor; got rank {tensor.Shape.Length} " +
239+
$"(shape {string.Join("×", tensor._shape)}).");
234240
}
235241

236242
var result = new List<Vector<T>>();

src/Interfaces/IAiModelBuilder.cs

Lines changed: 76 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using AiDotNet.Agentic.Tools;
2+
using AiDotNet.Augmentation;
23
using AiDotNet.Clustering.Interfaces;
4+
using AiDotNet.NeuralRadianceFields.Data;
35
using AiDotNet.Configuration;
46
using AiDotNet.Deployment.Configuration;
57
using AiDotNet.DistributedTraining;
@@ -913,12 +915,6 @@ IAiModelBuilder<T, TInput, TOutput> ConfigurePipelineParallelism(
913915
/// </remarks>
914916
IAiModelBuilder<T, TInput, TOutput> ConfigureAutoML(AutoMLOptions<T, TInput, TOutput>? options = null);
915917

916-
// NOTE: The advanced ConfigureAutoML(IAutoMLModel<T,TInput,TOutput>) overload is intentionally
917-
// NOT part of this interface. Adding it would be a breaking change for every external
918-
// IAiModelBuilder<T,TInput,TOutput> implementer. It remains a public method on the concrete
919-
// AiModelBuilder<T,TInput,TOutput>, which is also what the generated YAML applier targets
920-
// (it dispatches against the concrete builder type, not this interface).
921-
922918
/// <summary>
923919
/// Configures reinforcement learning options for training an RL agent.
924920
/// </summary>
@@ -1179,13 +1175,6 @@ IAiModelBuilder<T, TInput, TOutput> ConfigureKnowledgeDistillation(
11791175
/// <returns>The builder instance for method chaining.</returns>
11801176
IAiModelBuilder<T, TInput, TOutput> ConfigureTelemetry(TelemetryConfig? config = null);
11811177

1182-
// ConfigureWeightStreaming intentionally lives on
1183-
// IWeightStreamingCapableBuilder<T, TInput, TOutput> (declared at the
1184-
// bottom of this file) instead of on IAiModelBuilder<T, TInput, TOutput>,
1185-
// so adding it does not break external implementers of IAiModelBuilder.
1186-
// The concrete AiModelBuilder<T, TInput, TOutput> implements both
1187-
// interfaces, so existing fluent call sites continue to compile.
1188-
11891178
/// <summary>
11901179
/// Controls GPU backend diagnostic output visibility and routing.
11911180
/// Exposes all three controls from github.com/ooples/AiDotNet#1122:
@@ -1450,6 +1439,80 @@ IAiModelBuilder<T, TInput, TOutput> ConfigureJitCompilation(
14501439
/// </remarks>
14511440
IAiModelBuilder<T, TInput, TOutput> ConfigureMixedPrecision(MixedPrecisionConfig? config = null);
14521441

1442+
/// <summary>
1443+
/// Configures training memory management — gradient checkpointing and activation pooling — so large models
1444+
/// (e.g. deep transformers) can train within a bounded activation-memory budget.
1445+
/// </summary>
1446+
/// <param name="configuration">The training-memory configuration (presets such as
1447+
/// <c>TrainingMemoryConfig.ForTransformers()</c> or <c>MemoryEfficient()</c>); <c>null</c> applies the defaults.</param>
1448+
/// <returns>The builder instance for method chaining.</returns>
1449+
/// <remarks>
1450+
/// <b>For Beginners:</b> deep models store the intermediate results of every layer during training so they
1451+
/// can compute gradients. Gradient checkpointing trades a little extra compute to recompute some of those
1452+
/// instead of holding them all in memory, which lets a bigger model fit on the same hardware.
1453+
/// </remarks>
1454+
IAiModelBuilder<T, TInput, TOutput> ConfigureMemoryManagement(Training.Memory.TrainingMemoryConfig? configuration = null);
1455+
1456+
/// <summary>
1457+
/// Configures Float16/Int8 weight streaming so very large models can run without holding all weights in
1458+
/// memory at once.
1459+
/// </summary>
1460+
/// <param name="config">The weight-streaming configuration; <c>null</c> applies the defaults.</param>
1461+
/// <returns>The builder instance for method chaining.</returns>
1462+
IAiModelBuilder<T, TInput, TOutput> ConfigureWeightStreaming(WeightStreamingConfig? config = null);
1463+
1464+
/// <summary>
1465+
/// Configures a runtime profiling pass over the build/inference path to capture timing and resource usage.
1466+
/// </summary>
1467+
/// <param name="config">The profiling configuration; <c>null</c> applies the defaults.</param>
1468+
/// <returns>The builder instance for method chaining.</returns>
1469+
IAiModelBuilder<T, TInput, TOutput> ConfigureProfiling(ProfilingConfig? config = null);
1470+
1471+
/// <summary>
1472+
/// Configures model interpretability — feature attribution and explanations — on the built result.
1473+
/// </summary>
1474+
/// <param name="options">The interpretability options; <c>null</c> applies the defaults.</param>
1475+
/// <returns>The builder instance for method chaining.</returns>
1476+
/// <remarks>
1477+
/// <b>For Beginners:</b> interpretability tools tell you WHY the model made a prediction — which inputs
1478+
/// mattered most — instead of treating it as an opaque box.
1479+
/// </remarks>
1480+
IAiModelBuilder<T, TInput, TOutput> ConfigureInterpretability(InterpretabilityOptions? options = null);
1481+
1482+
/// <summary>
1483+
/// Configures the preprocessing stage directly from a prebuilt
1484+
/// <see cref="PreprocessingPipeline{T,TInput,TInput}"/> (the overload for callers that already assembled one).
1485+
/// </summary>
1486+
/// <param name="pipeline">The preprocessing pipeline; <c>null</c> applies the defaults.</param>
1487+
/// <returns>The builder instance for method chaining.</returns>
1488+
IAiModelBuilder<T, TInput, TOutput> ConfigurePreprocessing(PreprocessingPipeline<T, TInput, TInput>? pipeline = null);
1489+
1490+
/// <summary>
1491+
/// Configures training-time data augmentation with a type-parameterized augmentation configuration.
1492+
/// </summary>
1493+
/// <param name="config">The augmentation configuration.</param>
1494+
/// <returns>The builder instance for method chaining.</returns>
1495+
/// <remarks>
1496+
/// <b>For Beginners:</b> augmentation makes extra, slightly-varied copies of your training data (e.g.
1497+
/// jittered or shifted) so the model sees more variety and generalizes better.
1498+
/// </remarks>
1499+
IAiModelBuilder<T, TInput, TOutput> ConfigureAugmentation(AugmentationConfig<T, TInput>? config);
1500+
1501+
/// <summary>
1502+
/// Configures AutoML with a caller-supplied AutoML model implementation (the advanced overload; the
1503+
/// <see cref="ConfigureAutoML(AutoMLOptions{T,TInput,TOutput})"/> overload takes a budget preset instead).
1504+
/// </summary>
1505+
/// <param name="autoMLModel">The AutoML model that drives architecture/hyperparameter search.</param>
1506+
/// <returns>The builder instance for method chaining.</returns>
1507+
IAiModelBuilder<T, TInput, TOutput> ConfigureAutoML(IAutoMLModel<T, TInput, TOutput> autoMLModel);
1508+
1509+
/// <summary>
1510+
/// Configures the Neural Radiance Field image-view / pixel-batch data loader (the overload for NeRF pipelines).
1511+
/// </summary>
1512+
/// <param name="dataLoader">The image-view / pixel-batch data loader.</param>
1513+
/// <returns>The builder instance for method chaining.</returns>
1514+
IAiModelBuilder<T, TInput, TOutput> ConfigureDataLoader(IDataLoader<ImageView<T>, PixelBatch<T>> dataLoader);
1515+
14531516
/// <summary>
14541517
/// Configures advanced reasoning capabilities for the model using Chain-of-Thought, Tree-of-Thoughts, and Self-Consistency strategies.
14551518
/// </summary>
@@ -2323,28 +2386,3 @@ IAiModelBuilder<T, TInput, TOutput> ConfigureQueryStrategy(
23232386
IAiModelBuilder<T, TInput, TOutput> ConfigureSimilarityMetric(RetrievalAugmentedGeneration.VectorSearch.ISimilarityMetric<T> metric);
23242387

23252388
}
2326-
2327-
/// <summary>
2328-
/// Optional companion interface for builders that support PaLM-E-scale weight
2329-
/// streaming. Kept separate from <see cref="IAiModelBuilder{T, TInput, TOutput}"/>
2330-
/// so the introduction of <see cref="ConfigureWeightStreaming"/> does NOT
2331-
/// break external implementers of <c>IAiModelBuilder</c>. Cast a builder to
2332-
/// this interface (or use the concrete <see cref="AiModelBuilder{T, TInput, TOutput}"/>)
2333-
/// to opt into the streaming control surface.
2334-
/// </summary>
2335-
public interface IWeightStreamingCapableBuilder<T, TInput, TOutput> : IAiModelBuilder<T, TInput, TOutput>
2336-
{
2337-
/// <summary>
2338-
/// Configures weight streaming behaviour for the model under
2339-
/// construction. See the doc block on
2340-
/// <see cref="AiDotNet.AiModelBuilder{T, TInput, TOutput}.ConfigureWeightStreaming"/>
2341-
/// for the full behavioural contract — this declaration is the binding
2342-
/// surface callers should invoke through when they want to remain
2343-
/// abstract over the builder type.
2344-
/// </summary>
2345-
/// <param name="config">The streaming configuration. Pass null to
2346-
/// reset to default auto-detect behaviour; pass a populated config
2347-
/// to override.</param>
2348-
/// <returns>The builder instance for method chaining.</returns>
2349-
IAiModelBuilder<T, TInput, TOutput> ConfigureWeightStreaming(WeightStreamingConfig? config = null);
2350-
}

src/Optimizers/OptimizerBase.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,14 @@ private OptimizationStepData<T, TInput, TOutput> PrepareAndEvaluateSolutionCore(
603603
// vector, so column-subset feature selection doesn't apply. They use all features
604604
// and skip data subsetting entirely (Step 4 below). Fixes #1468.
605605
int totalFeatures = InputHelper<T, TInput>.GetInputSize(inputData.XTrain);
606-
bool hasNonFlatInput = HasNonFlatNeuralInput(solution);
606+
// "Non-flat" = the input is a spatial/sequence tensor, so column-subset feature selection doesn't apply.
607+
// HasNonFlatNeuralInput inspects the first layer's declared input rank, which misses Dense-embedding
608+
// forecasters (PatchTST/iTransformer start with a Dense patch/variate embedding, so their first-layer
609+
// rank is 1) even though they consume a rank-3 [batch, seq, features] tensor. Fall back to the ACTUAL
610+
// input rank so any rank>2 tensor input skips column extraction (which would otherwise index axis 1 of a
611+
// rank-3 tensor and throw). Matrix inputs and rank-2 tensors are unaffected.
612+
bool hasNonFlatInput = HasNonFlatNeuralInput(solution)
613+
|| (inputData.XTrain is Tensor<T> nonFlatTensor && nonFlatTensor.Shape.Length > 2);
607614
List<int> selectedFeaturesIndices;
608615

609616
if (!InterfaceGuard.Parameterizable(solution).SupportsParameterInitialization

src/Regression/MultipleRegression.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public override void Train(Matrix<T> x, Vector<T> y)
112112
x = x.AddConstantColumn(NumOps.One);
113113
var xTx = x.Transpose().Multiply(x);
114114
var regularizedXTx = xTx.Add(Regularization.Regularize(xTx));
115+
ApplyNumericalConditioning(regularizedXTx);
115116
var xTy = x.Transpose().Multiply(y);
116117
var solution = SolveSystem(regularizedXTx, xTy);
117118
if (Options.UseIntercept)
@@ -125,6 +126,43 @@ public override void Train(Matrix<T> x, Vector<T> y)
125126
}
126127
}
127128

129+
/// <summary>
130+
/// Adds a tiny Tikhonov diagonal-loading (ridge) to the normal-equations matrix for NUMERICAL CONDITIONING.
131+
/// Normal equations square the condition number, so a near-collinear design yields a severely ill-conditioned
132+
/// X'X. Cholesky still succeeds on it (it is positive-definite, just ill-conditioned) and returns absurd,
133+
/// blown-up coefficients — huge but FINITE, so the NaN/Infinity guard in SolveSystem never trips and the
134+
/// prediction explodes (e.g. ~1e10). The ridge is scaled to the matrix's own diagonal magnitude, so it is
135+
/// negligible for well-conditioned problems (below solver noise) yet bounds the blow-up for collinear ones.
136+
/// </summary>
137+
private void ApplyNumericalConditioning(Matrix<T> matrix)
138+
{
139+
var n = matrix.Rows;
140+
if (n == 0)
141+
{
142+
return;
143+
}
144+
145+
var trace = NumOps.Zero;
146+
for (var i = 0; i < n; i++)
147+
{
148+
trace = NumOps.Add(trace, matrix[i, i]);
149+
}
150+
151+
// ridge = 1e-9 * mean(diagonal), with a tiny absolute floor for a degenerate (zero-diagonal) matrix.
152+
var meanDiag = NumOps.Divide(trace, NumOps.FromDouble(n));
153+
var ridge = NumOps.Multiply(NumOps.FromDouble(1e-9), meanDiag);
154+
var floor = NumOps.FromDouble(1e-12);
155+
if (NumOps.LessThanOrEquals(ridge, floor))
156+
{
157+
ridge = floor;
158+
}
159+
160+
for (var i = 0; i < n; i++)
161+
{
162+
matrix[i, i] = NumOps.Add(matrix[i, i], ridge);
163+
}
164+
}
165+
128166
/// <summary>
129167
/// Creates a new instance of the Multiple Regression model with the same configuration.
130168
/// </summary>

src/TimeSeries/DeepARModel.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -465,13 +465,26 @@ public override T PredictSingle(Vector<T> input)
465465
/// </summary>
466466
private (T mean, T scale) PredictDistribution(Vector<T> input)
467467
{
468-
// If the window is shorter than the lookback, pad from the training tail.
469-
if (input.Length < _options.LookbackWindow && _trainingSeries.Length >= _options.LookbackWindow)
468+
// If the window is shorter than the lookback, LEFT-PAD it with its own first value while keeping the
469+
// caller's real values (in particular the most-recent one, which drives the residual skip at the head).
470+
// NOTE: this previously replaced the window with a fixed slice of the TRAINING tail — identical for every
471+
// row — so the LSTM ran on the same input each call and emitted one constant prediction for the whole
472+
// test block. Never overwrite the caller's window with training data.
473+
if (input.Length < _options.LookbackWindow)
470474
{
471475
var lb = new Vector<T>(_options.LookbackWindow);
472-
int start = _trainingSeries.Length - _options.LookbackWindow;
473-
for (int j = 0; j < _options.LookbackWindow; j++)
474-
lb[j] = _trainingSeries[start + j];
476+
int pad = _options.LookbackWindow - input.Length;
477+
T fill = input.Length > 0 ? input[0] : NumOps.Zero;
478+
for (int j = 0; j < pad; j++)
479+
{
480+
lb[j] = fill;
481+
}
482+
483+
for (int j = 0; j < input.Length; j++)
484+
{
485+
lb[pad + j] = input[j];
486+
}
487+
475488
input = lb;
476489
}
477490

0 commit comments

Comments
 (0)