Euromind
  • Javascript
    • Javascript

      Historiske administrative geografier i Google Maps

      20. april, 2022

      Javascript

      Kierkegaard injiceret med Javascript

      15. april, 2022

      Javascript

      Dansk evighedskalender

      7. december, 2020

      Javascript

      API til Statistikbanken

      21. september, 2019

      Javascript

      IntersectionObserver

      9. august, 2019

  • CSS/SCSS
    • CSS/SCSS

      Kierkegaard injiceret med Javascript

      15. april, 2022

      CSS/SCSS

      Dansk evighedskalender

      7. december, 2020

      CSS/SCSS

      Variable fonte med dansk tegnsæt i open source

      11. august, 2019

      CSS/SCSS

      Progressbar for dokumentposition

      31. juli, 2019

      CSS/SCSS

      Media Query i 2019

      18. juli, 2019

  • C#
    • C#

      Historiske administrative geografier i Google Maps

      20. april, 2022

      C#

      Authentication for IOS og Android med Firebase i…

      4. oktober, 2019

      C#

      Andersen, Grundvig, Kierkegaard og ML.NET – del 3

      5. september, 2019

      C#

      Hurtig eksport til Excel

      4. september, 2019

      C#

      Andersen, Grundtvig, Kierkegaard og ML.NET – del 2

      2. september, 2019

  • Javascript
    • Javascript

      Historiske administrative geografier i Google Maps

      20. april, 2022

      Javascript

      Kierkegaard injiceret med Javascript

      15. april, 2022

      Javascript

      Dansk evighedskalender

      7. december, 2020

      Javascript

      API til Statistikbanken

      21. september, 2019

      Javascript

      IntersectionObserver

      9. august, 2019

  • CSS/SCSS
    • CSS/SCSS

      Kierkegaard injiceret med Javascript

      15. april, 2022

      CSS/SCSS

      Dansk evighedskalender

      7. december, 2020

      CSS/SCSS

      Variable fonte med dansk tegnsæt i open source

      11. august, 2019

      CSS/SCSS

      Progressbar for dokumentposition

      31. juli, 2019

      CSS/SCSS

      Media Query i 2019

      18. juli, 2019

  • C#
    • C#

      Historiske administrative geografier i Google Maps

      20. april, 2022

      C#

      Authentication for IOS og Android med Firebase i…

      4. oktober, 2019

      C#

      Andersen, Grundvig, Kierkegaard og ML.NET – del 3

      5. september, 2019

      C#

      Hurtig eksport til Excel

      4. september, 2019

      C#

      Andersen, Grundtvig, Kierkegaard og ML.NET – del 2

      2. september, 2019

Euromind
C#

Andersen, Grundvig, Kierkegaard og ML.NET – del 1

af Per Lindsø Larsen 11. august, 2019
skrevet af Per Lindsø Larsen 11. august, 2019
Andersen, Grundvig, Kierkegaard og ML.NET – del 1

Machine Learning har hidtil været et domæne primært forbeholdt Python-talende Data Science-nørder, men som det altid sker med vidensområder i hastig udvikling, så opstår der nogle spin-off effekter, der gør det muligt for os andre dødelige – med ingen eller andre forudsætninger – at høste nogle af de lavesthængende frugter.

Microsofts Open-Source projekt ML .NET er et godt eksempel på det. Den er netop lanceret i version 1.0 i maj 2019. Nu er det muligt for c# og .net-udviklere at udvikle og integrere brugerdefinerede maskinindlæringsmodeller til .NET-apps af enhver art – web, mobil, desktop m.v.

ML.NET kombinerer dataindlæsning, transformationer og model-træning i en enkelt pipeline. De transformationer, der er defineret i din pipeline, anvendes  både til dine træningsdata og dine indtastnings-  eller testdata. ML .NET kan håndtere f.eks. tekst-transformationer, behandle af Misssing-Values, normalisering af data, NGram Featurisering af tekster m.v. Auto ML afprøver forskellige trænings-algoritmer og hjælper til at finde frem til den mest optimale træning af modellen.

Som Microsoft skriver: “you can easily integrate ML into your .NET apps without any prior ML experience.” Det er som om Bill Gates-imperiet taler direkte til mig her. I hvert fald er min ML-viden af et omfang, der gør mig kvalificeret til at tryk-teste den fremsatte påstand.

Det vil jeg så gøre i denne blog-serie, og for ikke at trampe mere rundt i det klassiske Iris-dataset, så sættes ML.NET på en utraditionel prøve. Den bliver fodret med nogle bøger, repræsenterende den bedre litteratur fra den danske guldalder: H.C. Andersen, Grundtvig og Søren Kierkegaard. Herefter vil følgende blive undersøgt:

  1. I hvilket omfang er ML .Net i stand til at identificere hvilken af de tre forfattere der er ophavsmand til ikke-trænede tekst-stumper og hvorledes afhænger præcisionen af tekst-stykkerne længde. Klassiske Stylometriske variabler vil blive sammenlignet med ML.Nets indbyggede NGram-tilgang.
  2. Nyere stylometrisk forskning har godtgjort at der sker stilistiske ændringer i løbet af en forfatters karriere. I hvilket omfang kan ML .NET afgøre hvilken af 16 Søren Kierkegaard bøger, ikke-trænede tekststykker er hentet fra?
  3. Kan ML.NET give et kvalificeret bud på, om Søren Kierkegaard er forfatteren bag tre anonyme artikler, som han i de første udgaver af samlede værker var udlagt som forfatteren af?

