Skip to content

Commit 2406e99

Browse files
committed
feat: complete uncertainty quantification MVP
1 parent cd991a9 commit 2406e99

9 files changed

Lines changed: 663 additions & 49 deletions

File tree

src/Helpers/LayerHelper.cs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1989,6 +1989,79 @@ public static IEnumerable<ILayer<T>> CreateDefaultNeuralNetworkLayers(NeuralNetw
19891989
yield return new ActivationLayer<T>(new[] { outputSize }, new SoftmaxActivation<T>() as IActivationFunction<T>);
19901990
}
19911991

1992+
/// <summary>
1993+
/// Creates a default configuration of layers for a Bayesian neural network (Bayes-by-Backprop style).
1994+
/// </summary>
1995+
/// <remarks>
1996+
/// This mirrors the library's default dense+activation patterns, but uses Bayesian dense layers so the network can
1997+
/// express epistemic uncertainty through weight distributions.
1998+
/// </remarks>
1999+
public static IEnumerable<ILayer<T>> CreateDefaultBayesianNeuralNetworkLayers(NeuralNetworkArchitecture<T> architecture)
2000+
{
2001+
if (architecture == null)
2002+
{
2003+
throw new ArgumentNullException(nameof(architecture));
2004+
}
2005+
2006+
int inputSize = architecture.GetInputShape()[0];
2007+
int outputSize = architecture.OutputSize;
2008+
2009+
if (inputSize <= 0)
2010+
{
2011+
throw new InvalidOperationException("Input size must be greater than zero.");
2012+
}
2013+
2014+
if (outputSize <= 0)
2015+
{
2016+
throw new InvalidOperationException("Output size must be greater than zero.");
2017+
}
2018+
2019+
List<int> hiddenLayerSizes = new List<int>();
2020+
switch (architecture.Complexity)
2021+
{
2022+
case NetworkComplexity.Simple:
2023+
hiddenLayerSizes.Add((inputSize + outputSize) / 2);
2024+
break;
2025+
case NetworkComplexity.Medium:
2026+
hiddenLayerSizes.Add(inputSize * 2);
2027+
hiddenLayerSizes.Add(inputSize);
2028+
break;
2029+
case NetworkComplexity.Deep:
2030+
hiddenLayerSizes.Add(inputSize * 2);
2031+
hiddenLayerSizes.Add(inputSize * 2);
2032+
hiddenLayerSizes.Add(inputSize);
2033+
break;
2034+
default:
2035+
hiddenLayerSizes.Add(inputSize);
2036+
break;
2037+
}
2038+
2039+
int firstHiddenLayerSize = hiddenLayerSizes.Count > 0 ? hiddenLayerSizes[0] : outputSize;
2040+
yield return new AiDotNet.UncertaintyQuantification.Layers.BayesianDenseLayer<T>(inputSize, firstHiddenLayerSize, new ReLUActivation<T>() as IActivationFunction<T>);
2041+
yield return new ActivationLayer<T>([firstHiddenLayerSize], new ReLUActivation<T>() as IActivationFunction<T>);
2042+
2043+
for (int i = 0; i < hiddenLayerSizes.Count - 1; i++)
2044+
{
2045+
int currentLayerSize = hiddenLayerSizes[i];
2046+
int nextLayerSize = hiddenLayerSizes[i + 1];
2047+
2048+
yield return new AiDotNet.UncertaintyQuantification.Layers.BayesianDenseLayer<T>(currentLayerSize, nextLayerSize, new ReLUActivation<T>() as IActivationFunction<T>);
2049+
yield return new ActivationLayer<T>([nextLayerSize], new ReLUActivation<T>() as IActivationFunction<T>);
2050+
}
2051+
2052+
if (hiddenLayerSizes.Count > 0)
2053+
{
2054+
int lastHiddenLayerSize = hiddenLayerSizes[hiddenLayerSizes.Count - 1];
2055+
yield return new AiDotNet.UncertaintyQuantification.Layers.BayesianDenseLayer<T>(lastHiddenLayerSize, outputSize, new SoftmaxActivation<T>() as IActivationFunction<T>);
2056+
}
2057+
else
2058+
{
2059+
yield return new AiDotNet.UncertaintyQuantification.Layers.BayesianDenseLayer<T>(inputSize, outputSize, new SoftmaxActivation<T>() as IActivationFunction<T>);
2060+
}
2061+
2062+
yield return new ActivationLayer<T>(new[] { outputSize }, new SoftmaxActivation<T>() as IActivationFunction<T>);
2063+
}
2064+
19922065
/// <summary>
19932066
/// Creates a default configuration of layers for a Liquid State Machine (LSM) neural network.
19942067
/// </summary>
@@ -2289,4 +2362,4 @@ public static IEnumerable<ILayer<T>> CreateDefaultFeedForwardLayers(
22892362

22902363
yield return new DenseLayer<T>(hiddenLayerSize, architecture.OutputSize, outputActivation);
22912364
}
2292-
}
2365+
}

