Skip to content

Commit 2fb0f85

Browse files
ooplesclaude
andcommitted
fix: use randomhelper for seeded random instances in time series models
- Replace direct Random instantiation with RandomHelper.CreateSeededRandom() for reproducible behavior in InformerModel layer classes - Replace direct Random instantiation with RandomHelper.CreateSeededRandom() for reproducible behavior in ChronosFoundationModel layer class - Replace Convert.ToDouble with _numOps.ToDouble() in DeepANT for proper generic type handling Addresses PR review comments for unseeded Random and type conversion issues. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9e3c75c commit 2fb0f85

3 files changed

Lines changed: 86 additions & 15 deletions

File tree

src/TimeSeries/AnomalyDetection/DeepANT.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ public override ModelMetadata<T> GetModelMetadata()
362362
AdditionalInfo = new Dictionary<string, object>
363363
{
364364
{ "WindowSize", _options.WindowSize },
365-
{ "AnomalyThreshold", Convert.ToDouble(_anomalyThreshold) }
365+
{ "AnomalyThreshold", _numOps.ToDouble(_anomalyThreshold) }
366366
}
367367
};
368368
}

src/TimeSeries/ChronosFoundationModel.cs

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using AiDotNet.Tensors.Helpers;
2+
13
namespace AiDotNet.TimeSeries;
24

35
/// <summary>
@@ -114,6 +116,10 @@ private ChronosFoundationModel(ChronosOptions<T> options, bool initializeModel)
114116
_options = options;
115117
_numOps = MathHelper.GetNumericOperations<T>();
116118
_random = new Random(42);
119+
120+
// Validate options to prevent runtime failures
121+
ValidateOptions(options);
122+
117123
_vocabularySize = _options.VocabularySize;
118124
_binWidth = (_binMax - _binMin) / _vocabularySize;
119125
_transformerLayers = new List<ChronosTransformerLayer<T>>();
@@ -122,6 +128,34 @@ private ChronosFoundationModel(ChronosOptions<T> options, bool initializeModel)
122128
InitializeModel();
123129
}
124130