Den anvendte kildekode kan downloades, så resultaterne kan efterprøves.

Hvo er forfatteren?

“Hvo er forfatteren” er titlen på en artikel af Søren Kierkegaard fra 1843, hvor han diskuterer forfatterskabet til en bog, som han netop selv havde udgivet under pseudonym. Det kan også passende være titlen på dette afsnit, hvor jeg vil afprøve ML.NET’s evne til at afgøre, om tekststykker er skrevet af Andersen, Grundtvig eller Kierkegaard. Jeg vil afprøve det med henholdsvis brug af en håndfuld klassiske stylometriske egenskaber (ordlængde, hapax legomena etc.) og udelukkende brug af N-gram.

Tekstgrundlaget

Der er tilstræbt anvendelse af så ensartede tekster som muligt. Således er ikke anvendt H.C. Andensens eventyr, da de må antages stilmæssigt at have en anden komposition, ligesom der hos Kierkegaard og Grundvig af samme grund er fravalgt prædikener. Der er altså tale om den mere prosaiske del af forfatterskaberne. 

Alle tekster er tilgængeligt online og kan hentes følgende steder:

  • Søren Kierkegaards skrifter
  • Grundtvigs værker
  • H.C. Andersen Information

Teksterne er i første eksperiment delt op i et sæt til træning af modellen og et separat sæt tekster til test af den trænede models evner til at identificere forfatter.

Følgende bøger og afhandlinger danner baggrund for træning af modellen:

Fra de nævnte websteder er fuldteksterne copy-pasted til almindelige tekstfiler. Kolofoner og andet uvedkommende er groft sorteret fra. Ved indlæsning fjernes overflødige linjeskift, ombrydning af tekst delt ved linjeafslutning, ligesom dobbelt-a ændres til å. Herefter er stopord fjernet under anvendelse af Dansk stopordsliste (alternativt link her.) Stopord er populært sagt fyldord, som man typisk vil undlade i en google-søgning. I mange stylometriske sammenhænge kan de med fordel fjernes. Eksempler på stopord er: ad, af, aldrig, alle,alt, anden, andet, andre, at.

Alle rutinerne for behandling af råtektesterrne frem til ML.NET er beskrevet i blog-serien Stylometri i C#.

Klassiske stylometriske egenskaber.

Jeg har til dette forsøg valgt en håndfuld tilfældigt plukkede stylometriske variabler. Det er ikke udtryk for at de har særlig fortrin frem for de ca 1000 andre, der var været bragt i anvendelse i stylometrien. Jeg havde dem blot lige ved hånden, og dette projekt er blot en leg med ML.NET uden videnskabelige ambitioner. Det valgte variabler er:

  • Antal ord pr. tekstblok
  • Median ordlængde
  • Antal stopord pr. tekstblok
  • Antal lange ord pr tekstblok (defineret som ord længere end 6 bogstaver)
  • Antal forskellige ord pr. tekstblok
  • Antal hapax legomena pr. tekstblok (ord som kun optræder én gang)
  • Det beregnede Guiraud-index for tekstblokken
  • Det beregnede Smog-index for tekstblokken.

Guiraud og Smog er på forskellig måde udtryk for tekstens alimndelige lælsbarhed og er beregnet ved:

        public static double CalculateGuiraud(int wordsCount, int differentWordsCount, int decimals = 2)
        {
            try
            {
                return Math.Round(differentWordsCount / Math.Sqrt(wordsCount), decimals);
            }
            catch (Exception)
            {
                return 0;
            }
        }

        public static double CalculateSmogIndex(List<string> words, int sentencescount, int numberOfDecimals = 2)
        {
            int wordcount = words.Count;
            double cw = (double)30 / sentencescount;
            int complexwords = ComplexWordsCount(words);
            double indexval = 1.0430 * Math.Sqrt(complexwords * cw) + 3.1291;
            return Math.Round(indexval, numberOfDecimals);
        }

Hvad ML.NET får at vide om en tekstblok vil således typisk være noget i retning af:

Da der er tale om tekster i træningssættet, der kan være meget lange, er teksterne splittet op i tekstblokke af lige størrelser. Lidt eksperimenteren viste mig, at 15 sætninger pr blok var en passende størrelse til at ovennævnte variabler gav et ensartet udtryk for forfatterens stil. Mindre tekstblokke gav dårligere resultater, hvorimod det ikke var så væsentligt om de var større end 15 sætninger. Stopord blev fjernet.

Import-biblioteket gør ovenstående csv-fil klar til ML-NET, og den kan hentes ind med

public static IDataView LoadDataFile(MLContext mlContext, string DATA_FILEPATH, char seperatorChar = ';',
            bool hasHeader = true, bool allowQouting = true, bool allowSparse = false)
        {
            return mlContext.Data.LoadFromTextFile<ModelInput>(
                path: DATA_FILEPATH,
                hasHeader: hasHeader,
                separatorChar: seperatorChar,
                allowQuoting: allowQouting,
                allowSparse: allowSparse);
        }

