Monday, 21 January 2019

ML NET with Kmeans Clustering

This is part two of my deep dive into ML.NET. My last post on using SCDAs to predict turnover ended up being a pretty good gateway into ML.NET with a relatively easy problem to ΓÇ£solveΓÇ¥ and evaluate. Wanting to dive into a different algorithm, I chose k-means for clustering. In addition, Microsoft released version 0.9 of ML.NET. The API has changed slightly so I have updated the source code for the SCDA regression and communized library code to take advantage of the new API/syntax.

Being interested in security for years now and doing it daily at work, I wanted to switch gears to a security focus. A common problem in the ML world is once a threat is found, how do you classify it? Saying it is ΓÇ£AbnormalΓÇ¥ or ΓÇ£UnsafeΓÇ¥ as some of the industry conveys is pretty uninformative in my opinion. This is where clustering comes into play.

With k-means clustering, the idea behind the algorithm is to take a group of data based on a type and other features to create effectively a scatter plot. In my case, each cluster would be a threat category such as Trojan, PUA or generically Virus. In a production environment you would probably want to break it out further to Worms, Rootkits, Backdoors etc. But to keep it easy, I decided to keep it to just the three.

The next piece that I didnΓÇÖt need to do for my last deep dive is to actually do feature extraction. Again to keep things easy, I decided to keep it to just PE32/PE32+ files and I just utilized the PeNet NuGet package to extract two features:

  1. Size of Raw Data (from the Image Section Header)

  2. Number of Imports (from the Image Resource Dictionary)

In a production model this would need considerable more features, especially when doing more granular classification.

Some Code Cleanup for 0.9

One of the big things I did the other night after updating to 0.9 was commonizing the code more and luckily the new APIs provided allows that. One of the biggest achievements was to get the Predict function 100% generic:


public static TK Predict<t, tk="">(MLContext mlContext, string modelPath, T predictionData) where T : class where TK : class, new()

{

    ITransformer trainedModel;



    using (var stream = new FileStream(modelPath, FileMode.Open, FileAccess.Read, FileShare.Read))

    {

        trainedModel = mlContext.Model.Load(stream);

    }



    var predFunction = trainedModel.CreatePredictionEngine<t, tk="">(mlContext);



    return predFunction.Predict(predictionData);

}



public static TK Predict<t, tk="">(MLContext mlContext, string modelPath, string predictionFilePath) where T : class where TK : class, new()

{

    var data = File.ReadAllText(predictionFilePath);



    var predictionData = Newtonsoft.Json.JsonConvert.DeserializeObject<t>(data);



    return Predict<t, tk="">(mlContext, modelPath, predictionData);

}

For the clustering I took it a step further to allow either passing in the type of T or the file path for the JSON representation of T. The reason for this is typical tools for Threat Classification like ClamAV or VirusTotal provide the ability to just upload a file or scan it from a command line.

Another area of improvement was to standardize the command line arguments especially with future experiments on the horizon. An improved but not perfect change was to use an enum:


public enum MLOperations

{

    predict,

    train,

    featureextraction

}

And then in the Program.cs:


if (!Enum.TryParse(typeof(MLOperations), args[0], out var mlOperation))

{

    Console.WriteLine($"{args[0]} is an invalid argument");



    Console.WriteLine("Available Options:");



    Console.WriteLine(string.Join(", ", Enum.GetNames(typeof(MLOperations))));



    return; 

}



switch (mlOperation)

{

    case MLOperations.train:

        TrainModel<threatinformation>(mlContext, args[1], args[2]);

        break;

    case MLOperations.predict:

        var extraction = FeatureExtractFile(args[2], true);



        if (extraction == null)

        {

            return;

        }



        Console.WriteLine($"Predicting on {args[2]}:");



        var prediction = Predictor.Predict<threatinformation, threatpredictor="">(mlContext, args[1], extraction);



        PrettyPrintResult(prediction);

        break;

    case MLOperations.featureextraction:

        FeatureExtraction(args[1], args[2]);

        break;

}

Utilizing the Enum allowed a quick sanity check of the first argument and then able to utilize a switch/case for each of the operations. For the next deep dive I will clean this up a bit more to probably just have an Interface or Abstract Class to implement for each experiment.

K-means Clustering

Very similarly to SCDAs the code to train a model was very easy:


private static void TrainModel<t>(MLContext mlContext, string trainDataPath, string modelPath)

