From bb845098ca23c43ada5f06d9ee2a128102fa2f07 Mon Sep 17 00:00:00 2001 From: Emiliana <38952746+emilianavt@users.noreply.github.com> Date: Thu, 13 Feb 2020 22:24:41 +0100 Subject: [PATCH] It is now possible to train expression models with probability support. This however is a lot slower! Also, the C parameter for SVM training was set incorrectly. Now training works much better. --- Source/SVMWrapper.cpp | 20 ++++++++++++-------- Unity/OpenSeeExpression.cs | 18 ++++++++++++++++-- Unity/Plugins/SVMModel.cs | 28 ++++++++++++++++++---------- Unity/Plugins/SVMModel.dll | Bin 76800 -> 76800 bytes 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/Source/SVMWrapper.cpp b/Source/SVMWrapper.cpp index 5424619..314bae2 100644 --- a/Source/SVMWrapper.cpp +++ b/Source/SVMWrapper.cpp @@ -15,6 +15,7 @@ extern "C" void __cdecl print_log(const char *str) { struct SVMWrapper { struct svm_model *model; int cols; + int classes; double *means; double *sdevs; struct svm_parameter param; @@ -86,7 +87,7 @@ void destroy_problem(struct svm_problem *problem) { } extern "C" { - __declspec(dllexport) void *__cdecl trainModel(float features[], float labels[], int rows, int cols) { + __declspec(dllexport) void *__cdecl trainModel(float features[], float labels[], int rows, int cols, int classes, int probability, float C) { if (!(rows > 0 && cols > 0)) return NULL; @@ -94,6 +95,7 @@ extern "C" { struct SVMWrapper *sw = new SVMWrapper(); sw->cols = cols; + sw->classes = classes; sw->means = new double[cols]; sw->sdevs = new double[cols]; @@ -104,11 +106,11 @@ extern "C" { sw->param.coef0 = 0; sw->param.nu = 0.5; sw->param.cache_size = 100; - sw->param.C = 4; + sw->param.C = C; sw->param.eps = 0.001; sw->param.p = 0.1; sw->param.shrinking = 1; - sw->param.probability = 0; + sw->param.probability = probability; sw->param.nr_weight = 0; sw->param.weight_label = NULL; sw->param.weight = NULL; @@ -120,22 +122,23 @@ extern "C" { return sw; } - __declspec(dllexport) void __cdecl predict(void *ptr, float features[], float predictions[], int rows) { + __declspec(dllexport) void __cdecl predict(void *ptr, float features[], float predictions[], double probabilities[], int rows) { struct SVMWrapper *sw = (struct SVMWrapper *)ptr; int cols = sw->cols; svmnode **x = make_svmnode_array(features, rows, cols, sw->means, sw->sdevs, 0); for (int r = 0; r < rows; r++) - predictions[r] = (float)svm_predict(sw->model, x[r]); + predictions[r] = (float)svm_predict_probability(sw->model, x[r], &(probabilities[r * sw->classes])); destroy_svmnode_array(x); return; } - __declspec(dllexport) void *__cdecl loadModel(char *filename, int cols, double means[], double sdevs[]) { + __declspec(dllexport) void *__cdecl loadModel(char *filename, int cols, int classes, double means[], double sdevs[]) { struct SVMWrapper *sw = new SVMWrapper(); sw->cols = cols; sw->model = svm_load_model(filename); + sw->classes = classes; sw->means = new double[cols]; sw->sdevs = new double[cols]; for (int c = 0; c < cols; c++) { @@ -145,10 +148,11 @@ extern "C" { return sw; } - __declspec(dllexport) void *__cdecl loadModelString(char *modelString, int cols, double means[], double sdevs[]) { + __declspec(dllexport) void *__cdecl loadModelString(char *modelString, int cols, int classes, double means[], double sdevs[]) { struct SVMWrapper *sw = new SVMWrapper(); sw->cols = cols; sw->model = svm_load_model_string(modelString); + sw->classes = classes; sw->means = new double[cols]; sw->sdevs = new double[cols]; for (int c = 0; c < cols; c++) { @@ -168,7 +172,7 @@ extern "C" { } } - __declspec(dllexport) char *__cdecl getModel(void *ptr) { + __declspec(dllexport) char *__cdecl saveModelString (void *ptr) { struct SVMWrapper *sw = (struct SVMWrapper *)ptr; return svm_save_model_string(sw->model); } diff --git a/Unity/OpenSeeExpression.cs b/Unity/OpenSeeExpression.cs index 4143d62..ca251c8 100644 --- a/Unity/OpenSeeExpression.cs +++ b/Unity/OpenSeeExpression.cs @@ -25,6 +25,8 @@ public class OpenSeeExpression : MonoBehaviour public int overRecordingSkip = 5; [Tooltip("This is the filename used for loading and saving expression data using the load and save flags.")] public string filename = ""; + [Tooltip("When enabled, training will be done in such a way that the model can output probabilities. This is a lot slower!")] + public bool enableProbabilityTraining = false; [Header("Toggles")] [Tooltip("When enabled, calibration data will be collected from the given OpenSee component.")] public bool recording = false; @@ -54,6 +56,8 @@ public class OpenSeeExpression : MonoBehaviour public float expressionTime = 0f; [Tooltip("This is currently detected expression.")] public string expression = ""; + [Tooltip("These are the probabilities of all expressions. The labels in order can be retrieved through the GetClassLabels methods.")] + public double[] probabilities; [Header("Training statistics")] [Tooltip("This shows the accuracy result of the last training run.")] public float accuracy = 0.0f; @@ -362,7 +366,10 @@ public class OpenSeeExpression : MonoBehaviour i_test++; } } - model.TrainModel(X_train, y_train, rows_train, cols); + int probability = 0; + if (enableProbabilityTraining) + probability = 1; + model.TrainModel(X_train, y_train, rows_train, cols, probability); confusionMatrix = model.ConfusionMatrix(X_test, y_test, i_test, out accuracy); confusionMatrixString = SVMModel.FormatMatrix(confusionMatrix, classLabels); for (int label = 0; label < classes; label++) { @@ -399,7 +406,7 @@ public class OpenSeeExpression : MonoBehaviour float[] predictionData = new float[cols]; for (int i = 0; i < cols; i++) predictionData[i] = faceData[indices[i]]; - float[] prediction = model.Predict(predictionData, 1); + float[] prediction = model.Predict(predictionData, out probabilities, 1); int predictedExpression = (int)Mathf.Round(prediction[0]); if (predictedExpression == lastPrediction) lastPredictionCount++; @@ -464,6 +471,13 @@ public class OpenSeeExpression : MonoBehaviour return false; } } + + public string[] GetClassLabels() { + List labels = new List(); + foreach (string l in classLabels) + labels.Add(String.Copy(l)); + return labels.ToArray(); + } public string[] GetExpressions() { List keys = new List(); diff --git a/Unity/Plugins/SVMModel.cs b/Unity/Plugins/SVMModel.cs index 9beb8d1..ef5a0ad 100644 --- a/Unity/Plugins/SVMModel.cs +++ b/Unity/Plugins/SVMModel.cs @@ -12,16 +12,16 @@ namespace OpenSee { public class SVMModel { #region DllImport [DllImport("SVMModel", CallingConvention = CallingConvention.Cdecl)] - private static extern System.IntPtr trainModel(float[] features, float[] labels, int rows, int cols, float C); + private static extern System.IntPtr trainModel(float[] features, float[] labels, int rows, int cols, int classes, int probability, float C); [DllImport("SVMModel", CallingConvention = CallingConvention.Cdecl)] - private static extern void predict(System.IntPtr model, float[] features, [Out] float[] predictions, int rows); + private static extern void predict(System.IntPtr model, float[] features, [Out] float[] predictions, [Out] double[] probabilities, int rows); [DllImport("SVMModel", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - private static extern System.IntPtr loadModel([MarshalAs(UnmanagedType.LPStr)]string filename, int cols, double[] means, double[] sdevs); + private static extern System.IntPtr loadModel([MarshalAs(UnmanagedType.LPStr)]string filename, int cols, int classes, double[] means, double[] sdevs); [DllImport("SVMModel", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - private static extern System.IntPtr loadModelString([MarshalAs(UnmanagedType.LPStr)]string modelString, int cols, double[] means, double[] sdevs); + private static extern System.IntPtr loadModelString([MarshalAs(UnmanagedType.LPStr)]string modelString, int cols, int classes, double[] means, double[] sdevs); [DllImport("SVMModel", CallingConvention = CallingConvention.Cdecl)] private static extern void getScales(System.IntPtr model, [Out] double[] means, [Out] double[] sdevs); @@ -63,7 +63,7 @@ public class SVMModel { } cols = smr.cols; maxClasses = smr.maxClasses; - System.IntPtr model = loadModelString(smr.modelString, smr.cols, smr.means, smr.sdevs); + System.IntPtr model = loadModelString(smr.modelString, smr.cols, smr.maxClasses, smr.means, smr.sdevs); return model; } @@ -118,7 +118,7 @@ public class SVMModel { } // features is a rows*cols long float array with rows written one after another. labels is a rows long float array with the corresponding classes as integral numbers from 0 to num_classes-1, where all classes have to exist. Training on more than 10000 rows is disabled. - public bool TrainModel(float[] features, float[] labels, int rows, int cols) { + public bool TrainModel(float[] features, float[] labels, int rows, int cols, int probability) { DestroyModel(); if (rows > 10000) return false; @@ -143,7 +143,7 @@ public class SVMModel { maxClasses = max + 1; this.cols = cols; - model = trainModel(features, labels, rows, cols, 12f); + model = trainModel(features, labels, rows, cols, maxClasses, probability, 2f); haveModel = true; return true; @@ -163,7 +163,8 @@ public class SVMModel { } int[,] confusionMatrix = new int[maxClasses, maxClasses]; float[] predictions = new float[rows]; - predict(model, features, predictions, rows); + double[] probabilities = new double[rows * maxClasses]; + predict(model, features, predictions, probabilities, rows); for (int i = 0; i < rows; i++) { predictions[i] = (float)Math.Round(predictions[i]); if (predictions[i] >= 0 && predictions[i] < maxClasses) { @@ -219,15 +220,22 @@ public class SVMModel { } // features is a rows*cols long float array with rows written one after another. - public float[] Predict(float[] features, int rows) { + public float[] Predict(float[] features, out double[] probabilities, int rows) { + probabilities = null; if (!haveModel) return null; if (rows < 1 || features.Length != rows * cols) return null; float[] predictions = new float[rows]; - predict(model, features, predictions, rows); + probabilities = new double[rows * maxClasses]; + predict(model, features, predictions, probabilities, rows); return predictions; } + + public float[] Predict(float[] features, int rows) { + double[] probabilities; + return Predict(features, out probabilities, rows); + } public bool Ready() { return haveModel; diff --git a/Unity/Plugins/SVMModel.dll b/Unity/Plugins/SVMModel.dll index 831c85c495709c682ba97a5e320dff6b1c599b05..a273c8c1d375ce9eb2dded0610d940e518032eea 100644 GIT binary patch delta 6811 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 delta 6810 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 Lsn8|T8-D)-{$gW-