For at bygge den færdige pipeline skal jeg finde en måde at få normaliseret variablerne på. Det er forståeligt nok. Sværere bliver det for den udfordrede machinelearning-novice at beslutte hvilken af de mange normalizers, der lige er den rigtige at vælge. Det er ikke indlysende, men heldigvis lyder det beroligende i ML.NET Cookbook: These normalizers all have different properties and tradeoffs, but it’s not that big of a deal if you use one over another. Just make sure you use a normalizer when training linear models or other parametric models.

Det lyder betryggende, så jeg vælger NormalizeMeanVariance og kan bygge learning-pipelinen:

        public static List<string> GetTextFeatures(Options options)
        {
            var modelinput = new ModelInput();
            List<string> propertyList = new List<string>();
            string[] drop = options.dropColumns.Split(',');
            foreach (var prop in modelinput.GetType().GetProperties())
            {
                if ((prop.Name != options.LabelColumn) && (!drop.Contains(prop.Name)) &&
                    (prop.PropertyType == typeof(string)))
                {
                    propertyList.Add(prop.Name);
                }
            }

            return propertyList;
        }

        public static List<string> GetFloatFeatures(Options options)
        {
            var modelinput = new ModelInput();
            List<string> propertyList = new List<string>();
            var x = new ModelInput().GetType().GetProperties();
            string[] drop = options.dropColumns.Split(',');
            foreach (var prop in modelinput.GetType().GetProperties())
            {
                if ((prop.Name != options.LabelColumn) && (!drop.Contains(prop.Name)) &&
                    (prop.PropertyType != typeof(string)))
                {
                    propertyList.Add(prop.Name);
                }
            }
            return propertyList;
        }




        public static IEstimator<ITransformer> BuildDataProcessPipeline(MLContext mlContext, Options options)
        {
            var textFeaturizingoptions = ModelBuilder.SetTextFeaturizeoptions(options);
            ConsoleHelper.PrintTextFeauturizingOptions(textFeaturizingoptions);
            ConsoleHelper.Write(ConsoleColor.DarkGray, "Bygger dataprocess-pipeline...");
            List<string> textColumns = GetTextFeatures(options);
            List<string> floatColumns = GetFloatFeatures(options);
            string[] drop = options.dropColumns.Split(',');
            if (textColumns.Count > 0)
            {
                floatColumns.Add("Text_tf");
                return mlContext.Transforms.Conversion.MapValueToKey("Label", options.LabelColumn)
                    .Append(mlContext.Transforms.DropColumns(drop))
                    .Append(mlContext.Transforms.Text.FeaturizeText("Text_tf", textFeaturizingoptions,
                        GetTextFeatures(options).ToArray()))
                    .Append(mlContext.Transforms.Concatenate("Features", floatColumns.ToArray()))
                    .Append(mlContext.Transforms.NormalizeMinMax("Features", "Features"))
                    .AppendCacheCheckpoint(mlContext);
            }
            else
            {
                return mlContext.Transforms.Conversion.MapValueToKey("Label", options.LabelColumn)
                    .Append(mlContext.Transforms.DropColumns(drop))
                    .Append(mlContext.Transforms.Concatenate("Features", floatColumns.ToArray()))
                    .Append(mlContext.Transforms.NormalizeMeanVariance("Features", "Features"))
                    .AppendCacheCheckpoint(mlContext);
            }
        }

Da ML.NET skal finde forfatteren blandt tre er der tale om et MulticlassClassification setup. Så langt så godt. Så skal der vælges en egnet algoritme, hvor der er en hel del at vælge imellem f.eks. SdcaMaximumEntropy, lightGbm, LbfgsMaximumEntropy, NaiveBayes, SdcaNonCalibrated. Vi, der aldrig hørte efter i skolen og derfor ikke er så kvikke, kan godt lide iterative processer. Det er et smartass-udtryk, der bare betyder: vi famler os frem i blinde, til vi bumler ind i noget der virker. Jeg noterer derfor med en vis tilfredshed, at det i ML-Net dokumentationen under “How to choose an ML.NET algorithm” hedder: “It is important to note that training a machine learning model is an iterative process.  You might need to try multiple algorithms to find the one that works best.” Tak for det. Den er modtaget!

Auto-ML er materialiseringen af den iterative proces. Den er i skrivende stund i preview version 0.15.1:

PM> Install-Package Microsoft.ML.AutoML -Version 0.15.1