src/Models/Options/UncertaintyQuantificationOptions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,20 @@ public sealed class UncertaintyQuantificationOptions
7575
/// calibrated probabilities as the prediction output from uncertainty APIs.
7676
/// </remarks>
7777
public bool EnableTemperatureScaling { get; set; } = true;
78+
79+
/// <summary>
80+
/// Gets or sets the number of independently trained models used for deep ensemble uncertainty estimation.
81+
/// </summary>
82+
/// <remarks>
83+
/// This value is only used when <see cref="Method"/> is <see cref="UncertaintyQuantificationMethod.DeepEnsemble"/>.
84+
/// </remarks>
85+
public int DeepEnsembleSize { get; set; } = 5;
86+
87+
/// <summary>
88+
/// Gets or sets the standard deviation of the initial parameter perturbation applied when constructing ensemble members.
89+
/// </summary>
90+
/// <remarks>
91+
/// This helps ensure ensemble members do not collapse to identical solutions when created from a shared base model.
92+
/// </remarks>
93+
public double DeepEnsembleInitialNoiseStdDev { get; set; } = 0.01;
7894
}

src/Models/Results/PredictionModelResult.cs

Lines changed: 206 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,14 @@ internal void SetUncertaintyCalibrationArtifacts(UncertaintyCalibrationArtifacts
447447
TemperatureScalingTemperature = artifacts.TemperatureScalingTemperature;
448448
}
449449

450+
internal void SetDeepEnsembleModels(List<IFullModel<T, TInput, TOutput>> models)
451+
{
452+
_deepEnsembleModels = models ?? throw new ArgumentNullException(nameof(models));
453+
}
454+
455+
[JsonProperty]
456+
private List<IFullModel<T, TInput, TOutput>>? _deepEnsembleModels;
457+
450458
/// <summary>
451459
/// Initializes a new instance of the PredictionModelResult class with default values.
452460
/// </summary>
@@ -562,7 +570,7 @@ public TOutput Predict(TInput newData)
562570
/// </summary>
563571
/// <param name="newData">Input data to predict.</param>
564572
/// <param name="numSamples">Optional number of stochastic samples to draw (overrides configured defaults).</param>
565-
/// <returns>A tuple containing the mean prediction and an uncertainty estimate (variance).</returns>
573+
/// <returns>An uncertainty-augmented prediction result.</returns>
566574
/// <remarks>
567575
/// <para>
568576
/// When uncertainty quantification is enabled, this method performs multiple stochastic forward passes and
@@ -610,6 +618,16 @@ public UncertaintyPredictionResult<T, TOutput> PredictWithUncertainty(TInput new
610618
return PredictWithConformal(newData, uq, method);
611619
}
612620