{

    var modelObject = Activator.CreateInstance<t>();



    var textReader = mlContext.Data.CreateTextReader(columns: modelObject.ToColumns(), hasHeader: false, separatorChar: ',');



    var dataView = textReader.Read(trainDataPath);

    

    var pipeline = mlContext.Transforms

        .Concatenate(Constants.FEATURE_COLUMN_NAME, modelObject.ToColumnNames())

        .Append(mlContext.Clustering.Trainers.KMeans(

        Constants.FEATURE_COLUMN_NAME, 

                clustersCount: Enum.GetNames(typeof(ThreatTypes)).Length));



    var trainedModel = pipeline.Fit(dataView);



    using (var fs = File.Create(modelPath))

    {

        trainedModel.SaveTo(mlContext, fs);

    }



    Console.WriteLine($"Saved model to {modelPath}");

}

With the new 0.9 API, the Text Reader has been cleaned up (in conjunction with the Extension Methods I created earlier). The critical piece to keep in mind is the clustersCount argument in the KMeans Trainer constructor. You want this number to equal the number of categories you have. To keep my code flexible since IΓÇÖm using an Enum, I simply calculate the length. I strongly suggest following that path to avoid errors down the road. The rest of the code is generic (room for some refactoring in the next deep dive).

For my Threat Classification class it should look like a pretty normal class:


public class ThreatInformation

{

    public float NumberImports { get; set; }



    public float DataSizeInBytes { get; set; }



    public string Classification { get; set; }



    public override string ToString() =&gt; $"{Classification},{NumberImports},{DataSizeInBytes}";

}

I overrode ToString() for the FeatureExtraction, but otherwise pretty normal.

For my Prediction class it is a little different than with an SCDA:


public class ThreatPredictor

{

    [ColumnName("PredictedLabel")]

    public uint ThreatClusterId { get; set; }



    [ColumnName("Score")]

    public float[] Distances { get; set; }

}

Where as the SCDA or other regression models return values, the k-means trainer returns the cluster that it found to be best fit. The Distances array contains the Euclidian distances from the data that gets passed in for prediction to the cluster centroids. For my case, I added a translation from the ClusterId -> a human readable string value (i.e. Trojan, Malware etc.).

Closing Thoughts

In training the data and running a model I was surprised at how quick it was to do both. Digging into the code on GitHub, everything looks to be as parallized as possible. Having used other technologies that arenΓÇÖt multi-threaded ΓÇô this was a refreshing sight. As for working with the clustering further, I think the big thing I will probably work on the scalable feature extraction and training in an efficient manner (right now itΓÇÖs single threaded and loaded into memory all at once).

Intro

Last year while attending the Microsoft BUILD conference I got to see the debut of ML.NET in person. After going to the intro session, I was amazed at the potential opportunities at both work and in my own personal projects (like my image scaling project I work on from time to time). Over the time since the initial 0.1 release they have released a new version every month adding tons of new features. As they rapidly approach a 1.0 release I figured it was time to do another deep dive.

As fate would have it, a recent discussion among co-workers about employee retention and predicting when a co-worker would leave came up. My previous deep dive into ML.NET was in Binary Classification only, which would flip the question around to: given a set of attributes is the person going to leave. Using this as an opportunity to grow my ML skillset, I started my deep dive into SDCA (Stochastic Dual Coordinate Ascent) with a Regression Task.

Since last deep diving into ML.NET the API has changed considerably (mostly for the better), and fortunately have the moved deprecated calls to a Legacy namespace to avoid forcing major refactoring on anyone who wishes to use the latest version (0.8 at the time of this writing).

The Problem

When thinking about factors that could be treated as a feature in our ML model I reflected on the various people I have worked with my career and snapshotting data that could be tracked at the time they left/got fired.

A couple of features at first thought:

  1. Position Name - In case there is a correlation between position and duration

  2. Married or not - Figuring longevity might be longer with the increased financial responsibilities

  3. BS Degree or not - Figuring there maybe a correlation especially for more junior folks

  4. MS Degree or not - Figuring this would be true for more senior level folks

  5. Years Experience - Pretty obvious here

  6. Age at Hire - More youthful hires might be antsy to move for more money/new title

  7. Duration at Job - Used to help train the model

Knowing this was not a unique thought process and surely not the first to use ML for this problem, I came across a dataset from Kaggle. This dataset, while fictional, was created by IBM Data Scientists and provided what I was looking for: another set of minds thinking about features. Their dataset offered quite a few more features than I had come up with, but had all of the features I had come up with as well.