AutoML splitter trænings-sættet op i et trænings- og validerings-sæt og kører de forskellige relevante algoritmer igennem og vælger så den, der giver bedst resultat. Får den lov til at køre længe nok, justerer den også hyper-parametre og fintuner modellen. Desværre er preview-versionen i skrivende stund behæftet med nogle bugs, der gør den rimelig irriterende at bruge i denne sammenhæng. Jeg valgte derfor at lave en alternativ ChooseBestAlgorith, der et lille stykke af vejen gør det samme – blot uden at være irriterende.

        public static Dictionary<string, IEstimator<ITransformer>> GetTrainerList()
        {
            var trainers = new Dictionary<string, IEstimator<ITransformer>>();

            trainers.Add("One-Versus-All (AveragedPerceptron)",
                mlContext.MulticlassClassification.Trainers.OneVersusAll(
                    mlContext.BinaryClassification.Trainers.AveragedPerceptron(labelColumnName: "Label",
                        numberOfIterations: 1, featureColumnName: "Features"), labelColumnName: "Label"));
            trainers.Add("One-Versus-All (LbfgsLogisticRegression)",
                mlContext.MulticlassClassification.Trainers.OneVersusAll(
                    mlContext.BinaryClassification.Trainers.LbfgsLogisticRegression(labelColumnName: "Label",
                        l2Regularization: 0, l1Regularization: 0, featureColumnName: "Features"),
                    labelColumnName: "Label"));
            trainers.Add("One-Versus-All (LinearSVM)",
                mlContext.MulticlassClassification.Trainers.OneVersusAll(
                    mlContext.BinaryClassification.Trainers.LinearSvm(labelColumnName: "Label", numberOfIterations: 1,
                        featureColumnName: "Features"), labelColumnName: "Label"));

            trainers.Add("One-Versus-All (Gam)",
                mlContext.MulticlassClassification.Trainers.OneVersusAll(
                    mlContext.BinaryClassification.Trainers.Gam(labelColumnName: "Label", numberOfIterations: 1,
                        featureColumnName: "Features"), labelColumnName: "Label"));

            trainers.Add("One-Versus-All (SymbolicSgdLogisticRegression)",
                mlContext.MulticlassClassification.Trainers.OneVersusAll(
                    mlContext.BinaryClassification.Trainers.SymbolicSgdLogisticRegression(labelColumnName: "Label",
                        numberOfIterations: 1,
                        featureColumnName: "Features"), labelColumnName: "Label"));

            trainers.Add("One-Versus-All (FastForest)",
                mlContext.MulticlassClassification.Trainers.OneVersusAll(
                    mlContext.BinaryClassification.Trainers.FastForest(labelColumnName: "Label",
                        featureColumnName: "Features"), labelColumnName: "Label"));

            trainers.Add("One-Versus-All (FastTree)",
                mlContext.MulticlassClassification.Trainers.OneVersusAll(
                    mlContext.BinaryClassification.Trainers.FastTree(labelColumnName: "Label",
                        featureColumnName: "Features"), labelColumnName: "Label"));

            trainers.Add("One-Versus-All (SdcaNonCalibrated)",
                mlContext.MulticlassClassification.Trainers.OneVersusAll(
                    mlContext.BinaryClassification.Trainers.SdcaNonCalibrated(labelColumnName: "Label",
                        featureColumnName: "Features"), labelColumnName: "Label"));

            trainers.Add("One-Versus-All (SgdNonCalibrated)",
                mlContext.MulticlassClassification.Trainers.OneVersusAll(
                    mlContext.BinaryClassification.Trainers.SgdNonCalibrated(labelColumnName: "Label",
                        featureColumnName: "Features"), labelColumnName: "Label"));

            trainers.Add("One-Versus-All (SdcaLogisticRegression)",
                mlContext.MulticlassClassification.Trainers.OneVersusAll(
                    mlContext.BinaryClassification.Trainers.SdcaLogisticRegression(labelColumnName: "Label",
                        featureColumnName: "Features"), labelColumnName: "Label"));

            trainers.Add("One-Versus-All (SgdCalibrated)",
                mlContext.MulticlassClassification.Trainers.OneVersusAll(
                    mlContext.BinaryClassification.Trainers.SgdCalibrated(labelColumnName: "Label",
                        numberOfIterations: 1, featureColumnName: "Features"), labelColumnName: "Label"));
            trainers.Add("SdcaMaximumEntropy",
                mlContext.MulticlassClassification.Trainers.SdcaMaximumEntropy("Label", "Features"));
            trainers.Add("LbfgsMaximumEntropy",
                mlContext.MulticlassClassification.Trainers.LbfgsMaximumEntropy(labelColumnName: "Label",
                    featureColumnName: "Features"));
            trainers.Add("LightGbm",
                mlContext.MulticlassClassification.Trainers.LightGbm(labelColumnName: "Label",
                    featureColumnName: "Features"));
            trainers.Add("Naive-Bayes",
                mlContext.MulticlassClassification.Trainers.NaiveBayes(labelColumnName: "Label",
                    featureColumnName: "Features"));
            trainers.Add("SdcaNonCalibrated",
                mlContext.MulticlassClassification.Trainers.SdcaNonCalibrated(labelColumnName: "Label",
                    featureColumnName: "Features"));
            return trainers;
        }



        public static IEstimator<ITransformer> ChooseBestModel(MLContext mlContext, IDataView TrainingDataView,
            IDataView TestDataView, IEstimator<ITransformer> dataProcessPipeline, Options options)
        {
            List<ModelTestResults> results = new List<ModelTestResults>();
            Dictionary<string, IEstimator<ITransformer>> trainers = GetTrainerList();
            foreach (var item in trainers)
            {
                var watch = System.Diagnostics.Stopwatch.StartNew();

                Console.WriteLine($"Undersøger {item.Key} ({results.Count + 1}/{trainers.Count})");

                IEstimator<ITransformer> _trainer = item.Value
                    .Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel", "PredictedLabel"));
                var testPipeline = dataProcessPipeline.Append(_trainer);
                var testModel = testPipeline.Fit(TrainingDataView);
                var predictions = testModel.Transform(TestDataView);
                var metrics = mlContext.MulticlassClassification.Evaluate(predictions, "Label", "Score");
                results.Add(
                    new ModelTestResults()
                    {
                        Model = item.Key,
                        MicroAccuracy = metrics.MicroAccuracy,
                        MacroAccuracy = metrics.MacroAccuracy,
                        LogLoss = metrics.LogLoss,
                        LogLossReduction = metrics.LogLossReduction
                    });
                watch.Stop();
                long elapsedMs = watch.ElapsedMilliseconds;
                ConsoleHelper.Write(ConsoleColor.White,
                    $"   ==> MicroAccuracy: {metrics.MicroAccuracy}, MacroAccuracy: {metrics.MacroAccuracy}, Varighed: {elapsedMs / 1000} sek.");
            }

            var sortedResults = results.OrderByDescending(x => x.MicroAccuracy).ToList();
            var table = new ConsoleTable("Model", "MicroAccuracy", "MacroAccuracy", "LogLoss", "LogLossReduction");
            table.Options.EnableCount = false;
            foreach (var i in sortedResults)
            {
                table.AddRow(i.Model, $"{i.MicroAccuracy:0.####}", $"{i.MacroAccuracy:0.####}", $"{i.LogLoss:0.####}",
                    $"{i.LogLossReduction:0.####}");
            }

            options.strategyDescription = sortedResults.First().Model;
            ConsoleHelper.Write(ConsoleColor.Yellow, "Mest præcise modeller - vælger " + sortedResults.First().Model);
            table.Write();
            var bestRun = trainers[sortedResults.First().Model];
            options.Split = Split.NO_SPLIT;
            TrainingDataView = LoadDataFile(mlContext, TrainCSVFile(options), ';');
            return bestRun;
        }

