diff --git a/src/Microsoft.ML.Recommender/MatrixFactorizationTrainer.cs b/src/Microsoft.ML.Recommender/MatrixFactorizationTrainer.cs index 571287aaa0..dee4c412eb 100644 --- a/src/Microsoft.ML.Recommender/MatrixFactorizationTrainer.cs +++ b/src/Microsoft.ML.Recommender/MatrixFactorizationTrainer.cs @@ -31,24 +31,19 @@ namespace Microsoft.ML.Trainers /// and the value at the location specified by the two indexes. For an example data structure of a tuple, one can use: /// /// - /// // The following variables defines the shape of a m-by-n matrix. The variable firstRowIndex indicates the integer that - /// // would be mapped to the first row index. If user data uses 0-based indices for rows, firstRowIndex can be set to 0. - /// // Similarly, for 1-based indices, firstRowIndex could be 1. - /// const int firstRowIndex = 1; - /// const int firstColumnIndex = 1; + /// // The following variables defines the shape of a m-by-n matrix. Indexes start with 0; that is, our indexing system + /// // is 0-based. /// const int m = 60; /// const int n = 100; /// /// // A tuple of row index, column index, and rating. It specifies a value in the rating matrix. /// class MatrixElement /// { - /// // Matrix column index starts from firstColumnIndex and is at most firstColumnIndex+n-1. - /// // Contieuous=true means that all values from firstColumnIndex to firstColumnIndex+n-1 are allowed keys. - /// // [KeyType(Contiguous = true, Count = n, Min = firstColumnIndex)] - /// // public uint MatrixColumnIndex; - /// // Matrix row index starts from firstRowIndex and is at most firstRowIndex+m-1. - /// // Contieuous=true means that all values from firstRowIndex to firstRowIndex+m-1 are allowed keys. - /// [KeyType(Contiguous = true, Count = m, Min = firstRowIndex)] + /// // Matrix column index starts from 0 and is at most n-1. + /// [KeyType(n)] + /// public uint MatrixColumnIndex; + /// // Matrix row index starts from 0 and is at most m-1. + /// [KeyType(m)] /// public uint MatrixRowIndex; /// // The rating at the MatrixColumnIndex-th column and the MatrixRowIndex-th row. /// public float Value; @@ -65,7 +60,7 @@ namespace Microsoft.ML.Trainers /// R is approximated by the product of P's transpose and Q. This trainer implements /// a stochastic gradient method for finding P /// and Q via minimizing the distance between R and the product of P's transpose and Q.. - /// For users interested in the mathematical details, please see the references below. + /// The underlying library used in ML.NET matrix factorization can be found on a Github repository. For users interested in the mathematical details, please see the references below. /// /// /// A Fast Parallel Stochastic Gradient Method for Matrix Factorization in Shared Memory Systems @@ -76,6 +71,9 @@ namespace Microsoft.ML.Trainers /// /// LIBMF: A Library for Parallel Matrix Factorization in Shared-memory Systems /// + /// + /// Selection of Negative Samples for One-class Matrix Factorization + /// /// /// /// diff --git a/src/Microsoft.ML.Recommender/SafeTrainingAndModelBuffer.cs b/src/Microsoft.ML.Recommender/SafeTrainingAndModelBuffer.cs index 9bd70d9e1a..5072aac415 100644 --- a/src/Microsoft.ML.Recommender/SafeTrainingAndModelBuffer.cs +++ b/src/Microsoft.ML.Recommender/SafeTrainingAndModelBuffer.cs @@ -17,31 +17,50 @@ namespace Microsoft.ML.Recommender.Internal /// internal sealed class SafeTrainingAndModelBuffer : IDisposable { - [StructLayout(LayoutKind.Explicit)] + [StructLayout(LayoutKind.Sequential)] private struct MFNode { - [FieldOffset(0)] + /// + /// Row index. + /// public int U; - [FieldOffset(4)] + + /// + /// Column index; + /// public int V; - [FieldOffset(8)] + + /// + /// Matrix element's value at -th row and -th column. + /// public float R; } - [StructLayout(LayoutKind.Explicit)] + [StructLayout(LayoutKind.Sequential)] private unsafe struct MFProblem { - [FieldOffset(0)] + /// + /// Number of rows. + /// public int M; - [FieldOffset(4)] + + /// + /// Number of columns. + /// public int N; - [FieldOffset(8)] + + /// + /// Number of specified matrix elements in . + /// public long Nnz; - [FieldOffset(16)] + + /// + /// Specified matrix elements. + /// public MFNode* R; } - [StructLayout(LayoutKind.Explicit)] + [StructLayout(LayoutKind.Sequential)] private struct MFParameter { /// @@ -58,19 +77,16 @@ private struct MFParameter /// Fun 12 is solved by a coordinate descent method while other functions invoke /// a stochastic gradient method. /// - [FieldOffset(0)] public int Fun; /// /// Rank of factor matrices. /// - [FieldOffset(4)] public int K; /// /// Number of threads which can be used for training. /// - [FieldOffset(8)] public int NrThreads; /// @@ -78,110 +94,100 @@ private struct MFParameter /// method in LIBMF processes assigns each thread a block at one time. The ratings in one block /// would be sequentially accessed (not randomaly accessed like standard stochastic gradient methods). /// - [FieldOffset(12)] public int NrBins; /// /// Number of training iteration. At one iteration, all values in the training matrix are roughly accessed once. /// - [FieldOffset(16)] public int NrIters; /// /// L1-norm regularization coefficient of left factor matrix. /// - [FieldOffset(20)] public float LambdaP1; /// /// L2-norm regularization coefficient of left factor matrix. /// - [FieldOffset(24)] public float LambdaP2; /// /// L1-norm regularization coefficient of right factor matrix. /// - [FieldOffset(28)] public float LambdaQ1; /// /// L2-norm regularization coefficient of right factor matrix. /// - [FieldOffset(32)] public float LambdaQ2; /// /// Learning rate of LIBMF's stochastic gradient method. /// - [FieldOffset(36)] public float Eta; /// /// Coefficient of loss function on unobserved entries in the training matrix. It's used only with fun=12. /// - [FieldOffset(40)] public float Alpha; /// /// Desired value of unobserved entries in the training matrix. It's used only with fun=12. /// - [FieldOffset(44)] public float C; /// /// Specify if the factor matrices should be non-negative. /// - [FieldOffset(48)] - public int DoNmf; + public byte DoNmf; /// /// Set to true so that LIBMF may produce less information to STDOUT. /// - [FieldOffset(52)] - public int Quiet; + public byte Quiet; /// /// Set to false so that LIBMF may reuse and modifiy the data passed in. /// - [FieldOffset(56)] - public int CopyData; + public byte CopyData; } - [StructLayout(LayoutKind.Explicit)] + [StructLayout(LayoutKind.Sequential)] private unsafe struct MFModel { - [FieldOffset(0)] + /// + /// See . + /// public int Fun; + /// /// Number of rows in the training matrix. /// - [FieldOffset(4)] public int M; + /// /// Number of columns in the training matrix. /// - [FieldOffset(8)] public int N; + /// /// Rank of factor matrices. /// - [FieldOffset(12)] public int K; + /// /// Average value in the training matrix. /// - [FieldOffset(16)] public float B; + /// /// Left factor matrix. Its shape is M-by-K stored in row-major format. /// - [FieldOffset(24)] // pointer is 8-byte on 64-bit machine. public float* P; + /// /// Right factor matrix. Its shape is N-by-K stored in row-major format. /// - [FieldOffset(32)] // pointer is 8-byte on 64-bit machine. public float* Q; } @@ -223,9 +229,9 @@ public SafeTrainingAndModelBuffer(IHostEnvironment env, int fun, int k, int nrTh _mfParam.Eta = (float)eta; _mfParam.Alpha = (float)alpha; _mfParam.C = (float)c; - _mfParam.DoNmf = doNmf ? 1 : 0; - _mfParam.Quiet = quiet ? 1 : 0; - _mfParam.CopyData = copyData ? 1 : 0; + _mfParam.DoNmf = doNmf ? (byte)1 : (byte)0; + _mfParam.Quiet = quiet ? (byte)1 : (byte)0; + _mfParam.CopyData = copyData ? (byte)1 : (byte)0; } ~SafeTrainingAndModelBuffer() diff --git a/src/Native/MatrixFactorizationNative/CMakeLists.txt b/src/Native/MatrixFactorizationNative/CMakeLists.txt index 55c8f5c25b..8fed1afc90 100644 --- a/src/Native/MatrixFactorizationNative/CMakeLists.txt +++ b/src/Native/MatrixFactorizationNative/CMakeLists.txt @@ -1,12 +1,25 @@ project (MatrixFactorizationNative) add_definitions(-D_SCL_SECURE_NO_WARNINGS) +add_definitions(-DUSEOMP) +add_definitions(-DUSESSE) include_directories(libmf) -set(SOURCES - UnmanagedMemory.cpp - libmf/mf.cpp -) +if(UNIX) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -O3 -pthread -std=c++0x -march=native") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fopenmp") + if (APPLE) + include_directories("/usr/local/opt/libomp/include") + link_directories("/usr/local/opt/libomp/lib") + endif() +endif() + +if(WIN32) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /nologo /O2 /EHsc /D \"_CRT_SECURE_NO_DEPRECATE\" /openmp") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${OpenMP_EXE_LINKER_FLAGS}") +endif() + +set(SOURCES UnmanagedMemory.cpp libmf/mf.cpp) if(NOT WIN32) list(APPEND SOURCES ${VERSION_FILE_PATH}) diff --git a/src/Native/MatrixFactorizationNative/UnmanagedMemory.cpp b/src/Native/MatrixFactorizationNative/UnmanagedMemory.cpp index 6f93cf817e..83ed2f096f 100644 --- a/src/Native/MatrixFactorizationNative/UnmanagedMemory.cpp +++ b/src/Native/MatrixFactorizationNative/UnmanagedMemory.cpp @@ -9,28 +9,118 @@ using namespace mf; -EXPORT_API(void) MFDestroyModel(mf_model *&model) +inline mf_parameter TranslateToParam(const mf_parameter_bridge *param_bridge) { - return mf_destroy_model(&model); + mf_parameter param; + param.fun = param_bridge->fun; + param.k = param_bridge->k; + param.nr_threads = param_bridge->nr_threads; + param.nr_bins = param_bridge->nr_bins; + param.nr_iters = param_bridge->nr_iters; + param.lambda_p1 = param_bridge->lambda_p1; + param.lambda_p2 = param_bridge->lambda_p2; + param.lambda_q1 = param_bridge->lambda_q1; + param.lambda_q2 = param_bridge->lambda_q2; + param.eta = param_bridge->eta; + param.alpha = param_bridge->alpha; + param.c = param_bridge->c; + param.do_nmf = param_bridge->do_nmf != 0 ? true : false; + param.quiet = param_bridge->quiet != 0 ? true : false; + param.copy_data = param_bridge->copy_data != 0 ? true : false; + return param; } -EXPORT_API(mf_model*) MFTrain(const mf_problem *prob, const mf_parameter *param) +inline mf_problem TranslateToProblem(const mf_problem_bridge *prob_bridge) { - return mf_train(prob, *param); + mf_problem prob; + prob.m = prob_bridge->m; + prob.n = prob_bridge->n; + prob.nnz = prob_bridge->nnz; + prob.R = prob_bridge->R; + return prob; } -EXPORT_API(mf_model*) MFTrainWithValidation(const mf_problem *tr, const mf_problem *va, const mf_parameter *param) +inline void TranslateToModelBridge(const mf_model *model, mf_model_bridge *model_bridge) { - return mf_train_with_validation(tr, va, *param); + model_bridge->fun = model->fun; + model_bridge->m = model->m; + model_bridge->n = model->n; + model_bridge->k = model->k; + model_bridge->b = model->b; + model_bridge->P = model->P; + model_bridge->Q = model->Q; } +inline void TranslateToModel(const mf_model_bridge *model_bridge, mf_model *model) +{ + model->fun = model_bridge->fun; + model->m = model_bridge->m; + model->n = model_bridge->n; + model->k = model_bridge->k; + model->b = model_bridge->b; + model->P = model_bridge->P; + model->Q = model_bridge->Q; +} + +EXPORT_API(void) MFDestroyModel(mf_model_bridge *&model_bridge) +{ + // Transfer the ownership of P and Q back to the original LIBMF class, so that + // mf_destroy_model can be called. + auto model = new mf_model; + model->P = model_bridge->P; + model->Q = model_bridge->Q; + mf_destroy_model(&model); // delete model, model->P, amd model->Q. + + // Delete bridge class allocated in MFTrain, MFTrainWithValidation, or MFCrossValidation. + delete model_bridge; + model_bridge = nullptr; +} + +EXPORT_API(mf_model_bridge*) MFTrain(const mf_problem_bridge *prob_bridge, const mf_parameter_bridge *param_bridge) +{ + // Convert objects created outside LIBMF. Notice that the called LIBMF function doesn't take the ownership of + // allocated memory in those external objects. + auto prob = TranslateToProblem(prob_bridge); + auto param = TranslateToParam(param_bridge); + + // The model contains 3 allocated things --- itself, P, and Q. + // We will delete itself and transfer the ownership of P and Q to the associated bridge class. The bridge class + // will then be sent to C#. + auto model = mf_train(&prob, param); + auto model_bridge = new mf_model_bridge; + TranslateToModelBridge(model, model_bridge); + delete model; + return model_bridge; // To clean memory up, we need to delete model_bridge, model_bridge->P, and model_bridge->Q. +} + +EXPORT_API(mf_model_bridge*) MFTrainWithValidation(const mf_problem_bridge *tr_bridge, const mf_problem_bridge *va_bridge, const mf_parameter_bridge *param_bridge) +{ + // Convert objects created outside LIBMF. Notice that the called LIBMF function doesn't take the ownership of + // allocated memory in those external objects. + auto tr = TranslateToProblem(tr_bridge); + auto va = TranslateToProblem(va_bridge); + auto param = TranslateToParam(param_bridge); + + // The model contains 3 allocated things --- itself, P, and Q. + // We will delete itself and transfer the ownership of P and Q to the associated bridge class. The bridge class + // will then be sent to C#. + auto model = mf_train_with_validation(&tr, &va, param); + auto model_bridge = new mf_model_bridge; + TranslateToModelBridge(model, model_bridge); + delete model; + return model_bridge; // To clean memory up, we need to delete model_bridge, model_bridge->P, and model_bridge->Q. +} -EXPORT_API(float) MFCrossValidation(const mf_problem *prob, int nr_folds, const mf_parameter *param) +EXPORT_API(float) MFCrossValidation(const mf_problem_bridge *prob_bridge, int32_t nr_folds, const mf_parameter_bridge *param_bridge) { - return mf_cross_validation(prob, nr_folds, *param); + auto param = TranslateToParam(param_bridge); + auto prob = TranslateToProblem(prob_bridge); + return mf_cross_validation(&prob, nr_folds, param); } -EXPORT_API(float) MFPredict(const mf_model *model, int p_idx, int q_idx) +EXPORT_API(float) MFPredict(const mf_model_bridge *model_bridge, int32_t p_idx, int32_t q_idx) { - return mf_predict(model, p_idx, q_idx); + mf_model model; + TranslateToModel(model_bridge, &model); + return mf_predict(&model, p_idx, q_idx); } diff --git a/src/Native/MatrixFactorizationNative/UnmanagedMemory.h b/src/Native/MatrixFactorizationNative/UnmanagedMemory.h index 6007d35e30..c63096bf93 100644 --- a/src/Native/MatrixFactorizationNative/UnmanagedMemory.h +++ b/src/Native/MatrixFactorizationNative/UnmanagedMemory.h @@ -8,12 +8,50 @@ using namespace mf; -EXPORT_API(void) MFDestroyModel(mf_model *&model); +struct mf_parameter_bridge +{ + int32_t fun; + int32_t k; + int32_t nr_threads; + int32_t nr_bins; + int32_t nr_iters; + float lambda_p1; + float lambda_p2; + float lambda_q1; + float lambda_q2; + float eta; + float alpha; + float c; + uint8_t do_nmf; + uint8_t quiet; + uint8_t copy_data; +}; -EXPORT_API(mf_model*) MFTrain(const mf_problem *prob, const mf_parameter *param); +struct mf_problem_bridge +{ + int32_t m; + int32_t n; + int64_t nnz; + struct mf_node *R; +}; -EXPORT_API(mf_model*) MFTrainWithValidation(const mf_problem *tr, const mf_problem *va, const mf_parameter *param); +struct mf_model_bridge +{ + int32_t fun; + int32_t m; + int32_t n; + int32_t k; + float b; + float *P; + float *Q; +}; + +EXPORT_API(void) MFDestroyModel(mf_model_bridge *&model); + +EXPORT_API(mf_model_bridge*) MFTrain(const mf_problem_bridge *prob_bridge, const mf_parameter_bridge *parameter_bridge); + +EXPORT_API(mf_model_bridge*) MFTrainWithValidation(const mf_problem_bridge *tr, const mf_problem_bridge *va, const mf_parameter_bridge *parameter_bridge); -EXPORT_API(float) MFCrossValidation(const mf_problem *prob, int nr_folds, const mf_parameter* param); +EXPORT_API(float) MFCrossValidation(const mf_problem_bridge *prob, int32_t nr_folds, const mf_parameter_bridge* parameter_bridge); -EXPORT_API(float) MFPredict(const mf_model *model, int p_idx, int q_idx); +EXPORT_API(float) MFPredict(const mf_model_bridge *model, int32_t p_idx, int32_t q_idx); diff --git a/src/Native/MatrixFactorizationNative/libmf b/src/Native/MatrixFactorizationNative/libmf index 5b055ea473..298715a4e4 160000 --- a/src/Native/MatrixFactorizationNative/libmf +++ b/src/Native/MatrixFactorizationNative/libmf @@ -1 +1 @@ -Subproject commit 5b055ea473756bd14f56b49db7e0483271788cc2 +Subproject commit 298715a4e458bc09c6a27c8643a58095afbdadf1 diff --git a/test/Microsoft.ML.TestFramework/Attributes/MatrixFactorizationFactAttribute.cs b/test/Microsoft.ML.TestFramework/Attributes/MatrixFactorizationFactAttribute.cs index 9104008ce2..4b9969c54f 100644 --- a/test/Microsoft.ML.TestFramework/Attributes/MatrixFactorizationFactAttribute.cs +++ b/test/Microsoft.ML.TestFramework/Attributes/MatrixFactorizationFactAttribute.cs @@ -10,14 +10,14 @@ namespace Microsoft.ML.TestFramework.Attributes /// public sealed class MatrixFactorizationFactAttribute : EnvironmentSpecificFactAttribute { - public MatrixFactorizationFactAttribute() : base("Disabled - this test is being fixed as part of https://github.com/dotnet/machinelearning/issues/1441") + public MatrixFactorizationFactAttribute() : base("") { } /// protected override bool IsEnvironmentSupported() { - return Environment.Is64BitProcess; + return true; } } } \ No newline at end of file diff --git a/test/Microsoft.ML.Tests/TrainerEstimators/MatrixFactorizationTests.cs b/test/Microsoft.ML.Tests/TrainerEstimators/MatrixFactorizationTests.cs index 23289dcd48..f82d650e30 100644 --- a/test/Microsoft.ML.Tests/TrainerEstimators/MatrixFactorizationTests.cs +++ b/test/Microsoft.ML.Tests/TrainerEstimators/MatrixFactorizationTests.cs @@ -92,10 +92,10 @@ public void MatrixFactorizationSimpleTrainAndPredict() // MF produce different matrixes on different platforms, so at least test thier content on windows. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - Assert.Equal(leftMatrix[0], (double)0.3091519, 5); - Assert.Equal(leftMatrix[leftMatrix.Count - 1], (double)0.5639161, 5); - Assert.Equal(rightMatrix[0], (double)0.243584976, 5); - Assert.Equal(rightMatrix[rightMatrix.Count - 1], (double)0.380032182, 5); + Assert.Equal(0.33491, leftMatrix[0], 5); + Assert.Equal(0.571346991, leftMatrix[leftMatrix.Count - 1], 5); + Assert.Equal(0.2433036714792256, rightMatrix[0], 5); + Assert.Equal(0.381277978420258, rightMatrix[rightMatrix.Count - 1], 5); } // Read the test data set as an IDataView var testData = reader.Load(new MultiFileSource(GetDataPath(TestDatasets.trivialMatrixFactorization.testFilename))); @@ -123,7 +123,7 @@ public void MatrixFactorizationSimpleTrainAndPredict() if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { // Linux case - var expectedUnixL2Error = 0.616821448679879; // Linux baseline + var expectedUnixL2Error = 0.614457914950479; // Linux baseline Assert.InRange(metrices.MeanSquaredError, expectedUnixL2Error - tolerance, expectedUnixL2Error + tolerance); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) @@ -136,7 +136,7 @@ public void MatrixFactorizationSimpleTrainAndPredict() else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // Windows case - var expectedWindowsL2Error = 0.61528733643754685; // Windows baseline + var expectedWindowsL2Error = 0.6098110249191965; // Windows baseline Assert.InRange(metrices.MeanSquaredError, expectedWindowsL2Error - tolerance, expectedWindowsL2Error + tolerance); }