diff --git a/MyIA.Trading.Backtester.Tests/TradingModelsConfigTests.cs b/MyIA.Trading.Backtester.Tests/TradingModelsConfigTests.cs index 35fae59307..a915e6f446 100644 --- a/MyIA.Trading.Backtester.Tests/TradingModelsConfigTests.cs +++ b/MyIA.Trading.Backtester.Tests/TradingModelsConfigTests.cs @@ -8,9 +8,8 @@ namespace MyIA.Trading.Backtester.Tests { /// - /// Tranche 3 (EPIC #7357 geste 3) : couche ML config portee. + /// Tranche 3 (EPIC #7357 geste 3) : couche ML config portee, mise a jour tranche 4. /// - cablage TradingModelsConfig.ModelType -> CurrentModelConfig (AutoML / Svm) - /// - garde explicite du port SVM en attente (tranche 4) : ApplicationException, pas d'echec silencieux /// - formats GetModelName des deux configs (logique upstream verbatim) /// - AutoMlTradingModel.Predict de bout en bout : pipeline ML.NET inline + prediction /// sur samples separables (voie reelle du port, pas un mock) @@ -31,18 +30,20 @@ public void ModelType_MulticlassSvm_ReturnsSvmConfig() Assert.IsType(config.CurrentModelConfig); } + /// + /// Tranche 4 : la garde « port en attente » a disparu avec le stub — TrainModel + /// entraine desormais un vrai SVM a noyau. La substance de ce port est couverte + /// par TradingSvmModelConfigTests ; ce qui reste a verifier ici est que plus + /// aucune ApplicationException de port pending ne subsiste dans le type. + /// [Fact] - public void SvmModelConfig_TrainModel_ThrowsExplicitPendingPortException() + public void SvmModelConfig_NoLongerCarriesThePendingPortGuard() { var config = new TradingSvmModelConfig(); - var dataConfig = BuildDataConfig(); - double testError = -1; - var ex = Assert.Throws( - () => config.TrainModel(Console.WriteLine, dataConfig, ref testError)); - - Assert.Contains("tranche 4", ex.Message); - Assert.Contains("AutoML", ex.Message); + // Le noyau par defaut est instanciable : le chemin d'entrainement est cable, + // il ne leve plus « port SVM en attente ». + Assert.NotNull(config.GetKernel(config.Kernel)); } [Fact] @@ -64,18 +65,30 @@ public void GetModelName_Svm_ContainsKernelAndComplexity() Assert.Contains("-kernel-InverseMultiquadric-complexity--1-Model.bin", name); } + /// + /// La delegation TradingTrainingConfig -> ModelsConfig.CurrentModelConfig etait + /// prouvee en tranche 3 par l'exception du stub. Le stub porte, elle se prouve + /// sur GetModelName, qui emprunte exactement le meme aiguillage CurrentModelConfig + /// et reste deterministe (pur calcul de chaine, aucune lecture disque). + /// [Fact] - public void TradingTrainingConfig_TrainModel_DelegatesToCurrentModelConfig() + public void TradingTrainingConfig_DelegatesToCurrentModelConfig() { - var trainingConfig = new TradingTrainingConfig + var svmConfig = new TradingTrainingConfig { + DataConfig = BuildDataConfig(), ModelsConfig = new TradingModelsConfig { ModelType = TradingModelType.MulticlassSvm } }; - double testError = -1; + var autoMlConfig = new TradingTrainingConfig + { + DataConfig = BuildDataConfig(), + ModelsConfig = new TradingModelsConfig { ModelType = TradingModelType.AutoML } + }; - // La delegation doit surfacer la garde explicite du port SVM pending, pas l'etouffer. - Assert.Throws( - () => trainingConfig.TrainModel(Console.WriteLine, ref testError)); + // Le nom rendu est celui de la config selectionnee, pas un defaut commun : + // l'aiguillage est donc bien traverse. + Assert.Contains("-kernel-", svmConfig.GetModelName()); + Assert.Contains("-AutoML-", autoMlConfig.GetModelName()); } [Fact] diff --git a/MyIA.Trading.Backtester.Tests/TradingSvmModelConfigTests.cs b/MyIA.Trading.Backtester.Tests/TradingSvmModelConfigTests.cs new file mode 100644 index 0000000000..0cf0c0b197 --- /dev/null +++ b/MyIA.Trading.Backtester.Tests/TradingSvmModelConfigTests.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Accord.MachineLearning.VectorMachines; +using Accord.MachineLearning.VectorMachines.Learning; +using Accord.Statistics.Analysis; +using Accord.Statistics.Kernels; +using Xunit; + +namespace MyIA.Trading.Backtester.Tests +{ + /// + /// Tranche 4 (EPIC #7357 geste 3) : corps SVM porte depuis le fork upstream. + /// Ces tests portent sur la SUBSTANCE du port — que le SVM a noyau apprenne + /// reellement une frontiere non lineairement separable — et pas seulement sur le + /// cablage. Chaque assertion positive est appariee a une mutation qui ne change + /// QUE le champ teste (le noyau), et qui doit la faire tomber : un test qui ne + /// peut pas echouer ne mesure rien. + /// + public sealed class TradingSvmModelConfigTests + { + /// + /// XOR : quatre paquets aux coins, classe 1 sur une diagonale, classe 0 sur + /// l'autre. Aucun hyperplan ne les separe — c'est precisement le probleme que + /// l'astuce du noyau resout, et le cas degenere que Prong B interdit d'eviter. + /// + private static List BuildXorSamples() + { + var samples = new List(); + var offsets = new[] { -0.06, -0.02, 0.02, 0.06 }; + foreach (var dx in offsets) + { + foreach (var dy in offsets) + { + samples.Add(BuildSample(0.0 + dx, 0.0 + dy, 0)); + samples.Add(BuildSample(1.0 + dx, 1.0 + dy, 0)); + samples.Add(BuildSample(0.0 + dx, 1.0 + dy, 1)); + samples.Add(BuildSample(1.0 + dx, 0.0 + dy, 1)); + } + } + return samples; + } + + private static TradingTrainingSample BuildSample(double x, double y, int label) + { + return new TradingTrainingSample + { + Inputs = new List { x, y }, + Output = label + }; + } + + /// + /// Bruit deterministe et portable. Volontairement PAS System.Random : son + /// algorithme n'est pas garanti stable d'une version de runtime a l'autre, et un + /// test de calibration doit rendre le meme nombre demain. + /// + private static double Jitter(int i, int salt, double spread) + { + unchecked + { + uint h = (uint)(i * 2654435761u + salt * 40503u); + h ^= h >> 13; h *= 2246822519u; h ^= h >> 16; + return ((h % 1000) / 1000.0 - 0.5) * spread; + } + } + + /// + /// XOR aux paquets ELARGIS jusqu'au chevauchement (spread 1.1). C'est la seule + /// famille sur laquelle la calibration a quelque chose a calibrer : sur un XOR + /// net, mesure, la complexite d'amorce 0.0001 classe deja parfaitement, donc la + /// conserver est le bon resultat — et une garde posee la-dessus serait aveugle. + /// + private static List BuildOverlappingXorSamples() + { + var samples = new List(); + const double spread = 1.1; + for (int i = 0; i < 30; i++) + { + void Add(double cx, double cy, int label, int salt) + { + samples.Add(BuildSample(cx + Jitter(i, salt, spread), cy + Jitter(i, salt + 1, spread), label)); + } + Add(0, 0, 0, 1); Add(1, 1, 0, 3); Add(0, 1, 1, 5); Add(1, 0, 1, 7); + } + return samples; + } + + private static TradingTrainTestData BuildOverlappingXorData() + { + var samples = BuildOverlappingXorSamples(); + return new TradingTrainTestData(samples.Count, samples.Count) + { + Training = samples, + Test = samples + }; + } + + private static MulticlassSupportVectorMachine Learn(IKernel kernel, double complexity, double[][] x, int[] y) + { + var teacher = new MulticlassSupportVectorLearning() + { + Learner = (p) => new SequentialMinimalOptimization() + { + Complexity = complexity, + UseKernelEstimation = true, + Kernel = kernel + } + }; + return teacher.Learn(x, y); + } + + /// + /// Le port entraine un vrai SVM a noyau : sur XOR, l'InverseMultiquadric du + /// fork atteint 0 erreur, et le modele est consommable via l'enveloppe + /// ITradingModel portee (ClassifierTradingModel.Predict). + /// + [Fact] + public void SvmTradingModel_LearnsXorAndPredictsThroughTradingModelWrapper() + { + var config = new TradingSvmModelConfig(); + var samples = BuildXorSamples(); + var x = samples.GetInputMatrix(); + var y = samples.GetOutputClasses(); + + var machine = Learn(config.GetKernel(KnownKernel.InverseMultiquadric), 1.0, x, y); + + Assert.Equal(0.0, GeneralConfusionMatrix.Estimate(machine, x, y).Error, 10); + + // L'enveloppe portee doit rendre les memes decisions, echantillon par echantillon. + var model = new SvmTradingModel { Svm = machine }; + var predicted = model.Predict(samples); + + Assert.Equal(samples.Count, predicted.Count); + for (int i = 0; i < samples.Count; i++) + { + Assert.Equal(samples[i].Output, predicted[i].Output); + } + } + + /// + /// MUTATION DE CONTROLE du test precedent : meme donnees, meme pipeline, meme + /// complexite — seul le noyau change (Linear). XOR n'etant pas lineairement + /// separable, l'erreur DOIT etre non nulle. Sans ce negatif, le test ci-dessus + /// serait compatible avec un probleme trivial ou n'importe quoi passe. + /// + [Fact] + public void LinearKernel_FailsOnXor_ProvingTheKernelIsWhatCarriesTheResult() + { + var samples = BuildXorSamples(); + var x = samples.GetInputMatrix(); + var y = samples.GetOutputClasses(); + + var machine = Learn(new Linear(), 1.0, x, y); + + Assert.True( + GeneralConfusionMatrix.Estimate(machine, x, y).Error > 0.0, + "Un noyau lineaire ne peut pas separer XOR : une erreur nulle signalerait un probleme degenere."); + } + + /// + /// GetKernel est la surface de substitution du port : chaque membre de + /// KnownKernel doit rendre le noyau Accord correspondant, et un membre hors + /// domaine doit lever plutot que retomber silencieusement sur un defaut. + /// + [Fact] + public void GetKernel_MapsEveryKnownKernelMemberToItsAccordCounterpart() + { + var config = new TradingSvmModelConfig(); + + Assert.IsType(config.GetKernel(KnownKernel.InverseMultiquadric)); + Assert.IsType(config.GetKernel(KnownKernel.NormalizedPolynomial3)); + Assert.IsType(config.GetKernel(KnownKernel.Polynomial3)); + Assert.IsType(config.GetKernel(KnownKernel.TStudent2)); + + // Tous les membres declares sont couverts : si quelqu'un en ajoute un sans + // etendre GetKernel, ce compte tombe et le test le dit. + Assert.Equal(4, Enum.GetValues(typeof(KnownKernel)).Length); + + Assert.Throws(() => config.GetKernel((KnownKernel)999)); + } + + /// + /// Les degres passes aux noyaux parametres sont ceux que le NOM du membre + /// annonce. Une inversion Polynomial(3)/TStudent(2) serait invisible au test + /// de type ci-dessus. + /// + [Fact] + public void GetKernel_UsesTheDegreeAnnouncedByTheMemberName() + { + var config = new TradingSvmModelConfig(); + + Assert.Equal(3, ((Polynomial)config.GetKernel(KnownKernel.Polynomial3)).Degree); + Assert.Equal(3, ((NormalizedPolynomial)config.GetKernel(KnownKernel.NormalizedPolynomial3)).Degree); + Assert.Equal(2, ((TStudent)config.GetKernel(KnownKernel.TStudent2)).Degree); + } + + /// + /// L'ordre des membres de KnownKernel est celui deja committe en tranche 3, PAS + /// celui de l'upstream (qui place TStudent2 en 2e position). Ce test epingle les + /// valeurs entieres : les reordonner casserait toute config deja serialisee, et + /// le ferait silencieusement. Ecart delibere 2, cf en-tete de TradingSvmModelConfig. + /// + [Fact] + public void KnownKernel_OrdinalsArePinned_BecauseTheyAreSerialised() + { + Assert.Equal(0, (int)KnownKernel.InverseMultiquadric); + Assert.Equal(1, (int)KnownKernel.NormalizedPolynomial3); + Assert.Equal(2, (int)KnownKernel.Polynomial3); + Assert.Equal(3, (int)KnownKernel.TStudent2); + } + + /// + /// GARDE DE REGRESSION DE L'ECART 3. Le corps upstream de CalibrateComplexity + /// remplacait `teacher` dans le bloc sous limite de temps sans jamais appeler + /// Learn : `machine` restait null, la NRE suivante etait avalee, testError ne + /// bougeait jamais de double.MaxValue, et la methode renvoyait TOUJOURS sa + /// valeur initiale 0.0001 — sous un log affirmant « SVM complexity calibrated ». + /// + /// La garde est posee sur des donnees ou elle DISCRIMINE, ce qui a ete mesure et + /// non suppose : sur ce jeu, le corps repare rend 706.88 et le corps upstream + /// 0.0001 ; sur un XOR net les deux rendent 0.0001 et la garde ne prouverait + /// rien. Un controle qu'on n'a pas vu echouer ne controle pas. + /// + [Fact] + public void CalibrateComplexity_ActuallyExploresAndDoesNotReturnItsSeedValue() + { + var config = new TradingSvmModelConfig(); + + var complexity = config.CalibrateComplexity( + BuildOverlappingXorData(), config.GetKernel(KnownKernel.InverseMultiquadric)); + + Assert.True( + complexity > 0.0001, + $"CalibrateComplexity a rendu {complexity}, sa valeur d'amorce : le bloc d'entrainement n'a pas tourne (defaut upstream, ecart 3)."); + } + + /// + /// La calibration doit AMELIORER ce qu'elle optimise. Le score de + /// TradingModelConfig.TestModel (bad - good, plus bas = meilleur) doit etre + /// strictement meilleur a la complexite calibree qu'a la complexite d'amorce — + /// sans quoi la boucle explorerait sans jamais choisir. + /// + [Fact] + public void CalibratedComplexity_ScoresBetterThanTheSeedComplexity() + { + var config = new TradingSvmModelConfig(); + var data = BuildOverlappingXorData(); + var x = data.Training.GetInputMatrix(); + var y = data.Training.GetOutputClasses(); + var kernel = config.GetKernel(KnownKernel.InverseMultiquadric); + + var calibrated = config.CalibrateComplexity(data, kernel); + + double seedScore = TradingModelConfig.TestModel( + data, new ClassifierTradingModel { Classifier = Learn(kernel, 0.0001, x, y) }); + double calibratedScore = TradingModelConfig.TestModel( + data, new ClassifierTradingModel { Classifier = Learn(kernel, calibrated, x, y) }); + + Assert.True( + calibratedScore < seedScore, + $"score calibre {calibratedScore} vs amorce {seedScore} : la calibration n'a rien ameliore."); + } + } +} diff --git a/MyIA.Trading.Backtester/MyIA.Trading.Backtester.csproj b/MyIA.Trading.Backtester/MyIA.Trading.Backtester.csproj index c1770cfc5c..b33bf5b963 100644 --- a/MyIA.Trading.Backtester/MyIA.Trading.Backtester.csproj +++ b/MyIA.Trading.Backtester/MyIA.Trading.Backtester.csproj @@ -5,7 +5,10 @@ vers CoursIA net9.0 — EPIC #7357 geste 3, tranche 1 (scaffold). Substitutions mesurees (docs/reference/backtester-e2-cadrage.md, geste 2) : - HintPath DLL Aricie/DNN : abandonnees, socle = MyIA.AI.Shared - - Accord.MachineLearning 3.8.2-alpha : remplace par ML.NET (mapping tranche suivante) + - Accord.MachineLearning 3.8.2-alpha : CONSERVE (tranche 4). Le cadrage anticipait une + substitution vers ML.NET ; mesure faite, elle est sans objet — ML.NET n'expose pas de + SVM a noyau, et Accord 3.8.2-alpha restore/compile/execute tel quel sur net9.0 (0 + avertissement, pas de NU1701). Verdict SOTA-OK, cf docs/reference/backtester-e2-svm-kernel.md. - Microsoft.ML.AutoML 0.20.1 : paire preview 0.24.0-preview.26160.2 (seule paire teste bout-en-bout, cadrage 2.2), figee par le test d'integration MyIA.Trading.Backtester.Tests Les PackageReferences Flee/Markdig seront cablees par les tranches portant les fichiers @@ -26,6 +29,18 @@ + + + + + + + + diff --git a/MyIA.Trading.Backtester/TradingSvmModelConfig.cs b/MyIA.Trading.Backtester/TradingSvmModelConfig.cs index 5167c8111e..9242a20790 100644 --- a/MyIA.Trading.Backtester/TradingSvmModelConfig.cs +++ b/MyIA.Trading.Backtester/TradingSvmModelConfig.cs @@ -1,15 +1,38 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Xml.Serialization; +using Accord.MachineLearning; +using Accord.MachineLearning.VectorMachines; +using Accord.MachineLearning.VectorMachines.Learning; +using Accord.Statistics.Analysis; +using Accord.Statistics.Kernels; +using Newtonsoft.Json; namespace MyIA.Trading.Backtester { - // Port minimal tranche 3 (EPIC #7357 geste 3) : la logique de nommage est le code - // upstream verbatim (Kernel/Complexity inclus), mais l'entrainement SVM n'est pas - // encore porte — la substitution kernel-SVM Accord -> ML.NET/sklearn (cadrage - // geste 2, docs/reference/backtester-e2-cadrage.md) fera l'objet de la tranche 4. - // L'exception est explicite pour qu'aucun appelant ne puisse confondre ce stub - // avec un entrainement reussi. + // Port tranche 4 (EPIC #7357) : le corps SVM du fork MyIntelligenceAgency/Lean + // (branche MyIABacktesting_integration, sha 612dddf9) est porte ici. La tranche 3 + // avait laisse TrainModel en stub explicite ; il entraine desormais reellement. + // + // Substitution : AUCUNE. Le SVM a noyau du fork tourne tel quel sur net9.0 avec + // Accord.NET 3.8.2-alpha (la version du fork) — restore + build + execution + // verifies (voir docs/reference/backtester-e2-svm-kernel.md et le corps de la PR). + // C'est la resolution de l'hypothese « Accord -> ML.NET/sklearn » du commentaire + // de tranche 3 : ML.NET n'a pas de SVM a noyau, et il n'a pas eu a en fournir un. + // + // Trois ecarts DELIBERES avec l'upstream, tous documentes ici : + // 1. `using DotNetNuke.Services.Log.EventLog` supprime — import jamais utilise + // dans le corps (1 seule occurrence : le using lui-meme). C'est l'abandon des + // 6 DLL Aricie PKP prescrit par l'Option C tranchee par le user le 2026-07-19. + // 2. L'ordre des membres de `KnownKernel` est celui deja committe en tranche 3, + // PAS celui de l'upstream (qui place TStudent2 en 2e). Reordonner changerait + // silencieusement la valeur entiere de chaque membre pour toute config deja + // serialisee : le nom est stable, l'ordinal ne l'est pas. + // 3. `CalibrateComplexity` : le bloc sous limite de temps est repare. Voir le + // commentaire detaille sur la methode — en l'etat upstream elle ne calibre + // rien et renvoie toujours sa valeur initiale. public enum KnownKernel { InverseMultiquadric, @@ -18,6 +41,51 @@ public enum KnownKernel TStudent2 } + /// + /// Enveloppe autour d'un classifieur Accord quelconque : + /// convertit les echantillons de trading en matrice d'entrees, delegue la decision au + /// classifieur, et re-emballe les sorties dans des . + /// + public class ClassifierTradingModel : ITradingModel + { + + [XmlIgnore] + [JsonIgnore] + public IClassifier Classifier { get; set; } + + public IList Predict(IList inputs) + { + var inputsMatrix = inputs.GetInputMatrix(); + var output = Classifier.Decide(inputsMatrix); + + var toReturn = new List(inputs.Count); + for (var index = 0; index < inputs.Count; index++) + { + var sample = inputs[index]; + var newSample = new TradingTrainingSample() { Inputs = sample.Inputs, Sample = sample.Sample }; + + newSample.Output = output[index]; + toReturn.Add(newSample); + } + + return toReturn; + } + + } + + /// + /// Specialisation SVM multiclasse de . + /// + public class SvmTradingModel : ClassifierTradingModel + { + + public MulticlassSupportVectorMachine Svm + { + get => (MulticlassSupportVectorMachine)Classifier; + set => Classifier = value; + } + } + public class TradingSvmModelConfig : TradingModelConfig { @@ -30,12 +98,414 @@ public override string GetModelName(TradingTrainingDataConfig dataConfig) public override ITradingModel TrainModel(Action logger, TradingTrainingDataConfig dataConfig, ref double testError) { - throw new ApplicationException( - "TradingSvmModelConfig.TrainModel : port SVM en attente (EPIC #7357 tranche 4) — substitution kernel-SVM Accord->ML.NET/sklearn cadrée au geste 2. Utiliser TradingModelType.AutoML."); + var objSvm = TrainModelInternal(logger, dataConfig, ref testError); + if (objSvm != null) + { + return new SvmTradingModel() { Svm = objSvm }; + } + + return null; } public double Complexity { get; set; } = -1; public KnownKernel Kernel { get; set; } = KnownKernel.InverseMultiquadric; + + public IKernel GetKernel(KnownKernel objKnownKernel) + { + switch (objKnownKernel) + { + case KnownKernel.InverseMultiquadric: + return new InverseMultiquadric(); + case KnownKernel.NormalizedPolynomial3: + return new NormalizedPolynomial(3); + case KnownKernel.Polynomial3: + return new Polynomial(3); + case KnownKernel.TStudent2: + return new TStudent(2); + } + + throw new ApplicationException($"Kernel {objKnownKernel} not accounted for"); + } + + private static Dictionary> _CachedModels = new Dictionary>(); + + private MulticlassSupportVectorMachine TrainModelInternal(Action logger, TradingTrainingDataConfig dataConfig, ref double testingError) + { + + var modelName = GetModelName(dataConfig); + + MulticlassSupportVectorMachine toReturn = null; + var exceptionFileName = GetModelExceptionFileName(dataConfig); + if (File.Exists(exceptionFileName)) + { + logger($"Skipping previously failed Model: {exceptionFileName}"); + return null; + } + + if (File.Exists(modelName)) + { + logger($"Loading Saved Model: {modelName}"); + if (!_CachedModels.TryGetValue(modelName, out toReturn)) + { + toReturn = Accord.IO.Serializer.Load>(modelName); + _CachedModels[modelName] = toReturn; + } + } + + TradingTrainTestData data = null; + if (toReturn == null) + { + logger($"Training new Model: {modelName}"); + data = dataConfig.GetTrainingSets(logger); + var xTrain = data.Training.GetInputMatrix(); + var yTrain = data.Training.GetOutputClasses(); + var xTest = data.Test.GetInputMatrix(); + var yTest = data.Test.GetOutputClasses(); + + var objKernel = GetKernel(Kernel); + if (Complexity < 0) + { + Complexity = CalibrateComplexity(data, objKernel); + logger($"SVM complexity calibrated: {Complexity}"); + } + else + { + logger($"SVM complexity defined : {Complexity}"); + } + + var teacher = new MulticlassSupportVectorLearning() + { + Learner = (p) => new SequentialMinimalOptimization() + { + Complexity = Complexity, + UseKernelEstimation = true, + Kernel = objKernel + } + }; + + var startTime = DateTime.Now; + bool completed = ExecuteWithTimeLimit(TrainingTimeout, () => + { + try + { + logger($"Learn start"); + toReturn = teacher.Learn(xTrain, yTrain); + } + catch (Exception e) + { + string exceptionMessage = e.ToString(); + WriteExceptionFile(logger, exceptionFileName, exceptionMessage); + toReturn = null; + } + }); + + if (!completed) + { + var exceptionMessage = $"Training timed out: {DateTime.Now.Subtract(startTime).TotalSeconds}s"; + WriteExceptionFile(logger, exceptionFileName, exceptionMessage); + toReturn = null; + } + + if (toReturn == null) + { + return toReturn; + } + + double trainError = GeneralConfusionMatrix.Estimate(toReturn, xTrain, yTrain).Error; + double testError = GeneralConfusionMatrix.Estimate(toReturn, xTest, yTest).Error; + logger($"SVM train error: {trainError} | test error: {testError}"); + + // Calibration probabiliste des sorties (Platt scaling) : requise pour que + // MulticlassSupportVectorMachine.Probabilities() soit exploitable, cf SvmBenchmark. + var ml = new MulticlassSupportVectorLearning() + { + Model = toReturn, + + Learner = (p) => new ProbabilisticOutputCalibration() + { + Model = p.Model + } + }; + ml.Learn(xTrain, yTrain); + + if (!File.Exists(modelName)) + { + (new FileInfo(modelName)).Directory.Create(); + Accord.IO.Serializer.Save>(toReturn, modelName); + } + + logger("SVM Training finished"); + } + + if (dataConfig.EnsureModelTested && testingError < 0) + { + if (data == null) + { + data = dataConfig.GetTrainingSets(logger); + } + testingError = TestModel(data, new ClassifierTradingModel() { Classifier = toReturn }); + } + + return toReturn; + + } + + /// + /// Diagnostic console : entraine le modele puis detaille, echantillon par echantillon, + /// les bonnes decisions, faux positifs, faux negatifs et decisions opposees. + /// + public void SvmBenchmark(TradingTrainingDataConfig dataConfig, Action logger) + { + + var bitcoinTrain = dataConfig.GetTrainingSets(logger); + + logger("Entering SVM Benchmark"); + double testingError = double.MinValue; + var machine = TrainModelInternal(logger, dataConfig, ref testingError); + if (machine == null) + { + logger("SVM Benchmark aborted: no model was learnt"); + return; + } + + var xTrain = bitcoinTrain.Training.GetInputMatrix(); + var yTrain = bitcoinTrain.Training.GetOutputClasses(); + var xTest = bitcoinTrain.Test.GetInputMatrix(); + var yTest = bitcoinTrain.Test.GetOutputClasses(); + + double trainError = GeneralConfusionMatrix.Estimate(machine, xTrain, yTrain).Error; + double testError = GeneralConfusionMatrix.Estimate(machine, xTest, yTest).Error; + + var ml = new MulticlassSupportVectorLearning() + { + Model = machine, + + Learner = (p) => new ProbabilisticOutputCalibration() + { + Model = p.Model + } + }; + ml.Learn(xTrain, yTrain); + + logger("SVM Training finished"); + + var decisions = machine.Decide(xTrain); + var predictionProbas = machine.Probabilities(xTrain); + int j = 1, k = 1, l = 1, m = 1; + for (int i = 0; i < decisions.Length; i++) + { + if (decisions[i] != yTrain[i]) + { + logger($"Bad Prediction {i}: {JsonConvert.SerializeObject(decisions[i])} vs y: {yTrain[i].ToString()} ({JsonConvert.SerializeObject(predictionProbas[i])})"); + j++; + } + } + + logger($"SVM Training Error: {trainError}"); + logger($"SVM test Error: {testError}"); + + decisions = machine.Decide(xTest); + predictionProbas = machine.Probabilities(xTest); + j = 1; + for (int i = 0; i < decisions.Length; i++) + { + if (decisions[i] != yTest[i]) + { + if (yTest[i] != 0) + { + if (decisions[i] == 0) + { + logger($"False Negative {i}: {JsonConvert.SerializeObject(decisions[i])} vs y: {yTest[i].ToString()} ({JsonConvert.SerializeObject(predictionProbas[i])})"); + j++; + } + else + { + logger($"Bad Decision {i}: {JsonConvert.SerializeObject(decisions[i])} vs y: {yTest[i].ToString()} ({JsonConvert.SerializeObject(predictionProbas[i])})"); + m++; + } + } + else + { + logger($"False Positive {i}: {JsonConvert.SerializeObject(decisions[i])} vs y: {yTest[i].ToString()} ({JsonConvert.SerializeObject(predictionProbas[i])})"); + k++; + } + } + else + { + if (decisions[i] != 0) + { + logger($"Good decision {i}: {JsonConvert.SerializeObject(decisions[i])} vs y: {yTest[i].ToString()} ({JsonConvert.SerializeObject(predictionProbas[i])})"); + l++; + } + } + } + + logger($"Good decisions {l} vs {j + k + m} = {m} Bad + {k} False Positive + {j} False Negative"); + + } + + /// + /// Recherche la complexite (parametre C du SMO) minimisant le score de test, par + /// exploration geometrique puis bissection entre les bornes retenues. + /// + /// + /// ECART DELIBERE AVEC L'UPSTREAM (ecart 3, cf en-tete de fichier). Dans le fork, le + /// bloc passe a se contentait de + /// re-instancier `teacher` sans jamais appeler `Learn` : + /// + /// () => { try { teacher = new MulticlassSupportVectorLearning<IKernel>(); } catch { } } + /// + /// `machine` restait donc `null` a chaque iteration, `machine.Decide(xTrain)` levait une + /// NullReferenceException aussitot avalee par le `catch` englobant, et `testError` + /// n'etait jamais mis a jour : `testError < currentResult` etant toujours faux + /// (double.MaxValue), `bestComplexity` conservait sa valeur initiale. La methode + /// renvoyait donc TOUJOURS 0.0001, tout en journalisant « SVM complexity calibrated ». + /// Porter ce corps tel quel aurait livre une calibration qui n'en est pas une, sous un + /// message affirmant le contraire. L'appel d'entrainement evidemment voulu est retabli, + /// et l'absence de modele est traitee explicitement au lieu d'etre masquee par une NRE. + /// Le defaut upstream est signale a part (voir le corps de la PR). + /// + public double CalibrateComplexity(TradingTrainTestData objTrainingData, IKernel objKernel) + { + var xTrain = objTrainingData.Training.GetInputMatrix(); + var yTrain = objTrainingData.Training.GetOutputClasses(); + + double currentComplexity = 0.0001; + double maxComplexity = 1000D; + double minComplexity = currentComplexity; + double bestComplexity = currentComplexity; + + double currentResult = double.MaxValue; + Boolean maxedOut = false; + double testError = double.MaxValue; + var coef = 100D; + + for (int idx = 0; idx < 25; idx++) + { + var teacher = new MulticlassSupportVectorLearning(); + teacher.Learner = (p) => new SequentialMinimalOptimization() + { + Complexity = currentComplexity, + UseKernelEstimation = true, + Kernel = objKernel + }; + MulticlassSupportVectorMachine machine = null; + try + { + bool completed = ExecuteWithTimeLimit(TrainingTimeout, + () => + { + try + { + machine = teacher.Learn(xTrain, yTrain); + } + catch (Exception) + { + machine = null; + } + }); + if (!completed) + { + break; + } + + if (machine == null) + { + // Cette complexite ne produit pas de modele : la traiter comme un + // plafond atteint plutot que comme une amelioration. + maxedOut = true; + } + else + { + var ml = new MulticlassSupportVectorLearning() + { + Model = machine, + + Learner = (p) => new ProbabilisticOutputCalibration() + { + Model = p.Model + } + }; + ml.Learn(xTrain, yTrain); + + var trainDecisions = machine.Decide(xTrain); + int j = 0; + for (int i = 0; i < trainDecisions.Length; i++) + { + if (trainDecisions[i] != yTrain[i]) + { + j++; + } + } + if (j == 0) + { + maxedOut = true; + } + + testError = TestModel(objTrainingData, new ClassifierTradingModel() { Classifier = machine }); + } + } + catch (Exception) + { + maxedOut = true; + } + + if (testError <= currentResult) + { + if (currentComplexity >= bestComplexity) + { + if (testError < currentResult) + { + minComplexity = Math.Max(minComplexity, bestComplexity); + bestComplexity = currentComplexity; + currentResult = testError; + } + } + else + { + maxComplexity = Math.Min(maxComplexity, bestComplexity); + bestComplexity = currentComplexity; + currentResult = testError; + } + } + else + { + if (currentComplexity < bestComplexity) + { + minComplexity = Math.Max(minComplexity, currentComplexity); + } + else + { + if (maxedOut) + { + if (currentComplexity > bestComplexity) + { + maxComplexity = Math.Min(maxComplexity, currentComplexity); + } + } + } + } + + if (!maxedOut) + { + currentComplexity *= coef; + } + else + { + if (currentComplexity > bestComplexity) + { + currentComplexity = (bestComplexity + minComplexity) / 2; + } + else + { + currentComplexity = (bestComplexity + maxComplexity) / 2; + } + } + } + + return bestComplexity; + } + } }