diff --git a/.ci/ci.yml b/.ci/ci.yml index dd6357a32b1..a57440caffc 100644 --- a/.ci/ci.yml +++ b/.ci/ci.yml @@ -104,6 +104,12 @@ jobs: CXX: "clang++" cmakeOptions: '$(commonSWIGCMakeFlags) -DINTERFACE_RUBY=ON' interfaceName: 'ruby' + ruby: + CC: "clang" + CXX: "clang++" + cmakeOptions: '$(commonSWIGCMakeFlags) -DINTERFACE_COREML=ON' + interfaceName: 'coreml' + variables: testRunTitle: '$(build.sourceBranchName)-debian' diff --git a/cmake/ShogunUtils.cmake b/cmake/ShogunUtils.cmake index 4fcfb533c53..39867997e16 100644 --- a/cmake/ShogunUtils.cmake +++ b/cmake/ShogunUtils.cmake @@ -315,3 +315,39 @@ function(ADD_SHOGUN_BENCHMARK REL_BENCHMARK_NAME) set_tests_properties(${BENCHMARK_NAME} PROPERTIES ${ARGN}) endif() endfunction() + +function(ADD_SHOGUN_UNITTEST) + set(options) + set(oneValueArgs TARGET) + set(multiValueArgs LABELS) + cmake_parse_arguments(ADD_SHOGUN_UNITTEST "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT ENABLE_TESTING) + return() + endif() + get_filename_component(UNITTEST_NAME ${ADD_SHOGUN_UNITTEST_TARGET} NAME_WE) + + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${ADD_SHOGUN_UNITTEST_TARGET}.cc) + # This benchmark has a corresponding .cc file, set it up as an executable. + add_executable(${UNITTEST_NAME} "${ADD_SHOGUN_UNITTEST_TARGET}.cc") + set_target_properties (${UNITTEST_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + set_target_properties (${UNITTEST_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/bin) + set_target_properties (${UNITTEST_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/bin) + target_link_libraries(${UNITTEST_NAME} ${SHOGUN_UNITTEST_LINK_LIBS}) + add_dependencies(${UNITTEST_NAME} GoogleMock shogun::shogun) + target_include_directories(${UNITTEST_NAME} PRIVATE ${source_dir}/googlemock/include ${source_dir}/googletest/include) + set(NO_COLOR "--color_print=false") + endif() + + add_test(${UNITTEST_NAME} ${CMAKE_BINARY_DIR}/bin/${UNITTEST_NAME} ${NO_COLOR}) + if (ADD_SHOGUN_UNITTEST_LABELS) + set_tests_properties(${UNITTEST_NAME} PROPERTIES LABELS ${ADD_SHOGUN_UNITTEST_LABELS}) + else () + set_tests_properties(${UNITTEST_NAME} PROPERTIES LABELS "unit") + endif() + + if(ARGN) + set_tests_properties(${UNITTEST_NAME} PROPERTIES ${ARGN}) + endif() +endfunction() + diff --git a/cmake/external/GoogleTestNMock.cmake b/cmake/external/GoogleTestNMock.cmake index 8bec01282f1..f5e6230b0de 100644 --- a/cmake/external/GoogleTestNMock.cmake +++ b/cmake/external/GoogleTestNMock.cmake @@ -1,44 +1,46 @@ -MergeCFLAGS() -include(ExternalProject) +IF (NOT TARGET GoogleMock) + MergeCFLAGS() + include(ExternalProject) -IF (MSVC) - SET (CUSTOM_CMAKE_ARGS -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY:PATH=${THIRD_PARTY_DIR}/libs/gmock - -DCMAKE_LIBRARY_OUTPUT_DIRECTORY:PATH=${THIRD_PARTY_DIR}/libs/gmock - -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS}${CMAKE_DEFINITIONS} - -DCMAKE_CXX_FLAGS_RELEASE:STRING=${CMAKE_CXX_FLAGS_RELEASE} - -DCMAKE_CXX_FLAGS_DISTRIBUTION:STRING=${CMAKE_CXX_FLAGS_DISTRIBUTION} - -DCMAKE_CXX_FLAGS_DEBUG:STRING=${CMAKE_CXX_FLAGS_DEBUG} - -DCMAKE_C_COMPILER:STRING=${CMAKE_C_COMPILER} - -DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER} - ) -ELSE () - SET(MERGED_CXX_FLAGS "${MERGED_CXX_FLAGS} -fPIC") - SET (CUSTOM_CMAKE_ARGS -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY:PATH=${THIRD_PARTY_DIR}/libs/gmock - -DCMAKE_LIBRARY_OUTPUT_DIRECTORY:PATH=${THIRD_PARTY_DIR}/libs/gmock - -DCMAKE_CXX_FLAGS:STRING=${MERGED_CXX_FLAGS}${CMAKE_DEFINITIONS} - -DCMAKE_C_COMPILER:STRING=${CMAKE_C_COMPILER} - -DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER} - ) -ENDIF() + IF (MSVC) + SET (CUSTOM_CMAKE_ARGS -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY:PATH=${THIRD_PARTY_DIR}/libs/gmock + -DCMAKE_LIBRARY_OUTPUT_DIRECTORY:PATH=${THIRD_PARTY_DIR}/libs/gmock + -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS}${CMAKE_DEFINITIONS} + -DCMAKE_CXX_FLAGS_RELEASE:STRING=${CMAKE_CXX_FLAGS_RELEASE} + -DCMAKE_CXX_FLAGS_DISTRIBUTION:STRING=${CMAKE_CXX_FLAGS_DISTRIBUTION} + -DCMAKE_CXX_FLAGS_DEBUG:STRING=${CMAKE_CXX_FLAGS_DEBUG} + -DCMAKE_C_COMPILER:STRING=${CMAKE_C_COMPILER} + -DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER} + ) + ELSE () + SET(MERGED_CXX_FLAGS "${MERGED_CXX_FLAGS} -fPIC") + SET (CUSTOM_CMAKE_ARGS -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY:PATH=${THIRD_PARTY_DIR}/libs/gmock + -DCMAKE_LIBRARY_OUTPUT_DIRECTORY:PATH=${THIRD_PARTY_DIR}/libs/gmock + -DCMAKE_CXX_FLAGS:STRING=${MERGED_CXX_FLAGS}${CMAKE_DEFINITIONS} + -DCMAKE_C_COMPILER:STRING=${CMAKE_C_COMPILER} + -DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER} + ) + ENDIF() -IF(EXISTS /usr/src/googletest) - ExternalProject_Add( - GoogleMock - DOWNLOAD_COMMAND "" - SOURCE_DIR /usr/src/googletest - PREFIX ${CMAKE_BINARY_DIR}/GoogleMock - INSTALL_COMMAND "" - CMAKE_ARGS ${CUSTOM_CMAKE_ARGS} - ) -ELSE() - ExternalProject_Add( - GoogleMock - URL https://github.com/google/googletest/archive/release-1.8.1.tar.gz - URL_MD5 2e6fbeb6a91310a16efe181886c59596 - TIMEOUT 10 - PREFIX ${CMAKE_BINARY_DIR}/GoogleMock - DOWNLOAD_DIR ${THIRD_PARTY_DIR}/GoogleMock - INSTALL_COMMAND "" - CMAKE_ARGS ${CUSTOM_CMAKE_ARGS} - ) + IF(EXISTS /usr/src/googletest) + ExternalProject_Add( + GoogleMock + DOWNLOAD_COMMAND "" + SOURCE_DIR /usr/src/googletest + PREFIX ${CMAKE_BINARY_DIR}/GoogleMock + INSTALL_COMMAND "" + CMAKE_ARGS ${CUSTOM_CMAKE_ARGS} + ) + ELSE() + ExternalProject_Add( + GoogleMock + URL https://github.com/google/googletest/archive/release-1.8.1.tar.gz + URL_MD5 2e6fbeb6a91310a16efe181886c59596 + TIMEOUT 10 + PREFIX ${CMAKE_BINARY_DIR}/GoogleMock + DOWNLOAD_DIR ${THIRD_PARTY_DIR}/GoogleMock + INSTALL_COMMAND "" + CMAKE_ARGS ${CUSTOM_CMAKE_ARGS} + ) + ENDIF() ENDIF() diff --git a/src/interfaces/coreml/CMakeLists.txt b/src/interfaces/coreml/CMakeLists.txt index 3d0c0acc03d..d4d3e1f28fd 100644 --- a/src/interfaces/coreml/CMakeLists.txt +++ b/src/interfaces/coreml/CMakeLists.txt @@ -1,11 +1,17 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) +add_library(shogun-coreml SHARED CoreMLModel.cc CoreMLConverter.cc KernelConverter.cc SVMConverter.cc) +set_property(TARGET shogun-coreml PROPERTY POSITION_INDEPENDENT_CODE ON) +target_link_libraries(shogun-coreml shogun::shogun) + IF (PROTOBUF_FOUND AND ENABLE_PROTOBUF) FILE(GLOB COREML_SRC "${CMAKE_CURRENT_SOURCE_DIR}/*.proto") FILE(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + SET(COREML_SOURCES) FOREACH (SRC_FILE ${COREML_SRC}) GET_FILENAME_COMPONENT(SRC_FILE_WE ${SRC_FILE} NAME_WE) - SET(COMPILED_SRC "${CMAKE_CURRENT_BINARY_DIR}/coreml/${SRC_FILE_WE}") + SET(COMPILED_SRC "${CMAKE_CURRENT_BINARY_DIR}/${SRC_FILE_WE}") SET(COMPILED_SRC_H "${COMPILED_SRC}.pb.h") - SET(COMPILED_SRC_CPP "${COMPILED_SRC}.pb.cpp") + SET(COMPILED_SRC_CPP "${COMPILED_SRC}.pb.cc") ADD_CUSTOM_COMMAND( OUTPUT "${COMPILED_SRC_H}" "${COMPILED_SRC_CPP}" COMMAND "${PROTOBUF_PROTOC_EXECUTABLE}" @@ -16,8 +22,29 @@ IF (PROTOBUF_FOUND AND ENABLE_PROTOBUF) ) SET(PROTO_TARGET_NAME "CoreML_${SRC_FILE_WE}") ADD_CUSTOM_TARGET("${PROTO_TARGET_NAME}" DEPENDS "${COMPILED_SRC_H}" "${COMPILED_SRC_CPP}") - ADD_DEPENDENCIES(libshogun "${PROTO_TARGET_NAME}") + LIST(APPEND COREML_SOURCES ${COMPILED_SRC_CPP}) ENDFOREACH() + add_library(coreml STATIC ${COREML_SOURCES}) + target_link_libraries(shogun-coreml coreml ${Protobuf_LITE_LIBRARIES}) ELSE() MESSAGE(FATAL_ERROR "Protobuf is required for CoreML") ENDIF() + +IF (ENABLE_TESTING) + include(external/GoogleTestNMock) + ExternalProject_Get_Property(GoogleMock source_dir) + LINK_DIRECTORIES(${THIRD_PARTY_DIR}/libs/gmock) + + enable_testing() + add_library(coreml_unittest_main ${CMAKE_CURRENT_SOURCE_DIR}/unittest_main.cc) + target_link_libraries(coreml_unittest_main gmock gtest shogun::shogun) + add_dependencies(coreml_unittest_main GoogleMock shogun::shogun) + target_include_directories(coreml_unittest_main + PRIVATE + ${source_dir}/googlemock/include + ${source_dir}/googletest/include) + set(SHOGUN_UNITTEST_LINK_LIBS coreml_unittest_main shogun::shogun shogun-coreml gmock gtest) + + ADD_SHOGUN_UNITTEST(TARGET SVMConverter_test LABELS coreml-unit) + ADD_SHOGUN_UNITTEST(TARGET KernelConverter_test LABELS coreml-unit) +ENDIF() \ No newline at end of file diff --git a/src/interfaces/coreml/CoreMLConverter.cc b/src/interfaces/coreml/CoreMLConverter.cc new file mode 100644 index 00000000000..bea4936986c --- /dev/null +++ b/src/interfaces/coreml/CoreMLConverter.cc @@ -0,0 +1,27 @@ +#include "CoreMLConverter.h" +#include "SVMConverter.h" + +#include + + +using namespace shogun; +using namespace shogun::coreml; + +std::shared_ptr shogun::coreml::convert(const CMachine* m) +{ + /* + auto converter_registry = ConverterFactory::instance(); + std::string machine_name(m->get_name()); + if (!m->is_trained()) + throw NotFittedException("The supplied machine is not trained!"); + + auto model = std::make_shared(); + auto spec = model->get_specification(); + //(*converter_registry)(machine_name)(m, spec); + + return model; + */ + return nullptr; +} + + diff --git a/src/interfaces/coreml/CoreMLConverter.h b/src/interfaces/coreml/CoreMLConverter.h new file mode 100644 index 00000000000..686bdcc2ba7 --- /dev/null +++ b/src/interfaces/coreml/CoreMLConverter.h @@ -0,0 +1,93 @@ +#ifndef __COREML_CONVERTER_H__ +#define __COREML_CONVERTER_H__ + +#include +#include +#include + +#include + +#include "CoreMLModel.h" + +namespace shogun +{ + namespace coreml + { + static constexpr int32_t SPECIFICATION_VERSION = 1; + + struct visitor; + + struct ICoreMLConverter { + virtual void convert(visitor& v, const CSGObject* o) const = 0; + }; + + template + struct CoreMLConverter; + + struct visitor { + template + void visit(T*, I*); + }; + + template + struct CoreMLConverter: ICoreMLConverter { + typedef I input_type; + typedef O output_type; + + std::shared_ptr m_spec; + CoreMLConverter(std::shared_ptr spec): m_spec(spec) {} + + void convert(visitor& c, const CSGObject* o) const override + { + auto x = o->as(); + c.visit(this, x); + } + + static O* convert(const I* o); + static const std::unordered_set supported_types; + }; +/* + + class ConverterFactory + { + typedef std::function(std::shared_ptr)> ConverterFactoryFunction; + + public: + bool register_converter(const std::string& machine_name, ConverterFunction f) + { + return m_registry.emplace(std::make_pair(machine_name, f)).second; + } + + ConverterFunction& operator()(const std::string& m) + { + auto f = m_registry.find(m); + if (f == m_registry.end()) + throw std::runtime_error("The provided machine cannot be converted to CoreML format!"); + return f->second; + } + static ConverterFactory* instance() + { + static ConverterFactory* f = new ConverterFactory(); + return f; + } + + private: + std::unordered_map m_registry; + }; +*/ + + std::shared_ptr convert(const CMachine* m); + +#define REGISTER_COREML_CONVERTER(factory, classname, machines, function) \ + static int register_converter##classname = []() { \ + for (auto m = machines.cbegin(); m != machines.cend(); ++m) \ + factory->register_converter(*m, function); \ + return factory->size(); \ + }(); + +#define REGISTER_CONVERTER(classname, machines, function) \ + REGISTER_COREML_CONVERTER(ConverterFactory::instance(), classname, machines, function) + } +} + +#endif diff --git a/src/interfaces/coreml/CoreMLModel.cc b/src/interfaces/coreml/CoreMLModel.cc new file mode 100644 index 00000000000..c24a3b9bf8f --- /dev/null +++ b/src/interfaces/coreml/CoreMLModel.cc @@ -0,0 +1,33 @@ +#include "CoreMLModel.h" + +#include + +#include + +#include "Model.pb.h" + +using namespace shogun::coreml; + +CoreMLModel::CoreMLModel(): + m_spec(std::make_shared()) +{ +} + +CoreMLModel::~CoreMLModel() +{ + m_spec.reset(); +} + +void CoreMLModel::save(const std::string& filename) const +{ + std::fstream out(filename, std::ios::binary | std::ios::out); + this->save(out); + out.close(); +} + +void CoreMLModel::save(std::ostream& out) const +{ + ::google::protobuf::io::OstreamOutputStream pb_out(&out); + if (!m_spec->SerializeToZeroCopyStream(&pb_out)) + throw std::runtime_error("could not save"); +} diff --git a/src/interfaces/coreml/CoreMLModel.h b/src/interfaces/coreml/CoreMLModel.h new file mode 100644 index 00000000000..b211d2fd672 --- /dev/null +++ b/src/interfaces/coreml/CoreMLModel.h @@ -0,0 +1,39 @@ +#ifndef __COREML_MODEL_H__ +#define __COREML_MODEL_H__ + +#include +#include +#include + +namespace CoreML +{ + namespace Specification + { + class Model; + } +} + +namespace shogun +{ + namespace coreml + { + class CoreMLModel + { + public: + CoreMLModel(); + ~CoreMLModel(); + + void save(const std::string& filename) const; + void save(std::ostream& out) const; + + std::shared_ptr get_specification() const + { + return m_spec; + } + + private: + std::shared_ptr m_spec; + }; + } +} +#endif diff --git a/src/interfaces/coreml/KernelConverter.cc b/src/interfaces/coreml/KernelConverter.cc new file mode 100644 index 00000000000..bb88c683ef3 --- /dev/null +++ b/src/interfaces/coreml/KernelConverter.cc @@ -0,0 +1,75 @@ +#include "KernelConverter.h" + +#include "Model.pb.h" +#include "SVM.pb.h" + +using namespace shogun; +using namespace shogun::coreml; + +template +struct converter +{ + static auto convert(const I* k); + static auto convert(const CKernel* k) + { + return convert(k->as()); + } +}; + +template<> +auto converter::convert(const CGaussianKernel *k) +{ + auto rbf = new CoreML::Specification::RBFKernel(); + rbf->set_gamma(k->get_width()); + return rbf; +} + +template<> +auto converter::convert(const CSigmoidKernel *k) +{ + auto sigmoid_kernel = new CoreML::Specification::SigmoidKernel(); + sigmoid_kernel->set_gamma(k->get("gamma")); + sigmoid_kernel->set_c(k->get("coef0")); + return sigmoid_kernel; +} + +template<> +auto converter::convert(const CPolyKernel *k) +{ + auto poly_kernel = new CoreML::Specification::PolyKernel(); + poly_kernel->set_degree(k->get("degree")); + poly_kernel->set_c(k->get("c")); + poly_kernel->set_gamma(k->get("gamma")); + + return poly_kernel; +} + +CoreML::Specification::Kernel* KernelConverter::convert(const CKernel *k) +{ + auto kernel_spec = new CoreML::Specification::Kernel(); + auto kernel_type = std::string(k->get_name()); + + if (kernel_type == "LinearKernel") + { + kernel_spec->set_allocated_linearkernel(new CoreML::Specification::LinearKernel()); + } + else if (kernel_type == "GaussianKernel") + { + //auto gk = k->as(); + kernel_spec->set_allocated_rbfkernel(converter::convert(k)); + } + else if (kernel_type == "SigmoidKernel") + { + kernel_spec->set_allocated_sigmoidkernel(converter::convert(k)); + } + else if (kernel_type == "PolyKernel") + { + kernel_spec->set_allocated_polykernel(converter::convert(k)); + } + else + { + throw std::runtime_error("Kernel type is not supported in CoreML"); + } + + return kernel_spec; +}; diff --git a/src/interfaces/coreml/KernelConverter.h b/src/interfaces/coreml/KernelConverter.h new file mode 100644 index 00000000000..97b2fac3a44 --- /dev/null +++ b/src/interfaces/coreml/KernelConverter.h @@ -0,0 +1,31 @@ +#ifndef __KERNEL_CONVERTER_H__ +#define __KERNEL_CONVERTER_H__ + +#include "CoreMLConverter.h" + +#include +#include +#include +#include +#include + +namespace CoreML +{ + namespace Specification + { + class Kernel; + } +} + +namespace shogun +{ + namespace coreml + { + struct KernelConverter + { + static CoreML::Specification::Kernel* convert(const CKernel* k); + }; + } +} + +#endif diff --git a/src/interfaces/coreml/KernelConverter_test.cc b/src/interfaces/coreml/KernelConverter_test.cc new file mode 100644 index 00000000000..2666adc8d7c --- /dev/null +++ b/src/interfaces/coreml/KernelConverter_test.cc @@ -0,0 +1,55 @@ +#include +#include "KernelConverter.h" + +#include +#include +#include +#include + +#include "SVM.pb.h" + +using namespace shogun; +using namespace shogun::coreml; + +TEST(LinearKernel, convert) +{ + auto lk = some(); + auto descr = KernelConverter::convert(lk.get()); +} + +TEST(GaussianKernel, convert) +{ + auto k = some(); + auto descr = KernelConverter::convert(k.get()); + + ASSERT_TRUE(descr->has_rbfkernel()); + + auto rbf = descr->rbfkernel(); + ASSERT_EQ(k->get_width(), rbf.gamma()); +} + +TEST(PolyKernel, convert) +{ + auto k = std::make_shared(10, 3, 2.0, 2.2); + auto descr = KernelConverter::convert(k.get()); + + ASSERT_TRUE(descr->has_polykernel()); + + auto pk = descr->polykernel(); + ASSERT_EQ(k->get("gamma"), pk.gamma()); + ASSERT_EQ(k->get("c"), pk.c()); + ASSERT_EQ(k->get("degree"), pk.degree()); +} + +TEST(SigmoidKernel, convert) +{ + auto k = std::make_shared(10, 55.5, 2.3); + auto descr = KernelConverter::convert(k.get()); + + ASSERT_TRUE(descr->has_sigmoidkernel()); + + auto sk = descr->sigmoidkernel(); + ASSERT_EQ(k->get("gamma"), sk.gamma()); + ASSERT_EQ(k->get("coef0"), sk.c()); + +} diff --git a/src/interfaces/coreml/SVMConverter.cc b/src/interfaces/coreml/SVMConverter.cc new file mode 100644 index 00000000000..be505a004f8 --- /dev/null +++ b/src/interfaces/coreml/SVMConverter.cc @@ -0,0 +1,115 @@ +#include "SVMConverter.h" +#include "KernelConverter.h" + +#include "Model.pb.h" +#include "SVM.pb.h" + +#include +#include +#include +#include +#include +#include + +using namespace shogun; +using namespace shogun::coreml; + +using SVMRegressorConverterType = CoreMLConverter; + +template<> +const std::unordered_set SVMRegressorConverterType::supported_types = {"LibSVR", "SVRLight"}; + +template +static bool set_support_vectors(const CSVM* svm, const CKernel* k, T* machine_spec) +{ + auto svs = svm->get_support_vectors(); + auto lhs = k->get_lhs(); + if (lhs == nullptr) + throw std::runtime_error("Features are not set in kernel (required for support vectors), cannot export to CoreML!"); + + switch (lhs->get_feature_type()) + { + case F_UNKNOWN: + case F_BOOL: + case F_BYTE: + case F_ANY: + throw std::runtime_error("CoreML does not support the provided feature type!"); + } + + switch (lhs->get_feature_class()) + { + + case C_DENSE: + { + auto svs_spec = machine_spec->mutable_densesupportvectors(); + // FIXME: support all CDenseFeatures type! + auto dense_features = dynamic_cast*>(lhs); + for (auto sv_idx: svs) + { + auto sv = dense_features->get_feature_vector(sv_idx); + auto sv_spec = svs_spec->add_vectors(); + for (auto v: sv) + sv_spec->add_values(v); + } + + break; + } +/* + case C_SPARSE: + { + auto svs_spec = machine_spec->mutable_sparsesupportvectors(); + for (auto sv_idx: svs) + { + auto sv = lhs->get_sparse_feature_vector(sv_idx); + auto sv_spec = svs_spec->add_vectors(); + for (index_t i = 0; i < sv->num_feat_entries; ++i) + { + auto node = sv_spec->add_nodes(); + node->set_value(sv->features[i].entry); + node->set_index(sv->features[i].feat_index); + } + } + break; + } + */ + default: + throw std::runtime_error("CoreML does not support the provided feature class!"); + + } +} + +template<> +CoreML::Specification::SupportVectorRegressor* SVMRegressorConverterType::convert(const CSVM* svm) +{ + REQUIRE( + SVMRegressorConverterType::supported_types.find(svm->get_name()) + != SVMRegressorConverterType::supported_types.end(), + "Exporting %s to CoreML format is not supported!", svm->get_name()) + auto spec = new CoreML::Specification::SupportVectorRegressor(); + + // set kernel + auto kernel = svm->get("kernel"); + auto kernel_spec = KernelConverter::convert(kernel); + spec->set_allocated_kernel(kernel_spec); + + // set coefficients + auto coeffs = svm->get_alphas(); + auto coeffs_spec = spec->mutable_coefficients(); + for (auto i: coeffs) + coeffs_spec->add_alpha(i); + + // set bias + spec->set_rho(svm->get_bias()); + + // set support vectors + set_support_vectors(svm, kernel, spec); + + return spec; +} + +template<> +void visitor::visit(SVMRegressorConverter* rc, CSVM* b) +{ + rc->m_spec->set_specificationversion(SPECIFICATION_VERSION); + rc->m_spec->set_allocated_supportvectorregressor(rc->convert(b)); +} diff --git a/src/interfaces/coreml/SVMConverter.h b/src/interfaces/coreml/SVMConverter.h new file mode 100644 index 00000000000..2d113f79851 --- /dev/null +++ b/src/interfaces/coreml/SVMConverter.h @@ -0,0 +1,37 @@ +#ifndef __SVM_CONVERTER_H__ +#define __SVM_CONVERTER_H__ + +#include "CoreMLConverter.h" + +#include + +namespace CoreML +{ + namespace Specification + { + class SupportVectorRegressor; + class SupportVectorClassifier; + } +} + +namespace shogun +{ + namespace coreml + { + //static const std::unordered_set kShogunSVMs = {"LibSVM", "LibSVMOneClass", "MPDSVM", "SVMLight", "SVMLightOneClass", "LibSVR", "SVRLight", " ", "SVMSGD"}; + + struct SVMRegressorConverter: CoreMLConverter + { + SVMRegressorConverter(std::shared_ptr spec): CoreMLConverter(spec) {}; + }; + + struct SVMClassifierConverter: CoreMLConverter + { + SVMClassifierConverter(std::shared_ptr spec): CoreMLConverter(spec) {}; + }; + + //REGISTER_CONVERTER(SVM, kShogunSVMs, &shogun::coreml::convert_svm) + } +} + +#endif diff --git a/src/interfaces/coreml/SVMConverter_test.cc b/src/interfaces/coreml/SVMConverter_test.cc new file mode 100644 index 00000000000..51d7e8e9dfd --- /dev/null +++ b/src/interfaces/coreml/SVMConverter_test.cc @@ -0,0 +1,81 @@ +#include +#include "SVMConverter.h" + +#include + +#include +#include + +#include + +#include "SVM.pb.h" + +using namespace shogun; +using namespace shogun::coreml; + +template +class RegressorTest : public ::testing::Test {}; + +using RegressorTypes = ::testing::Types; +TYPED_TEST_CASE(RegressorTest, RegressorTypes); + +TYPED_TEST(RegressorTest, convert) +{ + //TODO: move fixture + index_t n = 100; + float64_t x_range=6; + SGMatrix feat_train(1, n); + SGVector lab_train(n); + + for (index_t i=0; i(lab_train); + auto features_train = some>(feat_train); + + // TODO: make kernel a parameter? + auto k = some(10); + k->init(features_train, features_train); + auto m = some(10.0, 0.01, k, labels_train); + m->train(); + + auto spec = std::shared_ptr(SVMRegressorConverter::convert(m)); + + // check rho + ASSERT_EQ(m->get_bias(), spec->rho()); + + // check coeffs + auto coeffs = m->get_alphas(); + auto coeffs_spec = spec->coefficients(); + ASSERT_EQ(coeffs.size(), coeffs_spec.alpha_size()); + + int ctr = 0; + for (auto c: coeffs) + ASSERT_EQ(c, coeffs_spec.alpha(ctr++)); + + // check kernel + ASSERT_TRUE(spec->has_kernel()); + auto kernel_spec = spec->kernel(); + ASSERT_TRUE(kernel_spec.has_rbfkernel()); + + // check support vectors + ASSERT_TRUE(spec->has_densesupportvectors()); + auto svs_idx = m->get_support_vectors(); + auto svs_spec = spec->densesupportvectors(); + + ASSERT_EQ(svs_idx.vlen, svs_spec.vectors_size()); + ctr = 0; + for (auto idx: svs_idx) + { + auto sv_spec = svs_spec.vectors(ctr++); + auto sv = features_train->get_feature_vector(idx); + ASSERT_EQ(sv.vlen, sv_spec.values_size()); + + int j = 0; + for (auto v: sv) + ASSERT_EQ(v, sv_spec.values(j++)); + } +} diff --git a/src/interfaces/coreml/unittest_main.cc b/src/interfaces/coreml/unittest_main.cc new file mode 100644 index 00000000000..7d45f455302 --- /dev/null +++ b/src/interfaces/coreml/unittest_main.cc @@ -0,0 +1,88 @@ +#include +#include +#include +#include + +/* +#include "environments/LinearTestEnvironment.h" +#include "environments/MultiLabelTestEnvironment.h" +#include "environments/RegressionTestEnvironment.h" +*/ + +using namespace shogun; +using ::testing::Test; +using ::testing::UnitTest; +using ::testing::TestCase; +using ::testing::TestInfo; +using ::testing::TestPartResult; +using ::testing::TestEventListener; +using ::testing::Environment; + +class FailurePrinter : public TestEventListener { +public: + explicit FailurePrinter(TestEventListener* listener) : TestEventListener() {_listener = listener;} + + virtual ~FailurePrinter() {} + + virtual void OnTestProgramStart(const UnitTest& unit_test) {} + virtual void OnTestIterationStart(const UnitTest& unit_test, int iteration) {} + virtual void OnEnvironmentsSetUpStart(const UnitTest& unit_test) {} + virtual void OnEnvironmentsSetUpEnd(const UnitTest& unit_test) {} + virtual void OnTestCaseStart(const TestCase& test_case) {} + virtual void OnTestStart(const TestInfo& test_info) {} + virtual void OnTestPartResult(const TestPartResult& result); + virtual void OnTestEnd(const TestInfo& test_info); + virtual void OnTestCaseEnd(const TestCase& test_case) {} + virtual void OnEnvironmentsTearDownStart(const UnitTest& unit_test) { } + virtual void OnEnvironmentsTearDownEnd(const UnitTest& unit_test) { } + virtual void OnTestIterationEnd(const UnitTest& unit_test, int iteration) { _listener->OnTestIterationEnd(unit_test, iteration); } + virtual void OnTestProgramEnd(const UnitTest& unit_test) { } + +protected: + TestEventListener* _listener; +}; + +void FailurePrinter::OnTestPartResult(const TestPartResult& test_part_result) +{ + if (test_part_result.failed()) + { + _listener->OnTestPartResult(test_part_result); + printf("\n"); + } +} + +void FailurePrinter::OnTestEnd(const TestInfo& test_info) +{ + if (test_info.result()->Failed()) + _listener->OnTestEnd(test_info); +} + +//LinearTestEnvironment* linear_test_env; + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + ::testing::InitGoogleMock(&argc, argv); + + if (argc > 1 && !strcmp(argv[1], "--only-on-failure")) + { + testing::TestEventListeners& listeners = + testing::UnitTest::GetInstance()->listeners(); + + testing::TestEventListener* default_printer + = listeners.Release(listeners.default_result_printer()); + listeners.Append(new FailurePrinter(default_printer)); + } + + //regression_test_env = new RegressionTestEnvironment(); + //::testing::AddGlobalTestEnvironment(regression_test_env); + + init_shogun_with_defaults(); + sg_io->set_loglevel(MSG_WARN); + int ret = RUN_ALL_TESTS(); + + exit_shogun(); + + return ret; +} +