Med reservering af 20% af træningssættet til validering køres alle algoritmer igennem og efterfølgende trænes hele sættet med den algoritme, der antyder bedste resultater. I dette tilfælde med otte klassiske stylometriske variabler, var det sdcaNonCalibrated der kom ud som vinderen. Den trænede model gemmes herefter som zip-fil.

        public static ITransformer TrainModel(MLContext mlContext, IDataView trainingDataView,
            IEstimator<ITransformer> trainingPipeline, Options options)
        {
            Console.WriteLine("Anvendt træner: " + options.strategyDescription);
            var watch = System.Diagnostics.Stopwatch.StartNew();
            Console.WriteLine("Træner modellen .... Vent.....");
            var model = trainingPipeline.Fit(trainingDataView);
            watch.Stop();
            long elapsedMs = watch.ElapsedMilliseconds;
            Console.WriteLine($"Træning afsluttet - Varighed: {elapsedMs / 1000} sekunder");
            Console.WriteLine();
            return model;
        }

        public static void SaveModel(MLContext mlContext, ITransformer mlModel, DataViewSchema modelInputSchema,
            Options options)
        {

            Console.WriteLine($"Gemmer den trænede model...");
            mlContext.Model.Save(mlModel, modelInputSchema, ModelFile(options));
            Console.WriteLine("Modellen er gemt til {0}", ModelFile(options));
            WriteToBinaryFile<Options>(InfoFile(options), options);
            Console.WriteLine("");
        }

Træning af modellen tager mindre end et minut. Så er den klar til at stå sin prøve.

Testsæt

For ikke at gøre opgaven for let, er tekster til Test-sættet hentet fra helt andre bøger og afhandlinger end de, der indgår i trænings-sættet. Det drejer sig om:

På samme måde som med trænings-sættet deles teksterne op i ekstblokke af 15 sætningerns længde. Det giver i alt 647 tekstblokke fra de tre forfattere, som ML.NET kan forsøge sig på:

        private static PredictionItem PredictItem(PredictionEngine<ModelInput, ModelOutput> predEngine, ModelInput item,
            Options options)
        {
            PredictionItem prediction = new PredictionItem();
            PropertyInfo prop = typeof(ModelInput).GetProperty(options.LabelColumn);
            string Label = prop.GetValue(item, null).ToString();
            var p = predEngine.Predict(item);
            prediction.ActualLabel = Label;
            prediction.PredictedLabel = p.Prediction;
            prediction.Title = item.Title;
            for (int i = 0; i < p.Score.Length; i++)
            {
                var score = new PredictionScoreItem();
                score.Label = options.labelnames[i];
                score.Score = p.Score[i];
                prediction.ScoreList.Add(score);
            }

            return prediction;
        }


        public static void PredictBatch(ITransformer trainedModel, IDataView dataview, Options options)
        {
            Console.WriteLine($"Starter prediction af emner i TEST-sæt...");
            Console.WriteLine();

            var predEngine = ModelBuilder.mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(trainedModel);
            int total = 0;
            int counter = 0;
            var testdata = ModelBuilder.mlContext.Data.CreateEnumerable<ModelInput>(dataview, reuseRowObject: false);
            var predictions = new List<PredictionItem>();
            foreach (var item in testdata)
            {
                total++;
                var prediction = PredictItem(predEngine, item, options);
                if (prediction.ActualLabel == prediction.PredictedLabel) counter++;
                //            summary.AddTable(item.Title,prediction.ScoreList[0].Score);
                predictions.Add(prediction);
            }

            //          ConsoleHelper.PrintSummary(summary);
            ConsoleHelper.PrintPredictions(predictions, options.labelnames, options.ShowResultsForEachLabel,
                options.ShowSummaryLabel);
            Console.WriteLine();
            Console.WriteLine($"Korrekt svar: {counter} af {total} ({(double)(counter * 100) / total:0.##} %)");
        }