Figuring this was validation enough for my little deep dive, I then proceeded to Visual Studio

ML.NET Implementation

First off, all of the code discussed here is checked into my ML.NET Deep Dive repo. Feel free to clone/improvement/give feedback.

To begin I defined my data structure:


public class EmploymentHistory

{

    public string PositionName { get; set; }



    [Label(0, 150)]

    public float DurationInMonths { get; set; }



    public float IsMarried { get; set; }



    public float BSDegree { get; set; }



    public float MSDegree { get; set; }



    public float YearsExperience { get; set; }



    public float AgeAtHire { get; set; }

}

The Label Attribute in this case is a custom attribute where I use it to filter out the anomalous data (someone there for 0 months or 12+ years).

As with the older ML.NET API, when you predict data, you need both a data structure to train and predict against as well as a solution object. In this case we want to predict on the DurationInMonths property, so I defined my EmploymentHistoryPrediction object:


public class EmploymentHistoryPrediction

{

    [ColumnName("Score")]

    public float DurationInMonths;

}

To keep the code some what generic I wrote a couple Extension Methods so I can use C# Generics (thinking longer term I could re-use as much of this code for other applications). These are found in the mldeepdivelib\Common\ExtenionMethods.cs file.

Skipping to the actual ML.NET code, overall the structure of any ML.NET application to train a model is the following:

  1. Create an ML Context

  2. Create your Data Reader (in this case a CSV ΓÇô TextReader is built in)

  3. Transform and Normalize data (in particular string data)

  4. Choose your Trainer Algorithm

  5. Train the model

  6. Save the model

Thankfully most of these steps are extremely easily, especially compared to TensorFlow (you have to drop back to Python in TensorFlowΓÇÖs case).

For my case it was just a handful of lines to do steps 3 through 6:


var dataProcessPipeline = mlContext.Transforms.CopyColumns(label.Name, "Label")

    .Append(mlContext.Transforms.Categorical.OneHotEncoding("PositionName", "PositionNameEncoded"))

    .Append(mlContext.Transforms.Normalize("IsMarried", mode: NormalizingEstimator.NormalizerMode.MeanVariance))

    .Append(mlContext.Transforms.Normalize("BSDegree", mode: NormalizingEstimator.NormalizerMode.MeanVariance))

    .Append(mlContext.Transforms.Normalize(inputName: "MSDegree", mode: NormalizingEstimator.NormalizerMode.MeanVariance))

    .Append(mlContext.Transforms.Normalize(inputName: "YearsExperience", mode: NormalizingEstimator.NormalizerMode.MeanVariance))

    .Append(mlContext.Transforms.Normalize(inputName: "AgeAtHire", mode: NormalizingEstimator.NormalizerMode.MeanVariance))

    .Append(mlContext.Transforms.Concatenate("Features", "PositionNameEncoded", "IsMarried", "BSDegree", "MSDegree", "YearsExperience", "AgeAtHire"));



var trainer = mlContext.Regression.Trainers.StochasticDualCoordinateAscent();

var trainingPipeline = dataProcessPipeline.Append(trainer);



var trainedModel = trainingPipeline.Fit(trainingDataView);



using (var fs = File.Create(modelPath))

{

    trainedModel.SaveTo(mlContext, fs);

}

Once the model has saved in the last line, it is very trivial to call MakePrediction. Fortunately, I was able to make this method 100% generic:


private static TK Predict<t, tk="">(MLContext mlContext, string modelPath, string predictionFilePath) where T : class where TK : class, new()

{

    var predictionData = Newtonsoft.Json.JsonConvert.DeserializeObject<t>(File.ReadAllText(predictionFilePath));



    ITransformer trainedModel;



    using (var stream = new FileStream(modelPath, FileMode.Open, FileAccess.Read, FileShare.Read))

    {

        trainedModel = mlContext.Model.Load(stream);

    }

  

    var predFunction = trainedModel.MakePredictionFunction<t, tk="">(mlContext);



    return predFunction.Predict(predictionData);

}

First thing I do here is read in a File, parse the JSON and then convert it to the type of T to feed into the Model. The method then returns the Prediction.

Findings

Once compiled, we need to train the model. I took the Kaggle dataset above, trimmed it down to the features I thought were important and then called the app like so:


.\mlregression.exe build .\ibmclean.csv ibmmodel.mdl

On my Razer Blade Pro, it took less than 2 seconds to train. Afterwards I had my model.