131+
/// <summary>
132+
/// Validates configuration options to prevent division-by-zero and invalid dimensions.
133+
/// </summary>
134+
/// <exception cref="ArgumentException">Thrown when options contain invalid values.</exception>
135+
private static void ValidateOptions(ChronosOptions<T> options)
136+
{
137+
if (options.VocabularySize < 2)
138+
throw new ArgumentException($"VocabularySize must be at least 2, got {options.VocabularySize}", nameof(options));
139+
140+
if (options.EmbeddingDim <= 0)
141+
throw new ArgumentException($"EmbeddingDim must be positive, got {options.EmbeddingDim}", nameof(options));
142+
143+
if (options.NumHeads <= 0)
144+
throw new ArgumentException($"NumHeads must be positive, got {options.NumHeads}", nameof(options));
145+
146+
if (options.EmbeddingDim % options.NumHeads != 0)
147+
throw new ArgumentException($"EmbeddingDim ({options.EmbeddingDim}) must be divisible by NumHeads ({options.NumHeads})", nameof(options));
148+
149+
if (options.NumLayers <= 0)
150+
throw new ArgumentException($"NumLayers must be positive, got {options.NumLayers}", nameof(options));
151+
152+
if (options.ContextLength <= 0)
153+
throw new ArgumentException($"ContextLength must be positive, got {options.ContextLength}", nameof(options));
154+
155+
if (options.ForecastHorizon <= 0)
156+
throw new ArgumentException($"ForecastHorizon must be positive, got {options.ForecastHorizon}", nameof(options));
157+
}
158+
125159
private void InitializeModel()
126160
{
127161
double stddev = Math.Sqrt(2.0 / _options.EmbeddingDim);
@@ -453,6 +487,13 @@ public override T PredictSingle(Vector<T> input)
453487

454488
private Vector<T> ApplyLayerNorm(Vector<T> input, Vector<T> gamma, Vector<T> beta)
455489
{
490+
// Validate dimensions match (should always be true after option validation)
491+
if (input.Length != gamma.Length || input.Length != beta.Length)
492+
{
493+
throw new InvalidOperationException(
494+
$"Layer normalization dimension mismatch: input={input.Length}, gamma={gamma.Length}, beta={beta.Length}");
495+
}
496+
456497
// Compute mean
457498
double mean = 0;
458499
for (int i = 0; i < input.Length; i++)
@@ -471,7 +512,7 @@ private Vector<T> ApplyLayerNorm(Vector<T> input, Vector<T> gamma, Vector<T> bet
471512
// Normalize
472513
double stddev = Math.Sqrt(variance + 1e-6);
473514
var output = new Vector<T>(input.Length);
474-
for (int i = 0; i < input.Length && i < gamma.Length; i++)
515+
for (int i = 0; i < input.Length; i++)
475516
{
476517
double normalized = (Convert.ToDouble(input[i]) - mean) / stddev;
477518
output[i] = _numOps.Add(
@@ -852,6 +893,7 @@ public ChronosOptions(ChronosOptions<T> other)
852893
internal class ChronosTransformerLayer<T>
853894
{
854895
private readonly INumericOperations<T> _numOps;
896+
private readonly Random _random;
855897
private readonly int _embeddingDim;
856898
private readonly int _numHeads;
857899
private readonly int _headDim;
@@ -883,11 +925,12 @@ internal class ChronosTransformerLayer<T>
883925
public ChronosTransformerLayer(int embeddingDim, int numHeads, int seed = 42)
884926
{
885927
_numOps = MathHelper.GetNumericOperations<T>();
928+
_random = RandomHelper.CreateSeededRandom(seed);
886929
_embeddingDim = embeddingDim;
887930
_numHeads = numHeads;
888931
_headDim = embeddingDim / numHeads;
889932

890-
var random = new Random(seed);
933+
var random = RandomHelper.CreateSeededRandom(seed);
891934
double attnStddev = Math.Sqrt(2.0 / embeddingDim);
892935
double ffnStddev = Math.Sqrt(2.0 / (embeddingDim * 4));
893936

@@ -914,6 +957,7 @@ public ChronosTransformerLayer(int embeddingDim, int numHeads, int seed = 42)
914957
private ChronosTransformerLayer()
915958
{
916959
_numOps = MathHelper.GetNumericOperations<T>();
960+
_random = RandomHelper.CreateSeededRandom(42);
917961
_embeddingDim = 0;
918962
_numHeads = 1;
919963
_headDim = 0;
@@ -1115,7 +1159,7 @@ private T DotProduct(Vector<T> a, Vector<T> b)
11151159
public void UpdateWeights(Vector<T> input, T target, T learningRate, T epsilon, T twoEpsilon,
11161160
Func<Vector<T>, T> predict, int sampleSize)
11171161
{
1118-
var random = new Random();
1162+
// Use seeded Random for reproducibility
11191163
var allMatrices = new[] { _queryProj, _keyProj, _valueProj, _outputProj, _ffn1, _ffn2 };
11201164

11211165
foreach (var matrix in allMatrices)
@@ -1125,7 +1169,7 @@ public void UpdateWeights(Vector<T> input, T target, T learningRate, T epsilon,
11251169

11261170
for (int s = 0; s < actualSample; s++)
11271171
{
1128-
int flatIdx = random.Next(totalWeights);
1172+
int flatIdx = _random.Next(totalWeights);
11291173
int i = flatIdx / matrix.Columns;
11301174
int j = flatIdx % matrix.Columns;
11311175

src/TimeSeries/InformerModel.cs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using AiDotNet.Tensors.Helpers;
2+
13
namespace AiDotNet.TimeSeries;
24

35
/// <summary>
@@ -467,6 +469,23 @@ public Vector<T> ForecastHorizon(Vector<T> input)
467469
/// Embeds input time series into embedding space.
468470
/// Each time step is projected from scalar to embedding dimension.
469471
/// </summary>
472+
/// <param name="input">The input time series vector.</param>
473+
/// <returns>A list of embedding vectors, one per time step.</returns>
474+
/// <remarks>
475+
/// <para>
476+
/// <b>Input Length Handling:</b>
477+
/// The input is adjusted to match the configured LookbackWindow size:
478+
/// - If input.Length &gt; LookbackWindow: Only the first LookbackWindow elements are used.
479+
/// This truncation is intentional as Informer uses a fixed encoder length for efficiency.
480+
/// - If input.Length &lt; LookbackWindow: Zero-padding is added at the beginning (left-padding)
481+
/// to reach the required length.
482+
/// </para>
483+
/// <para><b>For Beginners:</b> The model needs a fixed-length input to work efficiently.
484+
/// If you provide more data than the lookback window, the extra oldest data points are ignored.
485+
/// If you provide less data, zeros are added at the start. For best results, provide exactly
486+
/// LookbackWindow data points.
487+
/// </para>
488+
/// </remarks>
470489
private List<Vector<T>> EmbedSequence(Vector<T> input)
471490
{
472491
var embedded = new List<Vector<T>>();
@@ -824,6 +843,7 @@ internal class InformerEncoderLayer<T>
824843
private readonly int _numHeads;
825844
private readonly int _headDim;
826845
private readonly int _sparsityFactor;
846+
private readonly Random _random;
827847

828848
// Multi-head attention weights (Q, K, V projections)
829849
private Matrix<T> _queryProj;
@@ -855,12 +875,13 @@ internal class InformerEncoderLayer<T>
855875
public InformerEncoderLayer(int embeddingDim, int numHeads, int sparsityFactor, double dropoutRate, int seed = 42)
856876
{
857877
_numOps = MathHelper.GetNumericOperations<T>();
878+
_random = RandomHelper.CreateSeededRandom(seed);
858879
_embeddingDim = embeddingDim;
859880
_numHeads = numHeads;
860881
_headDim = embeddingDim / numHeads;
861882
_sparsityFactor = sparsityFactor;
862883

863-
var random = new Random(seed);
884+
var random = RandomHelper.CreateSeededRandom(seed);
864885
double attnStddev = Math.Sqrt(2.0 / embeddingDim);
865886
double ffnStddev = Math.Sqrt(2.0 / (embeddingDim * 4));
866887

@@ -887,6 +908,7 @@ public InformerEncoderLayer(int embeddingDim, int numHeads, int sparsityFactor,
887908
private InformerEncoderLayer()
888909
{
889910
_numOps = MathHelper.GetNumericOperations<T>();
911+
_random = RandomHelper.CreateSeededRandom(42);
890912
_embeddingDim = 0;
891913
_numHeads = 1;
892914
_headDim = 0;
@@ -1166,8 +1188,7 @@ private T DotProduct(Vector<T> a, Vector<T> b)
11661188
public void UpdateWeights(Vector<T> input, T target, T learningRate, T epsilon, T twoEpsilon,
11671189
Func<Vector<T>, T> predict, int sampleSize)
11681190
{
1169-
// Update a random subset of weights using numerical gradients
1170-
var random = new Random();
1191+
// Update a random subset of weights using numerical gradients (using seeded Random for reproducibility)
11711192
var allMatrices = new[] { _queryProj, _keyProj, _valueProj, _outputProj, _ffn1, _ffn2 };
11721193

11731194
foreach (var matrix in allMatrices)
@@ -1177,7 +1198,7 @@ public void UpdateWeights(Vector<T> input, T target, T learningRate, T epsilon,
11771198

11781199
for (int s = 0; s < actualSample; s++)
11791200
{
1180-
int flatIdx = random.Next(totalWeights);
1201+
int flatIdx = _random.Next(totalWeights);
11811202
int i = flatIdx / matrix.Columns;
11821203
int j = flatIdx % matrix.Columns;
11831204

@@ -1329,6 +1350,7 @@ private static Vector<T> DeserializeVector(BinaryReader reader, INumericOperatio
13291350
internal class DistillingConv<T>
13301351
{
13311352
private readonly INumericOperations<T> _numOps;
1353+
private readonly Random _random;
13321354
private Matrix<T> _convWeights;
13331355
private Vector<T> _convBias;
13341356
private readonly int _poolingFactor;
@@ -1338,9 +1360,10 @@ internal class DistillingConv<T>
13381360
public DistillingConv(int embeddingDim, int poolingFactor, int seed = 42)
13391361
{
13401362
_numOps = MathHelper.GetNumericOperations<T>();
1363+
_random = RandomHelper.CreateSeededRandom(seed);
13411364
_poolingFactor = poolingFactor;
13421365

1343-
var random = new Random(seed);
1366+
var random = RandomHelper.CreateSeededRandom(seed);
13441367
double stddev = Math.Sqrt(2.0 / embeddingDim);
13451368

13461369
// 1D convolution weights (kernel size = 3, same embedding dim)
@@ -1355,6 +1378,7 @@ public DistillingConv(int embeddingDim, int poolingFactor, int seed = 42)
13551378
private DistillingConv()
13561379
{
13571380
_numOps = MathHelper.GetNumericOperations<T>();
1381+
_random = RandomHelper.CreateSeededRandom(42);
13581382
_convWeights = new Matrix<T>(0, 0);
13591383
_convBias = new Vector<T>(0);
13601384
_poolingFactor = 2;
@@ -1423,13 +1447,13 @@ public List<Vector<T>> Forward(List<Vector<T>> input)
14231447
public void UpdateWeights(Vector<T> input, T target, T learningRate, T epsilon, T twoEpsilon,
14241448
Func<Vector<T>, T> predict, int sampleSize)
14251449
{
1426-
var random = new Random();
1450+
// Use seeded Random for reproducibility
14271451
int totalWeights = _convWeights.Rows * _convWeights.Columns;
14281452
int actualSample = Math.Min(sampleSize, totalWeights);
14291453

14301454
for (int s = 0; s < actualSample; s++)
14311455
{
1432-
int flatIdx = random.Next(totalWeights);
1456+
int flatIdx = _random.Next(totalWeights);
14331457
int i = flatIdx / _convWeights.Columns;
14341458
int j = flatIdx % _convWeights.Columns;
14351459

@@ -1531,6 +1555,7 @@ public static DistillingConv<T> Deserialize(BinaryReader reader)
15311555
internal class InformerDecoderLayer<T>
15321556
{
15331557
private readonly INumericOperations<T> _numOps;
1558+
private readonly Random _random;
15341559
private readonly int _embeddingDim;
15351560
private readonly int _numHeads;
15361561
private readonly int _headDim;
@@ -1571,11 +1596,12 @@ internal class InformerDecoderLayer<T>
15711596
public InformerDecoderLayer(int embeddingDim, int numHeads, int sparsityFactor, double dropoutRate, int seed = 42)
15721597
{
15731598
_numOps = MathHelper.GetNumericOperations<T>();
1599+
_random = RandomHelper.CreateSeededRandom(seed);
15741600
_embeddingDim = embeddingDim;
15751601
_numHeads = numHeads;
15761602
_headDim = embeddingDim / numHeads;
15771603

1578-
var random = new Random(seed);
1604+
var random = RandomHelper.CreateSeededRandom(seed);
15791605
double attnStddev = Math.Sqrt(2.0 / embeddingDim);
15801606
double ffnStddev = Math.Sqrt(2.0 / (embeddingDim * 4));
15811607

@@ -1610,6 +1636,7 @@ public InformerDecoderLayer(int embeddingDim, int numHeads, int sparsityFactor,
16101636
private InformerDecoderLayer()
16111637
{
16121638
_numOps = MathHelper.GetNumericOperations<T>();
1639+
_random = RandomHelper.CreateSeededRandom(42);
16131640
_embeddingDim = 0;
16141641
_numHeads = 1;
16151642
_headDim = 0;
@@ -1862,7 +1889,7 @@ private T DotProduct(Vector<T> a, Vector<T> b)
18621889
public void UpdateWeights(Vector<T> input, T target, T learningRate, T epsilon, T twoEpsilon,
18631890
Func<Vector<T>, T> predict, int sampleSize)
18641891
{
1865-
var random = new Random();
1892+
// Use seeded Random for reproducibility
18661893
var allMatrices = new[] {
18671894
_selfQueryProj, _selfKeyProj, _selfValueProj, _selfOutputProj,
18681895
_crossQueryProj, _crossKeyProj, _crossValueProj, _crossOutputProj,
@@ -1876,7 +1903,7 @@ public void UpdateWeights(Vector<T> input, T target, T learningRate, T epsilon,
18761903

18771904
for (int s = 0; s < actualSample; s++)
18781905
{
1879-
int flatIdx = random.Next(totalWeights);
1906+
int flatIdx = _random.Next(totalWeights);
18801907
int i = flatIdx / matrix.Columns;
18811908
int j = flatIdx % matrix.Columns;
18821909

0 commit comments

Comments
 (0)