621+
if (method == UncertaintyQuantificationMethod.DeepEnsemble)
622+
{
623+
return PredictWithDeepEnsemble(newData, uq, method);
624+
}
625+
626+
if (method == UncertaintyQuantificationMethod.BayesianNeuralNetwork)
627+
{
628+
return PredictWithBayesianNeuralNetwork(newData, uq, method);
629+
}
630+
613631
var effectiveSamples = numSamples ?? uq.NumSamples;
614632
if (effectiveSamples < 1)
615633
{
@@ -637,7 +655,9 @@ public UncertaintyPredictionResult<T, TOutput> PredictWithUncertainty(TInput new
637655
{
638656
var (meanTensor, varianceTensor, predictiveEntropy, mutualInformation) = ComputeMonteCarloMomentsAndMetrics(
639657
normalizedNewData,
640-
effectiveSamples);
658+
effectiveSamples,
659+
mcDropoutLayers,
660+
uq.RandomSeed);
641661

642662
var meanOutput = ConvertFromTensor(meanTensor);
643663
var denormalizedMean = NormalizationInfo.Normalizer.Denormalize(meanOutput, NormalizationInfo.YParams);
@@ -713,6 +733,162 @@ private UncertaintyPredictionResult<T, TOutput> PredictWithConformal(
713733
classificationSet: classificationSet);
714734
}
715735

736+
private UncertaintyPredictionResult<T, TOutput> PredictWithDeepEnsemble(
737+
TInput newData,
738+
UncertaintyQuantificationOptions uq,
739+
UncertaintyQuantificationMethod method)
740+
{
741+
if (_deepEnsembleModels is not { Count: > 0 })
742+
{
743+
var deterministic = Predict(newData);
744+
var fallbackMetrics = CreateDefaultUncertaintyMetrics(deterministic, mutualInformation: null);
745+
return new UncertaintyPredictionResult<T, TOutput>(
746+
methodUsed: method,
747+
prediction: deterministic,
748+
variance: CreateZeroLike(deterministic),
749+
metrics: fallbackMetrics);
750+
}
751+
752+
var (normalizedNewData, _) = NormalizationInfo.Normalizer!.NormalizeInput(newData);
753+
754+
var numOps = MathHelper.GetNumericOperations<T>();
755+
var samples = new List<Tensor<T>>(_deepEnsembleModels.Count);
756+
757+
for (int i = 0; i < _deepEnsembleModels.Count; i++)
758+
{
759+
var normalizedPrediction = _deepEnsembleModels[i].Predict(normalizedNewData);
760+
samples.Add(ConversionsHelper.ConvertToTensor<T>(normalizedPrediction!).Clone());
761+
}
762+
763+
var first = samples[0];
764+
var firstVector = first.ToVector();
765+
var (treatAsProbabilities, batch, classes) = InferProbabilityDistributionLayout(first, firstVector);
766+
767+
if (treatAsProbabilities && classes > 1 && HasTemperatureScaling)
768+
{
769+
for (int i = 0; i < samples.Count; i++)
770+
{
771+
samples[i] = ApplyTemperatureScalingToProbabilityTensor(samples[i], TemperatureScalingTemperature, batch, classes);
772+
}
773+
}
774+
775+
var (meanTensor, varianceTensor) = ComputeMeanAndVariance(samples);
776+
777+
var meanOutput = ConvertFromTensor(meanTensor);
778+
var denormalizedMean = NormalizationInfo.Normalizer!.Denormalize(meanOutput, NormalizationInfo.YParams);
779+
780+
if (uq.DenormalizeUncertainty)
781+
{
782+
varianceTensor = DenormalizeVarianceIfSupported(varianceTensor, NormalizationInfo.YParams);
783+
}
784+
785+
var varianceOutput = ConvertFromTensor(varianceTensor);
786+
787+
var predictiveEntropy = CreateZeroVectorTensor(batch);
788+
var mutualInformation = CreateZeroVectorTensor(batch);
789+
790+
if (treatAsProbabilities && classes > 1)
791+
{
792+
var expectedEntropySum = new Vector<T>(batch);
793+
foreach (var sample in samples)
794+
{
795+
var sampleEntropy = ComputePerSampleEntropy(sample.ToVector(), batch, classes);
796+
for (int b = 0; b < batch; b++)
797+
{
798+
expectedEntropySum[b] = numOps.Add(expectedEntropySum[b], sampleEntropy[b]);
799+
}
800+
}
801+
802+
var meanVector = meanTensor.ToVector();
803+
var predictiveEntropyVec = ComputePerSampleEntropy(meanVector, batch, classes);
804+
var expectedEntropyVec = new Vector<T>(batch);
805+
for (int b = 0; b < batch; b++)
806+
{
807+
expectedEntropyVec[b] = numOps.Divide(expectedEntropySum[b], numOps.FromDouble(samples.Count));
808+
}
809+
810+
var miVec = new Vector<T>(batch);
811+
for (int b = 0; b < batch; b++)
812+
{
813+
var mi = numOps.Subtract(predictiveEntropyVec[b], expectedEntropyVec[b]);
814+
if (numOps.LessThan(mi, numOps.Zero))
815+
{
816+
mi = numOps.Zero;
817+
}
818+
miVec[b] = mi;
819+
}
820+
821+
predictiveEntropy = new Tensor<T>([batch], predictiveEntropyVec);
822+
mutualInformation = new Tensor<T>([batch], miVec);
823+
}
824+
825+
var metrics = CreateDefaultUncertaintyMetrics(denormalizedMean, mutualInformation);
826+
metrics[PredictiveEntropyMetricKey] = predictiveEntropy;
827+
metrics[MutualInformationMetricKey] = mutualInformation;
828+
829+
return new UncertaintyPredictionResult<T, TOutput>(
830+
methodUsed: method,
831+
prediction: denormalizedMean,
832+
variance: varianceOutput,
833+
metrics: metrics);
834+
}
835+
836+
private UncertaintyPredictionResult<T, TOutput> PredictWithBayesianNeuralNetwork(
837+
TInput newData,
838+
UncertaintyQuantificationOptions uq,
839+
UncertaintyQuantificationMethod method)
840+
{
841+
var estimator = Model as AiDotNet.UncertaintyQuantification.Interfaces.IUncertaintyEstimator<T>;
842+
if (estimator == null)
843+
{
844+
var deterministic = Predict(newData);
845+
var fallbackMetrics = CreateDefaultUncertaintyMetrics(deterministic, mutualInformation: null);
846+
return new UncertaintyPredictionResult<T, TOutput>(
847+
methodUsed: method,
848+
prediction: deterministic,
849+
variance: CreateZeroLike(deterministic),
850+
metrics: fallbackMetrics);
851+
}
852+
853+
var (normalizedNewData, _) = NormalizationInfo.Normalizer!.NormalizeInput(newData);
854+
if (normalizedNewData == null)
855+
{
856+
throw new InvalidOperationException("Normalizer returned null input.");
857+
}
858+
var inputTensor = ConversionsHelper.ConvertToTensor<T>(normalizedNewData).Clone();
859+
860+
var uqResult = estimator.PredictWithUncertainty(inputTensor);
861+
862+
var meanOutput = ConvertFromTensor(uqResult.Prediction);
863+
var denormalizedMean = NormalizationInfo.Normalizer!.Denormalize(meanOutput, NormalizationInfo.YParams);
864+
865+
var varianceTensor = uqResult.Variance != null
866+
? uqResult.Variance.Clone()
867+
: new Tensor<T>(
868+
uqResult.Prediction.Shape,
869+
Vector<T>.CreateDefault(uqResult.Prediction.Length, MathHelper.GetNumericOperations<T>().Zero));
870+
if (uq.DenormalizeUncertainty)
871+
{
872+
varianceTensor = DenormalizeVarianceIfSupported(varianceTensor, NormalizationInfo.YParams);
873+
}
874+
var varianceOutput = ConvertFromTensor(varianceTensor);
875+
876+
var metrics = CreateDefaultUncertaintyMetrics(denormalizedMean, mutualInformation: null);
877+
if (uqResult.Metrics != null)
878+
{
879+
foreach (var kvp in uqResult.Metrics)
880+
{
881+
metrics[kvp.Key] = kvp.Value;
882+
}
883+
}
884+
885+
return new UncertaintyPredictionResult<T, TOutput>(
886+
methodUsed: method,
887+
prediction: denormalizedMean,
888+
variance: varianceOutput,
889+
metrics: metrics);
890+
}
891+
716892
private static (int batch, int classes) InferBatchAndClasses(Tensor<T> probabilities, int configuredClasses)
717893
{
718894
var classes = configuredClasses > 0
@@ -839,13 +1015,22 @@ private static Tensor<T> CreateZeroVectorTensor(int length)
8391015
}
8401016

8411017
private (Tensor<T> mean, Tensor<T> variance, Tensor<T> predictiveEntropy, Tensor<T> mutualInformation)
842-
ComputeMonteCarloMomentsAndMetrics(TInput normalizedNewData, int numSamples)
1018+
ComputeMonteCarloMomentsAndMetrics(
1019+
TInput normalizedNewData,
1020+
int numSamples,
1021+
IReadOnlyList<AiDotNet.UncertaintyQuantification.Layers.MCDropoutLayer<T>> mcDropoutLayers,
1022+
int? randomSeed)
8431023
{
8441024
var numOps = MathHelper.GetNumericOperations<T>();
8451025

8461026
var samples = new List<Tensor<T>>(capacity: numSamples);
8471027
for (int i = 0; i < numSamples; i++)
8481028
{
1029+
if (randomSeed.HasValue)
1030+
{
1031+
ResetMonteCarloDropoutRng(mcDropoutLayers, randomSeed.Value, i);
1032+
}
1033+
8491034
var normalizedPrediction = Model!.Predict(normalizedNewData);
8501035
samples.Add(ConversionsHelper.ConvertToTensor<T>(normalizedPrediction!).Clone());
8511036
}
@@ -905,6 +1090,24 @@ private static Tensor<T> CreateZeroVectorTensor(int length)
9051090
return (meanTensor, varianceTensor, predictiveEntropy, mutualInformation);
9061091
}
9071092

1093+
private static void ResetMonteCarloDropoutRng(
1094+
IReadOnlyList<AiDotNet.UncertaintyQuantification.Layers.MCDropoutLayer<T>> layers,
1095+
int baseSeed,
1096+
int sampleIndex)
1097+
{
1098+
unchecked
1099+
{
1100+
for (int i = 0; i < layers.Count; i++)
1101+
{
1102+
// Mix base seed, sample index, and layer index to get deterministic but distinct streams.
1103+
var mixed = baseSeed;
1104+
mixed = (mixed * 1000003) ^ (sampleIndex + 1);
1105+
mixed = (mixed * 1009) ^ (i + 1);
1106+
layers[i].ResetRng(mixed);
1107+
}
1108+
}
1109+
}
1110+
9081111
private TOutput ApplyTemperatureScalingToOutputProbabilities(TOutput outputProbabilities, T temperature)
9091112
{
9101113
var tensor = ConversionsHelper.ConvertToTensor<T>(outputProbabilities!).Clone();

src/NeuralNetworks/Layers/ActivationLayer.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace AiDotNet.NeuralNetworks.Layers;
55
/// <para>
66
/// Activation functions introduce non-linearity to neural networks. Non-linearity means the output isn't
77
/// simply proportional to the input (like y = 2x). Instead, it can follow curves or more complex patterns.
8-
/// Without non-linearity, a neural networkno matter how many layerswould behave just like a single layer,
8+
/// Without non-linearity, a neural networkno matter how many layerswould behave just like a single layer,
99
/// severely limiting what it can learn.
1010
/// </para>
1111
/// <para>
@@ -304,8 +304,9 @@ private Tensor<T> ApplyVectorActivation(Tensor<T> input)
304304
/// </remarks>
305305
private Tensor<T> BackwardScalarActivation(Tensor<T> outputGradient)
306306
{
307-
return _lastInput!.Transform((x, indices) =>
308-
NumOps.Multiply(ScalarActivation!.Derivative(x), outputGradient[indices]));
307+
var gradVector = outputGradient.ToVector();
308+
return _lastInput!.Transform((x, i) =>
309+
NumOps.Multiply(ScalarActivation!.Derivative(x), gradVector[i]));
309310
}
310311

311312

@@ -467,4 +468,4 @@ public override void ResetState()
467468
{
468469
_lastInput = null;
469470
}
470-
}
471+
}

0 commit comments

Comments
 (0)