Subsequently I created some test data to run the model against to get a prediction like so:


.\mlregression.exe predict ibmmodel.mdl testdata.json 

Closing Thoughts

Given the sample set, this is far from being even close to be considered solved. However, it did give me a chance to deep dive into ML.NETΓÇÖs 0.8 API, SDCA and digging around for sample data.

Looking forward to continuing research into other Trainers that ML.NET offers ΓÇô stay tuned.

Introduction/Backstory

A long time ago back in 2003 I had the amazing idea to use nVidia Cg on my GeForce 4 Ti4400 to accelerate image processing. I coined it imgFX at the time. While at the time I thought I was doing something no one else had, I quickly learned I was not and eventually shelved the project. Several years later in May 2008 I revived it with the HD Revolution rapidily approaching, renaming it to texelFX.

2008 texelFX logo

People had Standard Definition content and wanted to quickly release High Definition content cheaply. Using my Silicon Graphics Octane 2 (Dual R12k400mhz/V6 graphics) at the time I was writing a C++ OpenGL application to handle the scaling using the exclusive Silicon Graphics OpenGL extensions. This was working pretty well, although my scaling techniques were not much more advanced than a Nearest Neighbor scaler - I was struggling at the time with a Bicubic scaler (mostly due to my more mid-level programming abilities at the time). Results were sub-par mostly due to my programming abilities at the time. Fast forward to late summer 2017, I upgraded the GPU in my desktop to a GeForce 1080ti to take advantage of the numerous Cuda Libraries to accelerate floating point operations like I needed for image scaling. At that time I created the Github repro, if I ever become unshamed of my 2008 deep-dive I will commit them to a separate repo.

The main reasonly for reviving the project was the news earlier in 2017 that Star Trek: Deep Space Nine would most likely never get a proper 1080p or better remastering. While you could argue, popping the 2002-2003 DVD releases in a UHD upscale enabled blu-ray player might make it look the best it possibly could, I would argue those are not taking advantage of machine learning and simply applying noise reduction along with a bicubic scale.

Where I am today

Over the weekend I ported over my .NET Core 2.0 App I did back in August 2017 to a more split architecture:

  1. .NET Core 2.0 library (Contains all of the actual scaler code)

  2. ASP .NET Core 2.0 MVC App (Providing a quick interface to demonstrate the effectiveness of the algorithms)

  3. ASP .NET Core 2.0 WebAPI App (Providing a backend to support larger batch processes/mobile uploads/etc)

Along with the port, I got a Nearest Neighbor implementation done using the System.Drawing .NET Core 2.0 NuGet package. This will serve as the baseline for which I will compare my approach. My approach will utilize the newly released Microsoft Cognitive Toolkit to create a deep convolutional neural network to help with my image scaling solution. To dive in, take the following screencap from Season 6 Episode 1 "A Time to Stand":

DVD Screencap of A Time to Stand

Note the following:

-MPEG2 Compression Artifacts in the back left of the screencap where the 2 crew members are analyzing a screen and around the light

-Muted colors, granted DS9 was intentionally muted especially during the war seasons, but the color space of the DVD is vastly different from an HDR UHD of today.

Scaling the image to HD (1920x1080):

Nearest Neighbor Upscale Screencap of A Time to Stand

Without doing a side by side comparison it is a little hard to see just how bad it is, so lets zoom in on the area mentioned above upscaled:

Zoomed Upscale Screencap

The issues mentioned above in the DVD screencap are only exacerbated by the scaling, making the quality even worse when viewed at the new upscaled resolution.

What I hope to Achieve

Given the issues above, my main goals:

  1. Provide a web interface and REST Service to scale single images or videos

  2. Remove compression artifacts (specifically MPEG2)

  3. Apply Machine Learning to provide detail to objects where there are not enough pixels (such as a Standard Definition source)

And with any luck provide myself a true High Definition of Deep Space Nine.

Next Steps

With my goals outlined, the first step is to deep dive into the https://docs.microsoft.com/en-us/cognitive-toolkit/ and begin training a model to provide goals 2 and 3 a viable solution.

Continuing my dive back into C++, libcurl by default on Windows does not come statically compiled so I packaged together the latest release compiled on Visual Studio 2017 in Release mode statically with ipv6 and ssl enabled. You can get the executable, header aand lib here.

Figuring most folks diving into FLTK might be developing on Windows and not wanting to pull down the source and compile it yourself. I compiled the source on Visual Studio 2017 in Release mode. You can get the header aand libs here.