Det giver følgende resultat:

Den trænede model kan altså i 95,5% af tilfældene korrekt afgøre om en tekst af 15 sætningers længde er skrevet af Kierkegaard, Grundtvig eller Andersen.

Med den indbyggede “Permutation Feature Importance” er det muligt at få et indblik i, hvorledes hver enkel variabel har bidraget til resultatet.

      public static void FeatureImportance(TransformerChain<ITransformer> trainedModel, IDataView TrainingModelView,
            Options options)
        {
            var predictionTransformer = GetLastTransformer(trainedModel);
            var permutationMetrics = ModelBuilder.mlContext.MulticlassClassification.PermutationFeatureImportance(
                predictionTransformer, trainedModel.Transform(TrainingModelView));
            ConsoleHelper.PrintMultiClassClassificationPCIMetrics(permutationMetrics, options);
        }

Der som tak for indsatsen giver følgende resultat:

Smogindex, frekvensen af lange ord (mere end seks bogstaver) og variationen af ord har altså leveret de betydeligste bidrag.

Sammenlignet med andre stylometriske ekseperimenter er dette resultat – med tre mulige forfattere – ikke prangende. Her vil man nok forvente 98% eller mere, og de sidste 5% er som bekendt sværere end de første 95%. Til gengæld er mere stringente undersøgelser med bedre resultater ofte baseret på snesevis af variabler, i nogle tilfælde over 70, hvor der i dette tilfælde kun er tilfældigt udvalgt otte – der ovenikøbet har et vist overlap. På nedturs-siden tæller også, at tekstblokke i testsættet på 15 sætningers længde er ganske store tekststykker, der ofte er på 2500 bogstavers længde eller mere. Altså op til en A4-sides tekst.

Jeg vil derfor prøve, om der kan opnås bedre resultat med en anden tilgang.

N-Gram

Ord- og bogstav N-Gram ser ud til at være den nye sort i stylometrien, og truer med at feje mere end 100 års målinger af andre tekst-egenskaber helt af banen.

N-Gram udtrykker N antal ord eller bogstaver efterfølgende hinanden. Tages udgangspunkt i sætningen: “Dette er en forholdsvis lang sætning” så vil listen af 2-Gram (bi-gram) være Dette-er, er-en,en-forholdsvis,forholdsvis-lang, lang-sætning. Listen af 3-gram (tri-gram) være Dette-er-en, er-en-forholdsvis,en-forholdsvis-lang, forholdsvis-lang-sætning.
Det samme for ord N-gram, f.eks ord 3-gram: “Det,ett,tte…..”

ML.NET’s text-featurizing automatiserer dette på en herlig nem måde. Følgende funktion gør det muligt at se, hvad de færdige n-grams indeholder:

      public static void PeekNGrams(ITransformer trainedModel, IDataView trainingDataView, int itemsToShow)
        {
            var transformedData = trainedModel.Transform(trainingDataView);
            var slotLabelBuffer = default(VBuffer<ReadOnlyMemory<char>>);
            transformedData.Schema["Text_tf"].GetSlotNames(ref slotLabelBuffer);
            if (slotLabelBuffer.Length > 0)
            {
                List<string> charGram = new List<string>();
                List<string> wordGram = new List<string>();
                for (int i = 0; i < slotLabelBuffer.Length; i++)
                {
                    if ((slotLabelBuffer.GetItemOrDefault(i).ToString().Contains("Char.")) &&
                        (charGram.Count <= itemsToShow))
                    {
                        charGram.Add(slotLabelBuffer.GetItemOrDefault(i).ToString().Replace("Char.", ""));
                    }
                    else if ((slotLabelBuffer.GetItemOrDefault(i).ToString().Contains("Word.")) &&
                             (wordGram.Count <= itemsToShow))
                    {
                        wordGram.Add(slotLabelBuffer.GetItemOrDefault(i).ToString().Replace("Word.", ""));
                    }
                }

                Console.WriteLine();
                ConsoleHelper.Write(ConsoleColor.White, $"Første {itemsToShow} CHAR N-Grams");
                Console.WriteLine();
                for (int i = 0; i < charGram.Count; i++)
                {
                    Console.WriteLine(charGram[i]);
                }

                Console.WriteLine();
                ConsoleHelper.Write(ConsoleColor.White, $"Første {itemsToShow} WORD N-Grams");
                Console.WriteLine();
                for (int i = 0; i < wordGram.Count; i++)
                {
                    Console.WriteLine(wordGram[i]);
                }
            }
        }

Resultatet vil være et udprint som f.eks. dette:

Det er muligt at sætte en række options for N-gramificeringen. For både WordFeatureExtractor og CharFeatureExtractor kan sættes Længde af N-Gram. I ovenviste eksempel er det fire. UseAllLength er en boolean-værdi, der udtrykker hvorvidt alle N-Gram op til den givne længde skal medtages. I dette tilfælde er den sat til true, således at der også medtages uni-gram, bi-gram og tri-gram.

        public static TextFeaturizingEstimator.Options SetTextFeaturizeoptions(Options options)
        {
            var textFeaturizingoptions = new TextFeaturizingEstimator.Options()
            {
                WordFeatureExtractor = new WordBagEstimator.Options()
                {
                    SkipLength = options.textFearurize_Word_SkipLength,
                    Weighting = options.textFeaturize_Word_Weight,
                    NgramLength = options.textFearurize_Word_Length,
                    UseAllLengths = options.textFearurize_Word_UseAllLength
                },
                CharFeatureExtractor = new WordBagEstimator.Options()
                {
                    SkipLength = options.textFearurize_Char_SkipLength,
                    Weighting = options.textFeaturize_Char_Weight,
                    NgramLength = options.textFearurize_Char_Length,
                    UseAllLengths = options.textFearurize_Char_UseAllLength
                },
            };
            return textFeaturizingoptions;
        }

TextFeaturize-transformationen kan derefter tilfæjes trænings-pipeline:

               return mlContext.Transforms.DropColumns(drop)
                    .Append(mlContext.Transforms.Text.FeaturizeText("Text_tf", textFeaturizingoptions,
                        GetTextFeatures(options).ToArray()))
                    .Append(mlContext.Transforms.Concatenate("Features", floatColumns.ToArray()))
                    .AppendCacheCheckpoint(mlContext);

Parametre gældende for ord N-gram er de samme for bogstav N-Gram, bortset fra at N-gram længde er sat til tre:

Alt i alt er parametrene sat således:

Tekster i trænings- og test-sæt er de samme som ved forsøget med klassiske stylometriske variabler. Da sætningsstrukturen ikke længere er relevant, er teksten ophugget i rene ord. Også denne gang er stopord fjernet. Træningssættet er opdelt i tekstblokke af 25 ord og forsøgsmæssigt er test-sættet tekstblokke på 300 ord. En tekstblok i trænings-sættet vil derfor se således ud, efter lower-casing og fjernelse af stopord:

gjemt hemmelighed følte glæde smerte kjær kunde indvie måskee bragt berøring mennesker ahnede sådant tilfælde magt besnærelse istand bringe forborgne åbenbarelse måskee passer tilfældene ukjendt

Resultatet er overraskende på den fede måde:

Modellen rammer rigtig i 591 af 594 forsøg (99.5%). Det er klart bedre end med anvendelse af en håndfuld klassiske variabler. Spørgsmålet er så, hvor kort en tekststump modellen kan nøjes med, for at give et acceptabelt resultat. Til at afklare det, lavede jeg følge funktion EvaluateTestClusterSize, der kører testen igennem med forskellige størrelser af tekstblokke i test-sættet.

        public static void EvaluateTestClusterSize(int startValue, int endValue, int step, Options options)
        {
            TextWriter tw = null;
            var fname = EvaluateClusterSizeFile(options);
            string msg = $"Test af præcision vs. klyngestørrelse i test-sæt skriver til {fname}";
            ConsoleHelper.Write(ConsoleColor.Yellow, msg);
            Console.WriteLine("");
            tw = new StreamWriter(fname);
            tw.WriteLine("ClusterSize;MicroAccuracy;MacroAccuracy;LogLoss;LogLossReduction");
            ConsoleHelper.PrintMulticlassClassificationMetricsHeader2();
            for (int i = startValue; i < endValue; i += step)
            {
                options.testClusterSize = i;
                Importer.RunTestSet(options);
                var TestDataView = ModelBuilder.LoadDataFile(mlContext, TestCSVFile(options), ';');
                DataViewSchema modelSchema;
                var trainedModel = mlContext.Model.Load(ModelFile(options), out modelSchema);
                var predictions = trainedModel.Transform(TestDataView);
                var metrics = mlContext.MulticlassClassification.Evaluate(data: predictions, labelColumnName: "Label",
                    scoreColumnName: "Score");
                ConsoleHelper.PrintIterationMetrics(i, metrics);
                tw.WriteLine(
                    $"{i};{metrics.MicroAccuracy:0.####};{metrics.MacroAccuracy:0.####};{metrics.LogLoss:0.####};{metrics.LogLossReduction:0.####}"
                        .Replace(".", ","));
            }
            tw.Close();
        }

Ved af afprøve med tekstblokke fra 1-400 ord fås følgende resultater:

Nogle milepæle viser at brugen af N-gram behøver betydelig mindre tekststykker:

40 ord0,90
60 ord0,93
100 ord0,96
120 ord0,97
165 ord0,98
270 ord0,99

