diff --git a/.ci/ci.yaml b/.ci/ci.yaml index 7a1f41de0b..ab57443861 100644 --- a/.ci/ci.yaml +++ b/.ci/ci.yaml @@ -19,16 +19,15 @@ jobs: # RAM usage. CMakeArgs: '-DDEBUG=ON -DPROFILE=OFF -DBUILD_TESTS=ON -DBUILD_PYTHON_BINDINGS=OFF -DBUILD_JULIA_BINDINGS=OFF -DBUILD_GO_BINDINGS=OFF -DBUILD_R_BINDINGS=OFF -DUSE_PRECOMPILED_HEADERS=OFF' Python: + CMakeArgs: '-DDEBUG=OFF -DPROFILE=OFF -DBUILD_TESTS=ON -DBUILD_PYTHON_BINDINGS=ON -DPYTHON_EXECUTABLE=/usr/bin/python3 -DBUILD_GO_BINDINGS=OFF -DBUILD_JULIA_BINDINGS=OFF -DBUILD_R_BINDINGS=OFF' binding: 'python' python.version: '3.7' - CMakeArgs: '-DDEBUG=OFF -DPROFILE=OFF -DBUILD_TESTS=ON -DBUILD_PYTHON_BINDINGS=ON -DPYTHON_EXECUTABLE=/usr/bin/python3 -DBUILD_GO_BINDINGS=OFF -DBUILD_JULIA_BINDINGS=OFF -DBUILD_R_BINDINGS=OFF' Julia: - julia.version: '1.3.0' - CMakeArgs: '-DDEBUG=OFF -DPROFILE=OFF -DBUILD_TESTS=ON -DBUILD_PYTHON_BINDINGS=OFF -DBUILD_JULIA_BINDINGS=ON -DBUILD_GO_BINDINGS=OFF -DJULIA_EXECUTABLE=/opt/julia-1.6.3/bin/julia -DBUILD_R_BINDINGS=OFF' + CMakeArgs: '-DDEBUG=OFF -DPROFILE=OFF -DBUILD_TESTS=ON -DBUILD_PYTHON_BINDINGS=OFF -DBUILD_JULIA_BINDINGS=ON -DBUILD_GO_BINDINGS=OFF -DJULIA_EXECUTABLE=/opt/julia-1.10.4/bin/julia -DBUILD_R_BINDINGS=OFF' + binding: 'julia' Go: - binding: 'go' - go.version: '1.11.0' CMakeArgs: '-DDEBUG=OFF -DPROFILE=OFF -DBUILD_TESTS=ON -DBUILD_PYTHON_BINDINGS=OFF -DBUILD_JULIA_BINDINGS=OFF -DBUILD_GO_BINDINGS=ON -DBUILD_R_BINDINGS=OFF' + binding: 'go' Markdown: CMakeArgs: '-DDEBUG=OFF -DPROFILE=OFF -DBUILD_TESTS=ON -DBUILD_MARKDOWN_BINDINGS=ON -DBUILD_PYTHON_BINDINGS=OFF -DBUILD_GO_BINDINGS=OFF -DBUILD_JULIA_BINDINGS=OFF -DBUILD_R_BINDINGS=OFF' @@ -44,20 +43,19 @@ jobs: # clang on OS X segfaults when using precompiled headers, so we disable # them. Plain: + python.version: '3.8' CMakeArgs: '-DDEBUG=ON -DPROFILE=OFF -DBUILD_TESTS=ON -DBUILD_PYTHON_BINDINGS=OFF -DBUILD_JULIA_BINDINGS=OFF -DBUILD_GO_BINDINGS=OFF -DBUILD_R_BINDINGS=OFF -DUSE_PRECOMPILED_HEADERS=OFF' - python.version: '3.8' Python: - binding: 'python' python.version: '3.8' + binding: 'python' CMakeArgs: '-DDEBUG=ON -DPROFILE=OFF -DBUILD_TESTS=ON -DBUILD_PYTHON_BINDINGS=ON -DBUILD_JULIA_BINDINGS=OFF -DBUILD_GO_BINDINGS=OFF -DBUILD_R_BINDINGS=OFF -DUSE_PRECOMPILED_HEADERS=OFF' Julia: python.version: '3.8' - julia.version: '1.6.3' + binding: 'julia' CMakeArgs: '-DDEBUG=ON -DPROFILE=OFF -DBUILD_TESTS=ON -DBUILD_JULIA_BINDINGS=ON -DBUILD_PYTHON_BINDINGS=OFF -DBUILD_GO_BINDINGS=OFF -DBUILD_R_BINDINGS=OFF -DUSE_PRECOMPILED_HEADERS=OFF' Go: - binding: 'go' python.version: '3.8' - go.version: '1.11.0' + binding: 'go' CMakeArgs: '-DDEBUG=ON -DPROFILE=OFF -DBUILD_TESTS=ON -DBUILD_PYTHON_BINDINGS=OFF -DBUILD_JULIA_BINDINGS=OFF -DBUILD_GO_BINDINGS=ON -DBUILD_R_BINDINGS=OFF -DUSE_PRECOMPILED_HEADERS=OFF' steps: diff --git a/.ci/linux-steps.yaml b/.ci/linux-steps.yaml index ebaa342552..cb669ba27e 100644 --- a/.ci/linux-steps.yaml +++ b/.ci/linux-steps.yaml @@ -7,7 +7,8 @@ steps: # Set python version - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + versionSpec: '$(python.version)' + condition: ne(variables['python.version'], '') # Install build dependencies. - script: | @@ -24,19 +25,18 @@ steps: sudo apt-get install -y --allow-unauthenticated libopenblas-dev g++ xz-utils - if [ "$(binding)" == "python" ]; then - export PYBIN=$(which python) - $PYBIN -m pip install --upgrade pip - $PYBIN -m pip install --upgrade --ignore-installed setuptools cython pandas wheel + if [ "$BINDING" = "python" ]; then + python -m pip install --upgrade pip + python -m pip install --upgrade --ignore-installed setuptools cython pandas wheel fi - if [ "a$(julia.version)" != "a" ]; then - wget https://julialang-s3.julialang.org/bin/linux/x64/1.6/julia-1.6.3-linux-x86_64.tar.gz - sudo tar -C /opt/ -xvpf julia-1.6.3-linux-x86_64.tar.gz + if [ "$BINDING" = "julia" ]; then + wget https://julialang-s3.julialang.org/bin/linux/x64/1.10/julia-1.10.4-linux-x86_64.tar.gz + sudo tar -C /opt/ -xvpf julia-1.10.4-linux-x86_64.tar.gz fi # Install armadillo. - curl -k -L https://sourceforge.net/projects/arma/files/armadillo-9.800.6.tar.xz | tar -xvJ && \ + curl -k -L https://sourceforge.net/projects/arma/files/armadillo-10.8.2.tar.xz | tar -xvJ && \ cd armadillo* && \ cmake . && \ make && \ @@ -68,14 +68,13 @@ steps: # Configure mlpack (CMake) - script: | - unset BOOST_ROOT mkdir build && cd build - if [ "$(binding)" == "go" ]; then + if [ "$BINDING" = "go" ]; then export GOPATH=$PWD/src/mlpack/bindings/go export GO111MODULE=off go get -u -t gonum.org/v1/gonum/... fi - cmake $(CMakeArgs) -DPYTHON_EXECUTABLE=`which python` -DCEREAL_INCLUDE_DIR=/usr/include/ .. + cmake $CMAKEARGS -DPYTHON_EXECUTABLE=`which python` -DCEREAL_INCLUDE_DIR=/usr/include/ .. displayName: 'CMake' # Build mlpack diff --git a/.ci/macos-steps.yaml b/.ci/macos-steps.yaml index 0607b6d431..d6f177f28a 100644 --- a/.ci/macos-steps.yaml +++ b/.ci/macos-steps.yaml @@ -15,12 +15,12 @@ steps: sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer brew install libomp openblas armadillo cereal ensmallen - if [ "$(binding)" == "python" ]; then + if [ "$BINDING" = "python" ]; then pip install --upgrade pip pip install cython numpy pandas zipp configparser wheel fi - if [ "a$(julia.version)" != "a" ]; then + if [ "$BINDING" = "julia" ]; then brew install --cask julia fi @@ -29,16 +29,15 @@ steps: # Configure mlpack (CMake) - script: | mkdir build && cd build - if [ "$(binding)" == "go" ]; then + if [ "$BINDING" = "go" ]; then export GOPATH=$PWD/src/mlpack/bindings/go export GO111MODULE=off go get -u -t gonum.org/v1/gonum/... fi - if [ "$(binding)" == "python" ]; then - export PYPATH=$(which python) - cmake $(CMakeArgs) -DPYTHON_EXECUTABLE=$PYPATH .. + if [ "$BINDING" = "python" ]; then + cmake $CMAKEARGS -DPYTHON_EXECUTABLE=$(which python) .. else - cmake $(CMakeArgs) .. + cmake $CMAKEARGS .. fi displayName: 'CMake' diff --git a/.ci/windows-steps.yaml b/.ci/windows-steps.yaml index 2d3c13058f..c40acff1a5 100644 --- a/.ci/windows-steps.yaml +++ b/.ci/windows-steps.yaml @@ -27,10 +27,10 @@ steps: - bash: | git clone --depth 1 https://github.com/mlpack/jenkins-conf.git conf - curl -O -L https://sourceforge.net/projects/arma/files/armadillo-9.800.6.tar.xz -o armadillo-9.800.6.tar.xz - tar -xvf armadillo-9.800.6.tar.xz + curl -O -L https://sourceforge.net/projects/arma/files/armadillo-10.8.2.tar.xz -o armadillo-10.8.2.tar.xz + tar -xvf armadillo-10.8.2.tar.xz - cd armadillo-9.800.6/ && cmake $(CMakeGenerator) \ + cd armadillo-10.8.2/ && cmake $(CMakeGenerator) \ -DBLAS_LIBRARY:FILEPATH=$(Agent.ToolsDirectory)/OpenBLAS.0.2.14.1/lib/native/lib/x64/libopenblas.dll.a \ -DLAPACK_LIBRARY:FILEPATH=$(Agent.ToolsDirectory)/OpenBLAS.0.2.14.1/lib/native/lib/x64/libopenblas.dll.a \ -DCMAKE_PREFIX:FILEPATH=../../armadillo \ @@ -41,7 +41,7 @@ steps: # Build armadillo - task: MSBuild@1 inputs: - solution: 'armadillo-9.800.6/*.sln' + solution: 'armadillo-10.8.2/*.sln' msbuildLocationMethod: 'location' msbuildVersion: $(MSBuildVersion) configuration: 'Release' @@ -60,8 +60,8 @@ steps: $(CMakeArgs) ` -DBLAS_LIBRARIES:FILEPATH=$(Agent.ToolsDirectory)\OpenBLAS.0.2.14.1\lib\native\lib\x64\libopenblas.dll.a ` -DLAPACK_LIBRARIES:FILEPATH=$(Agent.ToolsDirectory)\OpenBLAS.0.2.14.1\lib\native\lib\x64\libopenblas.dll.a ` - -DARMADILLO_INCLUDE_DIR="..\armadillo-9.800.6\tmp\include" ` - -DARMADILLO_LIBRARY="..\armadillo-9.800.6\Release\armadillo.lib" ` + -DARMADILLO_INCLUDE_DIR="..\armadillo-10.8.2\tmp\include" ` + -DARMADILLO_LIBRARY="..\armadillo-10.8.2\Release\armadillo.lib" ` -DCEREAL_INCLUDE_DIR="..\cereal-1.3.2\include" ` -DENSMALLEN_INCLUDE_DIR=$(Agent.ToolsDirectory)\ensmallen.2.17.0\installed\x64-linux\include ` -DBUILD_JULIA_BINDINGS=OFF ` diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml index 81c58d2033..1270e838d1 100644 --- a/.github/workflows/auto-approve.yml +++ b/.github/workflows/auto-approve.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Auto-approve pull requests - uses: rcurtin/auto-approve@v1.0.1 + uses: rcurtin/actions/auto-approve@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} approval-message: diff --git a/.github/workflows/stickers.yaml b/.github/workflows/stickers.yaml index 89ef40d37c..aafb7d390b 100644 --- a/.github/workflows/stickers.yaml +++ b/.github/workflows/stickers.yaml @@ -11,7 +11,7 @@ jobs: if: github.event.pull_request.merged == true steps: # Forked version of first-interaction that runs only on first merged PR. - - uses: rcurtin/first-interaction@v1.0.0 + - uses: rcurtin/actions/stickers@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} pr-message: "Hello there! Thanks for your contribution. Congratulations on your first contribution to mlpack! If you'd like to add your name to the list of contributors in `COPYRIGHT.txt` and you haven't already, please feel free to push a change to this PR---or, if it gets merged before you can, feel free to open another PR.\n\nIn addition, if you'd like some stickers to put on your laptop, we can get them in the mail for you. Just send an email with your physical mailing address to stickers@mlpack.org, and then one of the mlpack maintainers will put some stickers in an envelope for you. It may take a few weeks to get them, depending on your location. :+1:" diff --git a/CMake/ConfigureCrossCompile.cmake b/CMake/ConfigureCrossCompile.cmake index a320d32932..0a39b829ed 100644 --- a/CMake/ConfigureCrossCompile.cmake +++ b/CMake/ConfigureCrossCompile.cmake @@ -28,6 +28,7 @@ macro(search_openblas version) get_deps(https://github.com/xianyi/OpenBLAS/releases/download/v${version}/OpenBLAS-${version}.tar.gz OpenBLAS OpenBLAS-${version}.tar.gz) if (NOT MSVC) if (NOT EXISTS "${CMAKE_BINARY_DIR}/deps/OpenBLAS-${version}/libopenblas.a") + set(ENV{COMMON_OPT} "${CMAKE_OPENBLAS_FLAGS}") # Pass our flags to OpenBLAS execute_process(COMMAND make TARGET=${OPENBLAS_TARGET} BINARY=${OPENBLAS_BINARY} HOSTCC=gcc CC=${CMAKE_C_COMPILER} FC=${CMAKE_FORTRAN_COMPILER} NO_SHARED=1 WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/deps/OpenBLAS-${version}) endif() diff --git a/CMakeLists.txt b/CMakeLists.txt index 75f6fe1b9d..ced015958b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,7 @@ option(USE_PRECOMPILED_HEADERS "Use precompiled headers for mlpack_test build." # For Armadillo, try to keep the minimum required version less than or equal to # what's available on the current Ubuntu LTS or most recent stable RHEL release. # See https://github.com/mlpack/mlpack/issues/3033 for some more discussion. -set(ARMADILLO_VERSION "9.800") +set(ARMADILLO_VERSION "10.8") set(ENSMALLEN_VERSION "2.10.0") set(CEREAL_VERSION "1.1.2") @@ -249,8 +249,12 @@ if (DEBUG) else() add_definitions(-DNDEBUG) if (NOT MSVC) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -O3") + if (NOT CMAKE_CROSSCOMPILING) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -O3") + else() + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99") + endif() else () set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /O3") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /O3") @@ -283,7 +287,7 @@ endif() # Download and compile OpenBLAS if we are cross compiling mlpack for a specific # architecture. The function takes the version of OpenBLAS as variable. if (CMAKE_CROSSCOMPILING) - search_openblas(0.3.13) + search_openblas(0.3.26) endif() if (NOT DOWNLOAD_DEPENDENCIES) diff --git a/HISTORY.md b/HISTORY.md index e98875f83b..3efc2e024d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,6 +11,8 @@ _????-??-??_ * Implemented the Find and Fill algorithm into the Dropout Layer and added OpenMP support (#3684). * Update Python bindings to support NumPy 2.x (#3752). + + * Bump minimum Armadillo version to 10.8 (#3760). ## mlpack 4.4.0 diff --git a/README.md b/README.md index 7bf1bf301a..70c3c2ab01 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Citations are beneficial for the growth and improvement of mlpack. **mlpack** requires the following additional dependencies: - C++17 compiler - - [Armadillo](https://arma.sourceforge.net)   >= 9.800 + - [Armadillo](https://arma.sourceforge.net)   >= 10.8 - [ensmallen](https://ensmallen.org)  >= 2.10.0 - [cereal](http://uscilab.github.io/cereal/)     >= 1.1.2 @@ -333,7 +333,7 @@ dependencies are installed: - R >= 4.0 - Rcpp >= 0.12.12 - - RcppArmadillo >= 0.9.800.0 + - RcppArmadillo >= 0.10.8.0 - RcppEnsmallen >= 0.2.10.0 - roxygen2 - testthat diff --git a/board/flags-config.cmake b/board/flags-config.cmake index 94b2060b44..5999ab86ac 100644 --- a/board/flags-config.cmake +++ b/board/flags-config.cmake @@ -4,63 +4,86 @@ # footprints. # Set generic minimization flags for all platforms. -# These flags are the same for all cross-compilation cases. -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Os -fdata-sections -ffunction-sections") +# These flags are the same for all cross-compilation cases and they are +# mainly to reduce the binary footprint. +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Os -s -fdata-sections -ffunction-sections") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fomit-frame-pointer -fno-unwind-tables") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-asynchronous-unwind-tables -fvisibility=hidden") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fshort-enums -finline-small-functions") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -findirect-inlining -fno-common") -#-flto -fuse-ld=gold # There is an issue with gold link when compiling on -# Ubuntu 16. At that point gcc linker did not integrate the flto support -# inside and it was a separate plugin that need to be added. Therefore, -# this can be added when mlpack Azure CI moves toward Ubuntu 20. +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fmerge-all-constants -fno-ident") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-unroll-loops -fno-math-errno") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-stack-protector") +set(CMAKE_OPENBLAS_FLAGS "${CMAKE_CXX_FLAGS}") # OpenBLAS does not supoport flto +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--hash-style=gnu -Wl,--build-id=none") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,norelro") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") +## Keep the following flag in comment, they might be relevant in the case of MCU's +#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,-nmagic,-Bsymbolic -nostartfiles") set(BOARD_NAME "" CACHE STRING "Specify Board name to optimize for.") string(TOUPPER ${BOARD_NAME} BOARD) # Set specific platforms CMAKE CXX flags. -if(BOARD MATCHES "RPI0" OR BOARD MATCHES "RPI1") +if(BOARD MATCHES "RPI0" OR BOARD MATCHES "RPI1" OR BOARD MATCHES "ARM11") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=arm1176jzf-s") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mcpu=arm1176jzf-s -mfloat-abi=hard -mfpu=vfp") set(OPENBLAS_TARGET "ARMV6") set(OPENBLAS_BINARY "32") -elseif(BOARD MATCHES "RPI2") +elseif(BOARD MATCHES "RPI2" OR BOARD MATCHES "CORTEXA7") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a7") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfloat-abi=hard -mfpu=neon-vfpv4") set(OPENBLAS_TARGET "ARMV7") set(OPENBLAS_BINARY "32") -elseif(BOARD MATCHES "RPI3") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a53") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") +elseif(BOARD MATCHES "CORTEXA8") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a8") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfloat-abi=hard -mfpu=neon") + set(OPENBLAS_TARGET "ARMV7") + set(OPENBLAS_BINARY "32") +elseif(BOARD MATCHES "CORTEXA9") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a9") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfloat-abi=hard -mfpu=neon") + set(OPENBLAS_TARGET "CORTEXA9") + set(OPENBLAS_BINARY "32") +elseif(BOARD MATCHES "CORTEXA15") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a15") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfloat-abi=hard -mfpu=neon") + set(OPENBLAS_TARGET "CORTEXA15") + set(OPENBLAS_BINARY "32") +elseif(BOARD MATCHES "RPI3" OR BOARD MATCHES "CORTEXA53") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a53 -ftree-vectorize") set(OPENBLAS_TARGET "CORTEXA53") set(OPENBLAS_BINARY "64") -elseif(BOARD MATCHES "RPI4") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a72") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") +elseif(BOARD MATCHES "RPI4" OR BOARD MATCHES "CORTEXA72") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a72 -ftree-vectorize") set(OPENBLAS_TARGET "CORTEXA72") set(OPENBLAS_BINARY "64") +elseif(BOARD MATCHES "JETSONAGX" OR BOARD MATCHES "CORTEXA76") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a76 -ftree-vectorize") + set(OPENBLAS_TARGET "CORTEXA76") + set(OPENBLAS_BINARY "64") elseif(BOARD MATCHES "BV") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") set(OPENBLAS_TARGET "RISCV64_GENERIC") set(OPENBLAS_BINARY "64") -elseif(BOARD MATCHES "JETSONAGX") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -matune=cortex-a76") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") - set(OPENBLAS_TARGET "ARM8") +elseif(BOARD MATCHES "C906") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=thead-c906") + set(OPENBLAS_TARGET "RISCV64_GENERIC") + set(OPENBLAS_BINARY "64") +elseif(BOARD MATCHES "x280") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=sifive-x280") + set(OPENBLAS_TARGET "x280") set(OPENBLAS_BINARY "64") elseif(BOARD MATCHES "KATAMI") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=pentium3") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") set(OPENBLAS_TARGET "KATAMI") set(OPENBLAS_BINARY "32") elseif(BOARD MATCHES "COPPERMINE") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=pentium3") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") set(OPENBLAS_TARGET "COPPERMINE") set(OPENBLAS_BINARY "32") elseif(BOARD MATCHES "NORTHWOOD") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=pentium4") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") set(OPENBLAS_TARGET "NORTHWOOD") set(OPENBLAS_BINARY "32") elseif(BOARD) diff --git a/doc/index.md b/doc/index.md index 59015a0a7a..8fe5b875b1 100644 --- a/doc/index.md +++ b/doc/index.md @@ -124,6 +124,10 @@ Transform data from one space to another. * [`AMF`](user/methods/amf.md): alternating matrix factorization * [`LocalCoordinateCoding`](user/methods/local_coordinate_coding.md): local coordinate coding with dictionary learning + * [`LMNN`](user/methods/lmnn.md): large margin nearest neighbor (distance + metric learning) + * [`NCA`](user/methods/nca.md): neighborhood components analysis (distance + metric learning) * [`NMF`](user/methods/nmf.md): non-negative matrix factorization * [`PCA`](user/methods/pca.md): principal components analysis * [`SparseCoding`](user/methods/sparse_coding.md): sparse coding with diff --git a/doc/sidebar.html b/doc/sidebar.html index a9555f315f..a5ec834955 100644 --- a/doc/sidebar.html +++ b/doc/sidebar.html @@ -181,6 +181,16 @@ when the sidebar is built for each page. LocalCoordinateCoding +
  • + + LMNN + +
  • +
  • + + NCA + +
  • NMF diff --git a/doc/user/bindings/cli.md b/doc/user/bindings/cli.md index bc02653831..adfd721f22 100644 --- a/doc/user/bindings/cli.md +++ b/doc/user/bindings/cli.md @@ -1677,9 +1677,9 @@ $ mlpack_linear_svm --input_model_file lsvm_model.bin --test_file test.csv $ mlpack_lmnn [--batch_size 50] [--center] [--distance_file ] [--help] [--info ] --input_file [--k 1] [--labels_file ] [--linear_scan] [--max_iterations 100000] [--normalize] - [--optimizer 'amsgrad'] [--passes 50] [--print_accuracy] [--range 1] - [--rank 0] [--regularization 0.5] [--seed 0] [--step_size 0.01] - [--tolerance 1e-07] [--verbose] [--version] [--centered_data_file + [--optimizer 'amsgrad'] [--passes 50] [--print_accuracy] [--rank 0] + [--regularization 0.5] [--seed 0] [--step_size 0.01] [--tolerance 1e-07] + [--update_interval 1] [--verbose] [--version] [--centered_data_file ] [--output_file ] [--transformed_data_file ] ``` @@ -1706,12 +1706,12 @@ An implementation of Large Margin Nearest Neighbors (LMNN), a distance learning | `--optimizer (-O)` | [`string`](#doc_string) | Optimizer to use; 'amsgrad', 'bbsgd', 'sgd', or 'lbfgs'. | `'amsgrad'` | | `--passes (-p)` | [`int`](#doc_int) | Maximum number of full passes over dataset for AMSGrad, BB_SGD and SGD. | `50` | | `--print_accuracy (-P)` | [`flag`](#doc_flag) | Print accuracies on initial and transformed dataset | | -| `--range (-R)` | [`int`](#doc_int) | Number of iterations after which impostors needs to be recalculated | `1` | | `--rank (-A)` | [`int`](#doc_int) | Rank of distance matrix to be optimized. | `0` | | `--regularization (-r)` | [`double`](#doc_double) | Regularization for LMNN objective function | `0.5` | | `--seed (-s)` | [`int`](#doc_int) | Random seed. If 0, 'std::time(NULL)' is used. | `0` | | `--step_size (-a)` | [`double`](#doc_double) | Step size for AMSGrad, BB_SGD and SGD (alpha). | `0.01` | | `--tolerance (-t)` | [`double`](#doc_double) | Maximum tolerance for termination of AMSGrad, BB_SGD, SGD or L-BFGS. | `1e-07` | +| `--update_interval (-R)` | [`int`](#doc_int) | Number of iterations after which impostors need to be recalculated. | `1` | | `--verbose (-v)` | [`flag`](#doc_flag) | Display informational messages and the full list of parameters and timers at the end of execution. | | | `--version (-V)` | [`flag`](#doc_flag) | Display the version of mlpack. Only exists in CLI binding. | | @@ -1731,7 +1731,7 @@ This program implements Large Margin Nearest Neighbors, a distance learning tech To work, this algorithm needs labeled data. It can be given as the last row of the input dataset (specified with `--input_file (-i)`), or alternatively as a separate matrix (specified with `--labels_file (-l)`). Additionally, a starting point for optimization (specified with `--distance_file (-d)`can be given, having (r x d) dimensionality. Here r should satisfy 1 <= r <= d, Consequently a Low-Rank matrix will be optimized. Alternatively, Low-Rank distance can be learned by specifying the `--rank (-A)`parameter (A Low-Rank matrix with uniformly distributed values will be used as initial learning point). -The program also requires number of targets neighbors to work with ( specified with `--k (-k)`), A regularization parameter can also be passed, It acts as a trade of between the pulling and pushing terms (specified with `--regularization (-r)`), In addition, this implementation of LMNN includes a parameter to decide the interval after which impostors must be re-calculated (specified with `--range (-R)`). +The program also requires number of targets neighbors to work with ( specified with `--k (-k)`), A regularization parameter can also be passed, It acts as a trade of between the pulling and pushing terms (specified with `--regularization (-r)`), In addition, this implementation of LMNN includes a parameter to decide the interval after which impostors must be re-calculated (specified with `--update_interval (-R)`). Output can either be the learned distance matrix (specified with `--output_file (-o)`), or the transformed dataset (specified with `--transformed_data_file (-D)`), or both. Additionally mean-centered dataset (specified with `--centered_data_file (-c)`) can be accessed given mean-centering (specified with `--center (-C)`) is performed on the dataset. Accuracy on initial dataset and final transformed dataset can be printed by specifying the `--print_accuracy (-P)`parameter. @@ -1755,10 +1755,10 @@ $ mlpack_lmnn --input_file iris.csv --labels_file iris_labels.csv --k 3 --optimizer bbsgd --output_file output.csv ``` -An another program call making use of range & regularization parameter with dataset having labels as last column can be made as: +Another program call making use of update interval & regularization parameter with dataset having labels as last column can be made as: ```bash -$ mlpack_lmnn --input_file letter_recognition.csv --k 5 --range 10 +$ mlpack_lmnn --input_file letter_recognition.csv --k 5 --update_interval 10 --regularization 0.4 --output_file output.csv ``` diff --git a/doc/user/bindings/go.md b/doc/user/bindings/go.md index c9baae764f..71878e2430 100644 --- a/doc/user/bindings/go.md +++ b/doc/user/bindings/go.md @@ -2069,12 +2069,12 @@ param.Normalize = false param.Optimizer = "amsgrad" param.Passes = 50 param.PrintAccuracy = false -param.Range = 1 param.Rank = 0 param.Regularization = 0.5 param.Seed = 0 param.StepSize = 0.01 param.Tolerance = 1e-07 +param.UpdateInterval = 1 param.Verbose = false centered_data, output, transformed_data := mlpack.Lmnn(input, param) @@ -2102,12 +2102,12 @@ There are two types of input options: required options, which are passed directl | `Optimizer` | [`string`](#doc_string) | Optimizer to use; 'amsgrad', 'bbsgd', 'sgd', or 'lbfgs'. | `"amsgrad"` | | `Passes` | [`int`](#doc_int) | Maximum number of full passes over dataset for AMSGrad, BB_SGD and SGD. | `50` | | `PrintAccuracy` | [`bool`](#doc_bool) | Print accuracies on initial and transformed dataset | `false` | -| `Range` | [`int`](#doc_int) | Number of iterations after which impostors needs to be recalculated | `1` | | `Rank` | [`int`](#doc_int) | Rank of distance matrix to be optimized. | `0` | | `Regularization` | [`float64`](#doc_float64) | Regularization for LMNN objective function | `0.5` | | `Seed` | [`int`](#doc_int) | Random seed. If 0, 'std::time(NULL)' is used. | `0` | | `StepSize` | [`float64`](#doc_float64) | Step size for AMSGrad, BB_SGD and SGD (alpha). | `0.01` | | `Tolerance` | [`float64`](#doc_float64) | Maximum tolerance for termination of AMSGrad, BB_SGD, SGD or L-BFGS. | `1e-07` | +| `UpdateInterval` | [`int`](#doc_int) | Number of iterations after which impostors need to be recalculated. | `1` | | `Verbose` | [`bool`](#doc_bool) | Display informational messages and the full list of parameters and timers at the end of execution. | `false` | ### Output options @@ -2127,7 +2127,7 @@ This program implements Large Margin Nearest Neighbors, a distance learning tech To work, this algorithm needs labeled data. It can be given as the last row of the input dataset (specified with `Input`), or alternatively as a separate matrix (specified with `Labels`). Additionally, a starting point for optimization (specified with `Distance`can be given, having (r x d) dimensionality. Here r should satisfy 1 <= r <= d, Consequently a Low-Rank matrix will be optimized. Alternatively, Low-Rank distance can be learned by specifying the `Rank`parameter (A Low-Rank matrix with uniformly distributed values will be used as initial learning point). -The program also requires number of targets neighbors to work with ( specified with `K`), A regularization parameter can also be passed, It acts as a trade of between the pulling and pushing terms (specified with `Regularization`), In addition, this implementation of LMNN includes a parameter to decide the interval after which impostors must be re-calculated (specified with `Range`). +The program also requires number of targets neighbors to work with ( specified with `K`), A regularization parameter can also be passed, It acts as a trade of between the pulling and pushing terms (specified with `Regularization`), In addition, this implementation of LMNN includes a parameter to decide the interval after which impostors must be re-calculated (specified with `UpdateInterval`). Output can either be the learned distance matrix (specified with `Output`), or the transformed dataset (specified with `TransformedData`), or both. Additionally mean-centered dataset (specified with `CenteredData`) can be accessed given mean-centering (specified with `Center`) is performed on the dataset. Accuracy on initial dataset and final transformed dataset can be printed by specifying the `PrintAccuracy`parameter. @@ -2156,13 +2156,13 @@ param.Optimizer = "bbsgd" _, output, _ := mlpack.Lmnn(iris, param) ``` -An another program call making use of range & regularization parameter with dataset having labels as last column can be made as: +Another program call making use of update interval & regularization parameter with dataset having labels as last column can be made as: ```go // Initialize optional parameters for Lmnn(). param := mlpack.LmnnOptions() param.K = 5 -param.Range = 10 +param.UpdateInterval = 10 param.Regularization = 0.4 _, output, _ := mlpack.Lmnn(letter_recognition, param) diff --git a/doc/user/bindings/julia.md b/doc/user/bindings/julia.md index 5d9e699354..6b81648246 100644 --- a/doc/user/bindings/julia.md +++ b/doc/user/bindings/julia.md @@ -1696,9 +1696,9 @@ julia> using mlpack: lmnn julia> centered_data, output, transformed_data = lmnn(input; batch_size=50, center=false, distance=zeros(0, 0), k=1, labels=Int[], linear_scan=false, max_iterations=100000, normalize=false, - optimizer="amsgrad", passes=50, print_accuracy=false, range=1, rank=0, + optimizer="amsgrad", passes=50, print_accuracy=false, rank=0, regularization=0.5, seed=0, step_size=0.01, tolerance=1e-07, - verbose=false) + update_interval=1, verbose=false) ``` An implementation of Large Margin Nearest Neighbors (LMNN), a distance learning technique. Given a labeled dataset, this learns a transformation of the data that improves k-nearest-neighbor performance; this can be useful as a preprocessing step. [Detailed documentation](#lmnn_detailed-documentation). @@ -1722,12 +1722,12 @@ An implementation of Large Margin Nearest Neighbors (LMNN), a distance learning | `optimizer` | [`String`](#doc_String) | Optimizer to use; 'amsgrad', 'bbsgd', 'sgd', or 'lbfgs'. | `"amsgrad"` | | `passes` | [`Int`](#doc_Int) | Maximum number of full passes over dataset for AMSGrad, BB_SGD and SGD. | `50` | | `print_accuracy` | [`Bool`](#doc_Bool) | Print accuracies on initial and transformed dataset | `false` | -| `range` | [`Int`](#doc_Int) | Number of iterations after which impostors needs to be recalculated | `1` | | `rank` | [`Int`](#doc_Int) | Rank of distance matrix to be optimized. | `0` | | `regularization` | [`Float64`](#doc_Float64) | Regularization for LMNN objective function | `0.5` | | `seed` | [`Int`](#doc_Int) | Random seed. If 0, 'std::time(NULL)' is used. | `0` | | `step_size` | [`Float64`](#doc_Float64) | Step size for AMSGrad, BB_SGD and SGD (alpha). | `0.01` | | `tolerance` | [`Float64`](#doc_Float64) | Maximum tolerance for termination of AMSGrad, BB_SGD, SGD or L-BFGS. | `1e-07` | +| `update_interval` | [`Int`](#doc_Int) | Number of iterations after which impostors need to be recalculated. | `1` | | `verbose` | [`Bool`](#doc_Bool) | Display informational messages and the full list of parameters and timers at the end of execution. | `false` | ### Output options @@ -1747,7 +1747,7 @@ This program implements Large Margin Nearest Neighbors, a distance learning tech To work, this algorithm needs labeled data. It can be given as the last row of the input dataset (specified with `input`), or alternatively as a separate matrix (specified with `labels`). Additionally, a starting point for optimization (specified with `distance`can be given, having (r x d) dimensionality. Here r should satisfy 1 <= r <= d, Consequently a Low-Rank matrix will be optimized. Alternatively, Low-Rank distance can be learned by specifying the `rank`parameter (A Low-Rank matrix with uniformly distributed values will be used as initial learning point). -The program also requires number of targets neighbors to work with ( specified with `k`), A regularization parameter can also be passed, It acts as a trade of between the pulling and pushing terms (specified with `regularization`), In addition, this implementation of LMNN includes a parameter to decide the interval after which impostors must be re-calculated (specified with `range`). +The program also requires number of targets neighbors to work with ( specified with `k`), A regularization parameter can also be passed, It acts as a trade of between the pulling and pushing terms (specified with `regularization`), In addition, this implementation of LMNN includes a parameter to decide the interval after which impostors must be re-calculated (specified with `update_interval`). Output can either be the learned distance matrix (specified with `output`), or the transformed dataset (specified with `transformed_data`), or both. Additionally mean-centered dataset (specified with `centered_data`) can be accessed given mean-centering (specified with `center`) is performed on the dataset. Accuracy on initial dataset and final transformed dataset can be printed by specifying the `print_accuracy`parameter. @@ -1774,13 +1774,13 @@ julia> _, output, _ = lmnn(iris; k=3, labels=iris_labels, optimizer="bbsgd") ``` -An another program call making use of range & regularization parameter with dataset having labels as last column can be made as: +Another program call making use of update interval & regularization parameter with dataset having labels as last column can be made as: ```julia julia> using CSV julia> letter_recognition = CSV.read("letter_recognition.csv") -julia> _, output, _ = lmnn(letter_recognition; k=5, range=10, - regularization=0.4) +julia> _, output, _ = lmnn(letter_recognition; k=5, + regularization=0.4, update_interval=10) ``` ### See also diff --git a/doc/user/bindings/python.md b/doc/user/bindings/python.md index a0a777fc72..8399b13531 100644 --- a/doc/user/bindings/python.md +++ b/doc/user/bindings/python.md @@ -1715,8 +1715,8 @@ Then, to use that model to predict classes for the dataset '`'test'`', storing t copy_all_inputs=False, distance=np.empty([0, 0]), input_=np.empty([0, 0]), k=1, labels=np.empty([0], dtype=np.uint64), linear_scan=False, max_iterations=100000, normalize=False, optimizer='amsgrad', passes=50, - print_accuracy=False, range=1, rank=0, regularization=0.5, seed=0, - step_size=0.01, tolerance=1e-07, verbose=False) + print_accuracy=False, rank=0, regularization=0.5, seed=0, + step_size=0.01, tolerance=1e-07, update_interval=1, verbose=False) >>> centered_data = d['centered_data'] >>> output = d['output'] >>> transformed_data = d['transformed_data'] @@ -1744,12 +1744,12 @@ An implementation of Large Margin Nearest Neighbors (LMNN), a distance learning | `optimizer` | [`str`](#doc_str) | Optimizer to use; 'amsgrad', 'bbsgd', 'sgd', or 'lbfgs'. | `'amsgrad'` | | `passes` | [`int`](#doc_int) | Maximum number of full passes over dataset for AMSGrad, BB_SGD and SGD. | `50` | | `print_accuracy` | [`bool`](#doc_bool) | Print accuracies on initial and transformed dataset | `False` | -| `range` | [`int`](#doc_int) | Number of iterations after which impostors needs to be recalculated | `1` | | `rank` | [`int`](#doc_int) | Rank of distance matrix to be optimized. | `0` | | `regularization` | [`float`](#doc_float) | Regularization for LMNN objective function | `0.5` | | `seed` | [`int`](#doc_int) | Random seed. If 0, 'std::time(NULL)' is used. | `0` | | `step_size` | [`float`](#doc_float) | Step size for AMSGrad, BB_SGD and SGD (alpha). | `0.01` | | `tolerance` | [`float`](#doc_float) | Maximum tolerance for termination of AMSGrad, BB_SGD, SGD or L-BFGS. | `1e-07` | +| `update_interval` | [`int`](#doc_int) | Number of iterations after which impostors need to be recalculated. | `1` | | `verbose` | [`bool`](#doc_bool) | Display informational messages and the full list of parameters and timers at the end of execution. | `False` | ### Output options @@ -1769,7 +1769,7 @@ This program implements Large Margin Nearest Neighbors, a distance learning tech To work, this algorithm needs labeled data. It can be given as the last row of the input dataset (specified with `input_`), or alternatively as a separate matrix (specified with `labels`). Additionally, a starting point for optimization (specified with `distance`can be given, having (r x d) dimensionality. Here r should satisfy 1 <= r <= d, Consequently a Low-Rank matrix will be optimized. Alternatively, Low-Rank distance can be learned by specifying the `rank`parameter (A Low-Rank matrix with uniformly distributed values will be used as initial learning point). -The program also requires number of targets neighbors to work with ( specified with `k`), A regularization parameter can also be passed, It acts as a trade of between the pulling and pushing terms (specified with `regularization`), In addition, this implementation of LMNN includes a parameter to decide the interval after which impostors must be re-calculated (specified with `range`). +The program also requires number of targets neighbors to work with ( specified with `k`), A regularization parameter can also be passed, It acts as a trade of between the pulling and pushing terms (specified with `regularization`), In addition, this implementation of LMNN includes a parameter to decide the interval after which impostors must be re-calculated (specified with `update_interval`). Output can either be the learned distance matrix (specified with `output`), or the transformed dataset (specified with `transformed_data`), or both. Additionally mean-centered dataset (specified with `centered_data`) can be accessed given mean-centering (specified with `center`) is performed on the dataset. Accuracy on initial dataset and final transformed dataset can be printed by specifying the `print_accuracy`parameter. @@ -1793,10 +1793,10 @@ Example - Let's say we want to learn distance on iris dataset with number of tar >>> output = output['output'] ``` -An another program call making use of range & regularization parameter with dataset having labels as last column can be made as: +Another program call making use of update interval & regularization parameter with dataset having labels as last column can be made as: ```python ->>> output = lmnn(input_=letter_recognition, k=5, range=10, +>>> output = lmnn(input_=letter_recognition, k=5, update_interval=10, regularization=0.4) >>> output = output['output'] ``` diff --git a/doc/user/bindings/r.md b/doc/user/bindings/r.md index 2aa0037959..b6a242b521 100644 --- a/doc/user/bindings/r.md +++ b/doc/user/bindings/r.md @@ -1689,9 +1689,9 @@ R> library(mlpack) R> d <- lmnn(batch_size=50, center=FALSE, distance=matrix(numeric(), 0, 0), input=matrix(numeric(), 0, 0), k=1, labels=matrix(integer(), 0, 0), linear_scan=FALSE, max_iterations=100000, normalize=FALSE, - optimizer="amsgrad", passes=50, print_accuracy=FALSE, range=1, rank=0, + optimizer="amsgrad", passes=50, print_accuracy=FALSE, rank=0, regularization=0.5, seed=0, step_size=0.01, tolerance=1e-07, - verbose=getOption("mlpack.verbose", FALSE)) + update_interval=1, verbose=getOption("mlpack.verbose", FALSE)) R> centered_data <- d$centered_data R> output <- d$output R> transformed_data <- d$transformed_data @@ -1718,12 +1718,12 @@ An implementation of Large Margin Nearest Neighbors (LMNN), a distance learning | `optimizer` | [`character`](#doc_character) | Optimizer to use; 'amsgrad', 'bbsgd', 'sgd', or 'lbfgs'. | `"amsgrad"` | | `passes` | [`integer`](#doc_integer) | Maximum number of full passes over dataset for AMSGrad, BB_SGD and SGD. | `50` | | `print_accuracy` | [`logical`](#doc_logical) | Print accuracies on initial and transformed dataset | `FALSE` | -| `range` | [`integer`](#doc_integer) | Number of iterations after which impostors needs to be recalculated | `1` | | `rank` | [`integer`](#doc_integer) | Rank of distance matrix to be optimized. | `0` | | `regularization` | [`numeric`](#doc_numeric) | Regularization for LMNN objective function | `0.5` | | `seed` | [`integer`](#doc_integer) | Random seed. If 0, 'std::time(NULL)' is used. | `0` | | `step_size` | [`numeric`](#doc_numeric) | Step size for AMSGrad, BB_SGD and SGD (alpha). | `0.01` | | `tolerance` | [`numeric`](#doc_numeric) | Maximum tolerance for termination of AMSGrad, BB_SGD, SGD or L-BFGS. | `1e-07` | +| `update_interval` | [`integer`](#doc_integer) | Number of iterations after which impostors need to be recalculated. | `1` | | `verbose` | [`logical`](#doc_logical) | Display informational messages and the full list of parameters and timers at the end of execution. | `getOption("mlpack.verbose", FALSE)` | ### Output options @@ -1743,7 +1743,7 @@ This program implements Large Margin Nearest Neighbors, a distance learning tech To work, this algorithm needs labeled data. It can be given as the last row of the input dataset (specified with `input`), or alternatively as a separate matrix (specified with `labels`). Additionally, a starting point for optimization (specified with `distance`can be given, having (r x d) dimensionality. Here r should satisfy 1 <= r <= d, Consequently a Low-Rank matrix will be optimized. Alternatively, Low-Rank distance can be learned by specifying the `rank`parameter (A Low-Rank matrix with uniformly distributed values will be used as initial learning point). -The program also requires number of targets neighbors to work with ( specified with `k`), A regularization parameter can also be passed, It acts as a trade of between the pulling and pushing terms (specified with `regularization`), In addition, this implementation of LMNN includes a parameter to decide the interval after which impostors must be re-calculated (specified with `range`). +The program also requires number of targets neighbors to work with ( specified with `k`), A regularization parameter can also be passed, It acts as a trade of between the pulling and pushing terms (specified with `regularization`), In addition, this implementation of LMNN includes a parameter to decide the interval after which impostors must be re-calculated (specified with `update_interval`). Output can either be the learned distance matrix (specified with `output`), or the transformed dataset (specified with `transformed_data`), or both. Additionally mean-centered dataset (specified with `centered_data`) can be accessed given mean-centering (specified with `center`) is performed on the dataset. Accuracy on initial dataset and final transformed dataset can be printed by specifying the `print_accuracy`parameter. @@ -1767,10 +1767,10 @@ R> output <- lmnn(input=iris, labels=iris_labels, k=3, optimizer="bbsgd") R> output <- output$output ``` -An another program call making use of range & regularization parameter with dataset having labels as last column can be made as: +Another program call making use of update interval & regularization parameter with dataset having labels as last column can be made as: ```R -R> output <- lmnn(input=letter_recognition, k=5, range=10, +R> output <- lmnn(input=letter_recognition, k=5, update_interval=10, regularization=0.4) R> output <- output$output ``` diff --git a/doc/user/core.md b/doc/user/core.md index 4a81258ac7..22ce081c0b 100644 --- a/doc/user/core.md +++ b/doc/user/core.md @@ -862,9 +862,9 @@ including: * [`NeighborSearch`](/src/mlpack/methods/neighbor_search/neighbor_search.hpp) * [`RangeSearch`](/src/mlpack/methods/range_search/range_search.hpp) - * [`LMNN`](/src/mlpack/methods/lmnn/lmnn.hpp) + * [`LMNN`](methods/lmnn.md) * [`EMST`](/src/mlpack/methods/emst/emst.hpp) - * [`NCA`](/src/mlpack/methods/nca/nca.hpp) + * [`NCA`](methods/nca.md) * [`RANN`](/src/mlpack/methods/rann/rann.hpp) * [`KMeans`](/src/mlpack/methods/kmeans/kmeans.hpp) @@ -1281,6 +1281,11 @@ std::cout << "Squared Mahalanobis distance on 32-bit floating point data:" << std::endl; std::cout << " - Points 3 and 5: " << d1 << "." << std::endl; std::cout << " - Points 11 and 31: " << d2 << "." << std::endl; + +// Note that an equivalent transformation matrix can be recovered from Q with +// an upper Cholesky decomposition (Q -> R.t() * R). +arma::mat recoveredW = arma::chol(md.Q(), "lower"); +// A transformed dataset can be created with `(recoveredW * dataset)`. ``` --- diff --git a/doc/user/methods/lmnn.md b/doc/user/methods/lmnn.md new file mode 100644 index 0000000000..322a5f683e --- /dev/null +++ b/doc/user/methods/lmnn.md @@ -0,0 +1,454 @@ +## LMNN + +The `LMNN` class implements large margin nearest neighbor, which can be used +as both a linear dimensionality reduction technique and a distance learning +technique (also called metric learning). LMNN finds a linear transformation of +the dataset that improves `k`-nearest-neighbor classification performance. + +#### Simple usage example: + +```c++ +// Learn a distance metric that improves kNN classification performance. + +// All data and labels are uniform random; 10 dimensional data, 5 classes. +// Replace with a data::Load() call or similar for a real application. +arma::mat dataset(10, 1000, arma::fill::randu); // 1000 points. +arma::Row labels = + arma::randi>(1000, arma::distr_param(0, 4)); + +mlpack::LMNN lmnn(3 /* neighbors to consider */); // Step 1: create object. +arma::mat distance; +lmnn.LearnDistance(dataset, labels, distance); // Step 2: learn distance. + +// `distance` can now be used as a transformation matrix for the data. +arma::mat transformedData = distance * dataset; +// Or, you can create a MahalanobisDistance to evaluate points in the +// transformed dataset space. +arma::mat q = distance.t() * distance; +mlpack::MahalanobisDistance d(std::move(q)); + +std::cout << "Distance between points 0 and 1:" << std::endl; +std::cout << " - Before LMNN: " + << mlpack::EuclideanDistance::Evaluate(dataset.col(0), dataset.col(1)) + << "." << std::endl; +std::cout << " - After LMNN: " + << d.Evaluate(dataset.col(0), dataset.col(1)) << "." << std::endl; +``` +

    More examples...

    + +#### Quick links: + + * [Constructors](#constructors): create `LMNN` objects. + * [`LearnDistance()`](#learning-distances): learn distance metrics. + * [Other functionality](#other-functionality) for loading and saving. + * [Examples](#simple-examples) of simple usage and integration with other + techniques. + +#### See also: + + + + * [mlpack distance metrics](../core.md#distances) + * [`NCA`](nca.md) + * [Metric learning on Wikipedia](https://en.wikipedia.org/wiki/Similarity_learning#Metric_learning) + * [Large margin nearest neighbor on Wikipedia](https://en.wikipedia.org/wiki/Large_margin_nearest_neighbor) + * [Distance metric learning for Large Margin Nearest Neighbor Classification (pdf)](https://proceedings.neurips.cc/paper_files/paper/2005/file/a7f592cef8b130a6967a90617db5681b-Paper.pdf) + +### Constructors + + * `lmnn = LMNN(k, regularization=0.5, updateInterval=1)` + - Create an `LMNN` object considering the specified number `k` of neighbors. + - Optionally, specify the regularization to be applied to the LMNN cost + function (a `double`), and the number of iterations between recomputation + of neighbors (`updateInterval`, a `size_t`). + +--- + + * `lmnn = LMNN(k, regularization=0.5, updateInterval=1)` + * `lmnn = LMNN(k, regularization, updateInterval, distance)` + - Create an `LMNN` object using a custom + [`DistanceType`](../core.md#distances). + - `k` specifies the number of neighbors to consider. + - `regularization` specifies the regularization penalty to be applied to the + LMNN cost function (a `double`). + - `updateInterval` specifies the number of iterations between recomputation + of neighbors (a `size_t`). + - An instantiated `DistanceType` can optionally be passed with the `distance` + parameter. + - Using a custom `DistanceType` means that `LearnDistance()` will learn a + linear transformation for the data *in the metric space of the custom + `DistanceType`*. + * This means any learned distance may not necessarily improve + classification performance with the + [Euclidean distance](../core.md#lmetric). + * Instead, classification performance will be improved when the learned + distance is used with the given `DistanceType` only. + - Any mlpack `DistanceType` can be used as a drop-in replacement, or a + [custom `DistanceType`](../../developer/distances.md). + * A list of mlpack's provided distance metrics can be found + [here](../core.md#distances). + - ***Note: be sure that you understand the implications of a custom + `DistanceType` before using this version.*** + +--- + +***Notes***: + + - A larger `k` will cause `LearnDistance()` to take longer to compute, but will + give more accurate results. It is generally suggested to keep `k` in roughly + the `3` to `5` range, depending on the dataset. Using `k = 1` can provide + fast convergence, but the learned distance metric may be of lower quality. + + - `regularization` controls the balance between encouraging small distances for + points of the same class and penalizing small distances for points of + different classes. When `regularization` is increased, small distances for + points of different classes are further penalized. + + - Setting `updateInterval` greater than `1` will allow the LMNN algorithm to + take multiple steps without the expensive recomputation of neighbors, but + this means that subsequent optimization steps may not be using the true + nearest neighbors. + * If using an SGD-like algorithm (i.e. an optimizer for a + [differentiable separable function](https://www.ensmallen.org/docs.html#differentiable-separable-functions)), + this can often be set to a relatively high value (100 is not unreasonable). + * If using an optimizer like L-BFGS (i.e. a full-batch optimizer for + [differentiable functions](https://www.ensmallen.org/docs.html#differentiable-functions)), + this should be kept relatively low (going above 10 is not advised). + * It is worth cross-validating different values of the parameter to see what + works for your dataset. + +--- + +### Learning Distances + +Once an `LMNN` object has been created, the `LearnDistance()` method can be used +to learn a distance. + + * `lmnn.LearnDistance(data, labels, distance, [callbacks...])` + * `lmnn.LearnDistance(data, labels, distance, optimizer, [callbacks...])` + - Learn a distance metric on the given `data` and `labels`, filling + `distance` with a transformation matrix that can be used to map the data + into the space of the learned distance. + - Optionally, pass an instantiated + [ensmallen optimizer](https://www.ensmallen.org) and/or + [ensmallen callbacks](https://www.ensmallen.org/docs.html#callback-documentation) + to be used for the learning process. + - If no optimizer is passed, + [`ens::AMSGrad`](https://www.ensmallen.org/docs.html#amsgrad) is used. + - If `distance` already has size `r` x `data.n_rows` for some `r` less than + or equal to `data.n_rows`, it will be used as the starting point for + optimization. Otherwise, the identity matrix with size `data.n_rows` x + `data.n_rows` will be used. + - When optimization is complete, `distance` will have size `r` x + `data.n_rows`, where `r` is less than or equal to `data.n_rows`. + * *Note*: If `r < data.n_rows`, then LMNN has learned a distance metric + that also reduces the dimensionality of the data. See the + [last example](#simple-examples). + +To use `distance`, either: + + * Compute a new transformed dataset as `distance * data`, or + * Use an instantiated [`MahalanobisDistance`](../core.md#mahalanobisdistance) + with `distance.t() * distance` as the `Q` matrix. + +See the [examples section](#simple-examples) for more details. + +#### `LearnDistance()` Parameters: + +| **name** | **type** | **description** | +|----------|----------|-----------------| +| `data` | [`arma::mat`](../matrices.md) | [Column-major](../matrices.md#representing-data-in-mlpack) training matrix. | +| `labels` | [`arma::Row`](../matrices.md) | Training labels, [between `0` and `numClasses - 1`](../load_save.md#normalizing-labels) (inclusive). Should have length `data.n_cols`. | +| `distance` | [`arma::mat`](../matrices.md) | Output matrix to store transformation matrix representing learned distance. | +| `optimizer` | [any ensmallen optimizer](https://www.ensmallen.org) | Instantiated ensmallen optimizer for [differentiable functions](https://www.ensmallen.org/docs.html#differentiable-functions) or [differentiable separable functions](https://www.ensmallen.org/docs.html#differentiable-separable-functions). | `ens::AMSGrad()` | +| `callbacks...` | [any set of ensmallen callbacks](https://www.ensmallen.org/docs.html#callback-documentation) | Optional callbacks for the ensmallen optimizer, such as e.g. `ens::ProgressBar()`, `ens::Report()`, or others. | _(N/A)_ | + +***Note***: any matrix type can be used for `data` and `distance`, so long as +that type implements the Armadillo API. So, e.g., `arma::fmat` can be used. + +### Other Functionality + + * An `LMNN` object can be serialized with + [`data::Save()` and `data::Load()`](../load_save.md#mlpack-objects). + Note that this is only meaningful if a custom `DistanceType` is being used, + and that custom `DistanceType` has state to be saved. + + * `lmnn.K()` returns the number of neighbors used by LMNN, and `lmnn.K() = k` + will set the number of neighbors to use to `k`. + + * `lmnn.Regularization()` returns the current regularization value of the LMNN + object (as a `double`), and `lmnn.Regularization() = r` can be used to set + the regularization value to `r`. + + * `lmnn.UpdateInterval()` returns the current number of iterations between + neighbor recomputation (as a `size_t`), and `lmnn.UpdateInterval() = i` sets + the number of iterations between neighbor recomputation to `i`. + + * `lmnn.Distance()` will return the `DistanceType` being used for learning. + Unless a custom `DistanceType` was specified in the constructor, + this simply returns a [`SquaredEuclideanDistance`](../core.md#lmetric) + object. + +### Simple Examples + +Learn a distance metric to improve classification performance on the iris +dataset, and show improved performance when using +[`NaiveBayesClassifier`](naive_bayes_classifier.md). + +```c++ +// See https://datasets.mlpack.org/satellite.test.csv. +// (We are using the test set here just because it is a little smaller and +// we want this example to run quickly.) +arma::mat dataset; +mlpack::data::Load("satellite.test.csv", dataset, true); +// See https://datasets.mlpack.org/satellite.test.labels.csv. +arma::Row labels; +mlpack::data::Load("satellite.test.labels.csv", labels, true); + +// Create an LMNN object using 5 nearest neighbors and learn a distance. +arma::mat distance; +mlpack::LMNN lmnn(5); +lmnn.LearnDistance(dataset, labels, distance); + +// The distance matrix has size equal to the dimensionality of the data. +std::cout << "Learned distance size: " << distance.n_rows << " x " + << distance.n_cols << "." << std::endl; + +// Learn a NaiveBayesClassifier model on the data and print the performance. +mlpack::NaiveBayesClassifier nbc1(dataset, labels, 2); +arma::Row predictions; +nbc1.Classify(dataset, predictions); +std::cout << "Naive Bayes Classifier without LMNN: " + << arma::accu(labels == predictions) << " of " << labels.n_elem + << " correct." << std::endl; + +// Now transform the data and learn another NaiveBayesClassifier. +arma::mat transformedDataset = distance * dataset; +mlpack::NaiveBayesClassifier nbc2(transformedDataset, labels, 2); +nbc2.Classify(transformedDataset, predictions); +std::cout << "Naive Bayes Classifier with LMNN: " + << arma::accu(labels == predictions) << " of " << labels.n_elem + << " correct." << std::endl; +``` + +--- + +Learn a distance metric on the vehicle dataset, using 32-bit floating point to +represent the data and metric. + +```c++ +// See https://datasets.mlpack.org/vehicle.csv. +arma::fmat dataset; +mlpack::data::Load("vehicle.csv", dataset, true); + +// The labels are contained as the last row of the dataset. +arma::Row labels = + arma::conv_to>::from(dataset.row(dataset.n_rows - 1)); +dataset.shed_row(dataset.n_rows - 1); + +// Create an LMNN object with k=1 and learn distance on float32 data. +// Set updateInterval to a large value (100) because we are using the default +// AMSGrad optimizer (which will take very many small steps). +arma::fmat distance; +mlpack::LMNN lmnn(1, 0.5, 100); + +lmnn.LearnDistance(dataset, labels, distance, ens::ProgressBar()); + +// We want to compute six quantities: +// +// - Average distance to points of the same class before LMNN. +// - Average distance to points of the same class after LMNN, using +// MahalanobisDistance. +// - Average distance to points of the same class after LMNN, using the +// transformed dataset. +// +// - The same three quantities above, but for points of the other class. +// +// LMNN should reduce the average distance to points in the same class, while +// increasing the average distance to points in other classes. +float distSums[6] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; +size_t sameCount = 0; +arma::fmat q = distance.t() * distance; +mlpack::MahalanobisDistance md(std::move(q)); +arma::fmat transformedDataset = distance * dataset; +for (size_t i = 1; i < dataset.n_cols; ++i) +{ + const double d1 = mlpack::EuclideanDistance::Evaluate( + dataset.col(0), dataset.col(i)); + const double d2 = md.Evaluate(dataset.col(0), dataset.col(i)); + const double d3 = mlpack::EuclideanDistance::Evaluate( + transformedDataset.col(0), transformedDataset.col(i)); + + // Determine whether the point has the same label as point 0. + if (labels[i] == labels[0]) + { + distSums[0] += d1; + distSums[1] += d2; + distSums[2] += d3; + ++sameCount; + } + else + { + distSums[3] += d1; + distSums[4] += d2; + distSums[5] += d3; + } +} + +// Turn the results into average distances across the class. +distSums[0] /= sameCount; +distSums[1] /= sameCount; +distSums[2] /= sameCount; +distSums[3] /= (dataset.n_cols - sameCount); +distSums[4] /= (dataset.n_cols - sameCount); +distSums[5] /= (dataset.n_cols - sameCount); + +// Print the results. +std::cout << "Average distance between point 0 and other points of the same " + << "class:" << std::endl; +std::cout << " - Before LMNN: " << distSums[0] << "." + << std::endl; +std::cout << " - After LMNN (with MahalanobisDistance): " << distSums[1] << "." + << std::endl; +std::cout << " - After LMNN (with transformed dataset): " << distSums[2] << "." + << std::endl; +std::cout << std::endl; + +std::cout << "Average distance between point 0 and points of other classes: " + << std::endl; +std::cout << " - Before LMNN: " << distSums[3] << "." + << std::endl; +std::cout << " - After LMNN (with MahalanobisDistance): " << distSums[4] << "." + << std::endl; +std::cout << " - After LMNN (with transformed dataset): " << distSums[5] << "." + << std::endl; +std::cout << std::endl; + +std::cout << "Ratio of other-class to same-class distances:" << std::endl; +std::cout << "(We expect this to go up.)" << std::endl; +std::cout << " - Before LMNN: " << (distSums[3] / distSums[0]) << "." + << std::endl; +std::cout << " - After LMNN: " << (distSums[5] / distSums[2]) << "." + << std::endl; +``` + +--- + +Learn a distance metric on the iris dataset, using the L-BFGS optimizer with +callbacks. + +```c++ +// See https://datasets.mlpack.org/iris.csv. +arma::mat dataset; +mlpack::data::Load("iris.csv", dataset, true); +// See https://datasets.mlpack.org/iris.labels.csv. +arma::Row labels; +mlpack::data::Load("iris.labels.csv", labels, true); + +// Learn a distance with ensmallen's L-BFGS optimizer. +ens::L_BFGS lbfgs; +lbfgs.NumBasis() = 5; +lbfgs.MaxIterations() = 1000; + +// Use 5 neighbors for LMNN, and leave updateInterval at the default of 1, +// because we are using L-BFGS (a full-back optimizer). +mlpack::LMNN lmnn(5); + +// Use a callback that prints a final optimization report. +arma::mat distance; +lmnn.LearnDistance(dataset, labels, distance, lbfgs, ens::Report()); +``` + +--- + +Learn a distance metric on the vehicle dataset, but instead of using the +Euclidean distance as the underlying metric, use the Manhattan distance. This +means that LMNN is optimizing k-NN performance under the Manhattan distance, not +under the Euclidean distance. + +```c++ +// See https://datasets.mlpack.org/vehicle.csv. +arma::mat dataset; +mlpack::data::Load("vehicle.csv", dataset, true); + +// The labels are contained as the last row of the dataset. +arma::Row labels = + arma::conv_to>::from(dataset.row(dataset.n_rows - 1)); +dataset.shed_row(dataset.n_rows - 1); + +// Create the LMNN object and optimize. Use k=3 and Nesterov momentum SGD, +// printing a progress bar during optimization. Because Nesterov momentum SGD +// is an ensmallen optimizer for differentiable separable functions, we increase +// updateInterval to reduce the number of neighbor recomputations. We also set +// the regularization parameter to 1.0 to increase the penalty for nearby +// neighbors of a different class. +mlpack::LMNN lmnn(3, 1.0, 100); +arma::mat distance; +ens::NesterovMomentumSGD opt(0.000001 /* step size */, + 32 /* batch size */, + 20 * dataset.n_cols /* 20 epochs */); +lmnn.LearnDistance(dataset, labels, distance, opt, ens::ProgressBar()); + +// Now inspect distances between points with the Euclidean distance and with the +// inner product distance. +arma::mat transformedDataset = distance * dataset; + +// Points 0 and 1 have the same label (0). See their original distance---with +// both the Euclidean and Manhattan distances---and their transformed distances. +// We expect these points to get closer together, in the Manhattan distance. +const double d1 = mlpack::ManhattanDistance::Evaluate( + dataset.col(0), dataset.col(1)); +const double d2 = mlpack::ManhattanDistance::Evaluate( + transformedDataset.col(0), transformedDataset.col(1)); + +std::cout << "Distance between points 0 and 1 (same class):" << std::endl; +std::cout << " - Manhattan distance:" << std::endl; +std::cout << " * Before LMNN: " << d1 << std::endl; +std::cout << " * After LMNN: " << d2 << std::endl; +std::cout << std::endl; + +// Point 3 has a different label. We therefore expect this point to get further +// from point 0 with the Manhattan distance, but not necessarily with the +// Euclidean distance. +const double d3 = mlpack::ManhattanDistance::Evaluate( + dataset.col(0), dataset.col(3)); +const double d4 = mlpack::ManhattanDistance::Evaluate( + transformedDataset.col(0), transformedDataset.col(3)); + +std::cout << "Distance between points 0 and 3 (different class):" << std::endl; +std::cout << " - Manhattan distance:" << std::endl; +std::cout << " * Before LMNN: " << d3 << std::endl; +std::cout << " * After LMNN: " << d4 << std::endl; + +// Note that point 3 has been moved further away from point 0 than point 1. +``` + +--- + +Learn a distance metric while also performing dimensionality reduction, reducing +the dimensionality of the satellite dataset by 3 dimensions. + +```c++ +// See https://datasets.mlpack.org/satellite.train.csv. +arma::mat dataset; +mlpack::data::Load("satellite.train.csv", dataset, true); +// See https://datasets.mlpack.org/satellite.labels.csv. +arma::Row labels; +mlpack::data::Load("satellite.train.labels.csv", labels, true); + +// Use a random initialization for the distance transformation, with the +// specified output dimensionality. +arma::mat distance(dataset.n_rows - 3, dataset.n_rows, arma::fill::randu); +mlpack::LMNN lmnn(3); +ens::L_BFGS opt; +opt.MaxIterations() = 10; // You may want more in a real application. +lmnn.LearnDistance(dataset, labels, distance, opt, ens::Report()); + +// Now transform the dataset. +arma::mat transformedData = distance * dataset; + +std::cout << "Original data has size " << dataset.n_rows << " x " + << dataset.n_cols << "." << std::endl; +std::cout << "Transformed data has size " << transformedData.n_rows << " x " + << transformedData.n_cols << "." << std::endl; +``` diff --git a/doc/user/methods/nca.md b/doc/user/methods/nca.md new file mode 100644 index 0000000000..4870907af8 --- /dev/null +++ b/doc/user/methods/nca.md @@ -0,0 +1,440 @@ +## NCA + +The `NCA` class implements neighborhood components analysis, which can be used +as both a linear dimensionality reduction technique and a distance learning +technique (also called metric learning). Neighborhood components analysis finds +a linear transformation of the dataset that improves `k`-nearest-neighbor +classification performance. + +Note that `NCA` is a computationally intensive technique (each optimization +iteration takes time quadratic in the data size!), and may be slow to run even +for datasets of only moderate size. See [`LMNN`](lmnn.md) for another distance +learning technique that scales better to larger datasets. + +#### Simple usage example: + +```c++ +// Learn a distance metric that improves kNN classification performance. + +// All data and labels are uniform random; 10 dimensional data, 5 classes. +// Replace with a data::Load() call or similar for a real application. +arma::mat dataset(10, 1000, arma::fill::randu); // 1000 points. +arma::Row labels = + arma::randi>(1000, arma::distr_param(0, 4)); + +mlpack::NCA nca; // Step 1: create object. +arma::mat distance; +nca.LearnDistance(dataset, labels, distance); // Step 2: learn distance. + +// `distance` can now be used as a transformation matrix for the data. +arma::mat transformedData = distance * dataset; +// Or, you can create a MahalanobisDistance to evaluate points in the +// transformed dataset space. +arma::mat q = distance.t() * distance; +mlpack::MahalanobisDistance d(std::move(q)); + +std::cout << "Distance between points 0 and 1:" << std::endl; +std::cout << " - Before NCA: " + << mlpack::EuclideanDistance::Evaluate(dataset.col(0), dataset.col(1)) + << "." << std::endl; +std::cout << " - After NCA: " + << d.Evaluate(dataset.col(0), dataset.col(1)) << "." << std::endl; +``` +

    More examples...

    + +#### Quick links: + + * [Constructors](#constructors): create `NCA` objects. + * [`LearnDistance()`](#learning-distances): learn distance metrics. + * [Other functionality](#other-functionality) for loading and saving. + * [Examples](#simple-examples) of simple usage and integration with other + techniques. + +#### See also: + + + + * [mlpack distance metrics](../core.md#distances) + * [`LMNN`](lmnn.md) + * [Metric learning on Wikipedia](https://en.wikipedia.org/wiki/Similarity_learning#Metric_learning) + * [Neighborhood Components Analysis on Wikipedia](https://en.wikipedia.org/wiki/Neighbourhood_components_analysis) + * [Neighbourhood Components Analysis (pdf)](https://proceedings.neurips.cc/paper_files/paper/2004/file/42fe880812925e520249e808937738d2-Paper.pdf) + +### Constructors + + * `nca = NCA()` + - Create an `NCA` object with default parameters. + +--- + + * `nca = NCA()` + * `nca = NCA(distance)` + - Create an `NCA` object using a custom + [`DistanceType`](../core.md#distances). + - An instantiated `DistanceType` can optionally be passed with the `distance` + parameter. + - Using a custom `DistanceType` means that `LearnDistance()` will learn a + linear transformation for the data *in the metric space of the custom + `DistanceType`*. + * This means any learned distance may not necessarily improve + classification performance with the + [Euclidean distance](../core.md#lmetric). + * Instead, classification performance will be improved when the learned + distance is used with the given `DistanceType` only. + - Any mlpack `DistanceType` can be used as a drop-in replacement, or a + [custom `DistanceType`](../../developer/distances.md). + * A list of mlpack's provided distance metrics can be found + [here](../core.md#distances). + - ***Note: be sure that you understand the implications of a custom + `DistanceType` before using this version.*** + +--- + +### Learning Distances + +Once an `NCA` object has been created, the `LearnDistance()` method can be used +to learn a distance. + + * `nca.LearnDistance(data, labels, distance, [callbacks...])` + * `nca.LearnDistance(data, labels, distance, optimizer, [callbacks...])` + - Learn a distance metric on the given `data` and `labels`, filling + `distance` with a transformation matrix that can be used to map the data + into the space of the learned distance. + - Optionally, pass an instantiated + [ensmallen optimizer](https://www.ensmallen.org) and/or + [ensmallen callbacks](https://www.ensmallen.org/docs.html#callback-documentation) + to be used for the learning process. + - If `distance` already has size `r` x `data.n_rows` for some `r` less than + or equal to `data.n_rows`, it will be used as the starting point for + optimization. Otherwise, the identity matrix with size `data.n_rows` x + `data.n_rows` will be used. + - When optimization is complete, `distance` will have size `r` x + `data.n_rows`, where `r` is less than or equal to `data.n_rows`. + * *Note*: If `r < data.n_rows`, then NCA has learned a distance metric that + also reduces the dimensionality of the data. See the + [last example](#simple-examples). + +To use `distance`, either: + + * Compute a new transformed dataset as `distance * data`, or + * Use an instantiated [`MahalanobisDistance`](../core.md#mahalanobisdistance) + with `distance.t() * distance` as the `Q` matrix. + +See the [examples section](#simple-examples) for more details. + +***Caveat:*** NCA operates by repeatedly computing expressions of the form +`exp(-distance.Evaluate(data.col(i), data.col(j)))` (that is, the exponential of +the negative distance between two points). When distances are very large, this +***quantity underflows to 0*** and results will not be reasonable. + + - This situation can be detected, usually by a result where `distance` is equal + to the identity matrix. + - Alternately, if the [`ens::ProgressBar()` + callback](https://www.ensmallen.org/docs.html#progressbar) is used, a loss of + 0 often means this situation has occurred. + - To mitigate the problem, consider scaling data such that the maximum pairwise + distance is less than 10. See the [simple examples](#simple-examples) that + use the `vehicle` dataset. + +#### `LearnDistance()` Parameters: + +| **name** | **type** | **description** | +|----------|----------|-----------------| +| `data` | [`arma::mat`](../matrices.md) | [Column-major](../matrices.md#representing-data-in-mlpack) training matrix. | +| `labels` | [`arma::Row`](../matrices.md) | Training labels, [between `0` and `numClasses - 1`](../load_save.md#normalizing-labels) (inclusive). Should have length `data.n_cols`. | +| `distance` | [`arma::mat`](../matrices.md) | Output matrix to store transformation matrix representing learned distance. | +| `optimizer` | [any ensmallen optimizer](https://www.ensmallen.org) | Instantiated ensmallen optimizer for [differentiable functions](https://www.ensmallen.org/docs.html#differentiable-functions) or [differentiable separable functions](https://www.ensmallen.org/docs.html#differentiable-separable-functions). | `ens::StandardSGD()` | +| `callbacks...` | [any set of ensmallen callbacks](https://www.ensmallen.org/docs.html#callback-documentation) | Optional callbacks for the ensmallen optimizer, such as e.g. `ens::ProgressBar()`, `ens::Report()`, or others. | _(N/A)_ | + +***Note***: any matrix type can be used for `data` and `distance`, so long as +that type implements the Armadillo API. So, e.g., `arma::fmat` can be used. + +### Other Functionality + + * An `NCA` object can be serialized with + [`data::Save()` and `data::Load()`](../load_save.md#mlpack-objects). + Note that this is only meaningful if a custom `DistanceType` is being used, + and that custom `DistanceType` has state to be saved. + + * `nca.Distance()` will return the `DistanceType` being used for learning. + Unless a custom `DistanceType` was specified in the constructor, + this simply returns a [`SquaredEuclideanDistance`](../core.md#lmetric) + object. + +### Simple Examples + +Learn a distance metric to improve classification performance on the iris +dataset, and show improved performance when using +[`NaiveBayesClassifier`](naive_bayes_classifier.md). + +```c++ +// See https://datasets.mlpack.org/iris.csv. +arma::mat dataset; +mlpack::data::Load("iris.csv", dataset, true); +// See https://datasets.mlpack.org/iris.labels.csv. +arma::Row labels; +mlpack::data::Load("iris.labels.csv", labels, true); + +// Create an NCA object and learn a distance. +arma::mat distance; +mlpack::NCA nca; +nca.LearnDistance(dataset, labels, distance); + +// The distance matrix has size equal to the dimensionality of the data. +std::cout << "Learned distance size: " << distance.n_rows << " x " + << distance.n_cols << "." << std::endl; + +// Learn a NaiveBayesClassifier model on the data and print the performance. +mlpack::NaiveBayesClassifier nbc1(dataset, labels, 3); +arma::Row predictions; +nbc1.Classify(dataset, predictions); +std::cout << "Naive Bayes Classifier without NCA: " + << arma::accu(labels == predictions) << " of " << labels.n_elem + << " correct." << std::endl; + +// Now transform the data and learn another NaiveBayesClassifier. +arma::mat transformedDataset = distance * dataset; +mlpack::NaiveBayesClassifier nbc2(transformedDataset, labels, 3); +nbc2.Classify(transformedDataset, predictions); +std::cout << "Naive Bayes Classifier with NCA: " + << arma::accu(labels == predictions) << " of " << labels.n_elem + << " correct." << std::endl; +``` + +--- + +Learn a distance metric on the ionosphere dataset, using 32-bit floating point +to represent the data and metric. + +```c++ +// See https://datasets.mlpack.org/ionosphere.csv. +arma::fmat dataset; +mlpack::data::Load("ionosphere.csv", dataset, true); + +// The labels are the last row of the dataset. +arma::Row labels = + arma::conv_to>::from(dataset.row(dataset.n_rows - 1)); +dataset.shed_row(dataset.n_rows - 1); + +// Create an NCA object and learn distance on float32 data. +// To keep computation time down, we use an instantiated optimizer that will +// only perform 10 epochs of training. (In a real application you may want to +// train for longer!) +arma::fmat distance; +mlpack::NCA nca; + +ens::StandardSGD opt; +opt.MaxIterations() = 10 * dataset.n_cols; +nca.LearnDistance(dataset, labels, distance, opt, ens::ProgressBar()); + +// We want to compute six quantities: +// +// - Average distance to points of the same class before NCA. +// - Average distance to points of the same class after NCA, using +// MahalanobisDistance. +// - Average distance to points of the same class after NCA, using the +// transformed dataset. +// +// - The same three quantities above, but for points of the other class. +// +// NCA should reduce the average distance to points in the same class, while +// increasing the average distance to points in other classes. +float distSums[6] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; +size_t sameCount = 0; +arma::fmat q = distance.t() * distance; +mlpack::MahalanobisDistance md(std::move(q)); +arma::fmat transformedDataset = distance * dataset; +for (size_t i = 1; i < dataset.n_cols; ++i) +{ + const double d1 = mlpack::EuclideanDistance::Evaluate( + dataset.col(0), dataset.col(i)); + const double d2 = md.Evaluate(dataset.col(0), dataset.col(i)); + const double d3 = mlpack::EuclideanDistance::Evaluate( + transformedDataset.col(0), transformedDataset.col(i)); + + // Determine whether the point has the same label as point 0. + if (labels[i] == labels[0]) + { + distSums[0] += d1; + distSums[1] += d2; + distSums[2] += d3; + ++sameCount; + } + else + { + distSums[3] += d1; + distSums[4] += d2; + distSums[5] += d3; + } +} + +// Turn the results into average distances across the class. +distSums[0] /= sameCount; +distSums[1] /= sameCount; +distSums[2] /= sameCount; +distSums[3] /= (dataset.n_cols - sameCount); +distSums[4] /= (dataset.n_cols - sameCount); +distSums[5] /= (dataset.n_cols - sameCount); + +// Print the results. +std::cout << "Average distance between point 0 and other points of the same " + << "class:" << std::endl; +std::cout << " - Before NCA: " << distSums[0] << "." + << std::endl; +std::cout << " - After NCA (with MahalanobisDistance): " << distSums[1] << "." + << std::endl; +std::cout << " - After NCA (with transformed dataset): " << distSums[2] << "." + << std::endl; +std::cout << std::endl; + +std::cout << "Average distance between point 0 and points of other classes: " + << std::endl; +std::cout << " - Before NCA: " << distSums[3] << "." + << std::endl; +std::cout << " - After NCA (with MahalanobisDistance): " << distSums[4] << "." + << std::endl; +std::cout << " - After NCA (with transformed dataset): " << distSums[5] << "." + << std::endl; +std::cout << std::endl; + +std::cout << "Ratio of other-class to same-class distances:" << std::endl; +std::cout << "(We expect this to go up.)" << std::endl; +std::cout << " - Before NCA: " << (distSums[3] / distSums[0]) << "." + << std::endl; +std::cout << " - After NCA: " << (distSums[5] / distSums[2]) << "." + << std::endl; +``` + +--- + +Learn a distance metric on the iris dataset, using the L-BFGS optimizer with +callbacks. + +```c++ +// See https://datasets.mlpack.org/iris.csv. +arma::mat dataset; +mlpack::data::Load("iris.csv", dataset, true); +// See https://datasets.mlpack.org/iris.labels.csv. +arma::Row labels; +mlpack::data::Load("iris.labels.csv", labels, true); + +// Learn a distance with ensmallen's L-BFGS optimizer. +ens::L_BFGS lbfgs; +lbfgs.NumBasis() = 5; +lbfgs.MaxIterations() = 1000; + +arma::mat distance; +mlpack::NCA nca; + +// Use a callback that prints a final optimization report. +nca.LearnDistance(dataset, labels, distance, lbfgs, ens::Report()); +``` + +--- + + + +Learn a distance metric on the vehicle dataset, but instead of using the +Euclidean distance as the underlying metric, use the Manhattan distance. This +means that NCA is optimizing k-NN performance under the Manhattan distance, not +under the Euclidean distance. + +```c++ +// See https://datasets.mlpack.org/vehicle.csv. +arma::mat dataset; +mlpack::data::Load("vehicle.csv", dataset, true); + +// The labels are contained as the last row of the dataset. +arma::Row labels = + arma::conv_to>::from(dataset.row(dataset.n_rows - 1)); +dataset.shed_row(dataset.n_rows - 1); + +// Because typical distances between points in the vehicle dataset are large, +// we will center the dataset and scale it to have points in the unit ball. +// (That is, all points will have values in each dimension between -1 and 1.) +// This means that the maximum pairwise distance is 2. +dataset.each_col() -= arma::mean(dataset, 1); +dataset /= arma::max(arma::max(arma::abs(dataset))); + +// Create the NCA object and optimize. Use Nesterov momentum SGD, printing a +// progress bar during optimization. +mlpack::NCA nca; +arma::mat distance; +ens::NesterovMomentumSGD opt(0.01 /* step size */, + 32 /* batch size */, + 20 * dataset.n_cols /* 20 epochs */); +nca.LearnDistance(dataset, labels, distance, opt, ens::ProgressBar()); + +// Now inspect distances between points with the Euclidean distance and with the +// inner product distance. +arma::mat transformedDataset = distance * dataset; + +// Points 0 and 1 have the same label (0). See their original distance---with +// both the Euclidean and Manhattan distances---and their transformed distances. +// We expect these points to get closer together, in the Manhattan distance. +const double d1 = mlpack::ManhattanDistance::Evaluate( + dataset.col(0), dataset.col(1)); +const double d2 = mlpack::ManhattanDistance::Evaluate( + transformedDataset.col(0), transformedDataset.col(1)); + +std::cout << "Distance between points 0 and 1 (same class):" << std::endl; +std::cout << " - Manhattan distance:" << std::endl; +std::cout << " * Before NCA: " << d1 << std::endl; +std::cout << " * After NCA: " << d2 << std::endl; +std::cout << std::endl; + +// Point 3 has a different label. We therefore expect this point to get further +// from point 0 with the Manhattan distance, but not necessarily with the +// Euclidean distance. +const double d3 = mlpack::ManhattanDistance::Evaluate( + dataset.col(0), dataset.col(3)); +const double d4 = mlpack::ManhattanDistance::Evaluate( + transformedDataset.col(0), transformedDataset.col(3)); + +std::cout << "Distance between points 0 and 3 (different class):" << std::endl; +std::cout << " - Manhattan distance:" << std::endl; +std::cout << " * Before NCA: " << d3 << std::endl; +std::cout << " * After NCA: " << d4 << std::endl; + +// Note that point 3 has been moved further away from point 0 than point 1. +``` + +--- + +Learn a distance metric while also performing dimensionality reduction, reducing +the dimensionality of the vehicle dataset by 2 dimensions. + +```c++ +// See https://datasets.mlpack.org/vehicle.csv. +arma::mat dataset; +mlpack::data::Load("vehicle.csv", dataset, true); + +// The labels are contained as the last row of the dataset. +arma::Row labels = + arma::conv_to>::from(dataset.row(dataset.n_rows - 1)); +dataset.shed_row(dataset.n_rows - 1); + +// Because typical distances between points in the vehicle dataset are large, +// we will center the dataset and scale it to have points in the unit ball. +// (That is, all points will have values in each dimension between -1 and 1.) +// This means that the maximum pairwise distance is 2. +dataset.each_col() -= arma::mean(dataset, 1); +dataset /= arma::max(arma::max(arma::abs(dataset))); + +// Use a random initialization for the distance transformation, with the +// specified output dimensionality. +arma::mat distance(dataset.n_rows - 2, dataset.n_rows, arma::fill::randu); +mlpack::NCA nca; +ens::L_BFGS opt; +opt.MaxIterations() = 10; // You may want more in a real application. +nca.LearnDistance(dataset, labels, distance, opt); + +// Now transform the dataset. +arma::mat transformedData = distance * dataset; + +std::cout << std::endl << std::endl; +std::cout << "Original data has size " << dataset.n_rows << " x " + << dataset.n_cols << "." << std::endl; +std::cout << "Transformed data has size " << transformedData.n_rows << " x " + << transformedData.n_cols << "." << std::endl; +``` diff --git a/src/mlpack/bindings/go/mlpack/capi/arma_util.hpp b/src/mlpack/bindings/go/mlpack/capi/arma_util.hpp index 0c57590e26..4034934ba7 100644 --- a/src/mlpack/bindings/go/mlpack/capi/arma_util.hpp +++ b/src/mlpack/bindings/go/mlpack/capi/arma_util.hpp @@ -39,9 +39,7 @@ inline typename T::elem_type* GetMemory(T& m) arma::access::rw(m.mem_state) = 1; // With Armadillo 10 and newer, we must set `n_alloc` to 0 so that // Armadillo does not deallocate the memory. - #if ARMA_VERSION_MAJOR >= 10 - arma::access::rw(m.n_alloc) = 0; - #endif + arma::access::rw(m.n_alloc) = 0; return m.memptr(); } } diff --git a/src/mlpack/bindings/julia/julia_util.cpp b/src/mlpack/bindings/julia/julia_util.cpp index 5c0320a311..c3340296a6 100644 --- a/src/mlpack/bindings/julia/julia_util.cpp +++ b/src/mlpack/bindings/julia/julia_util.cpp @@ -428,9 +428,7 @@ double* GetParamMat(void* params, const char* paramName) else { arma::access::rw(mat.mem_state) = 1; - #if ARMA_VERSION_MAJOR >= 10 - arma::access::rw(mat.n_alloc) = 0; - #endif + arma::access::rw(mat.n_alloc) = 0; return mat.memptr(); } } @@ -475,9 +473,7 @@ size_t* GetParamUMat(void* params, const char* paramName) else { arma::access::rw(mat.mem_state) = 1; - #if ARMA_VERSION_MAJOR >= 10 - arma::access::rw(mat.n_alloc) = 0; - #endif + arma::access::rw(mat.n_alloc) = 0; return mat.memptr(); } } @@ -513,9 +509,7 @@ double* GetParamCol(void* params, const char* paramName) else { arma::access::rw(vec.mem_state) = 1; - #if ARMA_VERSION_MAJOR >= 10 - arma::access::rw(vec.n_alloc) = 0; - #endif + arma::access::rw(vec.n_alloc) = 0; return vec.memptr(); } } @@ -552,9 +546,7 @@ size_t* GetParamUCol(void* params, const char* paramName) else { arma::access::rw(vec.mem_state) = 1; - #if ARMA_VERSION_MAJOR >= 10 - arma::access::rw(vec.n_alloc) = 0; - #endif + arma::access::rw(vec.n_alloc) = 0; return vec.memptr(); } } @@ -590,9 +582,7 @@ double* GetParamRow(void* params, const char* paramName) else { arma::access::rw(vec.mem_state) = 1; - #if ARMA_VERSION_MAJOR >= 10 - arma::access::rw(vec.n_alloc) = 0; - #endif + arma::access::rw(vec.n_alloc) = 0; return vec.memptr(); } } @@ -629,9 +619,7 @@ size_t* GetParamURow(void* params, const char* paramName) else { arma::access::rw(vec.mem_state) = 1; - #if ARMA_VERSION_MAJOR >= 10 - arma::access::rw(vec.n_alloc) = 0; - #endif + arma::access::rw(vec.n_alloc) = 0; return vec.memptr(); } } @@ -707,9 +695,7 @@ double* GetParamMatWithInfoPtr(void* params, const char* paramName) else { arma::access::rw(m.mem_state) = 1; - #if ARMA_VERSION_MAJOR >= 10 - arma::access::rw(m.n_alloc) = 0; - #endif + arma::access::rw(m.n_alloc) = 0; return m.memptr(); } } diff --git a/src/mlpack/bindings/python/mlpack/arma_util.hpp b/src/mlpack/bindings/python/mlpack/arma_util.hpp index 70f0dd1b3e..ca6a8d1a71 100644 --- a/src/mlpack/bindings/python/mlpack/arma_util.hpp +++ b/src/mlpack/bindings/python/mlpack/arma_util.hpp @@ -25,9 +25,8 @@ void SetMemState(T& t, int state) // If we just "released" the memory, so that the matrix does not own it, with // Armadillo 10 we must also ensure that the matrix does not deallocate the // memory by specifying `n_alloc = 0`. - #if ARMA_VERSION_MAJOR >= 10 - const_cast(t.n_alloc) = 0; - #endif + + const_cast(t.n_alloc) = 0; } /** diff --git a/src/mlpack/core.hpp b/src/mlpack/core.hpp index 73eeaaece4..7205587ab5 100644 --- a/src/mlpack/core.hpp +++ b/src/mlpack/core.hpp @@ -37,6 +37,7 @@ // Now the core mlpack classes. #include #include +#include #include #include #include diff --git a/src/mlpack/core/util/arma_traits.hpp b/src/mlpack/core/util/arma_traits.hpp index 2dd360761b..1a7133feb5 100644 --- a/src/mlpack/core/util/arma_traits.hpp +++ b/src/mlpack/core/util/arma_traits.hpp @@ -50,7 +50,7 @@ struct IsCube }; // Commenting out the first template per case, because -// Visual Studio doesn't like this instantiaion pattern (error C2910). +// Visual Studio doesn't like this instantiation pattern (error C2910). // template<> template struct IsVector > @@ -105,35 +105,17 @@ struct IsCube > const static bool value = true; }; +template +struct IsVector > +{ + const static bool value = true; +}; -#if ((ARMA_VERSION_MAJOR >= 10) || \ - ((ARMA_VERSION_MAJOR == 9) && (ARMA_VERSION_MINOR >= 869))) - - // Armadillo 9.869+ has SpSubview_col and SpSubview_row - - template - struct IsVector > - { - const static bool value = true; - }; - - template - struct IsVector > - { - const static bool value = true; - }; - -#else - - // fallback for older Armadillo versions - - template - struct IsVector > - { - const static bool value = true; - }; - -#endif +template +struct IsVector > +{ + const static bool value = true; +}; // Get the row vector type corresponding to a given MatType. diff --git a/src/mlpack/core/util/first_element_is_arma.hpp b/src/mlpack/core/util/first_element_is_arma.hpp new file mode 100644 index 0000000000..60adcc0fe3 --- /dev/null +++ b/src/mlpack/core/util/first_element_is_arma.hpp @@ -0,0 +1,44 @@ +/** + * @file core/util/first_element_is_arma.hpp + * @author Ryan Curtin + * + * Utility struct to detect whether the first element in a parameter pack is an + * Armadillo type. + */ +#ifndef MLPACK_CORE_UTIL_FIRST_ELEMENT_IS_ARMA_HPP +#define MLPACK_CORE_UTIL_FIRST_ELEMENT_IS_ARMA_HPP + +#include + +namespace mlpack { + +// This utility struct returns the first type of a parameter pack. +template +struct First +{ + typedef void type; +}; + +// This matches whenever CallbackTypes has one or more elements. +template +struct First +{ + typedef T type; +}; + +// This utility template struct detects whether the first element in a +// parameter pack is an Armadillo type. It is entirely for the deprecated +// constructor below and can be removed when that is removed during the +// release of mlpack 5.0.0. +template +struct FirstElementIsArma +{ + static constexpr bool value = arma::is_arma_type< + typename std::remove_reference< + typename First::type + >::type>::value; +}; + +} + +#endif diff --git a/src/mlpack/methods/lmnn/constraints.hpp b/src/mlpack/methods/lmnn/constraints.hpp index 082b8961ed..7704d239fe 100644 --- a/src/mlpack/methods/lmnn/constraints.hpp +++ b/src/mlpack/methods/lmnn/constraints.hpp @@ -27,12 +27,25 @@ namespace mlpack { * data point) and Triplets() (Generates sets of {dataset, target neighbors, * impostors} tripltets.) */ -template +template, + typename DistanceType = SquaredEuclideanDistance> class Constraints { public: //! Convenience typedef. - typedef NeighborSearch KNN; + typedef NeighborSearch KNN; + + // Convenience typedef for element type of data. + typedef typename MatType::elem_type ElemType; + // Convenience typedef for column vector of data. + typedef typename GetColType::type VecType; + // Convenience typedef for cube of data. + typedef typename GetCubeType::type CubeType; + // Convenience typedef for dense matrix of indices. + typedef typename GetUDenseMatType::type UMatType; + // Convenience typedef for dense vector of indices. + typedef typename GetColType::type UVecType; /** * Constructor for creating a Constraints instance. @@ -41,8 +54,8 @@ class Constraints * @param labels Input dataset labels. * @param k Number of target neighbors, impostors & triplets. */ - Constraints(const arma::mat& dataset, - const arma::Row& labels, + Constraints(const MatType& dataset, + const LabelsType& labels, const size_t k); /** @@ -54,10 +67,10 @@ class Constraints * @param labels Input dataset labels. * @param norms Input dataset norms. */ - void TargetNeighbors(arma::Mat& outputMatrix, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms); + void TargetNeighbors(UMatType& outputMatrix, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms); /** * Calculates k similar labeled nearest neighbors for a batch of dataset and @@ -70,10 +83,10 @@ class Constraints * @param begin Index of the initial point of dataset. * @param batchSize Number of data points to use. */ - void TargetNeighbors(arma::Mat& outputMatrix, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms, + void TargetNeighbors(UMatType& outputMatrix, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms, const size_t begin, const size_t batchSize); @@ -86,10 +99,10 @@ class Constraints * @param labels Input dataset labels. * @param norms Input dataset norms. */ - void Impostors(arma::Mat& outputMatrix, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms); + void Impostors(UMatType& outputMatrix, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms); /** * Calculates k differently labeled nearest neighbors & distances to @@ -101,11 +114,11 @@ class Constraints * @param labels Input dataset labels. * @param norms Input dataset norms. */ - void Impostors(arma::Mat& outputNeighbors, - arma::mat& outputDistance, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms); + void Impostors(UMatType& outputNeighbors, + MatType& outputDistance, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms); /** * Calculates k differently labeled nearest neighbors for a batch of dataset @@ -118,10 +131,10 @@ class Constraints * @param begin Index of the initial point of dataset. * @param batchSize Number of data points to use. */ - void Impostors(arma::Mat& outputMatrix, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms, + void Impostors(UMatType& outputMatrix, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms, const size_t begin, const size_t batchSize); @@ -137,11 +150,11 @@ class Constraints * @param begin Index of the initial point of dataset. * @param batchSize Number of data points to use. */ - void Impostors(arma::Mat& outputNeighbors, - arma::mat& outputDistance, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms, + void Impostors(UMatType& outputNeighbors, + MatType& outputDistance, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms, const size_t begin, const size_t batchSize); @@ -158,12 +171,12 @@ class Constraints * @param points Indices of data points to calculate impostors on. * @param numPoints Number of points to actually calculate impostors on. */ - void Impostors(arma::Mat& outputNeighbors, - arma::mat& outputDistance, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms, - const arma::uvec& points, + void Impostors(UMatType& outputNeighbors, + MatType& outputDistance, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms, + const UVecType& points, const size_t numPoints); /** @@ -175,10 +188,10 @@ class Constraints * @param labels Input dataset labels. * @param norms Input dataset norms. */ - void Triplets(arma::Mat& outputMatrix, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms); + void Triplets(UMatType& outputMatrix, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms); //! Get the number of target neighbors (k). const size_t& K() const { return k; } @@ -195,13 +208,13 @@ class Constraints size_t k; //! Store unique labels. - arma::Row uniqueLabels; + LabelsType uniqueLabels; //! Store indices of data points having similar label. - std::vector indexSame; + std::vector indexSame; //! Store indices of data points having different label. - std::vector indexDiff; + std::vector indexDiff; //! False if nothing has ever been precalculated. bool precalculated; @@ -210,15 +223,15 @@ class Constraints * Precalculate the unique labels, and indices of similar * and different datapoints on the basis of labels. */ - inline void Precalculate(const arma::Row& labels); + inline void Precalculate(const LabelsType& labels); /** * Re-order neighbors on the basis of increasing norm in case * of ties among distances. */ - inline void ReorderResults(const arma::mat& distances, - arma::Mat& neighbors, - const arma::vec& norms); + inline void ReorderResults(const MatType& distances, + UMatType& neighbors, + const VecType& norms); }; } // namespace mlpack diff --git a/src/mlpack/methods/lmnn/constraints_impl.hpp b/src/mlpack/methods/lmnn/constraints_impl.hpp index 6f227fd970..27ed28417a 100644 --- a/src/mlpack/methods/lmnn/constraints_impl.hpp +++ b/src/mlpack/methods/lmnn/constraints_impl.hpp @@ -17,10 +17,10 @@ namespace mlpack { -template -Constraints::Constraints( - const arma::mat& /* dataset */, - const arma::Row& labels, +template +Constraints::Constraints( + const MatType& /* dataset */, + const LabelsType& labels, const size_t k) : k(k), precalculated(false) @@ -36,11 +36,11 @@ Constraints::Constraints( } } -template -inline void Constraints::ReorderResults( - const arma::mat& distances, - arma::Mat& neighbors, - const arma::vec& norms) +template +inline void Constraints::ReorderResults( + const MatType& distances, + UMatType& neighbors, + const VecType& norms) { // Shortcut... if (neighbors.n_rows == 1) @@ -64,24 +64,21 @@ inline void Constraints::ReorderResults( if (start != end) { // We must sort these elements by norm. - arma::Col newNeighbors = - neighbors.col(i).subvec(start, end - 1); - arma::uvec indices = ConvTo::From(newNeighbors); - - arma::uvec order = arma::sort_index(norms.elem(indices)); - neighbors.col(i).subvec(start, end - 1) = - newNeighbors.elem(order); + UVecType indices = neighbors.col(i).subvec(start, end - 1); + UVecType order = arma::sort_index(norms.elem(indices)); + neighbors.col(i).subvec(start, end - 1) = indices.elem(order); } } } } // Calculates k similar labeled nearest neighbors. -template -void Constraints::TargetNeighbors(arma::Mat& outputMatrix, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms) +template +void Constraints::TargetNeighbors( + UMatType& outputMatrix, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms) { // Perform pre-calculation. If neccesary. Precalculate(labels); @@ -89,8 +86,8 @@ void Constraints::TargetNeighbors(arma::Mat& outputMatrix, // KNN instance. KNN knn; - arma::Mat neighbors; - arma::mat distances; + UMatType neighbors; + MatType distances; for (size_t i = 0; i < uniqueLabels.n_cols; ++i) { @@ -114,28 +111,29 @@ void Constraints::TargetNeighbors(arma::Mat& outputMatrix, // Calculates k similar labeled nearest neighbors on a // batch of data points. -template -void Constraints::TargetNeighbors(arma::Mat& outputMatrix, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms, - const size_t begin, - const size_t batchSize) +template +void Constraints::TargetNeighbors( + UMatType& outputMatrix, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms, + const size_t begin, + const size_t batchSize) { // Perform pre-calculation. If neccesary. Precalculate(labels); - arma::mat subDataset = dataset.cols(begin, begin + batchSize - 1); - arma::Row sublabels = labels.cols(begin, begin + batchSize - 1); + MatType subDataset = dataset.cols(begin, begin + batchSize - 1); + LabelsType sublabels = labels.cols(begin, begin + batchSize - 1); // KNN instance. KNN knn; - arma::Mat neighbors; - arma::mat distances; + UMatType neighbors; + MatType distances; // Vectors to store indices. - arma::uvec subIndexSame; + UVecType subIndexSame; for (size_t i = 0; i < uniqueLabels.n_cols; ++i) { @@ -161,11 +159,12 @@ void Constraints::TargetNeighbors(arma::Mat& outputMatrix, } // Calculates k differently labeled nearest neighbors. -template -void Constraints::Impostors(arma::Mat& outputMatrix, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms) +template +void Constraints::Impostors( + UMatType& outputMatrix, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms) { // Perform pre-calculation. If neccesary. Precalculate(labels); @@ -173,8 +172,8 @@ void Constraints::Impostors(arma::Mat& outputMatrix, // KNN instance. KNN knn; - arma::Mat neighbors; - arma::mat distances; + UMatType neighbors; + MatType distances; for (size_t i = 0; i < uniqueLabels.n_cols; ++i) { @@ -198,12 +197,13 @@ void Constraints::Impostors(arma::Mat& outputMatrix, // Calculates k differently labeled nearest neighbors. The function // writes back calculated neighbors & distances to passed matrices. -template -void Constraints::Impostors(arma::Mat& outputNeighbors, - arma::mat& outputDistance, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms) +template +void Constraints::Impostors( + UMatType& outputNeighbors, + MatType& outputDistance, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms) { // Perform pre-calculation. If neccesary. Precalculate(labels); @@ -211,8 +211,8 @@ void Constraints::Impostors(arma::Mat& outputNeighbors, // KNN instance. KNN knn; - arma::Mat neighbors; - arma::mat distances; + UMatType neighbors; + MatType distances; for (size_t i = 0; i < uniqueLabels.n_cols; ++i) { @@ -237,28 +237,29 @@ void Constraints::Impostors(arma::Mat& outputNeighbors, // Calculates k differently labeled nearest neighbors on a // batch of data points. -template -void Constraints::Impostors(arma::Mat& outputMatrix, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms, - const size_t begin, - const size_t batchSize) +template +void Constraints::Impostors( + UMatType& outputMatrix, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms, + const size_t begin, + const size_t batchSize) { // Perform pre-calculation. If neccesary. Precalculate(labels); - arma::mat subDataset = dataset.cols(begin, begin + batchSize - 1); - arma::Row sublabels = labels.cols(begin, begin + batchSize - 1); + MatType subDataset = dataset.cols(begin, begin + batchSize - 1); + LabelsType sublabels = labels.cols(begin, begin + batchSize - 1); // KNN instance. KNN knn; - arma::Mat neighbors; - arma::mat distances; + UMatType neighbors; + MatType distances; // Vectors to store indices. - arma::uvec subIndexSame; + UVecType subIndexSame; for (size_t i = 0; i < uniqueLabels.n_cols; ++i) { @@ -285,29 +286,30 @@ void Constraints::Impostors(arma::Mat& outputMatrix, // Calculates k differently labeled nearest neighbors & distances on a // batch of data points. -template -void Constraints::Impostors(arma::Mat& outputNeighbors, - arma::mat& outputDistance, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms, - const size_t begin, - const size_t batchSize) +template +void Constraints::Impostors( + UMatType& outputNeighbors, + MatType& outputDistance, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms, + const size_t begin, + const size_t batchSize) { // Perform pre-calculation. If neccesary. Precalculate(labels); - arma::mat subDataset = dataset.cols(begin, begin + batchSize - 1); - arma::Row sublabels = labels.cols(begin, begin + batchSize - 1); + MatType subDataset = dataset.cols(begin, begin + batchSize - 1); + LabelsType sublabels = labels.cols(begin, begin + batchSize - 1); // KNN instance. KNN knn; - arma::Mat neighbors; - arma::mat distances; + UMatType neighbors; + MatType distances; // Vectors to store indices. - arma::uvec subIndexSame; + UVecType subIndexSame; for (size_t i = 0; i < uniqueLabels.n_cols; ++i) { @@ -335,14 +337,15 @@ void Constraints::Impostors(arma::Mat& outputNeighbors, // Calculates k differently labeled nearest neighbors & distances over some // data points. -template -void Constraints::Impostors(arma::Mat& outputNeighbors, - arma::mat& outputDistance, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms, - const arma::uvec& points, - const size_t numPoints) +template +void Constraints::Impostors( + UMatType& outputNeighbors, + MatType& outputDistance, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms, + const UVecType& points, + const size_t numPoints) { // Perform pre-calculation. If neccesary. Precalculate(labels); @@ -350,11 +353,11 @@ void Constraints::Impostors(arma::Mat& outputNeighbors, // KNN instance. KNN knn; - arma::Mat neighbors; - arma::mat distances; + UMatType neighbors; + MatType distances; // Vectors to store indices. - arma::uvec subIndexSame; + UVecType subIndexSame; for (size_t i = 0; i < uniqueLabels.n_cols; ++i) { @@ -384,31 +387,35 @@ void Constraints::Impostors(arma::Mat& outputNeighbors, // Generates {data point, target neighbors, impostors} triplets using // TargetNeighbors() and Impostors(). -template -void Constraints::Triplets(arma::Mat& outputMatrix, - const arma::mat& dataset, - const arma::Row& labels, - const arma::vec& norms) +template +void Constraints::Triplets( + UMatType& outputMatrix, + const MatType& dataset, + const LabelsType& labels, + const VecType& norms) { // Perform pre-calculation. If neccesary. Precalculate(labels); size_t N = dataset.n_cols; - arma::Mat impostors(k, dataset.n_cols); + UMatType impostors(k, dataset.n_cols); Impostors(impostors, dataset, labels, norms); - arma::Mat targetNeighbors(k, dataset.n_cols);; + UMatType targetNeighbors(k, dataset.n_cols);; TargetNeighbors(targetNeighbors, dataset, labels, norms); - outputMatrix = arma::Mat(3, k * k * N , arma::fill::zeros); + outputMatrix = UMatType(3, k * k * N , arma::fill::zeros); - for (size_t i = 0, r = 0; i < N; ++i) + #pragma omp parallel for collapse(3) + for (size_t i = 0; i < N; ++i) { for (size_t j = 0; j < k; ++j) { - for (size_t l = 0; l < k; l++, r++) + for (size_t l = 0; l < k; l++) { + const size_t r = i * (k * k) + j * k + l; + // Generate triplets. outputMatrix(0, r) = i; outputMatrix(1, r) = targetNeighbors(j, i); @@ -418,9 +425,9 @@ void Constraints::Triplets(arma::Mat& outputMatrix, } } -template -inline void Constraints::Precalculate( - const arma::Row& labels) +template +inline void Constraints::Precalculate( + const LabelsType& labels) { // Make sure the calculation is necessary. if (precalculated) @@ -431,6 +438,7 @@ inline void Constraints::Precalculate( indexSame.resize(uniqueLabels.n_elem); indexDiff.resize(uniqueLabels.n_elem); + #pragma omp parallel for for (size_t i = 0; i < uniqueLabels.n_elem; ++i) { // Store same and diff indices. diff --git a/src/mlpack/methods/lmnn/lmnn.hpp b/src/mlpack/methods/lmnn/lmnn.hpp index 92646df11a..8dadc5b31e 100644 --- a/src/mlpack/methods/lmnn/lmnn.hpp +++ b/src/mlpack/methods/lmnn/lmnn.hpp @@ -49,7 +49,7 @@ namespace mlpack { * @tparam OptimizerType Optimizer to use for developing distance. */ template + typename DeprecatedOptimizerType = ens::AMSGrad> class LMNN { public: @@ -63,11 +63,27 @@ class LMNN * @param k Number of targets to consider. * @param distance Type of distance metric used for computation. */ + [[deprecated("Will be removed in mlpack 5.0.0. Pass the dataset directly to " + "LearnDistance() instead.")]] LMNN(const arma::mat& dataset, const arma::Row& labels, const size_t k, const DistanceType distance = DistanceType()); + /** + * Construct the LMNN object, optionally with an instantiated distance metric. + * + * @param k Number of target neighbors to consider. + * @param regularization Penalty to apply to objective function. + * @param updateInterval Number of iterations between each recomputation of + * true neighbors and impostors. + * @param distance Instantiated distance metric for computation. + */ + LMNN(const size_t k, + const double regularization = 0.5, + const size_t updateInterval = 1, + DistanceType distance = DistanceType()); + /** * Perform Large Margin Nearest Neighbors metric learning. The output @@ -80,25 +96,99 @@ class LMNN * @param callbacks Callback function for ensmallen optimizer `OptimizerType`. * See https://www.ensmallen.org/docs.html#callback-documentation. */ - template + template::value>::type, + typename = typename std::enable_if< + !FirstElementIsArma::value + >::type> + [[deprecated("Will be removed in mlpack 5.0.0. Use the version that takes a " + "dataset as a parameter.")]] void LearnDistance(arma::mat& outputMatrix, CallbackTypes&&... callbacks); + /** + * Perform Large Margin Nearest Neighbors metric learning. The output + * distance matrix is written into the passed reference. If the + * LearnDistance() is called with an outputMatrix with correct dimensions, + * then that matrix will be used as the starting point for optimization. + * + * @param dataset Dataset to learn distance metric on. + * @param labels Labels for dataset. + * @param outputMatrix Covariance matrix of Mahalanobis distance. + * @param callbacks Callback function for ensmallen optimizer `OptimizerType`. + * See https://www.ensmallen.org/docs.html#callback-documentation. + */ + template::type, + LMNNFunction, + MatType + >::value>::type, + typename = typename std::enable_if::value>::type> + void LearnDistance(const MatType& dataset, + const LabelsType& labels, + MatType& outputMatrix, + CallbackTypes&&... callbacks) const; + + /** + * Perform Large Margin Nearest Neighbors metric learning. The output + * distance matrix is written into the passed reference. If the + * LearnDistance() is called with an outputMatrix with correct dimensions, + * then that matrix will be used as the starting point for optimization. + * + * @param dataset Dataset to learn distance metric on. + * @param labels Labels for dataset. + * @param optimizer Instantiated ensmallen optimizer to use for LMNN. + * @param outputMatrix Covariance matrix of Mahalanobis distance. + * @param callbacks Callback function for ensmallen optimizer `OptimizerType`. + * See https://www.ensmallen.org/docs.html#callback-documentation. + */ + template, + MatType + >::value>::type> + void LearnDistance(const MatType& dataset, + const LabelsType& labels, + MatType& outputMatrix, + OptimizerType& optimizer, + CallbackTypes&&... callbacks) const; //! Get the dataset reference. - const arma::mat& Dataset() const { return dataset; } + [[deprecated("Will be removed in mlpack 5.0.0. Use the LearnDistance() " + "version that takes the optimizer as a parameter instead.")]] + const arma::mat& Dataset() const { return *dataset; } //! Get the labels reference. - const arma::Row& Labels() const { return labels; } + [[deprecated("Will be removed in mlpack 5.0.0. Use the LearnDistance() " + "version that takes the optimizer as a parameter instead.")]] + const arma::Row& Labels() const { return *labels; } //! Access the regularization value. const double& Regularization() const { return regularization; } //! Modify the regularization value. double& Regularization() { return regularization; } - //! Access the range value. - const size_t& Range() const { return range; } - //! Modify the range value. - size_t& Range() { return range; } + //! Access the iteration update interval value. + const size_t& UpdateInterval() const { return updateInterval; } + //! Modify the iteration update interval value. + size_t& UpdateInterval() { return updateInterval; } + + [[deprecated("Will be removed in mlpack 5.0.0. Use UpdateInterval() " + "instead.")]] + const size_t& Range() const { return updateInterval; } + [[deprecated("Will be removed in mlpack 5.0.0. Use UpdateInterval() " + "instead.")]] + size_t& Range() { return updateInterval; } //! Access the value of k. const size_t& K() const { return k; } @@ -106,15 +196,23 @@ class LMNN size_t K() { return k; } //! Get the optimizer. - const OptimizerType& Optimizer() const { return optimizer; } - OptimizerType& Optimizer() { return optimizer; } + [[deprecated("Will be removed in mlpack 5.0.0. Use the LearnDistance() " + "version that takes the optimizer as a parameter instead.")]] + const DeprecatedOptimizerType& Optimizer() const { return optimizer; } + //! Modify the optimizer. + [[deprecated("Will be removed in mlpack 5.0.0. Use the LearnDistance() " + "version that takes the optimizer as a parameter instead.")]] + DeprecatedOptimizerType& Optimizer() { return optimizer; } + + // Serialize the LMNN object. + template + void serialize(Archive& ar, const unsigned int /* version */); private: - //! Dataset reference. - const arma::mat& dataset; - - //! Labels reference. - const arma::Row& labels; + //! Dataset pointer (will be removed in mlpack 5.0.0). + const arma::mat* dataset; + //! Labels pointer (will be removed in mlpack 5.0.0). + const arma::Row* labels; //! Number of target points. size_t k; @@ -122,14 +220,14 @@ class LMNN //! Regularization value. double regularization; - //! Range after which impostors need to be recalculated. - size_t range; + //! Number of iterations after which impostors need to be recalculated. + size_t updateInterval; //! Distance to be used. DistanceType distance; - //! The optimizer to use. - OptimizerType optimizer; + //! The optimizer to use (will be removed in mlpack 5.0.0). + DeprecatedOptimizerType optimizer; }; // class LMNN } // namespace mlpack diff --git a/src/mlpack/methods/lmnn/lmnn_function.hpp b/src/mlpack/methods/lmnn/lmnn_function.hpp index f35fcf8cd0..e64b8ae0d0 100644 --- a/src/mlpack/methods/lmnn/lmnn_function.hpp +++ b/src/mlpack/methods/lmnn/lmnn_function.hpp @@ -41,9 +41,22 @@ namespace mlpack { * operate on one point in the dataset. This is useful for optimizers like * stochastic gradient descent (see ens::SGD). */ -template +template, + typename DistanceType = SquaredEuclideanDistance> class LMNNFunction { + // Convenience typedef for element type of data. + typedef typename MatType::elem_type ElemType; + // Convenience typedef for column vector of data. + typedef typename GetColType::type VecType; + // Convenience typedef for cube of data. + typedef typename GetCubeType::type CubeType; + // Convenience typedef for dense matrix of indices. + typedef typename GetUDenseMatType::type UMatType; + // Convenience typedef for dense vector of indices. + typedef typename GetColType::type UVecType; + public: /** * Constructor for LMNNFunction class. @@ -52,14 +65,14 @@ class LMNNFunction * @param labels Input dataset labels. * @param k Number of target neighbors to be used. * @param regularization Regularization value. - * @param range Range after which impostors need to be recalculated. + * @param updateInterval Number of iterations before impostors are recomputed. * @param distance Type of distance metric used for computation. */ - LMNNFunction(const arma::mat& dataset, - const arma::Row& labels, + LMNNFunction(const MatType& dataset, + const LabelsType& labels, size_t k, double regularization, - size_t range, + size_t updateInterval, DistanceType distance = DistanceType()); @@ -69,13 +82,13 @@ class LMNNFunction void Shuffle(); /** - * Evaluate the LMNN function for the given transformation matrix. This is the - * non-separable implementation, where the objective function is not + * Evaluate the LMNN function for the given transformation matrix. This is + * the non-separable implementation, where the objective function is not * decomposed into the sum of several objective functions. * * @param transformation Transformation matrix of Mahalanobis distance. */ - double Evaluate(const arma::mat& transformation); + ElemType Evaluate(const MatType& transformation); /** * Evaluate the LMNN objective function for the given transformation matrix on @@ -89,9 +102,9 @@ class LMNNFunction * @param begin Index of the initial point to use for objective function. * @param batchSize Number of points to use for objective function. */ - double Evaluate(const arma::mat& transformation, - const size_t begin, - const size_t batchSize = 1); + ElemType Evaluate(const MatType& transformation, + const size_t begin, + const size_t batchSize = 1); /** * Evaluate the gradient of the LMNN function for the given transformation @@ -103,7 +116,7 @@ class LMNNFunction * @param gradient Matrix to store the calculated gradient in. */ template - void Gradient(const arma::mat& transformation, GradType& gradient); + void Gradient(const MatType& transformation, GradType& gradient); /** * Evaluate the gradient of the LMNN function for the given transformation @@ -121,7 +134,7 @@ class LMNNFunction * @param batchSize Number of points to use for objective function. */ template - void Gradient(const arma::mat& transformation, + void Gradient(const MatType& transformation, const size_t begin, GradType& gradient, const size_t batchSize = 1); @@ -137,8 +150,8 @@ class LMNNFunction * @param gradient Matrix to store the calculated gradient in. */ template - double EvaluateWithGradient(const arma::mat& transformation, - GradType& gradient); + ElemType EvaluateWithGradient(const MatType& transformation, + GradType& gradient); /** * Evaluate the LMNN objective function together with gradient for the given @@ -156,13 +169,13 @@ class LMNNFunction * @param batchSize Number of points to use for objective function. */ template - double EvaluateWithGradient(const arma::mat& transformation, - const size_t begin, - GradType& gradient, - const size_t batchSize = 1); + ElemType EvaluateWithGradient(const MatType& transformation, + const size_t begin, + GradType& gradient, + const size_t batchSize = 1); //! Return the initial point for the optimization. - const arma::mat& GetInitialPoint() const { return initialPoint; } + const MatType& GetInitialPoint() const { return initialPoint; } /** * Get the number of functions the objective function can be decomposed into. @@ -171,7 +184,7 @@ class LMNNFunction size_t NumFunctions() const { return dataset.n_cols; } //! Return the dataset passed into the constructor. - const arma::mat& Dataset() const { return dataset; } + const MatType& Dataset() const { return dataset; } //! Access the regularization value. const double& Regularization() const { return regularization; } @@ -183,26 +196,26 @@ class LMNNFunction //! Modify the value of k. size_t& K() { return k; } - //! Access the value of range. - const size_t& Range() const { return range; } - //! Modify the value of k. - size_t& Range() { return range; } + //! Access the number of iterations between impostor recomputation. + const size_t& UpdateInterval() const { return updateInterval; } + //! Modify the number of iterations between impostor recomputation.. + size_t& UpdateInterval() { return updateInterval; } private: //! data. This will be an alias until Shuffle() is called. - arma::mat dataset; + MatType dataset; //! labels. This will be an alias until Shuffle() is called. - arma::Row labels; + LabelsType labels; //! Initial parameter point. - arma::mat initialPoint; + MatType initialPoint; //! Store transformed dataset. - arma::mat transformedDataset; + MatType transformedDataset; //! Store target neighbors of data points. - arma::Mat targetNeighbors; + UMatType targetNeighbors; //! Initial impostors. - arma::Mat impostors; + UMatType impostors; //! Cache distance. Used to avoid repetive calculation. - arma::mat distanceMat; + MatType distanceMat; //! Number of target neighbors. size_t k; //! The instantiated distance metric. @@ -211,28 +224,28 @@ class LMNNFunction double regularization; //! Keep iterations count. size_t iteration; - //! Range after which impostors need to be recalculated. - size_t range; + //! Number of iterations before impostors need to be recalculated. + size_t updateInterval; //! Constraints Object. - Constraints constraint; + Constraints constraint; //! Holds pre-calculated cij. - arma::mat pCij; + MatType pCij; //! Holds the norm of each data point. - arma::vec norm; + VecType norm; //! Hold previous eval values for each datapoint. - arma::cube evalOld; + CubeType evalOld; //! Hold previous maximum norm of impostor. - arma::mat maxImpNorm; + MatType maxImpNorm; //! Holds previous transformation matrix. Used for L-BFGS like optimizer. - arma::mat transformationOld; + MatType transformationOld; //! Holds previous transformation matrices. - std::vector oldTransformationMatrices; + std::vector oldTransformationMatrices; //! Holds number of points which are using each transformation matrix. std::vector oldTransformationCounts; //! Holds points to transformation matrix mapping. - arma::vec lastTransformationIndices; + VecType lastTransformationIndices; //! Used for storing points to re-calculate impostors for. - arma::uvec points; + UVecType points; //! Flag for controlling use of bounds over impostors. bool impBounds; /** @@ -242,12 +255,12 @@ class LMNNFunction */ inline void Precalculate(); //! Update cache transformation matrices. - inline void UpdateCache(const arma::mat& transformation, + inline void UpdateCache(const MatType& transformation, const size_t begin, const size_t batchSize); //! Calculate norm of change in transformation. - inline void TransDiff(std::map& transformationDiffs, - const arma::mat& transformation, + inline void TransDiff(std::unordered_map& transDiffs, + const MatType& transformation, const size_t begin, const size_t batchSize); }; diff --git a/src/mlpack/methods/lmnn/lmnn_function_impl.hpp b/src/mlpack/methods/lmnn/lmnn_function_impl.hpp index 01aa77afea..58dc54011c 100644 --- a/src/mlpack/methods/lmnn/lmnn_function_impl.hpp +++ b/src/mlpack/methods/lmnn/lmnn_function_impl.hpp @@ -18,18 +18,19 @@ namespace mlpack { -template -LMNNFunction::LMNNFunction(const arma::mat& datasetIn, - const arma::Row& labelsIn, - size_t k, - double regularization, - size_t range, - DistanceType distance) : +template +LMNNFunction::LMNNFunction( + const MatType& datasetIn, + const LabelsType& labelsIn, + size_t k, + double regularization, + size_t updateInterval, + DistanceType distance) : k(k), distance(distance), regularization(regularization), iteration(0), - range(range), + updateInterval(updateInterval), constraint(datasetIn, labelsIn, k), points(datasetIn.n_cols), impBounds(false) @@ -60,7 +61,7 @@ LMNNFunction::LMNNFunction(const arma::mat& datasetIn, lastTransformationIndices.zeros(); // Reserve the first element of cache. - arma::mat emptyMat; + MatType emptyMat; oldTransformationMatrices.push_back(emptyMat); oldTransformationCounts.push_back(dataset.n_cols); @@ -92,18 +93,18 @@ LMNNFunction::LMNNFunction(const arma::mat& datasetIn, } //! Shuffle the dataset. -template -void LMNNFunction::Shuffle() +template +void LMNNFunction::Shuffle() { - arma::mat newDataset = dataset; - arma::Mat newLabels = labels; - arma::cube newEvalOld = evalOld; - arma::vec newlastTransformationIndices = lastTransformationIndices; - arma::mat newMaxImpNorm = maxImpNorm; - arma::vec newNorm = norm; + MatType newDataset = dataset; + LabelsType newLabels = labels; + CubeType newEvalOld = evalOld; + VecType newlastTransformationIndices = lastTransformationIndices; + MatType newMaxImpNorm = maxImpNorm; + VecType newNorm = norm; // Generate ordering. - arma::uvec ordering = arma::shuffle(arma::linspace(0, + UVecType ordering = arma::shuffle(arma::linspace(0, dataset.n_cols - 1, dataset.n_cols)); ClearAlias(dataset); @@ -126,9 +127,9 @@ void LMNNFunction::Shuffle() } // Update cache transformation matrices. -template -inline void LMNNFunction::UpdateCache( - const arma::mat& transformation, +template +inline void LMNNFunction::UpdateCache( + const MatType& transformation, const size_t begin, const size_t batchSize) { @@ -162,31 +163,13 @@ inline void LMNNFunction::UpdateCache( } oldTransformationCounts[index] += batchSize; - - #ifdef DEBUG - size_t total = 0; - for (size_t i = 1; i < oldTransformationCounts.size(); ++i) - { - std::ostringstream oss; - oss << "transformation counts for matrix " << i - << " invalid (" << oldTransformationCounts[i] << ")!"; - Log::Assert(oldTransformationCounts[i] <= dataset.n_cols, oss.str()); - total += oldTransformationCounts[i]; - } - - std::ostringstream oss; - oss << "total count for transformation matrices invalid (" << total - << ", " << "should be " << dataset.n_cols << "!"; - if (begin + batchSize == dataset.n_cols) - Log::Assert(total == dataset.n_cols, oss.str()); - #endif } // Calculate norm of change in transformation. -template -inline void LMNNFunction::TransDiff( - std::map& transformationDiffs, - const arma::mat& transformation, +template +inline void LMNNFunction::TransDiff( + std::unordered_map& transformationDiffs, + const MatType& transformation, const size_t begin, const size_t batchSize) { @@ -209,22 +192,24 @@ inline void LMNNFunction::TransDiff( } //! Evaluate cost over whole dataset. -template -double LMNNFunction::Evaluate(const arma::mat& transformation) +template +typename MatType::elem_type +LMNNFunction::Evaluate( + const MatType& transformation) { - double cost = 0; + ElemType cost = 0; // Apply distance metric over dataset. transformedDataset = transformation * dataset; - double transformationDiff = 0; + ElemType transformationDiff = 0; if (!transformationOld.is_empty()) { // Calculate norm of change in transformation. transformationDiff = arma::norm(transformation - transformationOld); } - if (!transformationOld.is_empty() && iteration++ % range == 0) + if (!transformationOld.is_empty() && iteration++ % updateInterval == 0) { if (impBounds) { @@ -251,7 +236,7 @@ double LMNNFunction::Evaluate(const arma::mat& transformation) norm); } } - else if (iteration++ % range == 0) + else if (iteration++ % updateInterval == 0) { // Re-calculate impostors on transformed dataset. constraint.Impostors(impostors, distanceMat, transformedDataset, labels, @@ -263,7 +248,7 @@ double LMNNFunction::Evaluate(const arma::mat& transformation) for (size_t j = 0; j < k ; ++j) { // Calculate cost due to distance between target neighbors & data point. - double eval = distance.Evaluate(transformedDataset.col(i), + ElemType eval = distance.Evaluate(transformedDataset.col(i), transformedDataset.col(targetNeighbors(j, i))); cost += (1 - regularization) * eval; } @@ -276,7 +261,7 @@ double LMNNFunction::Evaluate(const arma::mat& transformation) { // Calculate cost due to {data point, target neighbors, impostors} // triplets. - double eval = 0; + ElemType eval = 0; // Bounds for eval. if (!transformationOld.is_empty() && evalOld(l, j, i) < -1) @@ -292,7 +277,7 @@ double LMNNFunction::Evaluate(const arma::mat& transformation) // Calculate exact eval value. if (eval > -1) { - if (iteration - 1 % range == 0) + if (iteration - 1 % updateInterval == 0) { eval = distance.Evaluate(transformedDataset.col(i), transformedDataset.col(targetNeighbors(j, i))) - @@ -338,21 +323,23 @@ double LMNNFunction::Evaluate(const arma::mat& transformation) } //! Calculate cost over batches. -template -double LMNNFunction::Evaluate(const arma::mat& transformation, - const size_t begin, - const size_t batchSize) +template +typename MatType::elem_type +LMNNFunction::Evaluate( + const MatType& transformation, + const size_t begin, + const size_t batchSize) { - double cost = 0; + ElemType cost = 0; // Calculate norm of change in transformation. - std::map transformationDiffs; + std::unordered_map transformationDiffs; TransDiff(transformationDiffs, transformation, begin, batchSize); // Apply distance metric over dataset. transformedDataset = transformation * dataset; - if (impBounds && iteration++ % range == 0) + if (impBounds && iteration++ % updateInterval == 0) { // Track number of data points to use for impostors calculatiom. size_t numPoints = 0; @@ -378,7 +365,7 @@ double LMNNFunction::Evaluate(const arma::mat& transformation, constraint.Impostors(impostors, distanceMat, transformedDataset, labels, norm, points, numPoints); } - else if (iteration++ % range == 0) + else if (iteration++ % updateInterval == 0) { // Re-calculate impostors on transformed dataset. constraint.Impostors(impostors, distanceMat, transformedDataset, labels, @@ -390,7 +377,7 @@ double LMNNFunction::Evaluate(const arma::mat& transformation, for (size_t j = 0; j < k ; ++j) { // Calculate cost due to distance between target neighbors & data point. - double eval = distance.Evaluate(transformedDataset.col(i), + ElemType eval = distance.Evaluate(transformedDataset.col(i), transformedDataset.col(targetNeighbors(j, i))); cost += (1 - regularization) * eval; } @@ -403,7 +390,7 @@ double LMNNFunction::Evaluate(const arma::mat& transformation, { // Calculate cost due to {data point, target neighbors, impostors} // triplets. - double eval = 0; + ElemType eval = 0; // Bounds for eval. if (lastTransformationIndices(i) && evalOld(l, j, i) < -1) @@ -419,7 +406,7 @@ double LMNNFunction::Evaluate(const arma::mat& transformation, // Calculate exact eval value. if (eval > -1) { - if (iteration - 1 % range == 0) + if (iteration - 1 % updateInterval == 0) { eval = distance.Evaluate(transformedDataset.col(i), transformedDataset.col(targetNeighbors(j, i))) - @@ -467,16 +454,16 @@ double LMNNFunction::Evaluate(const arma::mat& transformation, } //! Compute gradient over whole dataset. -template +template template -void LMNNFunction::Gradient(const arma::mat& transformation, - GradType& gradient) +void LMNNFunction::Gradient( + const MatType& transformation, GradType& gradient) { // Apply distance metric over dataset. transformedDataset = transformation * dataset; - double transformationDiff = 0; - if (!transformationOld.is_empty() && iteration++ % range == 0) + ElemType transformationDiff = 0; + if (!transformationOld.is_empty() && iteration++ % updateInterval == 0) { // Calculate norm of change in transformation. transformationDiff = arma::norm(transformation - transformationOld); @@ -506,7 +493,7 @@ void LMNNFunction::Gradient(const arma::mat& transformation, norm); } } - else if (iteration++ % range == 0) + else if (iteration++ % updateInterval == 0) { // Re-calculate impostors on transformed dataset. constraint.Impostors(impostors, distanceMat, transformedDataset, labels, @@ -516,10 +503,10 @@ void LMNNFunction::Gradient(const arma::mat& transformation, gradient.zeros(transformation.n_rows, transformation.n_cols); // Calculate gradient due to target neighbors. - arma::mat cij = pCij; + MatType cij = pCij; // Calculate gradient due to impostors. - arma::mat cil = zeros(dataset.n_rows, dataset.n_rows); + MatType cil = zeros(dataset.n_rows, dataset.n_rows); for (size_t i = 0; i < dataset.n_cols; ++i) { @@ -530,7 +517,7 @@ void LMNNFunction::Gradient(const arma::mat& transformation, { // Calculate cost due to {data point, target neighbors, impostors} // triplets. - double eval = 0; + ElemType eval = 0; // Bounds for eval. if (!transformationOld.is_empty() && evalOld(l, j, i) < -1) @@ -546,7 +533,7 @@ void LMNNFunction::Gradient(const arma::mat& transformation, // Calculate exact eval value. if (eval > -1) { - if (iteration - 1 % range == 0) + if (iteration - 1 % updateInterval == 0) { eval = distance.Evaluate(transformedDataset.col(i), transformedDataset.col(targetNeighbors(j, i))) - @@ -581,7 +568,7 @@ void LMNNFunction::Gradient(const arma::mat& transformation, } // Caculate gradient due to impostors. - arma::vec diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); + VecType diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); cil += diff * trans(diff); diff = dataset.col(i) - dataset.col(impostors(l, i)); @@ -598,21 +585,22 @@ void LMNNFunction::Gradient(const arma::mat& transformation, } //! Compute gradient over a batch of data points. -template +template template -void LMNNFunction::Gradient(const arma::mat& transformation, - const size_t begin, - GradType& gradient, - const size_t batchSize) +void LMNNFunction::Gradient( + const MatType& transformation, + const size_t begin, + GradType& gradient, + const size_t batchSize) { // Apply distance metric over dataset. transformedDataset = transformation * dataset; // Calculate norm of change in transformation. - std::map transformationDiffs; + std::unordered_map transformationDiffs; TransDiff(transformationDiffs, transformation, begin, batchSize); - if (impBounds && iteration++ % range == 0) + if (impBounds && iteration++ % updateInterval == 0) { // Track number of data points to use for impostors calculatiom. size_t numPoints = 0; @@ -638,7 +626,7 @@ void LMNNFunction::Gradient(const arma::mat& transformation, constraint.Impostors(impostors, distanceMat, transformedDataset, labels, norm, points, numPoints); } - else if (iteration++ % range == 0) + else if (iteration++ % updateInterval == 0) { // Re-calculate impostors on transformed dataset. constraint.Impostors(impostors, distanceMat, transformedDataset, labels, @@ -647,15 +635,15 @@ void LMNNFunction::Gradient(const arma::mat& transformation, gradient.zeros(transformation.n_rows, transformation.n_cols); - arma::mat cij = zeros(dataset.n_rows, dataset.n_rows); - arma::mat cil = zeros(dataset.n_rows, dataset.n_rows); + MatType cij = zeros(dataset.n_rows, dataset.n_rows); + MatType cil = zeros(dataset.n_rows, dataset.n_rows); for (size_t i = begin; i < begin + batchSize; ++i) { for (size_t j = 0; j < k ; ++j) { // Calculate gradient due to target neighbors. - arma::vec diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); + VecType diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); cij += diff * trans(diff); } @@ -666,7 +654,7 @@ void LMNNFunction::Gradient(const arma::mat& transformation, { // Calculate cost due to {data point, target neighbors, impostors} // triplets. - double eval = 0; + ElemType eval = 0; // Bounds for eval. if (lastTransformationIndices(i) && evalOld(l, j, i) < -1) @@ -682,7 +670,7 @@ void LMNNFunction::Gradient(const arma::mat& transformation, // Calculate exact eval value. if (eval > -1) { - if (iteration - 1 % range == 0) + if (iteration - 1 % updateInterval == 0) { eval = distance.Evaluate(transformedDataset.col(i), transformedDataset.col(targetNeighbors(j, i))) - @@ -719,7 +707,7 @@ void LMNNFunction::Gradient(const arma::mat& transformation, } // Caculate gradient due to impostors. - arma::vec diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); + VecType diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); cil += diff * trans(diff); diff = dataset.col(i) - dataset.col(impostors(l, i)); @@ -736,25 +724,26 @@ void LMNNFunction::Gradient(const arma::mat& transformation, } //! Compute cost & gradient over whole dataset. -template +template template -double LMNNFunction::EvaluateWithGradient( - const arma::mat& transformation, +typename MatType::elem_type +LMNNFunction::EvaluateWithGradient( + const MatType& transformation, GradType& gradient) { - double cost = 0; + ElemType cost = 0; // Apply distance metric over dataset. transformedDataset = transformation * dataset; - double transformationDiff = 0; + ElemType transformationDiff = 0; if (!transformationOld.is_empty()) { // Calculate norm of change in transformation. transformationDiff = arma::norm(transformation - transformationOld); } - if (!transformationOld.is_empty() && iteration++ % range == 0) + if (!transformationOld.is_empty() && iteration++ % updateInterval == 0) { if (impBounds) { @@ -781,7 +770,7 @@ double LMNNFunction::EvaluateWithGradient( norm); } } - else if (iteration++ % range == 0) + else if (iteration++ % updateInterval == 0) { // Re-calculate impostors on transformed dataset. constraint.Impostors(impostors, distanceMat, transformedDataset, labels, @@ -791,17 +780,17 @@ double LMNNFunction::EvaluateWithGradient( gradient.zeros(transformation.n_rows, transformation.n_cols); // Calculate gradient due to target neighbors. - arma::mat cij = pCij; + MatType cij = pCij; // Calculate gradient due to impostors. - arma::mat cil = zeros(dataset.n_rows, dataset.n_rows); + MatType cil = zeros(dataset.n_rows, dataset.n_rows); for (size_t i = 0; i < dataset.n_cols; ++i) { for (size_t j = 0; j < k ; ++j) { // Calculate cost due to distance between target neighbors & data point. - double eval = distance.Evaluate(transformedDataset.col(i), + ElemType eval = distance.Evaluate(transformedDataset.col(i), transformedDataset.col(targetNeighbors(j, i))); cost += (1 - regularization) * eval; } @@ -813,7 +802,7 @@ double LMNNFunction::EvaluateWithGradient( { // Calculate cost due to {data point, target neighbors, impostors} // triplets. - double eval = 0; + ElemType eval = 0; // Bounds for eval. if (!transformationOld.is_empty() && evalOld(l, j, i) < -1) @@ -829,7 +818,7 @@ double LMNNFunction::EvaluateWithGradient( // Calculate exact eval value. if (eval > -1) { - if (iteration - 1 % range == 0) + if (iteration - 1 % updateInterval == 0) { eval = distance.Evaluate(transformedDataset.col(i), transformedDataset.col(targetNeighbors(j, i))) - @@ -858,7 +847,7 @@ double LMNNFunction::EvaluateWithGradient( cost += regularization * (1 + eval); // Caculate gradient due to impostors. - arma::vec diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); + VecType diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); cil += diff * trans(diff); diff = dataset.col(i) - dataset.col(impostors(l, i)); @@ -877,24 +866,25 @@ double LMNNFunction::EvaluateWithGradient( } //! Compute cost & gradient over a batch of data points. -template +template template -double LMNNFunction::EvaluateWithGradient( - const arma::mat& transformation, +typename MatType::elem_type +LMNNFunction::EvaluateWithGradient( + const MatType& transformation, const size_t begin, GradType& gradient, const size_t batchSize) { - double cost = 0; + ElemType cost = 0; // Calculate norm of change in transformation. - std::map transformationDiffs; + std::unordered_map transformationDiffs; TransDiff(transformationDiffs, transformation, begin, batchSize); // Apply distance metric over dataset. transformedDataset = transformation * dataset; - if (impBounds && iteration++ % range == 0) + if (impBounds && iteration++ % updateInterval == 0) { // Track number of data points to use for impostors calculatiom. size_t numPoints = 0; @@ -920,7 +910,7 @@ double LMNNFunction::EvaluateWithGradient( constraint.Impostors(impostors, distanceMat, transformedDataset, labels, norm, points, numPoints); } - else if (iteration++ % range == 0) + else if (iteration++ % updateInterval == 0) { // Re-calculate impostors on transformed dataset. constraint.Impostors(impostors, distanceMat, transformedDataset, labels, @@ -929,20 +919,20 @@ double LMNNFunction::EvaluateWithGradient( gradient.zeros(transformation.n_rows, transformation.n_cols); - arma::mat cij = zeros(dataset.n_rows, dataset.n_rows); - arma::mat cil = zeros(dataset.n_rows, dataset.n_rows); + MatType cij = zeros(dataset.n_rows, dataset.n_rows); + MatType cil = zeros(dataset.n_rows, dataset.n_rows); for (size_t i = begin; i < begin + batchSize; ++i) { for (size_t j = 0; j < k ; ++j) { // Calculate cost due to distance between target neighbors & data point. - double eval = distance.Evaluate(transformedDataset.col(i), + ElemType eval = distance.Evaluate(transformedDataset.col(i), transformedDataset.col(targetNeighbors(j, i))); cost += (1 - regularization) * eval; // Calculate gradient due to target neighbors. - arma::vec diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); + VecType diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); cij += diff * trans(diff); } @@ -953,7 +943,7 @@ double LMNNFunction::EvaluateWithGradient( { // Calculate cost due to {data point, target neighbors, impostors} // triplets. - double eval = 0; + ElemType eval = 0; // Bounds for eval. if (lastTransformationIndices(i) && evalOld(l, j, i) < -1) @@ -969,7 +959,7 @@ double LMNNFunction::EvaluateWithGradient( // Calculate exact eval value. if (eval > -1) { - if (iteration - 1 % range == 0) + if (iteration - 1 % updateInterval == 0) { eval = distance.Evaluate(transformedDataset.col(i), transformedDataset.col(targetNeighbors(j, i))) - @@ -998,7 +988,7 @@ double LMNNFunction::EvaluateWithGradient( cost += regularization * (1 + eval); // Caculate gradient due to impostors. - arma::vec diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); + VecType diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); cil += diff * trans(diff); diff = dataset.col(i) - dataset.col(impostors(l, i)); @@ -1016,8 +1006,8 @@ double LMNNFunction::EvaluateWithGradient( return cost; } -template -inline void LMNNFunction::Precalculate() +template +inline void LMNNFunction::Precalculate() { pCij.zeros(dataset.n_rows, dataset.n_rows); @@ -1026,7 +1016,7 @@ inline void LMNNFunction::Precalculate() for (size_t j = 0; j < k ; ++j) { // Calculate gradient due to target neighbors. - arma::vec diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); + VecType diff = dataset.col(i) - dataset.col(targetNeighbors(j, i)); pCij += diff * trans(diff); } } diff --git a/src/mlpack/methods/lmnn/lmnn_impl.hpp b/src/mlpack/methods/lmnn/lmnn_impl.hpp index 740a3a6c0c..5dfc87bb78 100644 --- a/src/mlpack/methods/lmnn/lmnn_impl.hpp +++ b/src/mlpack/methods/lmnn/lmnn_impl.hpp @@ -21,27 +21,83 @@ namespace mlpack { * Takes in a reference to the dataset. Copies the data, initializes * all of the member variables and constraint object and generate constraints. */ -template -LMNN::LMNN(const arma::mat& dataset, - const arma::Row& labels, - const size_t k, - const DistanceType distance) : - dataset(dataset), - labels(labels), +template +LMNN::LMNN( + const arma::mat& dataset, + const arma::Row& labels, + const size_t k, + const DistanceType distance) : + dataset(&dataset), + labels(&labels), k(k), regularization(0.5), - range(1), + updateInterval(1), distance(distance) { /* nothing to do */ } -template -template -void LMNN::LearnDistance(arma::mat& outputMatrix, +template +LMNN::LMNN( + const size_t k, + const double regularization, + const size_t updateInterval, + const DistanceType distance) : + k(k), + regularization(regularization), + updateInterval(updateInterval), + distance(distance) +{ /* nothing to do */ } + +template +template +void LMNN::LearnDistance( + arma::mat& outputMatrix, CallbackTypes&&... callbacks) +{ + if (!dataset || !labels) + { + throw std::runtime_error("LMNN::LearnDistance(): cannot call without a " + "dataset!"); + } + + LearnDistance(*dataset, *labels, outputMatrix, optimizer, + std::forward(callbacks)...); +} + +template +template +void LMNN::LearnDistance( + const MatType& dataset, + const LabelsType& labels, + MatType& outputMatrix, + CallbackTypes&&... callbacks) const +{ + // This should be replaced with ens::StandardSGD when the deprecated members + // are removed for mlpack 5.0.0. + DeprecatedOptimizerType opt; + LearnDistance(dataset, labels, outputMatrix, opt, + std::forward(callbacks)...); +} + +template +template +void LMNN::LearnDistance( + const MatType& dataset, + const LabelsType& labels, + MatType& outputMatrix, + OptimizerType& opt, + CallbackTypes&&... callbacks) const { // LMNN objective function. - LMNNFunction objFunction(dataset, labels, k, - regularization, range); + LMNNFunction objFunction(dataset, labels, + k, regularization, updateInterval); // See if we were passed an initialized matrix. outputMatrix (L) must be // having r x d dimensionality. @@ -49,15 +105,23 @@ void LMNN::LearnDistance(arma::mat& outputMatrix, (outputMatrix.n_rows > dataset.n_rows) || !(arma::is_finite(outputMatrix))) { - Log::Info << "Initial learning point have invalid dimensionality. " - "Identity matrix will be used as initial learning point for " - "optimization." << std::endl; outputMatrix.eye(dataset.n_rows, dataset.n_rows); } - optimizer.Optimize(objFunction, outputMatrix, callbacks...); + opt.Optimize(objFunction, outputMatrix, callbacks...); } +// Serialize the LMNN object. +template +template +void LMNN::serialize( + Archive& ar, const unsigned int /* version */) +{ + ar(CEREAL_NVP(k)); + ar(CEREAL_NVP(regularization)); + ar(CEREAL_NVP(updateInterval)); + ar(CEREAL_NVP(distance)); +} } // namespace mlpack diff --git a/src/mlpack/methods/lmnn/lmnn_main.cpp b/src/mlpack/methods/lmnn/lmnn_main.cpp index 821c4bd62d..0c270624e6 100644 --- a/src/mlpack/methods/lmnn/lmnn_main.cpp +++ b/src/mlpack/methods/lmnn/lmnn_main.cpp @@ -57,7 +57,7 @@ BINDING_LONG_DESC( PRINT_PARAM_STRING("regularization") + "), In addition, this " "implementation of LMNN includes a parameter to decide the interval " "after which impostors must be re-calculated (specified with " + - PRINT_PARAM_STRING("range") + ")." + PRINT_PARAM_STRING("update_interval") + ")." "\n\n" "Output can either be the learned distance matrix (specified with " + PRINT_PARAM_STRING("output") +"), or the transformed dataset " @@ -124,11 +124,11 @@ BINDING_EXAMPLE( PRINT_CALL("lmnn", "input", "iris", "labels", "iris_labels", "k", 3, "optimizer", "bbsgd", "output", "output") + "\n\n" - "An another program call making use of range & regularization parameter " - "with dataset having labels as last column can be made as: " + "Another program call making use of update interval & regularization " + "parameter with dataset having labels as last column can be made as: " "\n\n" + PRINT_CALL("lmnn", "input", "letter_recognition", "k", 5, - "range", 10, "regularization", 0.4, "output", "output")); + "update_interval", 10, "regularization", 0.4, "output", "output")); // See also... BINDING_SEE_ALSO("@nca", "#nca"); @@ -174,8 +174,8 @@ PARAM_DOUBLE_IN("step_size", "Step size for AMSGrad, BB_SGD and SGD (alpha).", PARAM_FLAG("linear_scan", "Don't shuffle the order in which data points are " "visited for SGD or mini-batch SGD.", "L"); PARAM_INT_IN("batch_size", "Batch size for mini-batch SGD.", "b", 50); -PARAM_INT_IN("range", "Number of iterations after which impostors needs to be " - "recalculated", "R", 1); +PARAM_INT_IN("update_interval", "Number of iterations after which impostors " + "need to be recalculated.", "R", 1); PARAM_INT_IN("seed", "Random seed. If 0, 'std::time(NULL)' is used.", "s", 0); using namespace mlpack; @@ -264,8 +264,8 @@ void BINDING_FUNCTION(util::Params& params, util::Timers& timers) RequireParamValue(params, "k", [](int x) { return x > 0; }, true, "number of targets must be positive"); - RequireParamValue(params, "range", [](int x) { return x > 0; }, true, - "range must be positive"); + RequireParamValue(params, "update_interval", [](int x) { return x > 0; }, + true, "update interval must be positive"); RequireParamValue(params, "batch_size", [](int x) { return x > 0; }, true, "batch size must be positive"); RequireParamValue(params, "regularization", [](double x) @@ -294,7 +294,7 @@ void BINDING_FUNCTION(util::Params& params, util::Timers& timers) const bool printAccuracy = params.Has("print_accuracy"); const bool shuffle = !params.Has("linear_scan"); const size_t batchSize = (size_t) params.Get("batch_size"); - const size_t range = (size_t) params.Get("range"); + const size_t updateInterval = (size_t) params.Get("update_interval"); const size_t rank = (size_t) params.Get("rank"); // Load data. @@ -359,56 +359,49 @@ void BINDING_FUNCTION(util::Params& params, util::Timers& timers) // Now create the LMNN object and run the optimization. timers.Start("lmnn_optimization"); + LMNN lmnn(k, regularization, updateInterval); if (optimizerType == "amsgrad") { - LMNN> lmnn(data, labels, k); - lmnn.Regularization() = regularization; - lmnn.Range() = range; - lmnn.Optimizer().StepSize() = stepSize; - lmnn.Optimizer().MaxIterations() = passes * data.n_cols; - lmnn.Optimizer().Tolerance() = tolerance; - lmnn.Optimizer().Shuffle() = shuffle; - lmnn.Optimizer().BatchSize() = batchSize; + ens::AMSGrad opt; + opt.StepSize() = stepSize; + opt.MaxIterations() = passes * data.n_cols; + opt.Tolerance() = tolerance; + opt.Shuffle() = shuffle; + opt.BatchSize() = batchSize; - lmnn.LearnDistance(distance); + lmnn.LearnDistance(data, labels, distance, opt); } else if (optimizerType == "bbsgd") { - LMNN, ens::BBS_BB> lmnn(data, labels, k); - lmnn.Regularization() = regularization; - lmnn.Range() = range; - lmnn.Optimizer().StepSize() = stepSize; - lmnn.Optimizer().MaxIterations() = passes * data.n_cols; - lmnn.Optimizer().Tolerance() = tolerance; - lmnn.Optimizer().Shuffle() = shuffle; - lmnn.Optimizer().BatchSize() = batchSize; + ens::BBS_BB opt; + opt.StepSize() = stepSize; + opt.MaxIterations() = passes * data.n_cols; + opt.Tolerance() = tolerance; + opt.Shuffle() = shuffle; + opt.BatchSize() = batchSize; - lmnn.LearnDistance(distance); + lmnn.LearnDistance(data, labels, distance, opt); } else if (optimizerType == "sgd") { // Using SGD is not recommended as the learning matrix can // diverge to inf causing serious memory problems. - LMNN, ens::StandardSGD> lmnn(data, labels, k); - lmnn.Regularization() = regularization; - lmnn.Range() = range; - lmnn.Optimizer().StepSize() = stepSize; - lmnn.Optimizer().MaxIterations() = passes * data.n_cols; - lmnn.Optimizer().Tolerance() = tolerance; - lmnn.Optimizer().Shuffle() = shuffle; - lmnn.Optimizer().BatchSize() = batchSize; + ens::StandardSGD opt; + opt.StepSize() = stepSize; + opt.MaxIterations() = passes * data.n_cols; + opt.Tolerance() = tolerance; + opt.Shuffle() = shuffle; + opt.BatchSize() = batchSize; - lmnn.LearnDistance(distance); + lmnn.LearnDistance(data, labels, distance, opt); } else if (optimizerType == "lbfgs") { - LMNN, ens::L_BFGS> lmnn(data, labels, k); - lmnn.Regularization() = regularization; - lmnn.Range() = range; - lmnn.Optimizer().MaxIterations() = maxIterations; - lmnn.Optimizer().MinGradientNorm() = tolerance; + ens::L_BFGS opt; + opt.MaxIterations() = maxIterations; + opt.MinGradientNorm() = tolerance; - lmnn.LearnDistance(distance); + lmnn.LearnDistance(data, labels, distance, opt); } timers.Stop("lmnn_optimization"); diff --git a/src/mlpack/methods/nca/nca.hpp b/src/mlpack/methods/nca/nca.hpp index 4bf817124c..fd66154b28 100644 --- a/src/mlpack/methods/nca/nca.hpp +++ b/src/mlpack/methods/nca/nca.hpp @@ -42,7 +42,7 @@ namespace mlpack { * @endcode */ template + typename DeprecatedOptimizerType = ens::StandardSGD> class NCA { public: @@ -55,10 +55,18 @@ class NCA * @param labels Input dataset labels. * @param distance Instantiated distance metric to use. */ + [[deprecated("Will be removed in mlpack 5.0.0. Pass the dataset directly to " + "LearnDistance() instead.")]] NCA(const arma::mat& dataset, const arma::Row& labels, DistanceType distance = DistanceType()); + /** + * Construct the Neighborhood Components Analysis object, optionally with an + * instantiated distance metric. + */ + NCA(DistanceType distance = DistanceType()); + /** * Perform Neighborhood Components Analysis. The output distance learning * matrix is written into the passed reference. If LearnDistance() is called @@ -71,32 +79,109 @@ class NCA * @param callbacks Callback function for ensmallen optimizer `OptimizerType`. * See https://www.ensmallen.org/docs.html#callback-documentation. */ - template + template::value>::type, + typename = typename std::enable_if< + !FirstElementIsArma::value + >::type> + [[deprecated("Will be removed in mlpack 5.0.0. Use the version that takes a " + "dataset as a parameter.")]] void LearnDistance(arma::mat& outputMatrix, CallbackTypes&&... callbacks); + /** + * Perform Neighborhood Components Analysis. The output distance learning + * matrix is written into the passed reference. If LearnDistance() is called + * with an outputMatrix which has the correct size (dataset.n_rows x + * dataset.n_rows), that matrix will be used as the starting point for + * optimization. + * + * @param dataset Dataset to learn distance metric on. + * @param labels Labels for dataset. + * @param outputMatrix Covariance matrix of Mahalanobis distance. + * @param callbacks Callback function for ensmallen optimizer `OptimizerType`. + * See https://www.ensmallen.org/docs.html#callback-documentation. + */ + template::type, + SoftmaxErrorFunction, + MatType + >::value>::type, + typename = typename std::enable_if::value>::type> + void LearnDistance(const MatType& dataset, + const LabelsType& labels, + MatType& outputMatrix, + CallbackTypes&&... callbacks) const; + + /** + * Perform Neighborhood Components Analysis. The output distance learning + * matrix is written into the passed reference. If LearnDistance() is called + * with an outputMatrix which has the correct size (dataset.n_rows x + * dataset.n_rows), that matrix will be used as the starting point for + * optimization. + * + * @param dataset Dataset to learn distance metric on. + * @param labels Labels for dataset. + * @param optimizer Instantiated ensmallen optimizer to use for NCA. + * @param outputMatrix Covariance matrix of Mahalanobis distance. + * @param callbacks Callback function for ensmallen optimizer `OptimizerType`. + * See https://www.ensmallen.org/docs.html#callback-documentation. + */ + template, + MatType + >::value>::type> + void LearnDistance(const MatType& dataset, + const LabelsType& labels, + MatType& outputMatrix, + OptimizerType& optimizer, + CallbackTypes&&... callbacks) const; + //! Get the dataset reference. - const arma::mat& Dataset() const { return dataset; } + [[deprecated("Will be removed in mlpack 5.0.0.")]] + const arma::mat& Dataset() const { return *dataset; } //! Get the labels reference. - const arma::Row& Labels() const { return labels; } + [[deprecated("Will be removed in mlpack 5.0.0.")]] + const arma::Row& Labels() const { return *labels; } //! Get the optimizer. - const OptimizerType& Optimizer() const { return optimizer; } - OptimizerType& Optimizer() { return optimizer; } + [[deprecated("Will be removed in mlpack 5.0.0. Use the LearnDistance() " + "version that takes the optimizer as a parameter instead.")]] + const DeprecatedOptimizerType& Optimizer() const { return optimizer; } + //! Modify the optimizer. + [[deprecated("Will be removed in mlpack 5.0.0. Use the LearnDistance() " + "version that takes the optimizer as a parameter instead.")]] + DeprecatedOptimizerType& Optimizer() { return optimizer; } + + //! Get the distance. + const DistanceType Distance() const { return distance; } + //! Modify the distance. + DistanceType& Distance() { return distance; } + + template + void serialize(Archive& ar, const unsigned int /* version */); private: - //! Dataset reference. - const arma::mat& dataset; - //! Labels reference. - const arma::Row& labels; + //! Dataset pointer (will be removed in mlpack 5.0.0). + const arma::mat* dataset; + //! Labels reference (will be removed in mlpack 5.0.0). + const arma::Row* labels; + //! The optimizer to use (will be removed in mlpack 5.0.0). + DeprecatedOptimizerType optimizer; //! Distance to be used. DistanceType distance; - - //! The function to optimize. - SoftmaxErrorFunction errorFunction; - - //! The optimizer to use. - OptimizerType optimizer; }; } // namespace mlpack diff --git a/src/mlpack/methods/nca/nca_impl.hpp b/src/mlpack/methods/nca/nca_impl.hpp index c0b9dd7098..6b3f9f9103 100644 --- a/src/mlpack/methods/nca/nca_impl.hpp +++ b/src/mlpack/methods/nca/nca_impl.hpp @@ -18,27 +18,89 @@ namespace mlpack { // Just set the internal matrix reference. -template -NCA::NCA(const arma::mat& dataset, - const arma::Row& labels, - DistanceType distance) : - dataset(dataset), - labels(labels), - distance(distance), - errorFunction(dataset, labels, distance) +template +NCA::NCA( + const arma::mat& dataset, + const arma::Row& labels, + DistanceType distance) : + dataset(&dataset), + labels(&labels), + distance(std::move(distance)) { /* Nothing to do. */ } -template -template -void NCA::LearnDistance(arma::mat& outputMatrix, +template +NCA::NCA(DistanceType distance) : + distance(std::move(distance)) +{ /* Nothing to do. */ } + +template +template +void NCA::LearnDistance( + arma::mat& outputMatrix, CallbackTypes&&... callbacks) { + if (!dataset || !labels) + { + throw std::runtime_error("NCA::LearnDistance(): cannot call without a " + "dataset!"); + } + + LearnDistance(*dataset, *labels, outputMatrix, optimizer, + std::forward(callbacks)...); +} + +template +template +void NCA::LearnDistance( + const MatType& dataset, + const LabelsType& labels, + MatType& outputMatrix, + CallbackTypes&&... callbacks) const +{ + // This should be replaced with ens::StandardSGD when the deprecated members + // are removed for mlpack 5.0.0. + DeprecatedOptimizerType opt; + LearnDistance(dataset, labels, outputMatrix, opt, + std::forward(callbacks)...); +} + +template +template +void NCA::LearnDistance( + const MatType& dataset, + const LabelsType& labels, + MatType& outputMatrix, + OptimizerType& opt, + CallbackTypes&&... callbacks) const +{ + SoftmaxErrorFunction errorFunction( + dataset, labels, distance); + // See if we were passed an initialized matrix. if ((outputMatrix.n_rows != dataset.n_rows) || (outputMatrix.n_cols != dataset.n_rows)) outputMatrix.eye(dataset.n_rows, dataset.n_rows); - optimizer.Optimize(errorFunction, outputMatrix, callbacks...); + opt.Optimize(errorFunction, outputMatrix, + std::forward(callbacks)...); +} + +template +template +void NCA::serialize( + Archive& ar, const unsigned int /* version */) +{ + ar(CEREAL_NVP(distance)); } } // namespace mlpack diff --git a/src/mlpack/methods/nca/nca_main.cpp b/src/mlpack/methods/nca/nca_main.cpp index cae98ce87c..598efe0e12 100644 --- a/src/mlpack/methods/nca/nca_main.cpp +++ b/src/mlpack/methods/nca/nca_main.cpp @@ -240,30 +240,31 @@ void BINDING_FUNCTION(util::Params& params, util::Timers& timers) // Now create the NCA object and run the optimization. timers.Start("nca_optimization"); + NCA nca; if (optimizerType == "sgd") { - NCA > nca(data, labels); - nca.Optimizer().StepSize() = stepSize; - nca.Optimizer().MaxIterations() = maxIterations; - nca.Optimizer().Tolerance() = tolerance; - nca.Optimizer().Shuffle() = shuffle; - nca.Optimizer().BatchSize() = batchSize; + ens::StandardSGD opt; + opt.StepSize() = stepSize; + opt.MaxIterations() = maxIterations; + opt.Tolerance() = tolerance; + opt.Shuffle() = shuffle; + opt.BatchSize() = batchSize; - nca.LearnDistance(distance); + nca.LearnDistance(data, labels, distance, opt); } else if (optimizerType == "lbfgs") { - NCA, ens::L_BFGS> nca(data, labels); - nca.Optimizer().NumBasis() = numBasis; - nca.Optimizer().MaxIterations() = maxIterations; - nca.Optimizer().ArmijoConstant() = armijoConstant; - nca.Optimizer().Wolfe() = wolfe; - nca.Optimizer().MinGradientNorm() = tolerance; - nca.Optimizer().MaxLineSearchTrials() = maxLineSearchTrials; - nca.Optimizer().MinStep() = minStep; - nca.Optimizer().MaxStep() = maxStep; + ens::L_BFGS opt; + opt.NumBasis() = numBasis; + opt.MaxIterations() = maxIterations; + opt.ArmijoConstant() = armijoConstant; + opt.Wolfe() = wolfe; + opt.MinGradientNorm() = tolerance; + opt.MaxLineSearchTrials() = maxLineSearchTrials; + opt.MinStep() = minStep; + opt.MaxStep() = maxStep; - nca.LearnDistance(distance); + nca.LearnDistance(data, labels, distance, opt); } timers.Stop("nca_optimization"); diff --git a/src/mlpack/methods/nca/nca_softmax_error_function.hpp b/src/mlpack/methods/nca/nca_softmax_error_function.hpp index e4165c9c12..88237d88b3 100644 --- a/src/mlpack/methods/nca/nca_softmax_error_function.hpp +++ b/src/mlpack/methods/nca/nca_softmax_error_function.hpp @@ -40,10 +40,17 @@ namespace mlpack { * operate on one point in the dataset. This is useful for optimizers like * stochastic gradient descent (see mlpack::optimization::SGD). */ -template +template, + typename DistanceType = SquaredEuclideanDistance> class SoftmaxErrorFunction { public: + // Convenience typedef for element type of data. + typedef typename MatType::elem_type ElemType; + // Convenience typedef for column vector of data. + typedef typename GetColType::type VecType; + /** * Initialize with the given kernel; useful when the kernel has some state to * store, which is set elsewhere. If no kernel is given, an empty kernel is @@ -54,8 +61,8 @@ class SoftmaxErrorFunction * @param labels Vector of class labels for each point in the dataset. * @param metric Instantiated metric (optional). */ - SoftmaxErrorFunction(const arma::mat& dataset, - const arma::Row& labels, + SoftmaxErrorFunction(const MatType& dataset, + const LabelsType& labels, DistanceType metric = DistanceType()); /** @@ -70,7 +77,7 @@ class SoftmaxErrorFunction * * @param covariance Covariance matrix of Mahalanobis distance. */ - double Evaluate(const arma::mat& covariance); + ElemType Evaluate(const MatType& covariance); /** * Evaluate the softmax objective function for the given covariance matrix on @@ -84,9 +91,9 @@ class SoftmaxErrorFunction * @param begin Index of the initial point to use for objective function. * @param batchSize Number of points to use for objective function. */ - double Evaluate(const arma::mat& covariance, - const size_t begin, - const size_t batchSize = 1); + ElemType Evaluate(const MatType& covariance, + const size_t begin, + const size_t batchSize = 1); /** * Evaluate the gradient of the softmax function for the given covariance @@ -96,7 +103,7 @@ class SoftmaxErrorFunction * @param covariance Covariance matrix of Mahalanobis distance. * @param gradient Matrix to store the calculated gradient in. */ - void Gradient(const arma::mat& covariance, arma::mat& gradient); + void Gradient(const MatType& covariance, MatType& gradient); /** * Evaluate the gradient of the softmax function for the given covariance @@ -114,7 +121,7 @@ class SoftmaxErrorFunction * @param gradient Matrix to store the calculated gradient in. */ template - void Gradient(const arma::mat& covariance, + void Gradient(const MatType& covariance, const size_t begin, GradType& gradient, const size_t batchSize = 1); @@ -122,7 +129,7 @@ class SoftmaxErrorFunction /** * Get the initial point. */ - const arma::mat GetInitialPoint() const; + const MatType GetInitialPoint() const; /** * Get the number of functions the objective function can be decomposed into. @@ -132,23 +139,23 @@ class SoftmaxErrorFunction private: //! The dataset. This is an alias until Shuffle() is called. - arma::mat dataset; + MatType dataset; //! Labels for each point in the dataset. This is an alias until Shuffle() is //! called. - arma::Row labels; + LabelsType labels; //! The instantiated metric. DistanceType distance; //! Last coordinates. Used for the non-separable Evaluate() and Gradient(). - arma::mat lastCoordinates; + MatType lastCoordinates; //! Stretched dataset. Kept internal to avoid memory reallocations. - arma::mat stretchedDataset; + MatType stretchedDataset; //! Holds calculated p_i, for the non-separable Evaluate() and Gradient(). - arma::vec p; + VecType p; //! Holds denominators for calculation of p_ij, for the non-separable //! Evaluate() and Gradient(). - arma::vec denominators; + VecType denominators; //! False if nothing has ever been precalculated (only at construction time). bool precalculated; @@ -166,7 +173,7 @@ class SoftmaxErrorFunction * * @param coordinates Coordinates matrix to use for precalculation. */ - void Precalculate(const arma::mat& coordinates); + void Precalculate(const MatType& coordinates); }; } // namespace mlpack diff --git a/src/mlpack/methods/nca/nca_softmax_error_function_impl.hpp b/src/mlpack/methods/nca/nca_softmax_error_function_impl.hpp index 2708e6e247..01e133f338 100644 --- a/src/mlpack/methods/nca/nca_softmax_error_function_impl.hpp +++ b/src/mlpack/methods/nca/nca_softmax_error_function_impl.hpp @@ -20,10 +20,10 @@ namespace mlpack { // Initialize with the given kernel. -template -SoftmaxErrorFunction::SoftmaxErrorFunction( - const arma::mat& datasetIn, - const arma::Row& labelsIn, +template +SoftmaxErrorFunction::SoftmaxErrorFunction( + const MatType& datasetIn, + const LabelsType& labelsIn, DistanceType distance) : distance(distance), precalculated(false) @@ -33,11 +33,11 @@ SoftmaxErrorFunction::SoftmaxErrorFunction( } //! Shuffle the dataset. -template -void SoftmaxErrorFunction::Shuffle() +template +void SoftmaxErrorFunction::Shuffle() { - arma::mat newDataset; - arma::Row newLabels; + MatType newDataset; + LabelsType newLabels; ShuffleData(dataset, labels, newDataset, newLabels); @@ -49,8 +49,10 @@ void SoftmaxErrorFunction::Shuffle() } //! The non-separable implementation, which uses Precalculate() to save time. -template -double SoftmaxErrorFunction::Evaluate(const arma::mat& coordinates) +template +typename MatType::elem_type +SoftmaxErrorFunction::Evaluate( + const MatType& coordinates) { // Calculate the denominators and numerators, if necessary. Precalculate(coordinates); @@ -61,19 +63,23 @@ double SoftmaxErrorFunction::Evaluate(const arma::mat& coordinates //! The separated objective function, which does not use Precalculate(), //! for a given batch size and from an initial index. -template -double SoftmaxErrorFunction::Evaluate(const arma::mat& coordinates, - const size_t begin, - const size_t batchSize) +template +typename MatType::elem_type +SoftmaxErrorFunction::Evaluate( + const MatType& coordinates, + const size_t begin, + const size_t batchSize) { // Unfortunately each evaluation will take O(N) time because it requires a // scan over all points in the dataset. Our objective is to compute p_i. - double denominator = 0; - double numerator = 0; - double result = 0; + ElemType denominator = 0; + ElemType numerator = 0; + ElemType result = 0; // It's quicker to do this now than one point at a time later. stretchedDataset = coordinates * dataset; + + #pragma omp parallel for reduction(+:result) for (size_t i = begin; i < begin + batchSize; ++i) { for (size_t k = 0; k < dataset.n_cols; ++k) @@ -83,7 +89,7 @@ double SoftmaxErrorFunction::Evaluate(const arma::mat& coordinates continue; // We want to evaluate exp(-D(A x_i, A x_k)). - double eval = std::exp(-distance.Evaluate( + ElemType eval = std::exp(-distance.Evaluate( stretchedDataset.unsafe_col(i), stretchedDataset.unsafe_col(k))); // If they are in the same class, update the numerator. @@ -108,9 +114,9 @@ double SoftmaxErrorFunction::Evaluate(const arma::mat& coordinates } //! The non-separable implementation, where Precalculate() is used. -template -void SoftmaxErrorFunction::Gradient(const arma::mat& coordinates, - arma::mat& gradient) +template +void SoftmaxErrorFunction::Gradient( + const MatType& coordinates, MatType& gradient) { // Calculate the denominators and numerators, if necessary. Precalculate(coordinates); @@ -127,22 +133,22 @@ void SoftmaxErrorFunction::Gradient(const arma::mat& coordinates, // (((p_i - (1 / p_i)) p_ik) + ((p_k - (1 / p_k)) p_ki)) x_ik x_ik^T // otherwise, add // (p_i p_ik + p_k p_ki) x_ik x_ik^T - arma::mat sum; + MatType sum; sum.zeros(stretchedDataset.n_rows, stretchedDataset.n_rows); for (size_t i = 0; i < stretchedDataset.n_cols; ++i) { for (size_t k = (i + 1); k < stretchedDataset.n_cols; ++k) { // Calculate p_ik and p_ki first. - double eval = std::exp(-distance.Evaluate( + ElemType eval = std::exp(-distance.Evaluate( stretchedDataset.unsafe_col(i), stretchedDataset.unsafe_col(k))); - double p_ik = 0, p_ki = 0; + ElemType p_ik = 0, p_ki = 0; p_ik = eval / denominators(i); p_ki = eval / denominators(k); // Subtract x_i from x_k. We are not using stretched points here. - arma::vec x_ik = dataset.col(i) - dataset.col(k); - arma::mat secondTerm = (x_ik * trans(x_ik)); + VecType x_ik = dataset.col(i) - dataset.col(k); + MatType secondTerm = (x_ik * trans(x_ik)); if (labels[i] == labels[k]) sum += ((p[i] - 1) * p_ik + (p[k] - 1) * p_ki) * secondTerm; @@ -156,19 +162,20 @@ void SoftmaxErrorFunction::Gradient(const arma::mat& coordinates, } //! The separable implementation for a given batch size and an initial index. -template +template template -void SoftmaxErrorFunction::Gradient(const arma::mat& coordinates, - const size_t begin, - GradType& gradient, - const size_t batchSize) +void SoftmaxErrorFunction::Gradient( + const MatType& coordinates, + const size_t begin, + GradType& gradient, + const size_t batchSize) { // The gradient involves two matrix terms which are eventually combined into // one. GradType firstTerm, secondTerm; // We will need to calculate p_i before this evaluation is done, so // these two variables will hold the information necessary for that. - double numerator, denominator; + ElemType numerator, denominator; gradient.zeros(coordinates.n_rows, coordinates.n_rows); @@ -189,7 +196,7 @@ void SoftmaxErrorFunction::Gradient(const arma::mat& coordinates, continue; // Calculate the numerator of p_ik. - double eval = std::exp(-distance.Evaluate( + ElemType eval = std::exp(-distance.Evaluate( stretchedDataset.unsafe_col(i), stretchedDataset.unsafe_col(k))); // If the points are in the same class, we must add to the second term of @@ -210,7 +217,7 @@ void SoftmaxErrorFunction::Gradient(const arma::mat& coordinates, } // Calculate p_i. - double p = 0; + ElemType p = 0; if (denominator == 0) { Log::Warn << "Denominator of p_" << i << " is 0!" << std::endl; @@ -231,15 +238,16 @@ void SoftmaxErrorFunction::Gradient(const arma::mat& coordinates, } } -template -const arma::mat SoftmaxErrorFunction::GetInitialPoint() const +template +const MatType +SoftmaxErrorFunction::GetInitialPoint() const { - return arma::eye(dataset.n_rows, dataset.n_rows); + return arma::eye(dataset.n_rows, dataset.n_rows); } -template -void SoftmaxErrorFunction::Precalculate( - const arma::mat& coordinates) +template +void SoftmaxErrorFunction::Precalculate( + const MatType& coordinates) { // Ensure it is the right size. if (lastCoordinates.n_rows != coordinates.n_rows || @@ -265,22 +273,30 @@ void SoftmaxErrorFunction::Precalculate( // order of O((n * (n + 1)) / 2), which really isn't all that great. p.zeros(stretchedDataset.n_cols); denominators.zeros(stretchedDataset.n_cols); + + // A collapse(2) would be helpful here, but appears to not be supported fully + // until OpenMP 5.0. + #pragma omp parallel for for (size_t i = 0; i < stretchedDataset.n_cols; ++i) { for (size_t j = (i + 1); j < stretchedDataset.n_cols; ++j) { // Evaluate exp(-d(x_i, x_j)). - double eval = std::exp(-distance.Evaluate( + ElemType eval = std::exp(-distance.Evaluate( stretchedDataset.unsafe_col(i), stretchedDataset.unsafe_col(j))); // Add this to the denominators of both p_i and p_j: K(i, j) = K(j, i). + #pragma omp atomic denominators[i] += eval; + #pragma omp atomic denominators[j] += eval; // If i and j are the same class, add to numerator of both. if (labels[i] == labels[j]) { + #pragma omp atomic p[i] += eval; + #pragma omp atomic p[j] += eval; } } @@ -290,6 +306,7 @@ void SoftmaxErrorFunction::Precalculate( p /= denominators; // Clean up any bad values. + #pragma omp parallel for for (size_t i = 0; i < stretchedDataset.n_cols; ++i) { if (denominators[i] == 0.0) @@ -297,7 +314,7 @@ void SoftmaxErrorFunction::Precalculate( Log::Debug << "Denominator of p_{" << i << ", j} is 0." << std::endl; // Set to usable values. - denominators[i] = std::numeric_limits::infinity(); + denominators[i] = std::numeric_limits::infinity(); p[i] = 0; } } diff --git a/src/mlpack/methods/neighbor_search/neighbor_search.hpp b/src/mlpack/methods/neighbor_search/neighbor_search.hpp index b8f61c0155..d2470b1b19 100644 --- a/src/mlpack/methods/neighbor_search/neighbor_search.hpp +++ b/src/mlpack/methods/neighbor_search/neighbor_search.hpp @@ -222,9 +222,11 @@ class NeighborSearch * @param distances Matrix storing distances of neighbors for each query * point. */ + // TODO: templatize further to remove Armadillo type requirement + template void Search(const MatType& querySet, const size_t k, - arma::Mat& neighbors, + arma::Mat& neighbors, arma::Mat& distances); /** @@ -247,9 +249,11 @@ class NeighborSearch * @param sameSet Denotes whether or not the reference and query sets are the * same. */ + // TODO: templatize further to remove Armadillo type requirement + template void Search(Tree& queryTree, const size_t k, - arma::Mat& neighbors, + arma::Mat& neighbors, arma::Mat& distances, bool sameSet = false); @@ -267,8 +271,10 @@ class NeighborSearch * @param distances Matrix storing distances of neighbors for each query * point. */ + // TODO: templatize further to remove Armadillo type requirement + template void Search(const size_t k, - arma::Mat& neighbors, + arma::Mat& neighbors, arma::Mat& distances); /** @@ -300,8 +306,10 @@ class NeighborSearch * query point. * @return Recall. */ - static double Recall(arma::Mat& foundNeighbors, - arma::Mat& realNeighbors); + // TODO: templatize further to remove Armadillo type requirement + template + static double Recall(arma::Mat& foundNeighbors, + arma::Mat& realNeighbors); //! Return the total number of base case evaluations performed during the last //! search. diff --git a/src/mlpack/methods/neighbor_search/neighbor_search_impl.hpp b/src/mlpack/methods/neighbor_search/neighbor_search_impl.hpp index c73b7855f0..0aa0a7de0d 100644 --- a/src/mlpack/methods/neighbor_search/neighbor_search_impl.hpp +++ b/src/mlpack/methods/neighbor_search/neighbor_search_impl.hpp @@ -360,11 +360,12 @@ template class TreeType, template class DualTreeTraversalType, template class SingleTreeTraversalType> +template void NeighborSearch::Search( const MatType& querySet, const size_t k, - arma::Mat& neighbors, + arma::Mat& neighbors, arma::Mat& distances) { if (k > referenceSet->n_cols) @@ -385,7 +386,7 @@ DualTreeTraversalType, SingleTreeTraversalType>::Search( // indices back to their original indices when this computation is finished. // To avoid an extra copy, we will store the neighbors and distances in a // separate matrix. - arma::Mat* neighborPtr = &neighbors; + arma::Mat* neighborPtr = &neighbors; arma::Mat* distancePtr = &distances; // Mapping is only necessary if the tree rearranges points. @@ -394,10 +395,10 @@ DualTreeTraversalType, SingleTreeTraversalType>::Search( if (searchMode == DUAL_TREE_MODE) { distancePtr = new arma::Mat; // Query indices need to be mapped. - neighborPtr = new arma::Mat; + neighborPtr = new arma::Mat; } else if (!oldFromNewReferences.empty()) - neighborPtr = new arma::Mat; // Reference indices need mapping. + neighborPtr = new arma::Mat; // Reference indices need mapping. } // Set the size of the neighbor and distance matrices. @@ -565,11 +566,12 @@ template class TreeType, template class DualTreeTraversalType, template class SingleTreeTraversalType> +template void NeighborSearch::Search( Tree& queryTree, const size_t k, - arma::Mat& neighbors, + arma::Mat& neighbors, arma::Mat& distances, bool sameSet) { @@ -593,10 +595,10 @@ DualTreeTraversalType, SingleTreeTraversalType>::Search( const MatType& querySet = queryTree.Dataset(); // We won't need to map query indices, but will we need to map distances? - arma::Mat* neighborPtr = &neighbors; + arma::Mat* neighborPtr = &neighbors; if (!oldFromNewReferences.empty() && TreeTraits::RearrangesDataset) - neighborPtr = new arma::Mat; + neighborPtr = new arma::Mat; neighborPtr->set_size(k, querySet.n_cols); distances.set_size(k, querySet.n_cols); @@ -644,10 +646,11 @@ template class TreeType, template class DualTreeTraversalType, template class SingleTreeTraversalType> +template void NeighborSearch::Search( const size_t k, - arma::Mat& neighbors, + arma::Mat& neighbors, arma::Mat& distances) { if (k > referenceSet->n_cols) @@ -669,14 +672,14 @@ DualTreeTraversalType, SingleTreeTraversalType>::Search( baseCases = 0; scores = 0; - arma::Mat* neighborPtr = &neighbors; + arma::Mat* neighborPtr = &neighbors; arma::Mat* distancePtr = &distances; if (!oldFromNewReferences.empty() && TreeTraits::RearrangesDataset) { // We will always need to rearrange in this case. distancePtr = new MatType; - neighborPtr = new arma::Mat; + neighborPtr = new arma::Mat; } // Initialize results. @@ -861,10 +864,11 @@ template class TreeType, template class DualTreeTraversalType, template class SingleTreeTraversalType> +template double NeighborSearch::Recall( - arma::Mat& foundNeighbors, - arma::Mat& realNeighbors) + arma::Mat& foundNeighbors, + arma::Mat& realNeighbors) { if (foundNeighbors.n_rows != realNeighbors.n_rows || foundNeighbors.n_cols != realNeighbors.n_cols) diff --git a/src/mlpack/methods/neighbor_search/neighbor_search_rules.hpp b/src/mlpack/methods/neighbor_search/neighbor_search_rules.hpp index ef886663a7..057515ca7a 100644 --- a/src/mlpack/methods/neighbor_search/neighbor_search_rules.hpp +++ b/src/mlpack/methods/neighbor_search/neighbor_search_rules.hpp @@ -63,7 +63,10 @@ class NeighborSearchRules * @param distances Matrix storing distances of neighbors for each query * point. */ - void GetResults(arma::Mat& neighbors, arma::Mat& distances); + // TODO: templatize fully to remove requirement of Armadillo matrix + template + void GetResults(arma::Mat& neighbors, + arma::Mat& distances); /** * Get the distance from the query point to the reference point. diff --git a/src/mlpack/methods/neighbor_search/neighbor_search_rules_impl.hpp b/src/mlpack/methods/neighbor_search/neighbor_search_rules_impl.hpp index 24fa48c6d5..cfbd350090 100644 --- a/src/mlpack/methods/neighbor_search/neighbor_search_rules_impl.hpp +++ b/src/mlpack/methods/neighbor_search/neighbor_search_rules_impl.hpp @@ -59,8 +59,9 @@ NeighborSearchRules::NeighborSearchRules( } template +template void NeighborSearchRules::GetResults( - arma::Mat& neighbors, + arma::Mat& neighbors, arma::Mat& distances) { neighbors.set_size(k, querySet.n_cols); @@ -71,7 +72,7 @@ void NeighborSearchRules::GetResults( CandidateList& pqueue = candidates[i]; for (size_t j = 1; j <= k; ++j) { - neighbors(k - j, i) = pqueue.top().second; + neighbors(k - j, i) = (IndexType) pqueue.top().second; distances(k - j, i) = pqueue.top().first; pqueue.pop(); } diff --git a/src/mlpack/tests/callback_test.cpp b/src/mlpack/tests/callback_test.cpp index bd98dcad85..ae9b912c9d 100644 --- a/src/mlpack/tests/callback_test.cpp +++ b/src/mlpack/tests/callback_test.cpp @@ -151,12 +151,13 @@ TEST_CASE("LMNNWithOptimizerCallback", "[CallbackTest]") " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; arma::Row labels = " 0 0 0 1 1 1 "; - LMNN<> lmnn(dataset, labels, 1); + LMNN<> lmnn(1); arma::mat outputMatrix; std::stringstream stream; - lmnn.LearnDistance(outputMatrix, ens::ProgressBar(70, stream)); + lmnn.LearnDistance(dataset, labels, outputMatrix, + ens::ProgressBar(70, stream)); REQUIRE(stream.str().length() > 0); } @@ -170,12 +171,11 @@ TEST_CASE("NCAWithOptimizerCallback", "[CallbackTest]") " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; arma::Row labels = " 0 0 0 1 1 1 "; - NCA nca(data, labels); - arma::mat outputMatrix; std::stringstream stream; - nca.LearnDistance(outputMatrix, ens::ProgressBar(70, stream)); + NCA nca; + nca.LearnDistance(data, labels, outputMatrix, ens::ProgressBar(70, stream)); REQUIRE(stream.str().length() > 0); } diff --git a/src/mlpack/tests/distribution_test.cpp b/src/mlpack/tests/distribution_test.cpp index 78dab01161..0862da6c55 100644 --- a/src/mlpack/tests/distribution_test.cpp +++ b/src/mlpack/tests/distribution_test.cpp @@ -458,23 +458,23 @@ TEST_CASE("GaussianDistributionRandomTest", "[DistributionTest]") GaussianDistribution d(mean, cov); - arma::mat obs(2, 5000); + arma::mat obs(2, 7500); - for (size_t i = 0; i < 5000; ++i) + for (size_t i = 0; i < 7500; ++i) obs.col(i) = d.Random(); // Now make sure that reflects the actual distribution. arma::vec obsMean = arma::mean(obs, 1); arma::mat obsCov = ColumnCovariance(obs); - // 10% tolerance because this can be noisy. - REQUIRE(obsMean[0] == Approx(mean[0]).epsilon(0.1)); - REQUIRE(obsMean[1] == Approx(mean[1]).epsilon(0.1)); + // 12.5% tolerance because this can be noisy. + REQUIRE(obsMean[0] == Approx(mean[0]).epsilon(0.125)); + REQUIRE(obsMean[1] == Approx(mean[1]).epsilon(0.125)); - REQUIRE(obsCov(0, 0) == Approx(cov(0, 0)).epsilon(0.1)); - REQUIRE(obsCov(0, 1) == Approx(cov(0, 1)).epsilon(0.1)); - REQUIRE(obsCov(1, 0) == Approx(cov(1, 0)).epsilon(0.1)); - REQUIRE(obsCov(1, 1) == Approx(cov(1, 1)).epsilon(0.1)); + REQUIRE(obsCov(0, 0) == Approx(cov(0, 0)).epsilon(0.125)); + REQUIRE(obsCov(0, 1) == Approx(cov(0, 1)).epsilon(0.125)); + REQUIRE(obsCov(1, 0) == Approx(cov(1, 0)).epsilon(0.125)); + REQUIRE(obsCov(1, 1) == Approx(cov(1, 1)).epsilon(0.125)); } /** @@ -527,7 +527,7 @@ TEST_CASE("GaussianDistributionTrainWithProbabilitiesTest", arma::vec cov = ("2.0"); GaussianDistribution dist(mean, cov); - size_t N = 5000; + size_t N = 15000; size_t d = 1; arma::mat rdata(d, N); @@ -548,10 +548,10 @@ TEST_CASE("GaussianDistributionTrainWithProbabilitiesTest", REQUIRE(guDist.Mean()[0] == Approx(guDist2.Mean()[0]).epsilon(0.06)); REQUIRE(guDist.Covariance()[0] == - Approx(guDist2.Covariance()[0]).epsilon(0.06)); + Approx(guDist2.Covariance()[0]).epsilon(0.08)); REQUIRE(guDist.Mean()[0] == Approx(mean[0]).epsilon(0.06)); - REQUIRE(guDist.Covariance()[0] == Approx(cov[0]).epsilon(0.06)); + REQUIRE(guDist.Covariance()[0] == Approx(cov[0]).epsilon(0.08)); } /** @@ -819,11 +819,11 @@ TEST_CASE("GammaDistributionTrainTwoDistProbabilities1Test", GammaDistribution gDist; gDist.Train(rdata, probabilities); - REQUIRE(alphaReal2 == Approx(gDist.Alpha(0)).epsilon(0.05)); - REQUIRE(betaReal2 == Approx(gDist.Beta(0)).epsilon(0.05)); + REQUIRE(alphaReal2 == Approx(gDist.Alpha(0)).epsilon(0.075)); + REQUIRE(betaReal2 == Approx(gDist.Beta(0)).epsilon(0.075)); - REQUIRE(alphaReal2 == Approx(gDist.Alpha(1)).epsilon(0.05)); - REQUIRE(betaReal2 == Approx(gDist.Beta(1)).epsilon(0.05)); + REQUIRE(alphaReal2 == Approx(gDist.Alpha(1)).epsilon(0.075)); + REQUIRE(betaReal2 == Approx(gDist.Beta(1)).epsilon(0.075)); } /** @@ -933,7 +933,7 @@ TEST_CASE("GammaDistributionTrainStatisticsTest", "[DistributionTest]") TEST_CASE("GammaDistributionRandomTest", "[DistributionTest]") { const arma::vec a("2.0 2.5 3.0"), b("0.4 0.6 1.3"); - const size_t numPoints = 2000; + const size_t numPoints = 4000; // Distribution to generate points. GammaDistribution d1(a, b); @@ -946,8 +946,8 @@ TEST_CASE("GammaDistributionRandomTest", "[DistributionTest]") GammaDistribution d2(data); for (size_t i = 0; i < 3; ++i) { - REQUIRE(d2.Alpha(i) == Approx(a(i)).epsilon(0.1)); // Within 10% - REQUIRE(d2.Beta(i) == Approx(b(i)).epsilon(0.1)); + REQUIRE(d2.Alpha(i) == Approx(a(i)).epsilon(0.15)); // Within 15% + REQUIRE(d2.Beta(i) == Approx(b(i)).epsilon(0.15)); } } diff --git a/src/mlpack/tests/lmnn_test.cpp b/src/mlpack/tests/lmnn_test.cpp index 44eb0b0d66..6b0c0d254a 100644 --- a/src/mlpack/tests/lmnn_test.cpp +++ b/src/mlpack/tests/lmnn_test.cpp @@ -30,25 +30,27 @@ using namespace ens; * The target neighbors function should be correct. * point. */ -TEST_CASE("LMNNTargetNeighborsTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNTargetNeighborsTest", "[LMNNTest]", float, double) { - // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; - arma::Row labels = " 0 0 0 1 1 1 "; + typedef TestType ElemType; - Constraints<> constraint(dataset, labels, 1); + // Useful but simple dataset with six points and two classes. + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Row labels = " 0 0 0 1 1 1 "; + + Constraints, arma::Row> constraint(dataset, + labels, 1); // Calculate norm of datapoints. - arma::vec norm(dataset.n_cols); + arma::Col norm(dataset.n_cols); for (size_t i = 0; i < dataset.n_cols; ++i) { norm(i) = arma::norm(dataset.col(i)); } //! Store target neighbors of data points. - arma::Mat targetNeighbors = - arma::Mat(1, dataset.n_cols, arma::fill::zeros); + arma::umat targetNeighbors(1, dataset.n_cols, arma::fill::zeros); constraint.TargetNeighbors(targetNeighbors, dataset, labels, norm); @@ -63,25 +65,27 @@ TEST_CASE("LMNNTargetNeighborsTest", "[LMNNTest]") /** * The impostors function should be correct. */ -TEST_CASE("LMNNImpostorsTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNImpostorsTest", "[LMNNTest]", float, double) { - // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; - arma::Row labels = " 0 0 0 1 1 1 "; + typedef TestType ElemType; - Constraints<> constraint(dataset, labels, 1); + // Useful but simple dataset with six points and two classes. + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Row labels = " 0 0 0 1 1 1 "; + + Constraints, arma::Row> constraint(dataset, + labels, 1); // Calculate norm of datapoints. - arma::vec norm(dataset.n_cols); + arma::Col norm(dataset.n_cols); for (size_t i = 0; i < dataset.n_cols; ++i) { norm(i) = arma::norm(dataset.col(i)); } //! Store impostors of data points. - arma::Mat impostors = - arma::Mat(1, dataset.n_cols, arma::fill::zeros); + arma::umat impostors(1, dataset.n_cols, arma::fill::zeros); constraint.Impostors(impostors, dataset, labels, norm); @@ -101,300 +105,339 @@ TEST_CASE("LMNNImpostorsTest", "[LMNNTest]") * The LMNN function should return the identity matrix as its initial * point. */ -TEST_CASE("LMNNInitialPointTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNInitialPointTest", "[LMNNTest]", float, double) { + typedef TestType ElemType; + // Cheap fake dataset. - arma::mat dataset = arma::randu(5, 5); + arma::Mat dataset = arma::randu>(5, 5); arma::Row labels = "0 1 1 0 0"; - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.5, 1); + LMNNFunction> lmnnfn(dataset, labels, 1, 0.5, 1); // Verify the initial point is the identity matrix. - arma::mat initialPoint = lmnnfn.GetInitialPoint(); + const double eps = std::is_same::value ? 1e-4 : 1e-7; + const double margin = std::is_same::value ? 1e-4 : 1e-5; + arma::Mat initialPoint = lmnnfn.GetInitialPoint(); for (int row = 0; row < 5; row++) { for (int col = 0; col < 5; col++) { if (row == col) - REQUIRE(initialPoint(row, col) == Approx(1.0).epsilon(1e-7)); + REQUIRE(initialPoint(row, col) == Approx(1.0).epsilon(eps)); else - REQUIRE(initialPoint(row, col) == Approx(0.0).margin(1e-5)); + REQUIRE(initialPoint(row, col) == Approx(0.0).margin(margin)); } } } /*** - * Ensure non-seprable objective function is right. + * Ensure non-separable objective function is right. */ -TEST_CASE("LMNNInitialEvaluationTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNInitialEvaluationTest", "[LMNNTest]", float, double) { + typedef TestType ElemType; + // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; - arma::Row labels = " 0 0 0 1 1 1 "; + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Row labels = " 0 0 0 1 1 1 "; - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.6, 1); + LMNNFunction> lmnnfn(dataset, labels, 1, 0.6, 1); - double objective = lmnnfn.Evaluate(arma::eye(2, 2)); + ElemType objective = lmnnfn.Evaluate(arma::eye>(2, 2)); // Result calculated by hand. - REQUIRE(objective == Approx(9.456).epsilon(1e-7)); + const double eps = std::is_same::value ? 1e-4 : 1e-7; + REQUIRE(objective == Approx(9.456).epsilon(eps)); } /** - * Ensure non-seprable gradient function is right. + * Ensure non-separable gradient function is right. */ -TEST_CASE("LMNNInitialGradientTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNInitialGradientTest", "[LMNNTest]", float, double) { + typedef TestType ElemType; + // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; - arma::Row labels = " 0 0 0 1 1 1 "; + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Row labels = " 0 0 0 1 1 1 "; - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.6, 1); + LMNNFunction> lmnnfn(dataset, labels, 1, 0.6, 1); - arma::mat gradient; - arma::mat coordinates = arma::eye(2, 2); + arma::Mat gradient; + arma::Mat coordinates = arma::eye>(2, 2); lmnnfn.Gradient(coordinates, gradient); // Result calculated by hand. - REQUIRE(gradient(0, 0) == Approx(-0.288).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).margin(1e-5)); - REQUIRE(gradient(0, 1) == Approx(0.0).margin(1e-5)); - REQUIRE(gradient(1, 1) == Approx(12.0).epsilon(1e-7)); + const double eps = std::is_same::value ? 1e-4 : 1e-7; + const double margin = std::is_same::value ? 1e-4 : 1e-5; + REQUIRE(gradient(0, 0) == Approx(-0.288).epsilon(eps)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(12.0).epsilon(eps)); } /*** - * Ensure non-seprable EvaluateWithGradient function is right. + * Ensure non-separable EvaluateWithGradient function is right. */ -TEST_CASE("LMNNInitialEvaluateWithGradientTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNInitialEvaluateWithGradientTest", "[LMNNTest]", float, + double) { + typedef TestType ElemType; + // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; - arma::Row labels = " 0 0 0 1 1 1 "; + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Row labels = " 0 0 0 1 1 1 "; - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.6, 1); + LMNNFunction> lmnnfn(dataset, labels, 1, 0.6, 1); - arma::mat gradient; - arma::mat coordinates = arma::eye(2, 2); - double objective = lmnnfn.EvaluateWithGradient(coordinates, gradient); + arma::Mat gradient; + arma::Mat coordinates = arma::eye>(2, 2); + ElemType objective = lmnnfn.EvaluateWithGradient(coordinates, gradient); + + const double eps = std::is_same::value ? 1e-4 : 1e-7; + const double margin = std::is_same::value ? 1e-4 : 1e-5; // Result calculated by hand. - REQUIRE(objective == Approx(9.456).epsilon(1e-7)); + REQUIRE(objective == Approx(9.456).epsilon(eps)); // Check Gradient - REQUIRE(gradient(0, 0) == Approx(-0.288).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).margin(1e-5)); - REQUIRE(gradient(0, 1) == Approx(0.0).margin(1e-5)); - REQUIRE(gradient(1, 1) == Approx(12.0).epsilon(1e-7)); + REQUIRE(gradient(0, 0) == Approx(-0.288).epsilon(eps)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(12.0).epsilon(eps)); } /** * Ensure the separable objective function is right. */ -TEST_CASE("LMNNSeparableObjectiveTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNSeparableObjectiveTest", "[LMNNTest]", float, double) { - // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; - arma::Row labels = " 0 0 0 1 1 1 "; + typedef TestType ElemType; - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.6, 1); + // Useful but simple dataset with six points and two classes. + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Row labels = " 0 0 0 1 1 1 "; + + LMNNFunction> lmnnfn(dataset, labels, 1, 0.6, 1); // Result calculated by hand. - arma::mat coordinates = arma::eye(2, 2); - REQUIRE(lmnnfn.Evaluate(coordinates, 0, 1) == Approx(1.576).epsilon(1e-7)); - REQUIRE(lmnnfn.Evaluate(coordinates, 1, 1) == Approx(1.576).epsilon(1e-7)); - REQUIRE(lmnnfn.Evaluate(coordinates, 2, 1) == Approx(1.576).epsilon(1e-7)); - REQUIRE(lmnnfn.Evaluate(coordinates, 3, 1) == Approx(1.576).epsilon(1e-7)); - REQUIRE(lmnnfn.Evaluate(coordinates, 4, 1) == Approx(1.576).epsilon(1e-7)); - REQUIRE(lmnnfn.Evaluate(coordinates, 5, 1) == Approx(1.576).epsilon(1e-7)); + const double eps = std::is_same::value ? 1e-4 : 1e-7; + arma::Mat coordinates = arma::eye>(2, 2); + REQUIRE(lmnnfn.Evaluate(coordinates, 0, 1) == Approx(1.576).epsilon(eps)); + REQUIRE(lmnnfn.Evaluate(coordinates, 1, 1) == Approx(1.576).epsilon(eps)); + REQUIRE(lmnnfn.Evaluate(coordinates, 2, 1) == Approx(1.576).epsilon(eps)); + REQUIRE(lmnnfn.Evaluate(coordinates, 3, 1) == Approx(1.576).epsilon(eps)); + REQUIRE(lmnnfn.Evaluate(coordinates, 4, 1) == Approx(1.576).epsilon(eps)); + REQUIRE(lmnnfn.Evaluate(coordinates, 5, 1) == Approx(1.576).epsilon(eps)); } /** * Ensure the separable gradient is right. */ -TEST_CASE("LMNNSeparableGradientTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNSeparableGradientTest", "[LMNNTest]", float, double) { + typedef TestType ElemType; + // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; - arma::Row labels = " 0 0 0 1 1 1 "; + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Row labels = " 0 0 0 1 1 1 "; - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.6, 1); + LMNNFunction> lmnnfn(dataset, labels, 1, 0.6, 1); - arma::mat coordinates = arma::eye(2, 2); - arma::mat gradient(2, 2); + arma::Mat coordinates = arma::eye>(2, 2); + arma::Mat gradient(2, 2); lmnnfn.Gradient(coordinates, 0, gradient, 1); - REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(1e-7)); - REQUIRE(gradient(0, 1) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(1e-7)); + const double eps = std::is_same::value ? 1e-4 : 1e-7; + const double margin = std::is_same::value ? 1e-4 : 1e-5; + + REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(eps)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(eps)); lmnnfn.Gradient(coordinates, 1, gradient, 1); - REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(1e-7)); - REQUIRE(gradient(0, 1) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(1e-7)); + REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(eps)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(eps)); lmnnfn.Gradient(coordinates, 2, gradient, 1); - REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(1e-7)); - REQUIRE(gradient(0, 1) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(1e-7)); + REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(eps)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(eps)); lmnnfn.Gradient(coordinates, 3, gradient, 1); - REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(1e-7)); - REQUIRE(gradient(0, 1) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(1e-7)); + REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(eps)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(eps)); lmnnfn.Gradient(coordinates, 4, gradient, 1); - REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(1e-7)); - REQUIRE(gradient(0, 1) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(1e-7)); + REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(eps)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(eps)); lmnnfn.Gradient(coordinates, 5, gradient, 1); - REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(1e-7)); - REQUIRE(gradient(0, 1) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(1e-7)); + REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(eps)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(eps)); } /** * Ensure the separable EvaluateWithGradient function is right. */ -TEST_CASE("LMNNSeparableEvaluateWithGradientTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNSeparableEvaluateWithGradientTest", "[LMNNTest]", float, + double) { + typedef TestType ElemType; + // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; - arma::Row labels = " 0 0 0 1 1 1 "; + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Row labels = " 0 0 0 1 1 1 "; - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.6, 1); + LMNNFunction> lmnnfn(dataset, labels, 1, 0.6, 1); - arma::mat coordinates = arma::eye(2, 2); - arma::mat gradient(2, 2); + arma::Mat coordinates = arma::eye>(2, 2); + arma::Mat gradient(2, 2); - double objective = lmnnfn.EvaluateWithGradient(coordinates, 0, gradient, 1); + ElemType objective = lmnnfn.EvaluateWithGradient(coordinates, 0, gradient, 1); - REQUIRE(objective == Approx(1.576).epsilon(1e-7)); + const double eps = std::is_same::value ? 1e-4 : 1e-7; + const double margin = std::is_same::value ? 1e-4 : 1e-5; - REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(1e-7)); - REQUIRE(gradient(0, 1) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(1e-7)); + REQUIRE(objective == Approx(1.576).epsilon(eps)); + + REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(eps)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(eps)); objective = lmnnfn.EvaluateWithGradient(coordinates, 1, gradient, 1); - REQUIRE(objective == Approx(1.576).epsilon(1e-7)); + REQUIRE(objective == Approx(1.576).epsilon(eps)); - REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(1e-7)); - REQUIRE(gradient(0, 1) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(1e-7)); + REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(eps)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(eps)); objective = lmnnfn.EvaluateWithGradient(coordinates, 2, gradient, 1); - REQUIRE(objective == Approx(1.576).epsilon(1e-7)); + REQUIRE(objective == Approx(1.576).epsilon(eps)); - REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(1e-7)); - REQUIRE(gradient(0, 1) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(1e-7)); + REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(eps)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(eps)); objective = lmnnfn.EvaluateWithGradient(coordinates, 3, gradient, 1); - REQUIRE(objective == Approx(1.576).epsilon(1e-7)); + REQUIRE(objective == Approx(1.576).epsilon(eps)); - REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(1e-7)); - REQUIRE(gradient(0, 1) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(1e-7)); + REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(eps)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(eps)); objective = lmnnfn.EvaluateWithGradient(coordinates, 4, gradient, 1); - REQUIRE(objective == Approx(1.576).epsilon(1e-7)); + REQUIRE(objective == Approx(1.576).epsilon(eps)); - REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(1e-7)); - REQUIRE(gradient(0, 1) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(1e-7)); + REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(eps)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(eps)); objective = lmnnfn.EvaluateWithGradient(coordinates, 5, gradient, 1); - REQUIRE(objective == Approx(1.576).epsilon(1e-7)); + REQUIRE(objective == Approx(1.576).epsilon(eps)); - REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(1e-7)); - REQUIRE(gradient(0, 1) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 0) == Approx(0.0).epsilon(1e-7)); - REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(1e-7)); + REQUIRE(gradient(0, 0) == Approx(-0.048).epsilon(eps)); + REQUIRE(gradient(0, 1) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 0) == Approx(0.0).margin(margin)); + REQUIRE(gradient(1, 1) == Approx(2.0).epsilon(eps)); } // Check that final objective value using SGD optimizer is optimal. -TEST_CASE("LMNNSGDSimpleDatasetTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNSGDSimpleDatasetTest", "[LMNNTest]", float, double) { + typedef TestType ElemType; + // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; - arma::Row labels = " 0 0 0 1 1 1 "; + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Row labels = " 0 0 0 1 1 1 "; - LMNN<> lmnn(dataset, labels, 1); + LMNN<> lmnn(1); - arma::mat outputMatrix; - lmnn.LearnDistance(outputMatrix); + arma::Mat outputMatrix; + lmnn.LearnDistance(dataset, labels, outputMatrix); // Ensure that the objective function is better now. - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.6, 1); + LMNNFunction> lmnnfn(dataset, labels, 1, 0.6, 1); - double initObj = lmnnfn.Evaluate(arma::eye(2, 2)); - double finalObj = lmnnfn.Evaluate(outputMatrix); + ElemType initObj = lmnnfn.Evaluate(arma::eye>(2, 2)); + ElemType finalObj = lmnnfn.Evaluate(outputMatrix); // finalObj must be less than initObj. REQUIRE(finalObj < initObj); } // Check that final objective value using L-BFGS optimizer is optimal. -TEST_CASE("LMNNLBFGSSimpleDatasetTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNLBFGSSimpleDatasetTest", "[LMNNTest]", float, double) { + typedef TestType ElemType; + // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; - arma::Row labels = " 0 0 0 1 1 1 "; + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Row labels = " 0 0 0 1 1 1 "; - LMNN lmnn(dataset, labels, 1); + LMNN lmnn(1); - arma::mat outputMatrix; - lmnn.LearnDistance(outputMatrix); + arma::Mat outputMatrix; + ens::L_BFGS lbfgs; + lmnn.LearnDistance(dataset, labels, outputMatrix, lbfgs); // Ensure that the objective function is better now. - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.6, 1); + LMNNFunction> lmnnfn(dataset, labels, 1, 0.6, 1); - double initObj = lmnnfn.Evaluate(arma::eye(2, 2)); - double finalObj = lmnnfn.Evaluate(outputMatrix); + ElemType initObj = lmnnfn.Evaluate(arma::eye>(2, 2)); + ElemType finalObj = lmnnfn.Evaluate(outputMatrix); // finalObj must be less than initObj. REQUIRE(finalObj < initObj); } -double KnnAccuracy(const arma::mat& dataset, - const arma::Row& labels, +template +double KnnAccuracy(const MatType& dataset, + const LabelsType& labels, const size_t k) { - arma::Row uniqueLabels = arma::unique(labels); + typedef typename MatType::elem_type ElemType; + + LabelsType uniqueLabels = arma::unique(labels); arma::Mat neighbors; - arma::mat distances; + arma::Mat distances; - KNN knn; + NeighborSearch knn; knn.Train(dataset); knn.Search(k, neighbors, distances); @@ -404,43 +447,44 @@ double KnnAccuracy(const arma::mat& dataset, for (size_t i = 0; i < dataset.n_cols; ++i) { - arma::vec Map; - Map.zeros(uniqueLabels.n_cols); + arma::Col m; + m.zeros(uniqueLabels.n_cols); for (size_t j = 0; j < k; ++j) - Map(labels(neighbors(j, i))) += - 1 / std::pow(distances(j, i) + 1, 2); + m(labels(neighbors(j, i))) += 1 / std::pow(distances(j, i) + 1, 2); - size_t index = ConvTo::From(arma::find(Map - == arma::max(Map))); + size_t index = ConvTo::From(arma::find(m == arma::max(m))); // Increase count if labels match. if (index == labels(i)) count++; } - // return accuracy. + // Return accuracy. return ((double) count / dataset.n_cols) * 100; } // Check that final accuracy is greater than initial accuracy on // simple dataset. -TEST_CASE("LMNNAccuracyTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNAccuracyTest", "[LMNNTest]", float, double) { + typedef TestType ElemType; + // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; - arma::Row labels = " 0 0 0 1 1 1 "; + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Row labels = " 0 0 0 1 1 1 "; // Taking k = 3 as the case of k = 1 can be easily observed. double initAccuracy = KnnAccuracy(dataset, labels, 3); - LMNN<> lmnn(dataset, labels, 2); + LMNN<> lmnn(2); - arma::mat outputMatrix; - lmnn.LearnDistance(outputMatrix); + arma::Mat outputMatrix; + lmnn.LearnDistance(dataset, labels, outputMatrix); - double finalAccuracy = KnnAccuracy(outputMatrix * dataset, labels, 3); + arma::Mat transformedData = outputMatrix * dataset; + double finalAccuracy = KnnAccuracy(transformedData, labels, 3); // finalObj must be less than initObj. REQUIRE(initAccuracy < finalAccuracy); @@ -452,18 +496,20 @@ TEST_CASE("LMNNAccuracyTest", "[LMNNTest]") // Check that accuracy while learning square distance matrix is the same as when // we are learning low rank matrix. I'm ok if this passes only once out of // three tries. -TEST_CASE("LMNNLowRankAccuracyLBFGSTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNLowRankAccuracyLBFGSTest", "[LMNNTest]", float, double) { + typedef TestType ElemType; + bool success = false; for (size_t trial = 0; trial < 3; ++trial) { - arma::mat dataPart1; + arma::Mat dataPart1; dataPart1.randn(5, 50); arma::Row labelsPart1(50); labelsPart1.fill(0); - arma::mat dataPart2; + arma::Mat dataPart2; dataPart2.randn(5, 50); arma::Row labelsPart2(50); @@ -473,26 +519,29 @@ TEST_CASE("LMNNLowRankAccuracyLBFGSTest", "[LMNNTest]") arma::uvec ordering = arma::shuffle(arma::linspace(0, 99, 100)); // Generate datasets. - arma::mat dataset = join_rows(dataPart1, dataPart2); + arma::Mat dataset = join_rows(dataPart1, dataPart2); dataset = dataset.cols(ordering); // Generate labels. arma::Row labels = join_rows(labelsPart1, labelsPart2); labels = labels.cols(ordering); - LMNN lmnn(dataset, labels, 1); + LMNN lmnn(1); // Learn a square matrix. - arma::mat outputMatrix; - lmnn.LearnDistance(outputMatrix); + arma::Mat outputMatrix; + L_BFGS lbfgs; + lmnn.LearnDistance(dataset, labels, outputMatrix, lbfgs); - double acc1 = KnnAccuracy(outputMatrix * dataset, labels, 1); + arma::Mat transformedData = outputMatrix * dataset; + double acc1 = KnnAccuracy(transformedData, labels, 1); // Learn a low rank matrix. - outputMatrix = arma::randu(4, 5); - lmnn.LearnDistance(outputMatrix); + outputMatrix = arma::randu>(4, 5); + lmnn.LearnDistance(dataset, labels, outputMatrix, lbfgs); - double acc2 = KnnAccuracy(outputMatrix * dataset, labels, 1); + transformedData = outputMatrix * dataset; + double acc2 = KnnAccuracy(transformedData, labels, 1); // We keep the tolerance very high. We need to ensure the accuracy drop // isn't any more than 10%. @@ -507,18 +556,20 @@ TEST_CASE("LMNNLowRankAccuracyLBFGSTest", "[LMNNTest]") // Check that accuracy while learning square distance matrix is the same as when // we are learning low rank matrix. I'm ok if this passes only once out of // three tries. -TEST_CASE("LMNNLowRankAccuracyTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNLowRankAccuracyTest", "[LMNNTest]", float, double) { + typedef TestType ElemType; + bool success = false; for (size_t trial = 0; trial < 3; ++trial) { - arma::mat dataPart1; + arma::Mat dataPart1; dataPart1.randn(5, 50); arma::Row labelsPart1(50); labelsPart1.fill(0); - arma::mat dataPart2; + arma::Mat dataPart2; dataPart2.randn(5, 50); arma::Row labelsPart2(50); @@ -528,26 +579,28 @@ TEST_CASE("LMNNLowRankAccuracyTest", "[LMNNTest]") arma::uvec ordering = arma::shuffle(arma::linspace(0, 99, 100)); // Generate datasets. - arma::mat dataset = join_rows(dataPart1, dataPart2); + arma::Mat dataset = join_rows(dataPart1, dataPart2); dataset = dataset.cols(ordering); // Generate labels. arma::Row labels = join_rows(labelsPart1, labelsPart2); labels = labels.cols(ordering); - LMNN<> lmnn(dataset, labels, 1); + LMNN<> lmnn(1); // Learn a square matrix. - arma::mat outputMatrix; - lmnn.LearnDistance(outputMatrix); + arma::Mat outputMatrix; + lmnn.LearnDistance(dataset, labels, outputMatrix); - double acc1 = KnnAccuracy(outputMatrix * dataset, labels, 1); + arma::Mat transformedData = outputMatrix * dataset; + double acc1 = KnnAccuracy(transformedData, labels, 1); // Learn a low rank matrix. - outputMatrix = arma::randu(4, 5); - lmnn.LearnDistance(outputMatrix); + outputMatrix = arma::randu>(4, 5); + lmnn.LearnDistance(dataset, labels, outputMatrix); - double acc2 = KnnAccuracy(outputMatrix * dataset, labels, 1); + transformedData = outputMatrix * dataset; + double acc2 = KnnAccuracy(transformedData, labels, 1); // We keep the tolerance very high. We need to ensure the accuracy drop // isn't any more than 10%. @@ -621,29 +674,31 @@ TEST_CASE("LMNNLowRankAccuracyBBSGDTest", "[LMNNTest]") // Comprehensive gradient tests by Marcus Edel & Ryan Curtin. // Simple numerical gradient checker. -template +template double CheckGradient(FunctionType& function, - arma::mat& coordinates, - const double eps = 1e-7) + MatType& coordinates, + const typename MatType::elem_type eps = 1e-7) { + typedef typename MatType::elem_type ElemType; + // Get gradients for the current parameters. - arma::mat orgGradient, gradient, estGradient; + MatType orgGradient, gradient, estGradient; function.Gradient(coordinates, orgGradient); - estGradient = arma::zeros(orgGradient.n_rows, orgGradient.n_cols); + estGradient = arma::zeros(orgGradient.n_rows, orgGradient.n_cols); // Compute numeric approximations to gradient. for (size_t i = 0; i < orgGradient.n_elem; ++i) { - double tmp = coordinates(i); + ElemType tmp = coordinates(i); // Perturb parameter with a positive constant and get costs. coordinates(i) += eps; - double costPlus = function.Evaluate(coordinates); + ElemType costPlus = function.Evaluate(coordinates); // Perturb parameter with a negative constant and get costs. coordinates(i) -= (2 * eps); - double costMinus = function.Evaluate(coordinates); + ElemType costMinus = function.Evaluate(coordinates); // Restore the parameter value. coordinates(i) = tmp; @@ -657,74 +712,84 @@ double CheckGradient(FunctionType& function, arma::norm(orgGradient + estGradient); } -TEST_CASE("LMNNFunctionGradientTest", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNFunctionGradientTest", "[LMNNTest]", float, double) { + typedef TestType ElemType; + // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; arma::Row labels = " 0 0 0 1 1 1 "; - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.6, 1); + LMNNFunction> lmnnfn(dataset, labels, 1, 0.6, 1); // 10 trials with random positions. for (size_t i = 0; i < 10; ++i) { - arma::mat coordinates(2, 2, arma::fill::randn); + arma::Mat coordinates(2, 2, arma::fill::randn); CheckGradient(lmnnfn, coordinates); } } -TEST_CASE("LMNNFunctionGradientTest2", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNFunctionGradientTest2", "[LMNNTest]", float, double) { - // Useful but simple dataset with six points and two classes. - arma::mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" - " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; - arma::Row labels = " 0 0 0 1 1 1 "; + typedef TestType ElemType; - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.6, 1); + // Useful but simple dataset with six points and two classes. + arma::Mat dataset = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; + arma::Row labels = " 0 0 0 1 1 1 "; + + LMNNFunction> lmnnfn(dataset, labels, 1, 0.6, 1); // 10 trials with random positions. for (size_t i = 0; i < 10; ++i) { - arma::mat coordinates(2, 2, arma::fill::randu); + arma::Mat coordinates(2, 2, arma::fill::randu); CheckGradient(lmnnfn, coordinates); } } -TEST_CASE("LMNNFunctionGradientTest3", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNFunctionGradientTest3", "[LMNNTest]", float, double) { - arma::mat dataset; + typedef TestType ElemType; + + arma::Mat dataset; arma::Row labels; if (!data::Load("iris.csv", dataset)) FAIL("Cannot load dataset iris.csv"); if (!data::Load("iris_labels.txt", labels)) FAIL("Cannot load dataset iris_labels.txt"); - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.6, 1); + LMNNFunction> lmnnfn(dataset, labels, 1, 0.6, 1); // 10 trials with random positions. for (size_t i = 0; i < 10; ++i) { - arma::mat coordinates(dataset.n_rows, dataset.n_rows, arma::fill::randn); + arma::Mat coordinates(dataset.n_rows, dataset.n_rows, + arma::fill::randn); CheckGradient(lmnnfn, coordinates); } } -TEST_CASE("LMNNFunctionGradientTest4", "[LMNNTest]") +TEMPLATE_TEST_CASE("LMNNFunctionGradientTest4", "[LMNNTest]", float, double) { - arma::mat dataset; + typedef TestType ElemType; + + arma::Mat dataset; arma::Row labels; if (!data::Load("iris.csv", dataset)) FAIL("Cannot load dataset iris.csv"); if (!data::Load("iris_labels.txt", labels)) FAIL("Cannot load dataset iris_labels.txt"); - LMNNFunction<> lmnnfn(dataset, labels, 1, 0.6, 1); + LMNNFunction> lmnnfn(dataset, labels, 1, 0.6, 1); // 10 trials with random positions. for (size_t i = 0; i < 10; ++i) { - arma::mat coordinates(dataset.n_rows, dataset.n_rows, arma::fill::randu); + arma::Mat coordinates(dataset.n_rows, dataset.n_rows, + arma::fill::randu); CheckGradient(lmnnfn, coordinates); } } diff --git a/src/mlpack/tests/main_tests/lmnn_test.cpp b/src/mlpack/tests/main_tests/lmnn_test.cpp index 13b68b3d47..901033856a 100644 --- a/src/mlpack/tests/main_tests/lmnn_test.cpp +++ b/src/mlpack/tests/main_tests/lmnn_test.cpp @@ -542,7 +542,7 @@ TEST_CASE_METHOD(LMNNTestFixture, "LMNNDiffRegularizationTest", } /** - * Ensure that different value of range results in a + * Ensure that different value of update interval results in a * different output matrix. */ TEST_CASE_METHOD(LMNNTestFixture, "LMNNDiffRangeTest", @@ -573,7 +573,7 @@ TEST_CASE_METHOD(LMNNTestFixture, "LMNNDiffRangeTest", SetInputParam("input", std::move(inputData)); SetInputParam("labels", std::move(labels)); SetInputParam("linear_scan", (bool) true); - SetInputParam("range", 100); + SetInputParam("update_interval", 100); RUN_BINDING(); @@ -674,9 +674,9 @@ TEST_CASE_METHOD(LMNNTestFixture, "LMNNDiffPassesTest", } /** - * Ensure that number of targets, range, batch size must be always positive - * and regularization, step size, max iterations, rank, passes & tolerance are - * always non-negative + * Ensure that number of targets, update interval, batch size must be always + * positive and regularization, step size, max iterations, rank, passes & + * tolerance are always non-negative. */ TEST_CASE_METHOD(LMNNTestFixture, "LMNNBoundsTest", "[LMNNMainTest][BindingTests]") @@ -701,12 +701,12 @@ TEST_CASE_METHOD(LMNNTestFixture, "LMNNBoundsTest", // Reset settings. ResetSettings(); - // Test for range value. + // Test for update interval value. // Input training data. SetInputParam("input", inputData); SetInputParam("labels", labels); - SetInputParam("range", (int) 0); + SetInputParam("update_interval", (int) 0); REQUIRE_THROWS_AS(RUN_BINDING(), std::runtime_error); diff --git a/src/mlpack/tests/nca_test.cpp b/src/mlpack/tests/nca_test.cpp index 2dcfc0909d..20df35df87 100644 --- a/src/mlpack/tests/nca_test.cpp +++ b/src/mlpack/tests/nca_test.cpp @@ -26,26 +26,31 @@ using namespace ens; * The Softmax error function should return the identity matrix as its initial * point. */ -TEST_CASE("SoftmaxInitialPoint", "[NCATesT]") +TEMPLATE_TEST_CASE("SoftmaxInitialPoint", "[NCATest]", float, double) { + typedef TestType eT; + // Cheap fake dataset. - arma::mat data; + arma::Mat data; data.randu(5, 5); arma::Row labels; labels.zeros(5); - SoftmaxErrorFunction sef(data, labels); + SoftmaxErrorFunction, arma::Row, + SquaredEuclideanDistance> sef(data, labels); // Verify the initial point is the identity matrix. - arma::mat initialPoint = sef.GetInitialPoint(); + arma::Mat initialPoint = sef.GetInitialPoint(); + const double eps = std::is_same::value ? 1e-4 : 1e-7; + const double margin = std::is_same::value ? 1e-4 : 1e-5; for (int row = 0; row < 5; row++) { for (int col = 0; col < 5; col++) { if (row == col) - REQUIRE(initialPoint(row, col) == Approx(1.0).epsilon(1e-7)); + REQUIRE(initialPoint(row, col) == Approx(1.0).epsilon(eps)); else - REQUIRE(initialPoint(row, col) == Approx(0.0).margin(1e-5)); + REQUIRE(initialPoint(row, col) == Approx(0.0).margin(margin)); } } } @@ -54,16 +59,19 @@ TEST_CASE("SoftmaxInitialPoint", "[NCATesT]") * On a simple fake dataset, ensure that the initial function evaluation is * correct. */ -TEST_CASE("SoftmaxInitialEvaluation", "[NCATesT]") +TEMPLATE_TEST_CASE("SoftmaxInitialEvaluation", "[NCATest]", float, double) { + typedef TestType eT; + // Useful but simple dataset with six points and two classes. - arma::mat data = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + arma::Mat data = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; arma::Row labels = " 0 0 0 1 1 1 "; - SoftmaxErrorFunction sef(data, labels); + SoftmaxErrorFunction, arma::Row, + SquaredEuclideanDistance> sef(data, labels); - double objective = sef.Evaluate(arma::eye(2, 2)); + eT objective = sef.Evaluate(arma::eye>(2, 2)); // Result painstakingly calculated by hand by rcurtin (recorded forever in his // notebook). As a result of lack of precision of the by-hand result, the @@ -75,22 +83,28 @@ TEST_CASE("SoftmaxInitialEvaluation", "[NCATesT]") * On a simple fake dataset, ensure that the initial gradient evaluation is * correct. */ -TEST_CASE("SoftmaxInitialGradient", "[NCATesT]") +TEMPLATE_TEST_CASE("SoftmaxInitialGradient", "[NCATest]", float, double) { + typedef TestType eT; + // Useful but simple dataset with six points and two classes. - arma::mat data = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + arma::Mat data = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; arma::Row labels = " 0 0 0 1 1 1 "; - SoftmaxErrorFunction sef(data, labels); + SoftmaxErrorFunction, arma::Row, + SquaredEuclideanDistance> sef(data, labels); - arma::mat gradient; - arma::mat coordinates = arma::eye(2, 2); + arma::Mat gradient; + arma::Mat coordinates(2, 2, arma::fill::eye); sef.Gradient(coordinates, gradient); // Results painstakingly calculated by hand by rcurtin (recorded forever in // his notebook). As a result of lack of precision of the by-hand result, the // tolerance is fairly high. + // + // UPDATE 2024: that notebook definitely got thrown away over a decade ago. I + // don't even remember what it looked like. REQUIRE(gradient(0, 0) == Approx(-0.089766).epsilon(0.0005)); REQUIRE(gradient(1, 0) == Approx(0.0).margin(1e-5)); REQUIRE(gradient(0, 1) == Approx(0.0).margin(1e-5)); @@ -101,36 +115,43 @@ TEST_CASE("SoftmaxInitialGradient", "[NCATesT]") * On optimally separated datasets, ensure that the objective function is * optimal (equal to the negative number of points). */ -TEST_CASE("SoftmaxOptimalEvaluation", "[NCATesT]") +TEMPLATE_TEST_CASE("SoftmaxOptimalEvaluation", "[NCATest]", float, double) { + typedef TestType eT; + // Simple optimal dataset. - arma::mat data = " 500 500 -500 -500;" + arma::Mat data = " 500 500 -500 -500;" " 1 0 1 0 "; arma::Row labels = " 0 0 1 1 "; - SoftmaxErrorFunction sef(data, labels); + SoftmaxErrorFunction, arma::Row, + SquaredEuclideanDistance> sef(data, labels); - double objective = sef.Evaluate(arma::eye(2, 2)); + eT objective = sef.Evaluate(arma::eye>(2, 2)); // Use a very close tolerance for optimality; we need to be sure this function // gives optimal results correctly. - REQUIRE(objective == Approx(-4.0).epsilon(1e-12)); + const double eps = std::is_same::value ? 1e-6 : 1e-12; + REQUIRE(objective == Approx(-4.0).epsilon(eps)); } /** * On optimally separated datasets, ensure that the gradient is zero. */ -TEST_CASE("SoftmaxOptimalGradient", "[NCATesT]") +TEMPLATE_TEST_CASE("SoftmaxOptimalGradient", "[NCATest]", float, double) { + typedef TestType eT; + // Simple optimal dataset. - arma::mat data = " 500 500 -500 -500;" + arma::Mat data = " 500 500 -500 -500;" " 1 0 1 0 "; arma::Row labels = " 0 0 1 1 "; - SoftmaxErrorFunction sef(data, labels); + SoftmaxErrorFunction, arma::Row, + SquaredEuclideanDistance> sef(data, labels); - arma::mat gradient; - sef.Gradient(arma::eye(2, 2), gradient); + arma::Mat gradient; + sef.Gradient(arma::eye>(2, 2), gradient); REQUIRE(gradient(0, 0) == Approx(0.0).margin(1e-5)); REQUIRE(gradient(0, 1) == Approx(0.0).margin(1e-5)); @@ -141,19 +162,22 @@ TEST_CASE("SoftmaxOptimalGradient", "[NCATesT]") /** * Ensure the separable objective function is right. */ -TEST_CASE("SoftmaxSeparableObjective", "[NCATesT]") +TEMPLATE_TEST_CASE("SoftmaxSeparableObjective", "[NCATest]", float, double) { + typedef TestType eT; + // Useful but simple dataset with six points and two classes. - arma::mat data = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + arma::Mat data = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; arma::Row labels = " 0 0 0 1 1 1 "; - SoftmaxErrorFunction sef(data, labels); + SoftmaxErrorFunction, arma::Row, + SquaredEuclideanDistance> sef(data, labels); // Results painstakingly calculated by hand by rcurtin (recorded forever in // his notebook). As a result of lack of precision of the by-hand result, the // tolerance is fairly high. - arma::mat coordinates = arma::eye(2, 2); + arma::Mat coordinates = arma::eye>(2, 2); REQUIRE(sef.Evaluate(coordinates, 0, 1) == Approx(-0.22480).epsilon(0.0001)); REQUIRE(sef.Evaluate(coordinates, 1, 1) == Approx(-0.30613).epsilon(0.0001)); REQUIRE(sef.Evaluate(coordinates, 2, 1) == Approx(-0.22480).epsilon(0.0001)); @@ -165,16 +189,19 @@ TEST_CASE("SoftmaxSeparableObjective", "[NCATesT]") /** * Ensure the optimal separable objective function is right. */ -TEST_CASE("OptimalSoftmaxSeparableObjective", "[NCATesT]") +TEMPLATE_TEST_CASE("OptimalSoftmaxSeparableObjective", "[NCATest]", float, double) { + typedef TestType eT; + // Simple optimal dataset. - arma::mat data = " 500 500 -500 -500;" + arma::Mat data = " 500 500 -500 -500;" " 1 0 1 0 "; arma::Row labels = " 0 0 1 1 "; - SoftmaxErrorFunction sef(data, labels); + SoftmaxErrorFunction, arma::Row, + SquaredEuclideanDistance> sef(data, labels); - arma::mat coordinates = arma::eye(2, 2); + arma::Mat coordinates = arma::eye>(2, 2); // Use a very close tolerance for optimality; we need to be sure this function // gives optimal results correctly. @@ -187,17 +214,20 @@ TEST_CASE("OptimalSoftmaxSeparableObjective", "[NCATesT]") /** * Ensure the separable gradient is right. */ -TEST_CASE("SoftmaxSeparableGradient", "[NCATesT]") +TEMPLATE_TEST_CASE("SoftmaxSeparableGradient", "[NCATest]", float, double) { + typedef TestType eT; + // Useful but simple dataset with six points and two classes. - arma::mat data = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + arma::Mat data = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; arma::Row labels = " 0 0 0 1 1 1 "; - SoftmaxErrorFunction sef(data, labels); + SoftmaxErrorFunction, arma::Row, + SquaredEuclideanDistance> sef(data, labels); - arma::mat coordinates = arma::eye(2, 2); - arma::mat gradient(2, 2); + arma::Mat coordinates = arma::eye>(2, 2); + arma::Mat gradient(2, 2); sef.Gradient(coordinates, 0, gradient, 1); @@ -250,29 +280,33 @@ TEST_CASE("SoftmaxSeparableGradient", "[NCATesT]") * On our simple dataset, ensure that the NCA algorithm fully separates the * points. */ -TEST_CASE("NCASGDSimpleDataset", "[NCATesT]") +TEMPLATE_TEST_CASE("NCASGDSimpleDataset", "[NCATest]", float, double) { + typedef TestType eT; + // Useful but simple dataset with six points and two classes. - arma::mat data = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + arma::Mat data = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; arma::Row labels = " 0 0 0 1 1 1 "; // Huge learning rate because this is so simple. - NCA nca(data, labels); - nca.Optimizer().StepSize() = 1.2; - nca.Optimizer().MaxIterations() = 300000; - nca.Optimizer().Tolerance() = 0; - nca.Optimizer().Shuffle() = true; + ens::StandardSGD opt; + opt.StepSize() = 1.2; + opt.MaxIterations() = 300000; + opt.Tolerance() = 0; + opt.Shuffle() = true; - arma::mat outputMatrix; - nca.LearnDistance(outputMatrix); + arma::Mat outputMatrix; + NCA nca; + nca.LearnDistance(data, labels, outputMatrix, opt); // Ensure that the objective function is better now. - SoftmaxErrorFunction sef(data, labels); + SoftmaxErrorFunction, arma::Row, + SquaredEuclideanDistance> sef(data, labels); - double initObj = sef.Evaluate(arma::eye(2, 2)); - double finalObj = sef.Evaluate(outputMatrix); - arma::mat finalGradient; + eT initObj = sef.Evaluate(arma::eye>(2, 2)); + eT finalObj = sef.Evaluate(outputMatrix); + arma::Mat finalGradient; sef.Gradient(outputMatrix, finalGradient); // finalObj must be less than initObj. @@ -284,33 +318,36 @@ TEST_CASE("NCASGDSimpleDataset", "[NCATesT]") REQUIRE(arma::norm(finalGradient, 2) < 1e-4); } -TEST_CASE("NCALBFGSSimpleDataset", "[NCATesT]") +TEMPLATE_TEST_CASE("NCALBFGSSimpleDataset", "[NCATest]", float, double) { + typedef TestType eT; + // Useful but simple dataset with six points and two classes. - arma::mat data = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" + arma::Mat data = "-0.1 -0.1 -0.1 0.1 0.1 0.1;" " 1.0 0.0 -1.0 1.0 0.0 -1.0 "; arma::Row labels = " 0 0 0 1 1 1 "; - // Huge learning rate because this is so simple. - NCA nca(data, labels); - nca.Optimizer().NumBasis() = 5; + L_BFGS lbfgs; + lbfgs.NumBasis() = 5; - arma::mat outputMatrix; - nca.LearnDistance(outputMatrix); + arma::Mat outputMatrix; + NCA nca; + nca.LearnDistance(data, labels, outputMatrix, lbfgs); // Ensure that the objective function is better now. - SoftmaxErrorFunction sef(data, labels); + SoftmaxErrorFunction, arma::Row, + SquaredEuclideanDistance> sef(data, labels); - double initObj = sef.Evaluate(arma::eye(2, 2)); - double finalObj = sef.Evaluate(outputMatrix); - arma::mat finalGradient; + eT initObj = sef.Evaluate(arma::eye>(2, 2)); + eT finalObj = sef.Evaluate(outputMatrix); + arma::Mat finalGradient; sef.Gradient(outputMatrix, finalGradient); // finalObj must be less than initObj. REQUIRE(finalObj < initObj); // Verify that final objective is optimal. - REQUIRE(finalObj == Approx(-6.0).epsilon(1e-7)); + REQUIRE(finalObj == Approx(-6.0).epsilon(0.00001)); // The solution is not unique, so the best we can do is ensure the gradient // norm is close to 0. - REQUIRE(arma::norm(finalGradient, 2) < 1e-6); + REQUIRE(arma::norm(finalGradient, 2) < 1e-5); }