Er man i besiddelse af tvivlens nådegave, hvilket man sikkert gør klog i, når man som elev i maskinlæringens 0. klasse leger med ML.NET, så kan man få en mistanke om, at andre faktorer end forfatternes stilistik spiller ind, f.eks. forskelle i redaktionelle retningslinjer for f.eks. stavemåde og lign. ved udgivelsen af de tre forfatteres værker. For at afklare dette, så vil næste eksperiment udelukkende bestå af Søren Kierkegaards bøger. Udgivelsen på sks.dk, hvorfra teksterne er hentet, er 100% loyal mod de oprindelige udgaver, som Kierkegaard udgav og selv redigerede. Det skulle give sikkerhed for et yderst ensartet tekstgrundlag.
Jeg vil så samtidig stramme sværhedsgraden og undersøge, i hvilket omfang ML.NET kan afgøre hvilken af 16 Kierkegaard-bøger ikke-trænede tekststumper er hentet fra.

C#Machine LearningML.NETStylometri
0 Kommentarer
9
FacebookTwitterPinterestEmail
forrige post
IntersectionObserver
næste post
Variable fonte med dansk tegnsæt i open source

Relaterede indlæg

Historiske administrative geografier i Google Maps

20. april, 2022

Authentication for IOS og Android med Firebase i...

4. oktober, 2019

Andersen, Grundvig, Kierkegaard og ML.NET – del 3

5. september, 2019

Hurtig eksport til Excel

4. september, 2019

Andersen, Grundtvig, Kierkegaard og ML.NET – del 2

2. september, 2019

Forbind Visual Studio til IOS devices på 10...

12. juli, 2019

Stylometri i C# – del 4

9. juli, 2019

Serialisering og deserialisering i C#

5. juli, 2019

Stylometri i C# – del 3

2. juli, 2019

Stylometri i C# – del 2

7. juni, 2019

Efterlad en kommentar Afbryd svar

Gem mit navn, email, og website i denne browser til senere kommentarer.

Seneste indlæg

  • Historiske administrative geografier i Google Maps

    20. april, 2022
  • Kierkegaard injiceret med Javascript

    15. april, 2022
  • Dansk evighedskalender

    7. december, 2020

Kategorier

  • C#
  • CSS/SCSS
  • Excel
  • HTML
  • Javascript
  • Mobile
  • Webdesign
  • Xamarin

Om mig

Om mig

Per Lindsø Larsen

Freelance fullstack developer bosat i Aarhus.

Du kan hyre mig til korterevarende projekter eller konkrete opgaveløsninger.

Pæn rabat til non-profit organisationer og foreninger.

Når jeg ikke koder, deltager jeg løbende i diverse spændende forskningsprojekter om alt andet end kodning.

Keep in touch

Facebook Twitter Email Github

Tags

Adresser AMP AMP Story Android API Billedformater Billedoptimering Brand C# Codepen Cordova CPR Crome DevTools CSS Debug Ecmascript Excel Fonte Gmail Gulp HTML Ikoner IOS Javascript JsFiddle Machine Learning Mail Mediaquery ML.NET Mobile RegEx SCSS SMTP Stylometri Visual Studio Webdesign Xamarin

Nyhedsbrev

Timeld nyhedsbrev for info om nye blog-indlæg, tips m.v.

  • Facebook
  • Twitter
  • Email
  • Github

@2019 - Euromind.com - Code-To-Go. All Right Reserved.
lindsoe@gmail.com - mobil: 42797273


Tilbage til top
Euromind
  • Javascript
    • Javascript

      Historiske administrative geografier i Google Maps

      20. april, 2022

      Javascript

      Kierkegaard injiceret med Javascript

      15. april, 2022

      Javascript

      Dansk evighedskalender

      7. december, 2020

      Javascript

      API til Statistikbanken

      21. september, 2019

      Javascript

      IntersectionObserver

      9. august, 2019

  • CSS/SCSS
    • CSS/SCSS

      Kierkegaard injiceret med Javascript

      15. april, 2022

      CSS/SCSS

      Dansk evighedskalender

      7. december, 2020

      CSS/SCSS

      Variable fonte med dansk tegnsæt i open source

      11. august, 2019

      CSS/SCSS

      Progressbar for dokumentposition

      31. juli, 2019

      CSS/SCSS

      Media Query i 2019

      18. juli, 2019

  • C#
    • C#

      Historiske administrative geografier i Google Maps

      20. april, 2022

      C#

      Authentication for IOS og Android med Firebase i…

      4. oktober, 2019

      C#

      Andersen, Grundvig, Kierkegaard og ML.NET – del 3

      5. september, 2019

      C#

      Hurtig eksport til Excel

      4. september, 2019

      C#

      Andersen, Grundtvig, Kierkegaard og ML.NET – del 2

      2. september, 2019

Populære indlæg

  • 1

    Stylometri i C# – del 2

    7. juni, 2019
  • 2

    Andersen, Grundvig, Kierkegaard og ML.NET – del 1

    11. august, 2019
  • 3

    Send email fra Javascript med Gmail API

    21. juni, 2019
  • 4

    Gmail, Yahoo og Outlook som SMTP-server

    18. april, 2019
  • 5

    Registrer Gmail API til brug i javascript

    27. juni, 2019
@2019 - Euromind.com - Code-To-Go. All Right Reserved.
lindsoe@gmail.com - mobil: 42797273

Læs ogsåx

Stylometri i C# – del 4

9. juli, 2019

Historiske administrative geografier i Google Maps

20. april, 2022

Hurtig eksport til Excel

4. september, 2019