diff --git a/.github/issue_template.md b/.github/ISSUE_TEMPLATE/bug_report.md
similarity index 82%
rename from .github/issue_template.md
rename to .github/ISSUE_TEMPLATE/bug_report.md
index d4fb0abe..ae757838 100644
--- a/.github/issue_template.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,3 +1,9 @@
+---
+name: Reproducible bug report
+about: Create a reproducible bug report. Not for support requests.
+labels: 'bug'
+---
+
#### Description
@@ -42,3 +48,9 @@ $ pip show metric_learn | grep Version
)
-->
+
+---
+
+**Message from the maintainers**:
+
+Impacted by this bug? Give it a 👍. We prioritise the issues with the most 👍.
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..415acfcd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,18 @@
+blank_issues_enabled: false
+
+contact_links:
+ - name: Have you read the docs?
+ url: http://contrib.scikit-learn.org/metric-learn/
+ about: Much help can be found in the docs
+ - name: Ask a question
+ url: https://github.com/scikit-learn-contrib/metric-learn/discussions/new
+ about: Ask a question or start a discussion about metric-learn
+ - name: Stack Overflow
+ url: https://stackoverflow.com
+ about: Please ask and answer metric-learn usage questions (API, installation...) on Stack Overflow
+ - name: Cross Validated
+ url: https://stats.stackexchange.com
+ about: Please ask and answer metric learning questions (use cases, algorithms & theory...) on Cross Validated
+ - name: Blank issue
+ url: https://github.com/scikit-learn-contrib/metric-learn/issues/new
+ about: Please note that Github Discussions should be used in most cases instead
diff --git a/.github/ISSUE_TEMPLATE/doc_improvement.md b/.github/ISSUE_TEMPLATE/doc_improvement.md
new file mode 100644
index 00000000..753cf2f7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/doc_improvement.md
@@ -0,0 +1,23 @@
+---
+name: Documentation improvement
+about: Create a report to help us improve the documentation. Alternatively you can just open a pull request with the suggested change.
+labels: Documentation
+---
+
+#### Describe the issue linked to the documentation
+
+
+
+#### Suggest a potential alternative/fix
+
+
+
+---
+
+**Message from the maintainers**:
+
+Confused by this part of the doc too? Give it a 👍. We prioritise the issues with the most 👍.
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/enhancement_proposal.md b/.github/ISSUE_TEMPLATE/enhancement_proposal.md
new file mode 100644
index 00000000..01dfb1d7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/enhancement_proposal.md
@@ -0,0 +1,18 @@
+---
+name: Enhancement proposal
+about: Propose an enhancement for metric-learn
+labels: 'enhancement'
+---
+# Summary
+
+What change needs making?
+
+# Use Cases
+
+When would you use this?
+
+---
+
+**Message from the maintainers**:
+
+Want to see this feature happen? Give it a 👍. We prioritise the issues with the most 👍.
\ No newline at end of file
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 00000000..0935a109
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,42 @@
+name: CI
+
+# Controls when the workflow will run
+on:
+ # Triggers the workflow on push or pull request events but only for the master branch
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ # Run normal testing with the latest versions of all dependencies
+ build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ python-version: ['3.8', '3.9', '3.10', '3.11']
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Run Tests without skggm
+ run: |
+ sudo apt-get install liblapack-dev
+ pip install --upgrade pip pytest
+ pip install wheel cython numpy scipy codecov pytest-cov scikit-learn
+ pytest test --cov
+ bash <(curl -s https://codecov.io/bash)
+ - name: Run Tests with skggm
+ env:
+ SKGGM_VERSION: a0ed406586c4364ea3297a658f415e13b5cbdaf8
+ run: |
+ pip install git+https://github.com/skggm/skggm.git@${SKGGM_VERSION}
+ pytest test --cov
+ bash <(curl -s https://codecov.io/bash)
+ - name: Syntax checking with flake8
+ run: |
+ pip install flake8
+ flake8 --extend-ignore=E111,E114 --show-source;
diff --git a/.gitignore b/.gitignore
index 8321c7d2..66eb3551 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,6 @@ htmlcov/
.cache/
.pytest_cache/
doc/auto_examples/*
-doc/generated/*
\ No newline at end of file
+doc/generated/*
+venv/
+.vscode/
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index d294c294..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-language: python
-sudo: false
-cache: pip
-language: python
-env:
- global:
- - SKGGM_VERSION=a0ed406586c4364ea3297a658f415e13b5cbdaf8
-matrix:
- include:
- - name: "Pytest python 3.6 without skggm"
- python: "3.6"
- before_install:
- - sudo apt-get install liblapack-dev
- - pip install --upgrade pip pytest
- - pip install wheel cython numpy scipy codecov pytest-cov scikit-learn
- script:
- - pytest test --cov;
- after_success:
- - bash <(curl -s https://codecov.io/bash)
- - name: "Pytest python 3.6 with skggm"
- python: "3.6"
- before_install:
- - sudo apt-get install liblapack-dev
- - pip install --upgrade pip pytest
- - pip install wheel cython numpy scipy codecov pytest-cov scikit-learn
- - pip install git+https://github.com/skggm/skggm.git@${SKGGM_VERSION};
- script:
- - pytest test --cov;
- after_success:
- - bash <(curl -s https://codecov.io/bash)
- - name: "Pytest python 3.7 with skggm"
- python: "3.7"
- before_install:
- - sudo apt-get install liblapack-dev
- - pip install --upgrade pip pytest
- - pip install wheel cython numpy scipy codecov pytest-cov scikit-learn
- - pip install git+https://github.com/skggm/skggm.git@${SKGGM_VERSION};
- script:
- - pytest test --cov;
- after_success:
- - bash <(curl -s https://codecov.io/bash)
- - name: "Syntax checking with flake8"
- python: "3.7"
- before_install:
- - pip install flake8
- script:
- - flake8 --extend-ignore=E111,E114 --show-source;
- # Use this instead to have a syntax check only on the diff:
- # - source ./build_tools/travis/flake8_diff.sh;
-branches:
- only:
- - master
diff --git a/README.rst b/README.rst
index ff770932..b2f6e6d4 100644
--- a/README.rst
+++ b/README.rst
@@ -1,4 +1,4 @@
-|Travis-CI Build Status| |License| |PyPI version| |Code coverage|
+|GitHub Actions Build Status| |License| |PyPI version| |Code coverage|
metric-learn: Metric Learning in Python
=======================================
@@ -22,7 +22,7 @@ metric-learn contains efficient Python implementations of several popular superv
- Python 3.6+ (the last version supporting Python 2 and Python 3.5 was
`v0.5.0 `_)
-- numpy, scipy, scikit-learn>=0.20.3
+- numpy>= 1.11.0, scipy>= 0.17.0, scikit-learn>=0.21.3
**Optional dependencies**
@@ -49,23 +49,26 @@ If you use metric-learn in a scientific publication, we would appreciate
citations to the following paper:
`metric-learn: Metric Learning Algorithms in Python
-`_, de Vazelhes
-*et al.*, arXiv:1908.04710, 2019.
+`_, de Vazelhes
+*et al.*, Journal of Machine Learning Research, 21(138):1-6, 2020.
Bibtex entry::
- @techreport{metric-learn,
+ @article{metric-learn,
title = {metric-learn: {M}etric {L}earning {A}lgorithms in {P}ython},
author = {{de Vazelhes}, William and {Carey}, CJ and {Tang}, Yuan and
{Vauquier}, Nathalie and {Bellet}, Aur{\'e}lien},
- institution = {arXiv:1908.04710},
- year = {2019}
+ journal = {Journal of Machine Learning Research},
+ year = {2020},
+ volume = {21},
+ number = {138},
+ pages = {1--6}
}
.. _sphinx documentation: http://contrib.scikit-learn.org/metric-learn/
-.. |Travis-CI Build Status| image:: https://api.travis-ci.org/scikit-learn-contrib/metric-learn.svg?branch=master
- :target: https://travis-ci.org/scikit-learn-contrib/metric-learn
+.. |GitHub Actions Build Status| image:: https://github.com/scikit-learn-contrib/metric-learn/workflows/CI/badge.svg
+ :target: https://github.com/scikit-learn-contrib/metric-learn/actions?query=event%3Apush+branch%3Amaster
.. |License| image:: http://img.shields.io/:license-mit-blue.svg?style=flat
:target: http://badges.mit-license.org
.. |PyPI version| image:: https://badge.fury.io/py/metric-learn.svg
diff --git a/bench/benchmarks/iris.py b/bench/benchmarks/iris.py
index 5973f7b8..05035085 100644
--- a/bench/benchmarks/iris.py
+++ b/bench/benchmarks/iris.py
@@ -5,15 +5,15 @@
CLASSES = {
'Covariance': metric_learn.Covariance(),
- 'ITML_Supervised': metric_learn.ITML_Supervised(num_constraints=200),
+ 'ITML_Supervised': metric_learn.ITML_Supervised(n_constraints=200),
'LFDA': metric_learn.LFDA(k=2, dim=2),
- 'LMNN': metric_learn.LMNN(k=5, learn_rate=1e-6, verbose=False),
- 'LSML_Supervised': metric_learn.LSML_Supervised(num_constraints=200),
+ 'LMNN': metric_learn.LMNN(n_neighbors=5, learn_rate=1e-6, verbose=False),
+ 'LSML_Supervised': metric_learn.LSML_Supervised(n_constraints=200),
'MLKR': metric_learn.MLKR(),
'NCA': metric_learn.NCA(max_iter=700, n_components=2),
- 'RCA_Supervised': metric_learn.RCA_Supervised(dim=2, num_chunks=30,
+ 'RCA_Supervised': metric_learn.RCA_Supervised(dim=2, n_chunks=30,
chunk_size=2),
- 'SDML_Supervised': metric_learn.SDML_Supervised(num_constraints=1500)
+ 'SDML_Supervised': metric_learn.SDML_Supervised(n_constraints=1500)
}
diff --git a/build_tools/travis/flake8_diff.sh b/build_tools/travis/flake8_diff.sh
deleted file mode 100644
index aea926c8..00000000
--- a/build_tools/travis/flake8_diff.sh
+++ /dev/null
@@ -1,132 +0,0 @@
-# This file is not used yet but we keep it in case we need to check the pep8 difference
-# on the diff (see .travis.yml)
-#
-#!/bin/bash
-# copied-pasted and adapted from http://github.com/sklearn-contrib/imbalanced-learn
-# (more precisely: https://raw.githubusercontent.com/glemaitre/imbalanced-learn
-# /adcb9d8e6210b321dac2c1b06879e5e889d52d77/build_tools/travis/flake8_diff.sh)
-
-# This script is used in Travis to check that PRs do not add obvious
-# flake8 violations. It relies on two things:
-# - find common ancestor between branch and
-# scikit-learn/scikit-learn remote
-# - run flake8 --diff on the diff between the branch and the common
-# ancestor
-#
-# Additional features:
-# - the line numbers in Travis match the local branch on the PR
-# author machine.
-# - ./build_tools/travis/flake8_diff.sh can be run locally for quick
-# turn-around
-
-set -e
-# pipefail is necessary to propagate exit codes
-set -o pipefail
-
-PROJECT=scikit-learn-contrib/metric-learn
-PROJECT_URL=https://github.com/$PROJECT.git
-
-# Find the remote with the project name (upstream in most cases)
-REMOTE=$(git remote -v | grep $PROJECT | cut -f1 | head -1 || echo '')
-
-# Add a temporary remote if needed. For example this is necessary when
-# Travis is configured to run in a fork. In this case 'origin' is the
-# fork and not the reference repo we want to diff against.
-if [[ -z "$REMOTE" ]]; then
- TMP_REMOTE=tmp_reference_upstream
- REMOTE=$TMP_REMOTE
- git remote add $REMOTE $PROJECT_URL
-fi
-
-echo "Remotes:"
-echo '--------------------------------------------------------------------------------'
-git remote --verbose
-
-# Travis does the git clone with a limited depth (50 at the time of
-# writing). This may not be enough to find the common ancestor with
-# $REMOTE/master so we unshallow the git checkout
-if [[ -a .git/shallow ]]; then
- echo -e '\nTrying to unshallow the repo:'
- echo '--------------------------------------------------------------------------------'
- git fetch --unshallow
-fi
-
-if [[ "$TRAVIS" == "true" ]]; then
- if [[ "$TRAVIS_PULL_REQUEST" == "false" ]]
- then
- # In main repo, using TRAVIS_COMMIT_RANGE to test the commits
- # that were pushed into a branch
- if [[ "$PROJECT" == "$TRAVIS_REPO_SLUG" ]]; then
- if [[ -z "$TRAVIS_COMMIT_RANGE" ]]; then
- echo "New branch, no commit range from Travis so passing this test by convention"
- exit 0
- fi
- COMMIT_RANGE=$TRAVIS_COMMIT_RANGE
- fi
- else
- # We want to fetch the code as it is in the PR branch and not
- # the result of the merge into master. This way line numbers
- # reported by Travis will match with the local code.
- LOCAL_BRANCH_REF=travis_pr_$TRAVIS_PULL_REQUEST
- # In Travis the PR target is always origin
- git fetch origin pull/$TRAVIS_PULL_REQUEST/head:refs/$LOCAL_BRANCH_REF
- fi
-fi
-
-# If not using the commit range from Travis we need to find the common
-# ancestor between $LOCAL_BRANCH_REF and $REMOTE/master
-if [[ -z "$COMMIT_RANGE" ]]; then
- if [[ -z "$LOCAL_BRANCH_REF" ]]; then
- LOCAL_BRANCH_REF=$(git rev-parse --abbrev-ref HEAD)
- fi
- echo -e "\nLast 2 commits in $LOCAL_BRANCH_REF:"
- echo '--------------------------------------------------------------------------------'
- git log -2 $LOCAL_BRANCH_REF
-
- REMOTE_MASTER_REF="$REMOTE/master"
- # Make sure that $REMOTE_MASTER_REF is a valid reference
- echo -e "\nFetching $REMOTE_MASTER_REF"
- echo '--------------------------------------------------------------------------------'
- git fetch $REMOTE master:refs/remotes/$REMOTE_MASTER_REF
- LOCAL_BRANCH_SHORT_HASH=$(git rev-parse --short $LOCAL_BRANCH_REF)
- REMOTE_MASTER_SHORT_HASH=$(git rev-parse --short $REMOTE_MASTER_REF)
-
- COMMIT=$(git merge-base $LOCAL_BRANCH_REF $REMOTE_MASTER_REF) || \
- echo "No common ancestor found for $(git show $LOCAL_BRANCH_REF -q) and $(git show $REMOTE_MASTER_REF -q)"
-
- if [ -z "$COMMIT" ]; then
- exit 1
- fi
-
- COMMIT_SHORT_HASH=$(git rev-parse --short $COMMIT)
-
- echo -e "\nCommon ancestor between $LOCAL_BRANCH_REF ($LOCAL_BRANCH_SHORT_HASH)"\
- "and $REMOTE_MASTER_REF ($REMOTE_MASTER_SHORT_HASH) is $COMMIT_SHORT_HASH:"
- echo '--------------------------------------------------------------------------------'
- git show --no-patch $COMMIT_SHORT_HASH
-
- COMMIT_RANGE="$COMMIT_SHORT_HASH..$LOCAL_BRANCH_SHORT_HASH"
-
- if [[ -n "$TMP_REMOTE" ]]; then
- git remote remove $TMP_REMOTE
- fi
-
-else
- echo "Got the commit range from Travis: $COMMIT_RANGE"
-fi
-
-echo -e '\nRunning flake8 on the diff in the range' "$COMMIT_RANGE" \
- "($(git rev-list $COMMIT_RANGE | wc -l) commit(s)):"
-echo '--------------------------------------------------------------------------------'
-
-# to not include the context (some lines before and after the modified lines), add the
-# flag --unified=0 (warning: it will not include some errors like for instance adding too
-# much blank lines
-check_files() {
- git diff $COMMIT_RANGE | flake8 --diff --show-source --extend-ignore=E111,E114
-}
-
-check_files
-
-echo -e "No problem detected by flake8\n"
-
diff --git a/doc/_static/css/styles.css b/doc/_static/css/styles.css
new file mode 100644
index 00000000..6d350ae4
--- /dev/null
+++ b/doc/_static/css/styles.css
@@ -0,0 +1,36 @@
+.hatnote {
+ border-color: #e1e4e5 ;
+ border-style: solid ;
+ border-width: 1px ;
+ font-size: x-small ;
+ font-style: italic ;
+ margin-left: auto ;
+ margin-right: auto ;
+ margin-bottom: 24px;
+ padding: 12px;
+}
+.hatnote-gray {
+ background-color: #f5f5f5
+}
+.hatnote li {
+ list-style-type: square;
+ margin-left: 12px !important;
+}
+.hatnote ul {
+ list-style-type: square;
+ margin-left: 0px !important;
+ margin-bottom: 0px !important;
+}
+.deprecated {
+ color: #b94a48;
+ background-color: #F3E5E5;
+ border-color: #eed3d7;
+ margin-top: 0.5rem;
+ padding: 0.5rem;
+ border-radius: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.deprecated p {
+ margin-bottom: 0 !important;
+}
\ No newline at end of file
diff --git a/doc/conf.py b/doc/conf.py
index eac09b38..c472cc21 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import sys
import os
+import warnings
extensions = [
'sphinx.ext.autodoc',
@@ -20,12 +21,12 @@
# General information about the project.
project = u'metric-learn'
-copyright = (u'2015-2020, CJ Carey, Yuan Tang, William de Vazelhes, Aurélien '
+copyright = (u'2015-2023, CJ Carey, Yuan Tang, William de Vazelhes, Aurélien '
u'Bellet and Nathalie Vauquier')
author = (u'CJ Carey, Yuan Tang, William de Vazelhes, Aurélien Bellet and '
u'Nathalie Vauquier')
-version = '0.6.0'
-release = '0.6.0'
+version = '0.7.0'
+release = '0.7.0'
language = 'en'
exclude_patterns = ['_build']
@@ -37,9 +38,6 @@
html_static_path = ['_static']
htmlhelp_basename = 'metric-learndoc'
-# Option to only need single backticks to refer to symbols
-default_role = 'any'
-
# Option to hide doctests comments in the documentation (like # doctest:
# +NORMALIZE_WHITESPACE for instance)
trim_doctest_flags = True
@@ -66,10 +64,6 @@
# generate autosummary even if no references
autosummary_generate = True
-# Switch to old behavior with html4, for a good display of references,
-# as described in https://github.com/sphinx-doc/sphinx/issues/6705
-html4_writer = True
-
# Temporary work-around for spacing problem between parameter and parameter
# type in the doc, see https://github.com/numpy/numpydoc/issues/215. The bug
@@ -78,5 +72,11 @@
# In an ideal world, this would get fixed in this PR:
# https://github.com/readthedocs/sphinx_rtd_theme/pull/747/files
def setup(app):
- app.add_javascript('js/copybutton.js')
- app.add_stylesheet("basic.css")
+ app.add_js_file('js/copybutton.js')
+ app.add_css_file('css/styles.css')
+
+
+# Remove matplotlib agg warnings from generated doc when using plt.show
+warnings.filterwarnings("ignore", category=UserWarning,
+ message='Matplotlib is currently using agg, which is a'
+ ' non-GUI backend, so cannot show the figure.')
diff --git a/doc/getting_started.rst b/doc/getting_started.rst
index 44fd1436..90b7c7ee 100644
--- a/doc/getting_started.rst
+++ b/doc/getting_started.rst
@@ -19,7 +19,7 @@ metric-learn can be installed in either of the following ways:
- Python 3.6+ (the last version supporting Python 2 and Python 3.5 was
`v0.5.0 `_)
-- numpy, scipy, scikit-learn>=0.20.3
+- numpy>= 1.11.0, scipy>= 0.17.0, scikit-learn>=0.21.3
**Optional dependencies**
diff --git a/doc/index.rst b/doc/index.rst
index 8f000246..f9dfd83d 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -1,6 +1,6 @@
metric-learn: Metric Learning in Python
=======================================
-|Travis-CI Build Status| |License| |PyPI version| |Code coverage|
+|GitHub Actions Build Status| |License| |PyPI version| |Code coverage|
`metric-learn `_
contains efficient Python implementations of several popular supervised and
@@ -15,17 +15,20 @@ If you use metric-learn in a scientific publication, we would appreciate
citations to the following paper:
`metric-learn: Metric Learning Algorithms in Python
-`_, de Vazelhes
-*et al.*, arXiv:1908.04710, 2019.
+`_, de Vazelhes
+*et al.*, Journal of Machine Learning Research, 21(138):1-6, 2020.
Bibtex entry::
- @techreport{metric-learn,
+ @article{metric-learn,
title = {metric-learn: {M}etric {L}earning {A}lgorithms in {P}ython},
author = {{de Vazelhes}, William and {Carey}, CJ and {Tang}, Yuan and
{Vauquier}, Nathalie and {Bellet}, Aur{\'e}lien},
- institution = {arXiv:1908.04710},
- year = {2019}
+ journal = {Journal of Machine Learning Research},
+ year = {2020},
+ volume = {21},
+ number = {138},
+ pages = {1--6}
}
@@ -54,8 +57,8 @@ Documentation outline
:ref:`genindex` | :ref:`search`
-.. |Travis-CI Build Status| image:: https://api.travis-ci.org/scikit-learn-contrib/metric-learn.svg?branch=master
- :target: https://travis-ci.org/scikit-learn-contrib/metric-learn
+.. |GitHub Actions Build Status| image:: https://github.com/scikit-learn-contrib/metric-learn/workflows/CI/badge.svg
+ :target: https://github.com/scikit-learn-contrib/metric-learn/actions?query=event%3Apush+branch%3Amaster
.. |PyPI version| image:: https://badge.fury.io/py/metric-learn.svg
:target: http://badge.fury.io/py/metric-learn
.. |License| image:: http://img.shields.io/:license-mit-blue.svg?style=flat
diff --git a/doc/introduction.rst b/doc/introduction.rst
index 7d9f52d0..e9ff0015 100644
--- a/doc/introduction.rst
+++ b/doc/introduction.rst
@@ -123,26 +123,3 @@ to the following resources:
Survey `_ (2012)
- **Book:** `Metric Learning
`_ (2015)
-
-.. Methods [TO MOVE TO SUPERVISED/WEAK SECTIONS]
-.. =============================================
-
-.. Currently, each metric learning algorithm supports the following methods:
-
-.. - ``fit(...)``, which learns the model.
-.. - ``get_mahalanobis_matrix()``, which returns a Mahalanobis matrix
-.. - ``get_metric()``, which returns a function that takes as input two 1D
- arrays and outputs the learned metric score on these two points
-.. :math:`M = L^{\top}L` such that distance between vectors ``x`` and
-.. ``y`` can be computed as :math:`\sqrt{\left(x-y\right)M\left(x-y\right)}`.
-.. - ``components_from_metric(metric)``, which returns a transformation matrix
-.. :math:`L \in \mathbb{R}^{D \times d}`, which can be used to convert a
-.. data matrix :math:`X \in \mathbb{R}^{n \times d}` to the
-.. :math:`D`-dimensional learned metric space :math:`X L^{\top}`,
-.. in which standard Euclidean distances may be used.
-.. - ``transform(X)``, which applies the aforementioned transformation.
-.. - ``score_pairs(pairs)`` which returns the distance between pairs of
-.. points. ``pairs`` should be a 3D array-like of pairs of shape ``(n_pairs,
-.. 2, n_features)``, or it can be a 2D array-like of pairs indicators of
-.. shape ``(n_pairs, 2)`` (see section :ref:`preprocessor_section` for more
-.. details).
\ No newline at end of file
diff --git a/doc/metric_learn.rst b/doc/metric_learn.rst
index 8f91d91c..4d0676b9 100644
--- a/doc/metric_learn.rst
+++ b/doc/metric_learn.rst
@@ -13,6 +13,8 @@ Base Classes
metric_learn.Constraints
metric_learn.base_metric.BaseMetricLearner
+ metric_learn.base_metric.MetricTransformer
+ metric_learn.base_metric.MahalanobisMixin
metric_learn.base_metric._PairsClassifierMixin
metric_learn.base_metric._TripletsClassifierMixin
metric_learn.base_metric._QuadrupletsClassifierMixin
diff --git a/doc/supervised.rst b/doc/supervised.rst
index 1b1180e9..49548b83 100644
--- a/doc/supervised.rst
+++ b/doc/supervised.rst
@@ -69,10 +69,10 @@ Also, as explained before, our metric learners has learn a distance between
points. You can use this distance in two main ways:
- You can either return the distance between pairs of points using the
- `score_pairs` function:
+ `pair_distance` function:
->>> nca.score_pairs([[[3.5, 3.6], [5.6, 2.4]], [[1.2, 4.2], [2.1, 6.4]]])
-array([0.49627072, 3.65287282])
+>>> nca.pair_distance([[[3.5, 3.6], [5.6, 2.4]], [[1.2, 4.2], [2.1, 6.4]], [[3.3, 7.8], [10.9, 0.1]]])
+array([0.49627072, 3.65287282, 6.06079877])
- Or you can return a function that will return the distance (in the new
space) between two 1D arrays (the coordinates of the points in the original
@@ -82,6 +82,18 @@ array([0.49627072, 3.65287282])
>>> metric_fun([3.5, 3.6], [5.6, 2.4])
0.4962707194621285
+- Alternatively, you can use `pair_score` to return the **score** between
+ pairs of points (the larger the score, the more similar the pair).
+ For Mahalanobis learners, it is equal to the opposite of the distance.
+
+>>> score = nca.pair_score([[[3.5, 3.6], [5.6, 2.4]], [[1.2, 4.2], [2.1, 6.4]], [[3.3, 7.8], [10.9, 0.1]]])
+>>> score
+array([-0.49627072, -3.65287282, -6.06079877])
+
+This is useful because `pair_score` matches the **score** semantic of
+scikit-learn's `Classification metrics
+`_.
+
.. note::
If the metric learner that you use learns a :ref:`Mahalanobis distance
@@ -93,7 +105,6 @@ array([0.49627072, 3.65287282])
array([[0.43680409, 0.89169412],
[0.89169412, 1.9542479 ]])
-.. TODO: remove the "like it is the case etc..." if it's not the case anymore
Scikit-learn compatibility
--------------------------
@@ -105,6 +116,7 @@ All supervised algorithms are scikit-learn estimators
scikit-learn model selection routines
(`sklearn.model_selection.cross_val_score`,
`sklearn.model_selection.GridSearchCV`, etc).
+You can also use some of the scoring functions from `sklearn.metrics`.
Algorithms
==========
@@ -140,7 +152,7 @@ neighbors (with same labels) of :math:`\mathbf{x}_{i}`, :math:`y_{ij}=0`
indicates :math:`\mathbf{x}_{i}, \mathbf{x}_{j}` belong to different classes,
:math:`[\cdot]_+=\max(0, \cdot)` is the Hinge loss.
-.. topic:: Example Code:
+.. rubric:: Example Code
::
@@ -152,18 +164,18 @@ indicates :math:`\mathbf{x}_{i}, \mathbf{x}_{j}` belong to different classes,
X = iris_data['data']
Y = iris_data['target']
- lmnn = LMNN(k=5, learn_rate=1e-6)
- lmnn.fit(X, Y, verbose=False)
+ lmnn = LMNN(n_neighbors=5, learn_rate=1e-6, verbose=False)
+ lmnn.fit(X, Y)
-.. topic:: References:
+.. rubric:: References
- .. [1] Weinberger et al. `Distance Metric Learning for Large Margin
- Nearest Neighbor Classification
- `_.
- JMLR 2009
- .. [2] `Wikipedia entry on Large Margin Nearest Neighbor `_
-
+.. container:: hatnote hatnote-gray
+
+ [1]. Weinberger et al. `Distance Metric Learning for Large Margin Nearest Neighbor Classification `_. JMLR 2009.
+
+ [2]. `Wikipedia entry on Large Margin Nearest Neighbor `_.
+
.. _nca:
@@ -204,7 +216,7 @@ the sum of probability of being correctly classified:
\mathbf{L} = \text{argmax}\sum_i p_i
-.. topic:: Example Code:
+.. rubric:: Example Code
::
@@ -219,13 +231,14 @@ the sum of probability of being correctly classified:
nca = NCA(max_iter=1000)
nca.fit(X, Y)
-.. topic:: References:
+.. rubric:: References
+
- .. [1] Goldberger et al.
- `Neighbourhood Components Analysis `_.
- NIPS 2005
+.. container:: hatnote hatnote-gray
- .. [2] `Wikipedia entry on Neighborhood Components Analysis `_
+ [1]. Goldberger et al. `Neighbourhood Components Analysis `_. NIPS 2005.
+
+ [2]. `Wikipedia entry on Neighborhood Components Analysis `_.
.. _lfda:
@@ -277,7 +290,7 @@ nearby data pairs in the same class are made close and the data pairs in
different classes are separated from each other; far apart data pairs in the
same class are not imposed to be close.
-.. topic:: Example Code:
+.. rubric:: Example Code
::
@@ -292,15 +305,19 @@ same class are not imposed to be close.
lfda = LFDA(k=2, dim=2)
lfda.fit(X, Y)
-.. topic:: References:
+.. note::
+ LDFA suffers from a problem called “sign indeterminacy”, which means the sign of the ``components`` and the output from transform depend on a random state. This is directly related to the calculation of eigenvectors in the algorithm. The same input ran in different times might lead to different transforms, but both valid.
+
+ To work around this, fit instances of this class to data once, then keep the instance around to do transformations.
+
+.. rubric:: References
+
- .. [1] Sugiyama. `Dimensionality Reduction of Multimodal Labeled Data by Local
- Fisher Discriminant Analysis `_.
- JMLR 2007
+.. container:: hatnote hatnote-gray
- .. [2] Tang. `Local Fisher Discriminant Analysis on Beer Style Clustering
- `_.
+ [1]. Sugiyama. `Dimensionality Reduction of Multimodal Labeled Data by Local Fisher Discriminant Analysis `_. JMLR 2007.
+
+ [2]. Tang. `Local Fisher Discriminant Analysis on Beer Style Clustering `_.
.. _mlkr:
@@ -346,7 +363,7 @@ calculating a weighted average of all the training samples:
\hat{y}_i = \frac{\sum_{j\neq i}y_jk_{ij}}{\sum_{j\neq i}k_{ij}}
-.. topic:: Example Code:
+.. rubric:: Example Code
::
@@ -360,10 +377,12 @@ calculating a weighted average of all the training samples:
mlkr = MLKR()
mlkr.fit(X, Y)
-.. topic:: References:
+.. rubric:: References
+
+
+.. container:: hatnote hatnote-gray
- .. [1] Weinberger et al. `Metric Learning for Kernel Regression `_. AISTATS 2007
+ [1]. Weinberger et al. `Metric Learning for Kernel Regression `_. AISTATS 2007.
.. _supervised_version:
@@ -388,8 +407,8 @@ are similar (+1) or dissimilar (-1)), are sampled with the function
(of label +1), this method will look at all the samples from the same label and
sample randomly a pair among them. To sample negative pairs (of label -1), this
method will look at all the samples from a different class and sample randomly
-a pair among them. The method will try to build `num_constraints` positive
-pairs and `num_constraints` negative pairs, but sometimes it cannot find enough
+a pair among them. The method will try to build `n_constraints` positive
+pairs and `n_constraints` negative pairs, but sometimes it cannot find enough
of one of those, so forcing `same_length=True` will return both times the
minimum of the two lenghts.
@@ -400,7 +419,7 @@ quadruplets, where for each quadruplet the two first points are from the same
class, and the two last points are from a different class (so indeed the two
last points should be less similar than the two first points).
-.. topic:: Example Code:
+.. rubric:: Example Code
::
@@ -411,5 +430,5 @@ last points should be less similar than the two first points).
X = iris_data['data']
Y = iris_data['target']
- mmc = MMC_Supervised(num_constraints=200)
+ mmc = MMC_Supervised(n_constraints=200)
mmc.fit(X, Y)
diff --git a/doc/unsupervised.rst b/doc/unsupervised.rst
index 1191e805..110b07f9 100644
--- a/doc/unsupervised.rst
+++ b/doc/unsupervised.rst
@@ -20,7 +20,7 @@ It can be used for ZCA whitening of the data (see the Wikipedia page of
`whitening transformation `_).
-.. topic:: Example Code:
+.. rubric:: Example Code
::
@@ -32,6 +32,9 @@ Whitening_transformation>`_).
cov = Covariance().fit(iris)
x = cov.transform(iris)
-.. topic:: References:
+.. rubric:: References
- .. [1] On the Generalized Distance in Statistics, P.C.Mahalanobis, 1936
\ No newline at end of file
+
+.. container:: hatnote hatnote-gray
+
+ [1]. On the Generalized Distance in Statistics, P.C.Mahalanobis, 1936.
\ No newline at end of file
diff --git a/doc/weakly_supervised.rst b/doc/weakly_supervised.rst
index 174210b8..76f7c14e 100644
--- a/doc/weakly_supervised.rst
+++ b/doc/weakly_supervised.rst
@@ -62,8 +62,9 @@ The most intuitive way to represent tuples is to provide the algorithm with a
in a tuple (2 for pairs, 3 for triplets for instance), and `n_features` is
the number of features of each point.
-.. topic:: Example:
- Here is an artificial dataset of 4 pairs of 2 points of 3 features each:
+.. rubric:: Example Code
+
+Here is an artificial dataset of 4 pairs of 2 points of 3 features each:
>>> import numpy as np
>>> tuples = np.array([[[-0.12, -1.21, -0.20],
@@ -94,7 +95,9 @@ would be to keep the dataset of points `X` aside, and just represent tuples
as a collection of tuples of *indices* from the points in `X`. Since we loose
the feature dimension there, the resulting array is 2D.
-.. topic:: Example: An equivalent representation of the above pairs would be:
+.. rubric:: Example Code
+
+An equivalent representation of the above pairs would be:
>>> X = np.array([[-0.12, -1.21, -0.20],
>>> [+0.05, -0.19, -0.05],
@@ -134,7 +137,7 @@ are respected.
>>> from metric_learn import MMC
>>> mmc = MMC(random_state=42)
>>> mmc.fit(tuples, y)
-MMC(A0='deprecated', convergence_threshold=0.001, diagonal=False,
+MMC(A0='deprecated', tol=0.001, diagonal=False,
diagonal_c=1.0, init='auto', max_iter=100, max_proj=10000,
preprocessor=None, random_state=42, verbose=False)
@@ -160,9 +163,9 @@ Also, as explained before, our metric learner has learned a distance between
points. You can use this distance in two main ways:
- You can either return the distance between pairs of points using the
- `score_pairs` function:
+ `pair_distance` function:
->>> mmc.score_pairs([[[3.5, 3.6, 5.2], [5.6, 2.4, 6.7]],
+>>> mmc.pair_distance([[[3.5, 3.6, 5.2], [5.6, 2.4, 6.7]],
... [[1.2, 4.2, 7.7], [2.1, 6.4, 0.9]]])
array([7.27607365, 0.88853014])
@@ -175,6 +178,18 @@ array([7.27607365, 0.88853014])
>>> metric_fun([3.5, 3.6, 5.2], [5.6, 2.4, 6.7])
7.276073646278203
+- Alternatively, you can use `pair_score` to return the **score** between
+ pairs of points (the larger the score, the more similar the pair).
+ For Mahalanobis learners, it is equal to the opposite of the distance.
+
+>>> score = mmc.pair_score([[[3.5, 3.6], [5.6, 2.4]], [[1.2, 4.2], [2.1, 6.4]], [[3.3, 7.8], [10.9, 0.1]]])
+>>> score
+array([-0.49627072, -3.65287282, -6.06079877])
+
+ This is useful because `pair_score` matches the **score** semantic of
+ scikit-learn's `Classification metrics
+ `_.
+
.. note::
If the metric learner that you use learns a :ref:`Mahalanobis distance
@@ -187,8 +202,6 @@ array([[ 0.58603894, -5.69883982, -1.66614919],
[-5.69883982, 55.41743549, 16.20219519],
[-1.66614919, 16.20219519, 4.73697721]])
-.. TODO: remove the "like it is the case etc..." if it's not the case anymore
-
.. _sklearn_compat_ws:
Prediction and scoring
@@ -250,7 +263,7 @@ tuples).
>>> y_pairs = np.array([1, -1])
>>> mmc = MMC(random_state=42)
>>> mmc.fit(pairs, y_pairs)
-MMC(convergence_threshold=0.001, diagonal=False,
+MMC(tol=0.001, diagonal=False,
diagonal_c=1.0, init='auto', max_iter=100, max_proj=10000, preprocessor=None,
random_state=42, verbose=False)
@@ -344,8 +357,8 @@ returns the `sklearn.metrics.roc_auc_score` (which is threshold-independent).
.. note::
See :ref:`fit_ws` for more details on metric learners functions that are
- not specific to learning on pairs, like `transform`, `score_pairs`,
- `get_metric` and `get_mahalanobis_matrix`.
+ not specific to learning on pairs, like `transform`, `pair_distance`,
+ `pair_score`, `get_metric` and `get_mahalanobis_matrix`.
Algorithms
----------
@@ -400,7 +413,7 @@ for similar and dissimilar pairs respectively, and :math:`\mathbf{M}_0`
is the prior distance metric, set to identity matrix by default,
:math:`D_{\ell \mathrm{d}}(\cdot)` is the log determinant.
-.. topic:: Example Code:
+.. rubric:: Example Code
::
@@ -419,11 +432,14 @@ is the prior distance metric, set to identity matrix by default,
itml = ITML()
itml.fit(pairs, y)
-.. topic:: References:
+.. rubric:: References
- .. [1] Jason V. Davis, et al. `Information-theoretic Metric Learning `_. ICML 2007
- .. [2] Adapted from Matlab code at http://www.cs.utexas.edu/users/pjain/itml/
+.. container:: hatnote hatnote-gray
+
+ [1]. Jason V. Davis, et al. `Information-theoretic Metric Learning `_. ICML 2007.
+
+ [2]. Adapted from Matlab code at http://www.cs.utexas.edu/users/pjain/itml/ .
.. _sdml:
@@ -458,7 +474,7 @@ the sums of the row elements of :math:`\mathbf{K}`., :math:`||\cdot||_{1, off}`
is the off-diagonal L1 norm.
-.. topic:: Example Code:
+.. rubric:: Example Code
::
@@ -476,19 +492,19 @@ is the off-diagonal L1 norm.
sdml = SDML()
sdml.fit(pairs, y)
-.. topic:: References:
+.. rubric:: References
+
+
+.. container:: hatnote hatnote-gray
- .. [1] Qi et al.
- `An efficient sparse metric learning in high-dimensional space via
- L1-penalized log-determinant regularization `_.
- ICML 2009.
+ [1]. Qi et al. `An efficient sparse metric learning in high-dimensional space via L1-penalized log-determinant regularization `_. ICML 2009.
- .. [2] Code adapted from https://gist.github.com/kcarnold/5439945
+ [2]. Code adapted from https://gist.github.com/kcarnold/5439945 .
.. _rca:
:py:class:`RCA `
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Relative Components Analysis (:py:class:`RCA `)
@@ -512,7 +528,7 @@ where chunklet :math:`j` consists of :math:`\{\mathbf{x}_{ji}\}_{i=1}^{n_j}`
with a mean :math:`\hat{m}_j`. The inverse of :math:`\mathbf{C}^{-1}` is used
as the Mahalanobis matrix.
-.. topic:: Example Code:
+.. rubric:: Example Code
::
@@ -527,15 +543,16 @@ as the Mahalanobis matrix.
rca = RCA()
rca.fit(X, chunks)
-.. topic:: References:
+.. rubric:: References
+
+
+.. container:: hatnote hatnote-gray
- .. [1] Shental et al. `Adjustment learning and relevant component analysis
- `_. ECCV 2002
+ [1]. Shental et al. `Adjustment learning and relevant component analysis `_. ECCV 2002.
- .. [2] Bar-Hillel et al. `Learning distance functions using equivalence relations `_. ICML 2003
+ [2]. Bar-Hillel et al. `Learning distance functions using equivalence relations `_. ICML 2003.
- .. [3] Bar-Hillel et al. `Learning a Mahalanobis metric from equivalence constraints `_. JMLR 2005
+ [3]. Bar-Hillel et al. `Learning a Mahalanobis metric from equivalence constraints `_. JMLR 2005.
.. _mmc:
@@ -566,7 +583,7 @@ points, while constrains the sum of distances between dissimilar points:
\qquad \qquad \text{s.t.} \qquad \sum_{(\mathbf{x}_i, \mathbf{x}_j)
\in D} d^2_{\mathbf{M}}(\mathbf{x}_i, \mathbf{x}_j) \geq 1
-.. topic:: Example Code:
+.. rubric:: Example Code
::
@@ -584,13 +601,14 @@ points, while constrains the sum of distances between dissimilar points:
mmc = MMC()
mmc.fit(pairs, y)
-.. topic:: References:
+.. rubric:: References
+
+
+.. container:: hatnote hatnote-gray
- .. [1] Xing et al. `Distance metric learning with application to clustering with
- side-information `_. NIPS 2002
- .. [2] Adapted from Matlab code http://www.cs.cmu.edu/%7Eepxing/papers/Old_papers/code_Metric_online.tar.gz
+ [1]. Xing et al. `Distance metric learning with application to clustering with side-information `_. NIPS 2002.
+
+ [2]. Adapted from Matlab code http://www.cs.cmu.edu/%7Eepxing/papers/Old_papers/code_Metric_online.tar.gz .
.. _learning_on_triplets:
@@ -691,8 +709,8 @@ of triplets that have the right predicted ordering.
.. note::
See :ref:`fit_ws` for more details on metric learners functions that are
- not specific to learning on pairs, like `transform`, `score_pairs`,
- `get_metric` and `get_mahalanobis_matrix`.
+ not specific to learning on pairs, like `transform`, `pair_distance`,
+ `pair_score`, `get_metric` and `get_mahalanobis_matrix`.
@@ -734,7 +752,7 @@ is added to yield a sparse combination. The formulation is the following:
where :math:`[\cdot]_+` is the hinge loss.
-.. topic:: Example Code:
+.. rubric:: Example Code
::
@@ -748,14 +766,14 @@ where :math:`[\cdot]_+` is the hinge loss.
scml = SCML()
scml.fit(triplets)
-.. topic:: References:
+.. rubric:: References
- .. [1] Y. Shi, A. Bellet and F. Sha. `Sparse Compositional Metric Learning.
- `_. \
- (AAAI), 2014.
- .. [2] Adapted from original \
- `Matlab implementation.`_.
+.. container:: hatnote hatnote-gray
+
+ [1]. Y. Shi, A. Bellet and F. Sha. `Sparse Compositional Metric Learning. `_. (AAAI), 2014.
+
+ [2]. Adapted from original `Matlab implementation. `_.
.. _learning_on_quadruplets:
@@ -859,8 +877,8 @@ of quadruplets have the right predicted ordering.
.. note::
See :ref:`fit_ws` for more details on metric learners functions that are
- not specific to learning on pairs, like `transform`, `score_pairs`,
- `get_metric` and `get_mahalanobis_matrix`.
+ not specific to learning on pairs, like `transform`, `pair_distance`,
+ `pair_score`, `get_metric` and `get_mahalanobis_matrix`.
@@ -927,7 +945,7 @@ by default, :math:`D_{ld}(\mathbf{\cdot, \cdot})` is the LogDet divergence:
D_{ld}(\mathbf{M, M_0}) = \text{tr}(\mathbf{MM_0}) − \text{logdet}
(\mathbf{M})
-.. topic:: Example Code:
+.. rubric:: Example Code
::
@@ -944,12 +962,13 @@ by default, :math:`D_{ld}(\mathbf{\cdot, \cdot})` is the LogDet divergence:
lsml = LSML()
lsml.fit(quadruplets)
-.. topic:: References:
+.. rubric:: References
+
+
+.. container:: hatnote hatnote-gray
- .. [1] Liu et al.
- `Metric Learning from Relative Comparisons by Minimizing Squared
- Residual `_. ICDM 2012
+ [1]. Liu et al. `Metric Learning from Relative Comparisons by Minimizing Squared Residual `_. ICDM 2012.
- .. [2] Code adapted from https://gist.github.com/kcarnold/5439917
+ [2]. Code adapted from https://gist.github.com/kcarnold/5439917 .
diff --git a/examples/plot_metric_learning_examples.py b/examples/plot_metric_learning_examples.py
index 71229554..32759636 100644
--- a/examples/plot_metric_learning_examples.py
+++ b/examples/plot_metric_learning_examples.py
@@ -15,7 +15,11 @@
######################################################################
# Imports
# ^^^^^^^
+# .. note::
#
+# In order to show the charts of the examples you need a graphical
+# ``matplotlib`` backend installed. For intance, use ``pip install pyqt5``
+# to get Qt graphical interface or use your favorite one.
from sklearn.manifold import TSNE
@@ -35,9 +39,9 @@
# We will be using a synthetic dataset to illustrate the plotting,
# using the function `sklearn.datasets.make_classification` from
# scikit-learn. The dataset will contain:
-# - 100 points in 3 classes with 2 clusters per class
-# - 5 features, among which 3 are informative (correlated with the class
-# labels) and two are random noise with large magnitude
+# - 100 points in 3 classes with 2 clusters per class
+# - 5 features, among which 3 are informative (correlated with the class
+# labels) and two are random noise with large magnitude
X, y = make_classification(n_samples=100, n_classes=3, n_clusters_per_class=2,
n_informative=3, class_sep=4., n_features=5,
@@ -139,7 +143,7 @@ def plot_tsne(X, y, colormap=plt.cm.Paired):
#
# setting up LMNN
-lmnn = metric_learn.LMNN(k=5, learn_rate=1e-6)
+lmnn = metric_learn.LMNN(n_neighbors=5, learn_rate=1e-6)
# fit the data!
lmnn.fit(X, y)
@@ -310,7 +314,7 @@ def plot_tsne(X, y, colormap=plt.cm.Paired):
# - See more in the documentation of the class :py:class:`RCA
# `
-rca = metric_learn.RCA_Supervised(num_chunks=30, chunk_size=2)
+rca = metric_learn.RCA_Supervised(n_chunks=30, chunk_size=2)
X_rca = rca.fit_transform(X, y)
plot_tsne(X_rca, y)
diff --git a/examples/plot_sandwich.py b/examples/plot_sandwich.py
index d5856667..740852be 100644
--- a/examples/plot_sandwich.py
+++ b/examples/plot_sandwich.py
@@ -6,6 +6,13 @@
Sandwich demo based on code from http://nbviewer.ipython.org/6576096
"""
+######################################################################
+# .. note::
+#
+# In order to show the charts of the examples you need a graphical
+# ``matplotlib`` backend installed. For intance, use ``pip install pyqt5``
+# to get Qt graphical interface or use your favorite one.
+
import numpy as np
from matplotlib import pyplot as plt
from sklearn.metrics import pairwise_distances
@@ -28,9 +35,9 @@ def sandwich_demo():
mls = [
LMNN(),
- ITML_Supervised(num_constraints=200),
- SDML_Supervised(num_constraints=200, balance_param=0.001),
- LSML_Supervised(num_constraints=200),
+ ITML_Supervised(n_constraints=200),
+ SDML_Supervised(n_constraints=200, balance_param=0.001),
+ LSML_Supervised(n_constraints=200),
]
for ax_num, ml in enumerate(mls, start=3):
diff --git a/metric_learn/_util.py b/metric_learn/_util.py
index 764a34c8..868ececa 100644
--- a/metric_learn/_util.py
+++ b/metric_learn/_util.py
@@ -704,7 +704,7 @@ def _initialize_metric_mahalanobis(input, init='identity', random_state=None,
elif init == 'covariance':
if input.ndim == 3:
# if the input are tuples, we need to form an X by deduplication
- X = np.vstack({tuple(row) for row in input.reshape(-1, n_features)})
+ X = np.unique(np.vstack(input), axis=0)
else:
X = input
# atleast2d is necessary to deal with scalar covariance matrices
diff --git a/metric_learn/_version.py b/metric_learn/_version.py
index 8411e551..a71c5c7f 100644
--- a/metric_learn/_version.py
+++ b/metric_learn/_version.py
@@ -1 +1 @@
-__version__ = '0.6.1'
+__version__ = '0.7.0'
diff --git a/metric_learn/base_metric.py b/metric_learn/base_metric.py
index 721d7ba0..47efe4b7 100644
--- a/metric_learn/base_metric.py
+++ b/metric_learn/base_metric.py
@@ -2,13 +2,14 @@
Base module.
"""
-from sklearn.base import BaseEstimator
+from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.utils.extmath import stable_cumsum
from sklearn.utils.validation import _is_arraylike, check_is_fitted
from sklearn.metrics import roc_auc_score, roc_curve, precision_recall_curve
import numpy as np
from abc import ABCMeta, abstractmethod
from ._util import ArrayIndexer, check_input, validate_vector
+import warnings
class BaseMetricLearner(BaseEstimator, metaclass=ABCMeta):
@@ -27,13 +28,24 @@ def __init__(self, preprocessor=None):
@abstractmethod
def score_pairs(self, pairs):
- """Returns the score between pairs
+ """
+ Returns the score between pairs
(can be a similarity, or a distance/metric depending on the algorithm)
+ .. deprecated:: 0.7.0
+ Refer to `pair_distance` and `pair_score`.
+
+ .. warning::
+ This method will be removed in 0.8.0. Please refer to `pair_distance`
+ or `pair_score`. This change will occur in order to add learners
+ that don't necessarily learn a Mahalanobis distance.
+
Parameters
----------
- pairs : `numpy.ndarray`, shape=(n_samples, 2, n_features)
- 3D array of pairs.
+ pairs : array-like, shape=(n_pairs, 2, n_features) or (n_pairs, 2)
+ 3D Array of pairs to score, with each row corresponding to two points,
+ for 2D array of indices of pairs if the metric learner uses a
+ preprocessor.
Returns
-------
@@ -43,10 +55,71 @@ def score_pairs(self, pairs):
See Also
--------
get_metric : a method that returns a function to compute the metric between
- two points. The difference with `score_pairs` is that it works on two 1D
- arrays and cannot use a preprocessor. Besides, the returned function is
- independent of the metric learner and hence is not modified if the metric
- learner is.
+ two points. The difference between `score_pairs` is that it works on two
+ 1D arrays and cannot use a preprocessor. Besides, the returned function
+ is independent of the metric learner and hence is not modified if the
+ metric learner is.
+ """
+
+ @abstractmethod
+ def pair_score(self, pairs):
+ """
+ .. versionadded:: 0.7.0 Compute the similarity score between pairs
+
+ Returns the similarity score between pairs of points (the larger the score,
+ the more similar the pair). For metric learners that learn a distance,
+ the score is simply the opposite of the distance between pairs. All
+ learners have access to this method.
+
+ Parameters
+ ----------
+ pairs : array-like, shape=(n_pairs, 2, n_features) or (n_pairs, 2)
+ 3D Array of pairs to score, with each row corresponding to two points,
+ for 2D array of indices of pairs if the metric learner uses a
+ preprocessor.
+
+ Returns
+ -------
+ scores : `numpy.ndarray` of shape=(n_pairs,)
+ The score of every pair.
+
+ See Also
+ --------
+ get_metric : a method that returns a function to compute the metric between
+ two points. The difference with `pair_score` is that it works on two
+ 1D arrays and cannot use a preprocessor. Besides, the returned function
+ is independent of the metric learner and hence is not modified if the
+ metric learner is.
+ """
+
+ @abstractmethod
+ def pair_distance(self, pairs):
+ """
+ .. versionadded:: 0.7.0 Compute the distance between pairs
+
+ Returns the (pseudo) distance between pairs, when available. For metric
+ learners that do not learn a (pseudo) distance, an error is thrown
+ instead.
+
+ Parameters
+ ----------
+ pairs : array-like, shape=(n_pairs, 2, n_features) or (n_pairs, 2)
+ 3D Array of pairs for which to compute the distance, with each
+ row corresponding to two points, for 2D array of indices of pairs
+ if the metric learner uses a preprocessor.
+
+ Returns
+ -------
+ scores : `numpy.ndarray` of shape=(n_pairs,)
+ The distance between every pair.
+
+ See Also
+ --------
+ get_metric : a method that returns a function to compute the metric between
+ two points. The difference with `pair_distance` is that it works on two
+ 1D arrays and cannot use a preprocessor. Besides, the returned function
+ is independent of the metric learner and hence is not modified if the
+ metric learner is.
"""
def _check_preprocessor(self):
@@ -93,17 +166,23 @@ def _prepare_inputs(self, X, y=None, type_of_inputs='classic',
self._check_preprocessor()
check_is_fitted(self, ['preprocessor_'])
- return check_input(X, y,
+ outs = check_input(X, y,
type_of_inputs=type_of_inputs,
preprocessor=self.preprocessor_,
estimator=self,
tuple_size=getattr(self, '_tuple_size', None),
**kwargs)
+ # Conform to SLEP010
+ if not hasattr(self, 'n_features_in_'):
+ self.n_features_in_ = (outs if y is None else outs[0]).shape[1]
+ return outs
@abstractmethod
def get_metric(self):
- """Returns a function that takes as input two 1D arrays and outputs the
- learned metric score on these two points.
+ """Returns a function that takes as input two 1D arrays and outputs
+ the value of the learned metric on these two points. Depending on the
+ algorithm, it can return a distance or a similarity function between
+ pairs.
This function will be independent from the metric learner that learned it
(it will not be modified if the initial metric learner is modified),
@@ -136,15 +215,25 @@ def get_metric(self):
See Also
--------
- score_pairs : a method that returns the metric score between several pairs
- of points. Unlike `get_metric`, this is a method of the metric learner
- and therefore can change if the metric learner changes. Besides, it can
- use the metric learner's preprocessor, and works on concatenated arrays.
+ pair_distance : a method that returns the distance between several
+ pairs of points. Unlike `get_metric`, this is a method of the metric
+ learner and therefore can change if the metric learner changes. Besides,
+ it can use the metric learner's preprocessor, and works on concatenated
+ arrays.
+
+ pair_score : a method that returns the similarity score between
+ several pairs of points. Unlike `get_metric`, this is a method of the
+ metric learner and therefore can change if the metric learner changes.
+ Besides, it can use the metric learner's preprocessor, and works on
+ concatenated arrays.
"""
class MetricTransformer(metaclass=ABCMeta):
-
+ """
+ Base class for all learners that can transform data into a new space
+ with the metric learned.
+ """
@abstractmethod
def transform(self, X):
"""Applies the metric transformation.
@@ -182,13 +271,92 @@ class MahalanobisMixin(BaseMetricLearner, MetricTransformer,
"""
def score_pairs(self, pairs):
- r"""Returns the learned Mahalanobis distance between pairs.
+ r"""
+ Returns the learned Mahalanobis distance between pairs.
+
+ This distance is defined as: :math:`d_M(x, x') = \\sqrt{(x-x')^T M (x-x')}`
+ where ``M`` is the learned Mahalanobis matrix, for every pair of points
+ ``x`` and ``x'``. This corresponds to the euclidean distance between
+ embeddings of the points in a new space, obtained through a linear
+ transformation. Indeed, we have also: :math:`d_M(x, x') = \\sqrt{(x_e -
+ x_e')^T (x_e- x_e')}`, with :math:`x_e = L x` (See
+ :class:`MahalanobisMixin`).
+
+ .. deprecated:: 0.7.0
+ Please use `pair_distance` instead.
+
+ .. warning::
+ This method will be removed in 0.8.0. Please refer to `pair_distance`
+ or `pair_score`. This change will occur in order to add learners
+ that don't necessarily learn a Mahalanobis distance.
+
+ Parameters
+ ----------
+ pairs : array-like, shape=(n_pairs, 2, n_features) or (n_pairs, 2)
+ 3D Array of pairs to score, with each row corresponding to two points,
+ for 2D array of indices of pairs if the metric learner uses a
+ preprocessor.
+
+ Returns
+ -------
+ scores : `numpy.ndarray` of shape=(n_pairs,)
+ The learned Mahalanobis distance for every pair.
- This distance is defined as: :math:`d_M(x, x') = \sqrt{(x-x')^T M (x-x')}`
+ See Also
+ --------
+ get_metric : a method that returns a function to compute the metric between
+ two points. The difference with `score_pairs` is that it works on two
+ 1D arrays and cannot use a preprocessor. Besides, the returned function
+ is independent of the metric learner and hence is not modified if the
+ metric learner is.
+
+ :ref:`mahalanobis_distances` : The section of the project documentation
+ that describes Mahalanobis Distances.
+ """
+ dpr_msg = ("score_pairs will be deprecated in release 0.7.0. "
+ "Use pair_score to compute similarity scores, or "
+ "pair_distances to compute distances.")
+ warnings.warn(dpr_msg, category=FutureWarning)
+ return self.pair_distance(pairs)
+
+ def pair_score(self, pairs):
+ """
+ Returns the opposite of the learned Mahalanobis distance between pairs.
+
+ Parameters
+ ----------
+ pairs : array-like, shape=(n_pairs, 2, n_features) or (n_pairs, 2)
+ 3D Array of pairs to score, with each row corresponding to two points,
+ for 2D array of indices of pairs if the metric learner uses a
+ preprocessor.
+
+ Returns
+ -------
+ scores : `numpy.ndarray` of shape=(n_pairs,)
+ The opposite of the learned Mahalanobis distance for every pair.
+
+ See Also
+ --------
+ get_metric : a method that returns a function to compute the metric between
+ two points. The difference with `pair_score` is that it works on two
+ 1D arrays and cannot use a preprocessor. Besides, the returned function
+ is independent of the metric learner and hence is not modified if the
+ metric learner is.
+
+ :ref:`mahalanobis_distances` : The section of the project documentation
+ that describes Mahalanobis Distances.
+ """
+ return -1 * self.pair_distance(pairs)
+
+ def pair_distance(self, pairs):
+ """
+ Returns the learned Mahalanobis distance between pairs.
+
+ This distance is defined as: :math:`d_M(x, x') = \\sqrt{(x-x')^T M (x-x')}`
where ``M`` is the learned Mahalanobis matrix, for every pair of points
``x`` and ``x'``. This corresponds to the euclidean distance between
embeddings of the points in a new space, obtained through a linear
- transformation. Indeed, we have also: :math:`d_M(x, x') = \sqrt{(x_e -
+ transformation. Indeed, we have also: :math:`d_M(x, x') = \\sqrt{(x_e -
x_e')^T (x_e- x_e')}`, with :math:`x_e = L x` (See
:class:`MahalanobisMixin`).
@@ -207,10 +375,10 @@ def score_pairs(self, pairs):
See Also
--------
get_metric : a method that returns a function to compute the metric between
- two points. The difference with `score_pairs` is that it works on two 1D
- arrays and cannot use a preprocessor. Besides, the returned function is
- independent of the metric learner and hence is not modified if the metric
- learner is.
+ two points. The difference with `pair_distance` is that it works on two
+ 1D arrays and cannot use a preprocessor. Besides, the returned function
+ is independent of the metric learner and hence is not modified if the
+ metric learner is.
:ref:`mahalanobis_distances` : The section of the project documentation
that describes Mahalanobis Distances.
@@ -296,7 +464,7 @@ def get_mahalanobis_matrix(self):
return self.components_.T.dot(self.components_)
-class _PairsClassifierMixin(BaseMetricLearner):
+class _PairsClassifierMixin(BaseMetricLearner, ClassifierMixin):
"""Base class for pairs learners.
Attributes
@@ -307,6 +475,7 @@ class _PairsClassifierMixin(BaseMetricLearner):
classified as dissimilar.
"""
+ classes_ = np.array([0, 1])
_tuple_size = 2 # number of points in a tuple, 2 for pairs
def predict(self, pairs):
@@ -361,7 +530,7 @@ def decision_function(self, pairs):
pairs = check_input(pairs, type_of_inputs='tuples',
preprocessor=self.preprocessor_,
estimator=self, tuple_size=self._tuple_size)
- return - self.score_pairs(pairs)
+ return self.pair_score(pairs)
def score(self, pairs, y):
"""Computes score of pairs similarity prediction.
@@ -409,8 +578,14 @@ def set_threshold(self, threshold):
The pairs classifier with the new threshold set.
"""
check_is_fitted(self, 'preprocessor_')
-
- self.threshold_ = threshold
+ try:
+ self.threshold_ = float(threshold)
+ except TypeError:
+ raise ValueError('Parameter threshold must be a real number. '
+ 'Got {} instead.'.format(type(threshold)))
+ except ValueError:
+ raise ValueError('Parameter threshold must be a real number. '
+ 'Got {} instead.'.format(type(threshold)))
return self
def calibrate_threshold(self, pairs_valid, y_valid, strategy='accuracy',
@@ -466,7 +641,7 @@ def calibrate_threshold(self, pairs_valid, y_valid, strategy='accuracy',
evaluation tool in clinical medicine, MH Zweig, G Campbell -
Clinical chemistry, 1993
- .. [2] most of the code of this function is from scikit-learn's PR #10117
+ .. [2] Most of the code of this function is from scikit-learn's PR #10117
See Also
--------
@@ -578,10 +753,12 @@ def _validate_calibration_params(strategy='accuracy', min_rate=None,
'Got {} instead.'.format(type(beta)))
-class _TripletsClassifierMixin(BaseMetricLearner):
- """Base class for triplets learners.
+class _TripletsClassifierMixin(BaseMetricLearner, ClassifierMixin):
+ """
+ Base class for triplets learners.
"""
+ classes_ = np.array([0, 1])
_tuple_size = 3 # number of points in a tuple, 3 for triplets
def predict(self, triplets):
@@ -602,7 +779,7 @@ def predict(self, triplets):
prediction : `numpy.ndarray` of floats, shape=(n_constraints,)
Predictions of the ordering of pairs, for each triplet.
"""
- return np.sign(self.decision_function(triplets))
+ return 2 * (self.decision_function(triplets) > 0) - 1
def decision_function(self, triplets):
"""Predicts differences between sample distances in input triplets.
@@ -631,8 +808,8 @@ def decision_function(self, triplets):
triplets = check_input(triplets, type_of_inputs='tuples',
preprocessor=self.preprocessor_,
estimator=self, tuple_size=self._tuple_size)
- return (self.score_pairs(triplets[:, [0, 2]]) -
- self.score_pairs(triplets[:, :2]))
+ return (self.pair_score(triplets[:, :2]) -
+ self.pair_score(triplets[:, [0, 2]]))
def score(self, triplets):
"""Computes score on input triplets.
@@ -662,10 +839,12 @@ def score(self, triplets):
return self.predict(triplets).mean() / 2 + 0.5
-class _QuadrupletsClassifierMixin(BaseMetricLearner):
- """Base class for quadruplets learners.
+class _QuadrupletsClassifierMixin(BaseMetricLearner, ClassifierMixin):
+ """
+ Base class for quadruplets learners.
"""
+ classes_ = np.array([0, 1])
_tuple_size = 4 # number of points in a tuple, 4 for quadruplets
def predict(self, quadruplets):
@@ -716,8 +895,8 @@ def decision_function(self, quadruplets):
quadruplets = check_input(quadruplets, type_of_inputs='tuples',
preprocessor=self.preprocessor_,
estimator=self, tuple_size=self._tuple_size)
- return (self.score_pairs(quadruplets[:, 2:]) -
- self.score_pairs(quadruplets[:, :2]))
+ return (self.pair_score(quadruplets[:, :2]) -
+ self.pair_score(quadruplets[:, 2:]))
def score(self, quadruplets):
"""Computes score on input quadruplets
diff --git a/metric_learn/constraints.py b/metric_learn/constraints.py
index 2d86b819..4993e9ef 100644
--- a/metric_learn/constraints.py
+++ b/metric_learn/constraints.py
@@ -7,6 +7,7 @@
from sklearn.utils import check_random_state
from sklearn.neighbors import NearestNeighbors
+
__all__ = ['Constraints']
@@ -31,21 +32,21 @@ def __init__(self, partial_labels):
partial_labels = np.asanyarray(partial_labels, dtype=int)
self.partial_labels = partial_labels
- def positive_negative_pairs(self, num_constraints, same_length=False,
- random_state=None):
+ def positive_negative_pairs(self, n_constraints, same_length=False,
+ random_state=None, num_constraints='deprecated'):
"""
Generates positive pairs and negative pairs from labeled data.
- Positive pairs are formed by randomly drawing ``num_constraints`` pairs of
+ Positive pairs are formed by randomly drawing ``n_constraints`` pairs of
points with the same label. Negative pairs are formed by randomly drawing
- ``num_constraints`` pairs of points with different label.
+ ``n_constraints`` pairs of points with different label.
In the case where it is not possible to generate enough positive or
negative pairs, a smaller number of pairs will be returned with a warning.
Parameters
----------
- num_constraints : int
+ n_constraints : int
Number of positive and negative constraints to generate.
same_length : bool, optional (default=False)
@@ -55,6 +56,8 @@ def positive_negative_pairs(self, num_constraints, same_length=False,
random_state : int or numpy.RandomState or None, optional (default=None)
A pseudo random number generator object or a seed for it if int.
+ num_constraints : Renamed to n_constraints. Will be deprecated in 0.7.0
+
Returns
-------
a : array-like, shape=(n_constraints,)
@@ -69,10 +72,18 @@ def positive_negative_pairs(self, num_constraints, same_length=False,
d : array-like, shape=(n_constraints,)
1D array of indicators for the right elements of negative pairs.
"""
+ if num_constraints != 'deprecated':
+ warnings.warn('"num_constraints" parameter has been renamed to'
+ ' "n_constraints". It has been deprecated in'
+ ' version 0.6.3 and will be removed in 0.7.0'
+ '', FutureWarning)
+ self.n_constraints = num_constraints
+ else:
+ self.n_constraints = n_constraints
random_state = check_random_state(random_state)
- a, b = self._pairs(num_constraints, same_label=True,
+ a, b = self._pairs(n_constraints, same_label=True,
random_state=random_state)
- c, d = self._pairs(num_constraints, same_label=False,
+ c, d = self._pairs(n_constraints, same_label=False,
random_state=random_state)
if same_length and len(a) != len(c):
n = min(len(a), len(c))
@@ -95,12 +106,14 @@ def generate_knntriplets(self, X, k_genuine, k_impostor):
Parameters
----------
- X : (n x d) matrix
- Input data, where each row corresponds to a single instance.
- k_genuine : int
- Number of neighbors of the same class to be taken into account.
- k_impostor : int
- Number of neighbors of different classes to be taken into account.
+ X : (n x d) matrix
+ Input data, where each row corresponds to a single instance.
+
+ k_genuine : int
+ Number of neighbors of the same class to be taken into account.
+
+ k_impostor : int
+ Number of neighbors of different classes to be taken into account.
Returns
-------
@@ -188,15 +201,15 @@ def generate_knntriplets(self, X, k_genuine, k_impostor):
return triplets
- def _pairs(self, num_constraints, same_label=True, max_iter=10,
+ def _pairs(self, n_constraints, same_label=True, max_iter=10,
random_state=np.random):
known_label_idx, = np.where(self.partial_labels >= 0)
known_labels = self.partial_labels[known_label_idx]
num_labels = len(known_labels)
ab = set()
it = 0
- while it < max_iter and len(ab) < num_constraints:
- nc = num_constraints - len(ab)
+ while it < max_iter and len(ab) < n_constraints:
+ nc = n_constraints - len(ab)
for aidx in random_state.randint(num_labels, size=nc):
if same_label:
mask = known_labels[aidx] == known_labels
@@ -207,25 +220,26 @@ def _pairs(self, num_constraints, same_label=True, max_iter=10,
if len(b_choices) > 0:
ab.add((aidx, random_state.choice(b_choices)))
it += 1
- if len(ab) < num_constraints:
+ if len(ab) < n_constraints:
warnings.warn("Only generated %d %s constraints (requested %d)" % (
- len(ab), 'positive' if same_label else 'negative', num_constraints))
- ab = np.array(list(ab)[:num_constraints], dtype=int)
+ len(ab), 'positive' if same_label else 'negative', n_constraints))
+ ab = np.array(list(ab)[:n_constraints], dtype=int)
return known_label_idx[ab.T]
- def chunks(self, num_chunks=100, chunk_size=2, random_state=None):
+ def chunks(self, n_chunks=100, chunk_size=2, random_state=None,
+ num_chunks='deprecated'):
"""
Generates chunks from labeled data.
- Each of ``num_chunks`` chunks is composed of ``chunk_size`` points from
+ Each of ``n_chunks`` chunks is composed of ``chunk_size`` points from
the same class drawn at random. Each point can belong to at most 1 chunk.
- In the case where there is not enough points to generate ``num_chunks``
+ In the case where there is not enough points to generate ``n_chunks``
chunks of size ``chunk_size``, a ValueError will be raised.
Parameters
----------
- num_chunks : int, optional (default=100)
+ n_chunks : int, optional (default=100)
Number of chunks to generate.
chunk_size : int, optional (default=2)
@@ -234,12 +248,20 @@ def chunks(self, num_chunks=100, chunk_size=2, random_state=None):
random_state : int or numpy.RandomState or None, optional (default=None)
A pseudo random number generator object or a seed for it if int.
+ num_chunks : Renamed to n_chunks. Will be deprecated in 0.7.0
+
Returns
-------
chunks : array-like, shape=(n_samples,)
1D array of chunk indicators, where -1 indicates that the point does not
belong to any chunk.
"""
+ if num_chunks != 'deprecated':
+ warnings.warn('"num_chunks" parameter has been renamed to'
+ ' "n_chunks". It has been deprecated in'
+ ' version 0.6.3 and will be removed in 0.7.0'
+ '', FutureWarning)
+ n_chunks = num_chunks
random_state = check_random_state(random_state)
chunks = -np.ones_like(self.partial_labels, dtype=int)
uniq, lookup = np.unique(self.partial_labels, return_inverse=True)
@@ -247,13 +269,13 @@ def chunks(self, num_chunks=100, chunk_size=2, random_state=None):
all_inds = [set(np.where(lookup == c)[0]) for c in range(len(uniq))
if c not in unknown_uniq]
max_chunks = int(np.sum([len(s) // chunk_size for s in all_inds]))
- if max_chunks < num_chunks:
+ if max_chunks < n_chunks:
raise ValueError(('Not enough possible chunks of %d elements in each'
' class to form expected %d chunks - maximum number'
' of chunks is %d'
- ) % (chunk_size, num_chunks, max_chunks))
+ ) % (chunk_size, n_chunks, max_chunks))
idx = 0
- while idx < num_chunks and all_inds:
+ while idx < n_chunks and all_inds:
if len(all_inds) == 1:
c = 0
else:
diff --git a/metric_learn/covariance.py b/metric_learn/covariance.py
index 3b218e6d..2c05b28d 100644
--- a/metric_learn/covariance.py
+++ b/metric_learn/covariance.py
@@ -42,6 +42,10 @@ def __init__(self, preprocessor=None):
def fit(self, X, y=None):
"""
+ Calculates the covariance matrix of the input data.
+
+ Parameters
+ ----------
X : data matrix, (n x d)
y : unused
"""
diff --git a/metric_learn/itml.py b/metric_learn/itml.py
index 43872b60..9537eec2 100644
--- a/metric_learn/itml.py
+++ b/metric_learn/itml.py
@@ -9,6 +9,7 @@
from .base_metric import _PairsClassifierMixin, MahalanobisMixin
from .constraints import Constraints, wrap_pairs
from ._util import components_from_metric, _initialize_metric_mahalanobis
+import warnings
class _BaseITML(MahalanobisMixin):
@@ -16,12 +17,20 @@ class _BaseITML(MahalanobisMixin):
_tuple_size = 2 # constraints are pairs
- def __init__(self, gamma=1., max_iter=1000, convergence_threshold=1e-3,
+ def __init__(self, gamma=1., max_iter=1000, tol=1e-3,
prior='identity', verbose=False,
- preprocessor=None, random_state=None):
+ preprocessor=None, random_state=None,
+ convergence_threshold='deprecated'):
+ if convergence_threshold != 'deprecated':
+ warnings.warn('"convergence_threshold" parameter has been '
+ ' renamed to "tol". It has been deprecated in'
+ ' version 0.6.3 and will be removed in 0.7.0'
+ '', FutureWarning)
+ tol = convergence_threshold
+ self.convergence_threshold = 'deprecated' # Avoid errors
self.gamma = gamma
self.max_iter = max_iter
- self.convergence_threshold = convergence_threshold
+ self.tol = tol
self.prior = prior
self.verbose = verbose
self.random_state = random_state
@@ -32,7 +41,7 @@ def _fit(self, pairs, y, bounds=None):
type_of_inputs='tuples')
# init bounds
if bounds is None:
- X = np.vstack({tuple(row) for row in pairs.reshape(-1, pairs.shape[2])})
+ X = np.unique(np.vstack(pairs), axis=0)
self.bounds_ = np.percentile(pairwise_distances(X), (5, 95))
else:
bounds = check_array(bounds, allow_nd=False, ensure_min_samples=0,
@@ -86,7 +95,7 @@ def _fit(self, pairs, y, bounds=None):
conv = np.inf
break
conv = np.abs(lambdaold - _lambda).sum() / normsum
- if conv < self.convergence_threshold:
+ if conv < self.tol:
break
lambdaold = _lambda.copy()
if self.verbose:
@@ -122,7 +131,7 @@ class ITML(_BaseITML, _PairsClassifierMixin):
max_iter : int, optional (default=1000)
Maximum number of iteration of the optimization procedure.
- convergence_threshold : float, optional (default=1e-3)
+ tol : float, optional (default=1e-3)
Convergence tolerance.
prior : string or numpy array, optional (default='identity')
@@ -158,6 +167,8 @@ class ITML(_BaseITML, _PairsClassifierMixin):
A pseudo random number generator object or a seed for it if int. If
``prior='random'``, ``random_state`` is used to set the prior.
+ convergence_threshold : Renamed to tol. Will be deprecated in 0.7.0
+
Attributes
----------
bounds_ : `numpy.ndarray`, shape=(2,)
@@ -198,7 +209,7 @@ class ITML(_BaseITML, _PairsClassifierMixin):
----------
.. [1] Jason V. Davis, et al. `Information-theoretic Metric Learning
`_. ICML 2007.
+ /DavisKJSD07_ICML.pdf>`_. ICML 2007.
"""
def fit(self, pairs, y, bounds=None, calibration_params=None):
@@ -260,10 +271,10 @@ class ITML_Supervised(_BaseITML, TransformerMixin):
max_iter : int, optional (default=1000)
Maximum number of iterations of the optimization procedure.
- convergence_threshold : float, optional (default=1e-3)
+ tol : float, optional (default=1e-3)
Tolerance of the optimization procedure.
- num_constraints : int, optional (default=None)
+ n_constraints : int, optional (default=None)
Number of constraints to generate. If None, default to `20 *
num_classes**2`.
@@ -302,6 +313,9 @@ class ITML_Supervised(_BaseITML, TransformerMixin):
case, `random_state` is also used to randomly sample constraints from
labels.
+ num_constraints : Renamed to n_constraints. Will be deprecated in 0.7.0
+
+ convergence_threshold : Renamed to tol. Will be deprecated in 0.7.0
Attributes
----------
@@ -328,7 +342,7 @@ class ITML_Supervised(_BaseITML, TransformerMixin):
>>> iris_data = load_iris()
>>> X = iris_data['data']
>>> Y = iris_data['target']
- >>> itml = ITML_Supervised(num_constraints=200)
+ >>> itml = ITML_Supervised(n_constraints=200)
>>> itml.fit(X, Y)
See Also
@@ -338,14 +352,26 @@ class ITML_Supervised(_BaseITML, TransformerMixin):
that describes the supervised version of weakly supervised estimators.
"""
- def __init__(self, gamma=1.0, max_iter=1000, convergence_threshold=1e-3,
- num_constraints=None, prior='identity',
- verbose=False, preprocessor=None, random_state=None):
+ def __init__(self, gamma=1.0, max_iter=1000, tol=1e-3,
+ n_constraints=None, prior='identity',
+ verbose=False, preprocessor=None, random_state=None,
+ num_constraints='deprecated',
+ convergence_threshold='deprecated'):
_BaseITML.__init__(self, gamma=gamma, max_iter=max_iter,
- convergence_threshold=convergence_threshold,
+ tol=tol,
prior=prior, verbose=verbose,
- preprocessor=preprocessor, random_state=random_state)
- self.num_constraints = num_constraints
+ preprocessor=preprocessor,
+ random_state=random_state,
+ convergence_threshold=convergence_threshold)
+ if num_constraints != 'deprecated':
+ warnings.warn('"num_constraints" parameter has been renamed to'
+ ' "n_constraints". It has been deprecated in'
+ ' version 0.6.3 and will be removed in 0.7.0'
+ '', FutureWarning)
+ n_constraints = num_constraints
+ self.n_constraints = n_constraints
+ # Avoid test get_params from failing (all params passed sholud be set)
+ self.num_constraints = 'deprecated'
def fit(self, X, y, bounds=None):
"""Create constraints from labels and learn the ITML model.
@@ -369,13 +395,13 @@ def fit(self, X, y, bounds=None):
points in the training data `X`.
"""
X, y = self._prepare_inputs(X, y, ensure_min_samples=2)
- num_constraints = self.num_constraints
- if num_constraints is None:
+ n_constraints = self.n_constraints
+ if n_constraints is None:
num_classes = len(np.unique(y))
- num_constraints = 20 * num_classes**2
+ n_constraints = 20 * num_classes**2
c = Constraints(y)
- pos_neg = c.positive_negative_pairs(num_constraints,
+ pos_neg = c.positive_negative_pairs(n_constraints,
random_state=self.random_state)
pairs, y = wrap_pairs(X, pos_neg)
return _BaseITML._fit(self, pairs, y, bounds=bounds)
diff --git a/metric_learn/lfda.py b/metric_learn/lfda.py
index bfa3275e..82ae20eb 100644
--- a/metric_learn/lfda.py
+++ b/metric_learn/lfda.py
@@ -65,7 +65,7 @@ class LFDA(MahalanobisMixin, TransformerMixin):
>>> lfda.fit(X, Y)
References
- ------------------
+ ----------
.. [1] Masashi Sugiyama. `Dimensionality Reduction of Multimodal Labeled
Data by Local Fisher Discriminant Analysis
`_. JMLR 2007.
diff --git a/metric_learn/lmnn.py b/metric_learn/lmnn.py
index 8bdc4bf0..47bb065f 100644
--- a/metric_learn/lmnn.py
+++ b/metric_learn/lmnn.py
@@ -5,6 +5,7 @@
from collections import Counter
from sklearn.metrics import euclidean_distances
from sklearn.base import TransformerMixin
+import warnings
from ._util import _initialize_components, _check_n_components
from .base_metric import MahalanobisMixin
@@ -63,7 +64,7 @@ class LMNN(MahalanobisMixin, TransformerMixin):
:meth:`fit` and n_features_a must be less than or equal to that.
If ``n_components`` is not None, n_features_a must match it.
- k : int, optional (default=3)
+ n_neighbors : int, optional (default=3)
Number of neighbors to consider, not including self-edges.
min_iter : int, optional (default=50)
@@ -99,6 +100,8 @@ class LMNN(MahalanobisMixin, TransformerMixin):
transformation. If ``init='pca'``, ``random_state`` is passed as an
argument to PCA when initializing the transformation.
+ k : Renamed to n_neighbors. Will be deprecated in 0.7.0
+
Attributes
----------
n_iter_ : `int`
@@ -116,7 +119,7 @@ class LMNN(MahalanobisMixin, TransformerMixin):
>>> iris_data = load_iris()
>>> X = iris_data['data']
>>> Y = iris_data['target']
- >>> lmnn = LMNN(k=5, learn_rate=1e-6)
+ >>> lmnn = LMNN(n_neighbors=5, learn_rate=1e-6)
>>> lmnn.fit(X, Y, verbose=False)
References
@@ -128,12 +131,19 @@ class LMNN(MahalanobisMixin, TransformerMixin):
2005.
"""
- def __init__(self, init='auto', k=3, min_iter=50, max_iter=1000,
+ def __init__(self, init='auto', n_neighbors=3, min_iter=50, max_iter=1000,
learn_rate=1e-7, regularization=0.5, convergence_tol=0.001,
verbose=False, preprocessor=None,
- n_components=None, random_state=None):
+ n_components=None, random_state=None, k='deprecated'):
self.init = init
- self.k = k
+ if k != 'deprecated':
+ warnings.warn('"num_chunks" parameter has been renamed to'
+ ' "n_chunks". It has been deprecated in'
+ ' version 0.6.3 and will be removed in 0.7.0'
+ '', FutureWarning)
+ n_neighbors = k
+ self.k = 'deprecated' # To avoid no_attribute error
+ self.n_neighbors = n_neighbors
self.min_iter = min_iter
self.max_iter = max_iter
self.learn_rate = learn_rate
@@ -145,7 +155,7 @@ def __init__(self, init='auto', k=3, min_iter=50, max_iter=1000,
super(LMNN, self).__init__(preprocessor)
def fit(self, X, y):
- k = self.k
+ k = self.n_neighbors
reg = self.regularization
learn_rate = self.learn_rate
@@ -162,7 +172,7 @@ def fit(self, X, y):
self.verbose,
random_state=self.random_state)
required_k = np.bincount(label_inds).min()
- if self.k > required_k:
+ if self.n_neighbors > required_k:
raise ValueError('not enough class labels for specified k'
' (smallest class has %d)' % required_k)
@@ -275,12 +285,12 @@ def _loss_grad(self, X, L, dfG, k, reg, target_neighbors, label_inds):
return 2 * G, objective, total_active
def _select_targets(self, X, label_inds):
- target_neighbors = np.empty((X.shape[0], self.k), dtype=int)
+ target_neighbors = np.empty((X.shape[0], self.n_neighbors), dtype=int)
for label in self.labels_:
inds, = np.nonzero(label_inds == label)
dd = euclidean_distances(X[inds], squared=True)
np.fill_diagonal(dd, np.inf)
- nn = np.argsort(dd)[..., :self.k]
+ nn = np.argsort(dd)[..., :self.n_neighbors]
target_neighbors[inds] = inds[nn]
return target_neighbors
diff --git a/metric_learn/lsml.py b/metric_learn/lsml.py
index 28f65ce7..af7fa95b 100644
--- a/metric_learn/lsml.py
+++ b/metric_learn/lsml.py
@@ -9,6 +9,7 @@
from .base_metric import _QuadrupletsClassifierMixin, MahalanobisMixin
from .constraints import Constraints
from ._util import components_from_metric, _initialize_metric_mahalanobis
+import warnings
class _BaseLSML(MahalanobisMixin):
@@ -261,11 +262,11 @@ class LSML_Supervised(_BaseLSML, TransformerMixin):
(n_features, n_features), that will be used as such to set the
prior.
- num_constraints: int, optional (default=None)
+ n_constraints: int, optional (default=None)
Number of constraints to generate. If None, default to `20 *
num_classes**2`.
- weights : (num_constraints,) array of floats, optional (default=None)
+ weights : (n_constraints,) array of floats, optional (default=None)
Relative weight given to each constraint. If None, defaults to uniform
weights.
@@ -282,6 +283,8 @@ class LSML_Supervised(_BaseLSML, TransformerMixin):
prior. In any case, `random_state` is also used to randomly sample
constraints from labels.
+ num_constraints : Renamed to n_constraints. Will be deprecated in 0.7.0
+
Examples
--------
>>> from metric_learn import LSML_Supervised
@@ -289,7 +292,7 @@ class LSML_Supervised(_BaseLSML, TransformerMixin):
>>> iris_data = load_iris()
>>> X = iris_data['data']
>>> Y = iris_data['target']
- >>> lsml = LSML_Supervised(num_constraints=200)
+ >>> lsml = LSML_Supervised(n_constraints=200)
>>> lsml.fit(X, Y)
Attributes
@@ -303,12 +306,22 @@ class LSML_Supervised(_BaseLSML, TransformerMixin):
"""
def __init__(self, tol=1e-3, max_iter=1000, prior='identity',
- num_constraints=None, weights=None,
- verbose=False, preprocessor=None, random_state=None):
+ n_constraints=None, weights=None,
+ verbose=False, preprocessor=None, random_state=None,
+ num_constraints='deprecated'):
_BaseLSML.__init__(self, tol=tol, max_iter=max_iter, prior=prior,
verbose=verbose, preprocessor=preprocessor,
random_state=random_state)
- self.num_constraints = num_constraints
+ if num_constraints != 'deprecated':
+ warnings.warn('"num_constraints" parameter has been renamed to'
+ ' "n_constraints". It has been deprecated in'
+ ' version 0.6.3 and will be removed in 0.7.0'
+ '', FutureWarning)
+ self.n_constraints = num_constraints
+ else:
+ self.n_constraints = n_constraints
+ # Avoid test get_params from failing (all params passed sholud be set)
+ self.num_constraints = 'deprecated'
self.weights = weights
def fit(self, X, y):
@@ -323,13 +336,13 @@ def fit(self, X, y):
Data labels.
"""
X, y = self._prepare_inputs(X, y, ensure_min_samples=2)
- num_constraints = self.num_constraints
- if num_constraints is None:
+ n_constraints = self.n_constraints
+ if n_constraints is None:
num_classes = len(np.unique(y))
- num_constraints = 20 * num_classes**2
+ n_constraints = 20 * num_classes**2
c = Constraints(y)
- pos_neg = c.positive_negative_pairs(num_constraints, same_length=True,
+ pos_neg = c.positive_negative_pairs(n_constraints, same_length=True,
random_state=self.random_state)
return _BaseLSML._fit(self, X[np.column_stack(pos_neg)],
weights=self.weights)
diff --git a/metric_learn/mmc.py b/metric_learn/mmc.py
index 1ff30b1e..5cf166fd 100644
--- a/metric_learn/mmc.py
+++ b/metric_learn/mmc.py
@@ -6,19 +6,28 @@
from .base_metric import _PairsClassifierMixin, MahalanobisMixin
from .constraints import Constraints, wrap_pairs
from ._util import components_from_metric, _initialize_metric_mahalanobis
+import warnings
class _BaseMMC(MahalanobisMixin):
_tuple_size = 2 # constraints are pairs
- def __init__(self, max_iter=100, max_proj=10000, convergence_threshold=1e-3,
+ def __init__(self, max_iter=100, max_proj=10000, tol=1e-3,
init='identity', diagonal=False,
diagonal_c=1.0, verbose=False, preprocessor=None,
- random_state=None):
+ random_state=None,
+ convergence_threshold='deprecated'):
+ if convergence_threshold != 'deprecated':
+ warnings.warn('"convergence_threshold" parameter has been '
+ ' renamed to "tol". It has been deprecated in'
+ ' version 0.6.3 and will be removed in 0.7.0'
+ '', FutureWarning)
+ tol = convergence_threshold
+ self.convergence_threshold = 'deprecated' # Avoid errors
self.max_iter = max_iter
self.max_proj = max_proj
- self.convergence_threshold = convergence_threshold
+ self.tol = tol
self.init = init
self.diagonal = diagonal
self.diagonal_c = diagonal_c
@@ -145,13 +154,13 @@ def _fit_full(self, pairs, y):
A[:] = A_old + alpha * M
delta = np.linalg.norm(alpha * M) / np.linalg.norm(A_old)
- if delta < self.convergence_threshold:
+ if delta < self.tol:
break
if self.verbose:
print('mmc iter: %d, conv = %f, projections = %d' %
(cycle, delta, it + 1))
- if delta > self.convergence_threshold:
+ if delta > self.tol:
self.converged_ = False
if self.verbose:
print('mmc did not converge, conv = %f' % (delta,))
@@ -185,7 +194,7 @@ def _fit_diag(self, pairs, y):
reduction = 2.0
w = np.diag(self.A_).copy()
- while error > self.convergence_threshold and it < self.max_iter:
+ while error > self.tol and it < self.max_iter:
fD0, fD_1st_d, fD_2nd_d = self._D_constraint(neg_pairs, w)
obj_initial = np.dot(s_sum, w) + self.diagonal_c * fD0
@@ -332,7 +341,7 @@ class MMC(_BaseMMC, _PairsClassifierMixin):
max_proj : int, optional (default=10000)
Maximum number of projection steps.
- convergence_threshold : float, optional (default=1e-3)
+ tol : float, optional (default=1e-3)
Convergence threshold for the optimization procedure.
init : string or numpy array, optional (default='identity')
@@ -377,6 +386,8 @@ class MMC(_BaseMMC, _PairsClassifierMixin):
``init='random'``, ``random_state`` is used to initialize the random
transformation.
+ convergence_threshold : Renamed to tol. Will be deprecated in 0.7.0
+
Attributes
----------
n_iter_ : `int`
@@ -469,10 +480,10 @@ class MMC_Supervised(_BaseMMC, TransformerMixin):
max_proj : int, optional (default=10000)
Maximum number of projection steps.
- convergence_threshold : float, optional (default=1e-3)
+ tol : float, optional (default=1e-3)
Convergence threshold for the optimization procedure.
- num_constraints: int, optional (default=None)
+ n_constraints: int, optional (default=None)
Number of constraints to generate. If None, default to `20 *
num_classes**2`.
@@ -518,6 +529,10 @@ class MMC_Supervised(_BaseMMC, TransformerMixin):
Mahalanobis matrix. In any case, `random_state` is also used to
randomly sample constraints from labels.
+ num_constraints : Renamed to n_constraints. Will be deprecated in 0.7.0
+
+ convergence_threshold : Renamed to tol. Will be deprecated in 0.7.0
+
Examples
--------
>>> from metric_learn import MMC_Supervised
@@ -525,7 +540,7 @@ class MMC_Supervised(_BaseMMC, TransformerMixin):
>>> iris_data = load_iris()
>>> X = iris_data['data']
>>> Y = iris_data['target']
- >>> mmc = MMC_Supervised(num_constraints=200)
+ >>> mmc = MMC_Supervised(n_constraints=200)
>>> mmc.fit(X, Y)
Attributes
@@ -538,16 +553,29 @@ class MMC_Supervised(_BaseMMC, TransformerMixin):
metric (See function `components_from_metric`.)
"""
- def __init__(self, max_iter=100, max_proj=10000, convergence_threshold=1e-6,
- num_constraints=None, init='identity',
+ def __init__(self, max_iter=100, max_proj=10000, tol=1e-6,
+ n_constraints=None, init='identity',
diagonal=False, diagonal_c=1.0, verbose=False,
- preprocessor=None, random_state=None):
+ preprocessor=None, random_state=None,
+ num_constraints='deprecated',
+ convergence_threshold='deprecated'):
_BaseMMC.__init__(self, max_iter=max_iter, max_proj=max_proj,
- convergence_threshold=convergence_threshold,
+ tol=tol,
init=init, diagonal=diagonal,
diagonal_c=diagonal_c, verbose=verbose,
- preprocessor=preprocessor, random_state=random_state)
- self.num_constraints = num_constraints
+ preprocessor=preprocessor,
+ random_state=random_state,
+ convergence_threshold=convergence_threshold)
+ if num_constraints != 'deprecated':
+ warnings.warn('"num_constraints" parameter has been renamed to'
+ ' "n_constraints". It has been deprecated in'
+ ' version 0.6.3 and will be removed in 0.7.0'
+ '', FutureWarning)
+ self.n_constraints = num_constraints
+ else:
+ self.n_constraints = n_constraints
+ # Avoid test get_params from failing (all params passed sholud be set)
+ self.num_constraints = 'deprecated'
def fit(self, X, y):
"""Create constraints from labels and learn the MMC model.
@@ -561,13 +589,13 @@ def fit(self, X, y):
Data labels.
"""
X, y = self._prepare_inputs(X, y, ensure_min_samples=2)
- num_constraints = self.num_constraints
- if num_constraints is None:
+ n_constraints = self.n_constraints
+ if n_constraints is None:
num_classes = len(np.unique(y))
- num_constraints = 20 * num_classes**2
+ n_constraints = 20 * num_classes**2
c = Constraints(y)
- pos_neg = c.positive_negative_pairs(num_constraints,
+ pos_neg = c.positive_negative_pairs(n_constraints,
random_state=self.random_state)
pairs, y = wrap_pairs(X, pos_neg)
return _BaseMMC._fit(self, pairs, y)
diff --git a/metric_learn/rca.py b/metric_learn/rca.py
index 34f7f3ff..253b9c92 100644
--- a/metric_learn/rca.py
+++ b/metric_learn/rca.py
@@ -13,13 +13,13 @@
# mean center each chunklet separately
def _chunk_mean_centering(data, chunks):
- num_chunks = chunks.max() + 1
+ n_chunks = chunks.max() + 1
chunk_mask = chunks != -1
# We need to ensure the data is float so that we can substract the
# mean on it
chunk_data = data[chunk_mask].astype(float, copy=False)
chunk_labels = chunks[chunk_mask]
- for c in range(num_chunks):
+ for c in range(n_chunks):
mask = chunk_labels == c
chunk_data[mask] -= chunk_data[mask].mean(axis=0)
@@ -58,7 +58,7 @@ class RCA(MahalanobisMixin, TransformerMixin):
>>> rca.fit(X, chunks)
References
- ------------------
+ ----------
.. [1] Noam Shental, et al. `Adjustment learning and relevant component
analysis `_ .
@@ -112,7 +112,7 @@ def fit(self, X, chunks):
# Fisher Linear Discriminant projection
if dim < X.shape[1]:
total_cov = np.cov(X[chunk_mask], rowvar=0)
- tmp = np.linalg.lstsq(total_cov, inner_cov)[0]
+ tmp = np.linalg.lstsq(total_cov, inner_cov, rcond=None)[0]
vals, vecs = np.linalg.eig(tmp)
inds = np.argsort(vals)[:dim]
A = vecs[:, inds]
@@ -135,14 +135,14 @@ class RCA_Supervised(RCA):
`RCA_Supervised` creates chunks of similar points by first sampling a
class, taking `chunk_size` elements in it, and repeating the process
- `num_chunks` times.
+ `n_chunks` times.
Parameters
----------
n_components : int or None, optional (default=None)
Dimensionality of reduced space (if None, defaults to dimension of X).
- num_chunks: int, optional (default=100)
+ n_chunks: int, optional (default=100)
Number of chunks to generate.
chunk_size: int, optional (default=2)
@@ -156,6 +156,8 @@ class RCA_Supervised(RCA):
A pseudo random number generator object or a seed for it if int.
It is used to randomly sample constraints from labels.
+ num_chunks : Renamed to n_chunks. Will be deprecated in 0.7.0
+
Examples
--------
>>> from metric_learn import RCA_Supervised
@@ -163,7 +165,7 @@ class RCA_Supervised(RCA):
>>> iris_data = load_iris()
>>> X = iris_data['data']
>>> Y = iris_data['target']
- >>> rca = RCA_Supervised(num_chunks=30, chunk_size=2)
+ >>> rca = RCA_Supervised(n_chunks=30, chunk_size=2)
>>> rca.fit(X, Y)
Attributes
@@ -172,17 +174,25 @@ class RCA_Supervised(RCA):
The learned linear transformation ``L``.
"""
- def __init__(self, n_components=None, num_chunks=100, chunk_size=2,
- preprocessor=None, random_state=None):
+ def __init__(self, n_components=None, n_chunks=100, chunk_size=2,
+ preprocessor=None, random_state=None,
+ num_chunks='deprecated'):
"""Initialize the supervised version of `RCA`."""
RCA.__init__(self, n_components=n_components, preprocessor=preprocessor)
- self.num_chunks = num_chunks
+ if num_chunks != 'deprecated':
+ warnings.warn('"num_chunks" parameter has been renamed to'
+ ' "n_chunks". It has been deprecated in'
+ ' version 0.6.3 and will be removed in 0.7.0'
+ '', FutureWarning)
+ n_chunks = num_chunks
+ self.num_chunks = 'deprecated' # To avoid no_attribute error
+ self.n_chunks = n_chunks
self.chunk_size = chunk_size
self.random_state = random_state
def fit(self, X, y):
"""Create constraints from labels and learn the RCA model.
- Needs num_constraints specified in constructor.
+ Needs n_constraints specified in constructor. (Not true?)
Parameters
----------
@@ -192,11 +202,11 @@ def fit(self, X, y):
y : (n) data labels
"""
X, y = self._prepare_inputs(X, y, ensure_min_samples=2)
- chunks = Constraints(y).chunks(num_chunks=self.num_chunks,
+ chunks = Constraints(y).chunks(n_chunks=self.n_chunks,
chunk_size=self.chunk_size,
random_state=self.random_state)
- if self.num_chunks * (self.chunk_size - 1) < X.shape[1]:
+ if self.n_chunks * (self.chunk_size - 1) < X.shape[1]:
warnings.warn('Due to the parameters of RCA_Supervised, '
'the inner covariance matrix is not invertible, '
'so the transformation matrix will contain Nan values. '
diff --git a/metric_learn/scml.py b/metric_learn/scml.py
index c3fde272..fedf393d 100644
--- a/metric_learn/scml.py
+++ b/metric_learn/scml.py
@@ -53,7 +53,7 @@ def _fit(self, triplets, basis=None, n_basis=None):
raise ValueError("batch_size should be an integer, instead it is of type"
" %s" % type(self.batch_size))
- if(self.output_iter > self.max_iter):
+ if self.output_iter > self.max_iter:
raise ValueError("The value of output_iter must be equal or smaller than"
" max_iter.")
@@ -240,6 +240,12 @@ def _generate_bases_dist_diff(self, triplets, X):
raise ValueError("n_basis should be an integer, instead it is of type %s"
% type(self.n_basis))
+ if n_features > n_triplets:
+ raise ValueError(
+ "Number of features (%s) is greater than the number of triplets(%s).\n"
+ "Consider using dimensionality reduction or using another basis "
+ "generation scheme." % (n_features, n_triplets))
+
basis = np.zeros((n_basis, n_features))
# get all positive and negative pairs with lowest index first
@@ -260,11 +266,8 @@ def _generate_bases_dist_diff(self, triplets, X):
start = 0
finish = 0
-
- while(finish != n_basis):
-
+ while finish != n_basis:
# Select triplets to yield diff
-
select_triplet = rng.choice(n_triplets, size=n_features, replace=False)
# select n_features positive differences
@@ -322,9 +325,10 @@ class SCML(_BaseSCML, _TripletsClassifierMixin):
'triplet_diffs', and an array-like of shape (n_basis, n_features).
'triplet_diffs'
- The basis set is constructed from the differences between points of
- `n_basis` positive or negative pairs taken from the triplets
- constrains.
+ The basis set is constructed iteratively from differences between points
+ of `n_features` positive or negative pairs randomly sampled from the
+ triplets constraints. Requires the number of training triplets to be
+ great or equal to `n_features`.
array-like
A matrix of shape (n_basis, n_features), that will be used as
@@ -338,7 +342,7 @@ class SCML(_BaseSCML, _TripletsClassifierMixin):
gamma: float (default = 5e-3)
Learning rate for the optimization algorithm.
- max_iter : int (default = 100000)
+ max_iter : int (default = 10000)
Number of iterations for the algorithm.
output_iter : int (default = 5000)
@@ -377,8 +381,8 @@ class SCML(_BaseSCML, _TripletsClassifierMixin):
`_. \
(AAAI), 2014.
- .. [2] Adapted from original \
- `Matlab implementation.`_.
+ .. [2] Adapted from original `Matlab implementation. \
+ `_.
See Also
--------
@@ -473,13 +477,18 @@ class SCML_Supervised(_BaseSCML, TransformerMixin):
Examples
--------
- >>> from metric_learn import SCML
- >>> triplets = np.array([[[1.2, 3.2], [2.3, 5.5], [2.1, 0.6]],
- >>> [[4.5, 2.3], [2.1, 2.3], [7.3, 3.4]]])
- >>> scml = SCML(random_state=42)
- >>> scml.fit(triplets)
- SCML(beta=1e-5, B=None, max_iter=100000, verbose=False,
- preprocessor=None, random_state=None)
+ >>> from metric_learn import SCML_Supervised
+ >>> from sklearn.datasets import load_iris
+ >>> iris_data = load_iris()
+ >>> X = iris_data['data']
+ >>> Y = iris_data['target']
+ >>> scml = SCML_Supervised(random_state=33)
+ >>> scml.fit(X, Y)
+ SCML_Supervised(random_state=33)
+ >>> scml.score_pairs([[X[0], X[1]], [X[0], X[2]]])
+ array([1.84640733, 1.55984363])
+ >>> scml.get_metric()(X[0], X[1])
+ 1.8464073327922157
References
----------
@@ -487,8 +496,8 @@ class SCML_Supervised(_BaseSCML, TransformerMixin):
`_. \
(AAAI), 2014.
- .. [2] Adapted from original \
- `Matlab implementation.`_.
+ .. [2] Adapted from original `Matlab implementation. \
+ `_.
See Also
--------
@@ -549,7 +558,7 @@ def _initialize_basis_supervised(self, X, y):
case one is selected.
"""
- if self.basis == 'lda':
+ if isinstance(self.basis, str) and self.basis == 'lda':
basis, n_basis = self._generate_bases_LDA(X, y)
else:
basis, n_basis = None, None
@@ -597,8 +606,8 @@ def _generate_bases_LDA(self, X, y):
"should be smaller than %d" %
(n_basis, X.shape[0]*2*num_eig))
- kmeans = KMeans(n_clusters=n_clusters, random_state=self.random_state,
- algorithm='elkan').fit(X)
+ kmeans = KMeans(n_clusters=n_clusters, n_init=10,
+ random_state=self.random_state, algorithm='elkan').fit(X)
cX = kmeans.cluster_centers_
n_scales = 2
@@ -610,10 +619,10 @@ def _generate_bases_LDA(self, X, y):
k_class = np.vstack((np.minimum(class_count, scales[0]),
np.minimum(class_count, scales[1])))
- idx_set = [np.zeros((n_clusters, sum(k_class[0, :])), dtype=np.int),
- np.zeros((n_clusters, sum(k_class[1, :])), dtype=np.int)]
+ idx_set = [np.zeros((n_clusters, sum(k_class[0, :])), dtype=np.int64),
+ np.zeros((n_clusters, sum(k_class[1, :])), dtype=np.int64)]
- start_finish_indices = np.hstack((np.zeros((2, 1), np.int),
+ start_finish_indices = np.hstack((np.zeros((2, 1), np.int64),
k_class)).cumsum(axis=1)
neigh = NearestNeighbors()
diff --git a/metric_learn/sdml.py b/metric_learn/sdml.py
index a0736ffa..c4c427b9 100644
--- a/metric_learn/sdml.py
+++ b/metric_learn/sdml.py
@@ -6,7 +6,13 @@
import numpy as np
from sklearn.base import TransformerMixin
from scipy.linalg import pinvh
-from sklearn.covariance import graphical_lasso
+try:
+ from sklearn.covariance._graph_lasso import (
+ _graphical_lasso as graphical_lasso
+ )
+except ImportError:
+ from sklearn.covariance import graphical_lasso
+
from sklearn.exceptions import ConvergenceWarning
from .base_metric import MahalanobisMixin, _PairsClassifierMixin
@@ -43,6 +49,9 @@ def _fit(self, pairs, y):
print("SDML will use skggm's graphical lasso solver.")
pairs, y = self._prepare_inputs(pairs, y,
type_of_inputs='tuples')
+ n_features = pairs.shape[2]
+ if n_features < 2:
+ raise ValueError(f"Cannot fit SDML with {n_features} feature(s)")
# set up (the inverse of) the prior M
# if the prior is the default (None), we raise a warning
@@ -76,13 +85,14 @@ def _fit(self, pairs, y):
msg=self.verbose,
Theta0=theta0, Sigma0=sigma0)
else:
- _, M = graphical_lasso(emp_cov, alpha=self.sparsity_param,
- verbose=self.verbose,
- cov_init=sigma0)
+ _, M, *_ = graphical_lasso(emp_cov, alpha=self.sparsity_param,
+ verbose=self.verbose,
+ cov_init=sigma0)
raised_error = None
w_mahalanobis, _ = np.linalg.eigh(M)
not_spd = any(w_mahalanobis < 0.)
not_finite = not np.isfinite(M).all()
+ # TODO: Narrow this to the specific exceptions we expect.
except Exception as e:
raised_error = e
not_spd = False # not_spd not applicable here so we set to False
@@ -177,7 +187,7 @@ class SDML(_BaseSDML, _PairsClassifierMixin):
>>> iris_data = load_iris()
>>> X = iris_data['data']
>>> Y = iris_data['target']
- >>> sdml = SDML_Supervised(num_constraints=200)
+ >>> sdml = SDML_Supervised(n_constraints=200)
>>> sdml.fit(X, Y)
References
@@ -262,7 +272,7 @@ class SDML_Supervised(_BaseSDML, TransformerMixin):
(n_features, n_features), that will be used as such to set the
prior.
- num_constraints : int, optional (default=None)
+ n_constraints : int, optional (default=None)
Number of constraints to generate. If None, defaults to `20 *
num_classes**2`.
@@ -279,6 +289,8 @@ class SDML_Supervised(_BaseSDML, TransformerMixin):
prior. In any case, `random_state` is also used to randomly sample
constraints from labels.
+ num_constraints : Renamed to n_constraints. Will be deprecated in 0.7.0
+
Attributes
----------
components_ : `numpy.ndarray`, shape=(n_features, n_features)
@@ -293,13 +305,22 @@ class SDML_Supervised(_BaseSDML, TransformerMixin):
"""
def __init__(self, balance_param=0.5, sparsity_param=0.01, prior='identity',
- num_constraints=None, verbose=False, preprocessor=None,
- random_state=None):
+ n_constraints=None, verbose=False, preprocessor=None,
+ random_state=None, num_constraints='deprecated'):
_BaseSDML.__init__(self, balance_param=balance_param,
sparsity_param=sparsity_param, prior=prior,
verbose=verbose,
preprocessor=preprocessor, random_state=random_state)
- self.num_constraints = num_constraints
+ if num_constraints != 'deprecated':
+ warnings.warn('"num_constraints" parameter has been renamed to'
+ ' "n_constraints". It has been deprecated in'
+ ' version 0.6.3 and will be removed in 0.7.0'
+ '', FutureWarning)
+ self.n_constraints = num_constraints
+ else:
+ self.n_constraints = n_constraints
+ # Avoid test get_params from failing (all params passed sholud be set)
+ self.num_constraints = 'deprecated'
def fit(self, X, y):
"""Create constraints from labels and learn the SDML model.
@@ -318,13 +339,13 @@ def fit(self, X, y):
Returns the instance.
"""
X, y = self._prepare_inputs(X, y, ensure_min_samples=2)
- num_constraints = self.num_constraints
- if num_constraints is None:
+ n_constraints = self.n_constraints
+ if n_constraints is None:
num_classes = len(np.unique(y))
- num_constraints = 20 * num_classes**2
+ n_constraints = 20 * num_classes**2
c = Constraints(y)
- pos_neg = c.positive_negative_pairs(num_constraints,
+ pos_neg = c.positive_negative_pairs(n_constraints,
random_state=self.random_state)
pairs, y = wrap_pairs(X, pos_neg)
return _BaseSDML._fit(self, pairs, y)
diff --git a/metric_learn/sklearn_shims.py b/metric_learn/sklearn_shims.py
new file mode 100644
index 00000000..8d746890
--- /dev/null
+++ b/metric_learn/sklearn_shims.py
@@ -0,0 +1,25 @@
+"""This file is for fixing imports due to different APIs
+depending on the scikit-learn version"""
+import sklearn
+from packaging import version
+SKLEARN_AT_LEAST_0_22 = (version.parse(sklearn.__version__)
+ >= version.parse('0.22.0'))
+if SKLEARN_AT_LEAST_0_22:
+ from sklearn.utils._testing import (set_random_state,
+ ignore_warnings,
+ assert_allclose_dense_sparse,
+ _get_args)
+ from sklearn.utils.estimator_checks import (_is_public_parameter
+ as is_public_parameter)
+ from sklearn.metrics._scorer import get_scorer
+else:
+ from sklearn.utils.testing import (set_random_state,
+ ignore_warnings,
+ assert_allclose_dense_sparse,
+ _get_args)
+ from sklearn.utils.estimator_checks import is_public_parameter
+ from sklearn.metrics.scorer import get_scorer
+
+__all__ = ['set_random_state', 'set_random_state',
+ 'ignore_warnings', 'assert_allclose_dense_sparse', '_get_args',
+ 'is_public_parameter', 'get_scorer']
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 00000000..ef3c8acb
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]
+markers =
+ integration: mark a test as integration
+ unit: mark a test as unit
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 8677e7bf..23392077 100755
--- a/setup.py
+++ b/setup.py
@@ -63,12 +63,13 @@
],
packages=['metric_learn'],
install_requires=[
- 'numpy',
- 'scipy',
- 'scikit-learn>=0.20.3',
+ 'numpy>= 1.11.0',
+ 'scipy>= 0.17.0',
+ 'scikit-learn>=0.21.3',
],
extras_require=dict(
- docs=['sphinx', 'shinx_rtd_theme', 'numpydoc'],
+ docs=['sphinx', 'sphinx_rtd_theme', 'numpydoc', 'sphinx-gallery',
+ 'matplotlib'],
demo=['matplotlib'],
sdml=['skggm>=0.2.9']
),
diff --git a/test/metric_learn_test.py b/test/metric_learn_test.py
index 4db0a1fc..d457b52d 100644
--- a/test/metric_learn_test.py
+++ b/test/metric_learn_test.py
@@ -1,3 +1,4 @@
+import warnings
import unittest
import re
import pytest
@@ -9,13 +10,12 @@
make_spd_matrix)
from numpy.testing import (assert_array_almost_equal, assert_array_equal,
assert_allclose)
-from sklearn.utils.testing import assert_warns_message
from sklearn.exceptions import ConvergenceWarning
from sklearn.utils.validation import check_X_y
from sklearn.preprocessing import StandardScaler
try:
from inverse_covariance import quic
- assert(quic)
+ assert quic
except ImportError:
HAS_SKGGM = False
else:
@@ -79,19 +79,24 @@ def test_singular_returns_pseudo_inverse(self):
class TestSCML(object):
@pytest.mark.parametrize('basis', ('lda', 'triplet_diffs'))
def test_iris(self, basis):
+ """
+ SCML applied to Iris dataset should give better results when
+ computing class separation.
+ """
X, y = load_iris(return_X_y=True)
+ before = class_separation(X, y)
scml = SCML_Supervised(basis=basis, n_basis=85, k_genuine=7, k_impostor=5,
random_state=42)
scml.fit(X, y)
- csep = class_separation(scml.transform(X), y)
- assert csep < 0.24
+ after = class_separation(scml.transform(X), y)
+ assert before > after + 0.03 # It's better by a margin of 0.03
def test_big_n_features(self):
X, y = make_classification(n_samples=100, n_classes=3, n_features=60,
n_informative=60, n_redundant=0, n_repeated=0,
random_state=42)
X = StandardScaler().fit_transform(X)
- scml = SCML_Supervised(random_state=42)
+ scml = SCML_Supervised(random_state=42, n_basis=399)
scml.fit(X, y)
csep = class_separation(scml.transform(X), y)
assert csep < 0.7
@@ -102,7 +107,7 @@ def test_big_n_features(self):
[2, 0], [2, 1]]),
np.array([1, 0, 1, 0])))])
def test_bad_basis(self, estimator, data):
- model = estimator(basis='bad_basis')
+ model = estimator(basis='bad_basis', n_basis=33) # n_basis doesn't matter
msg = ("`basis` must be one of the options '{}' or an array of shape "
"(n_basis, n_features)."
.format("', '".join(model._authorized_basis)))
@@ -234,16 +239,23 @@ def test_lda_toy(self):
@pytest.mark.parametrize('n_features', [10, 50, 100])
@pytest.mark.parametrize('n_classes', [5, 10, 15])
def test_triplet_diffs(self, n_samples, n_features, n_classes):
+ """
+ Test that the correct value of n_basis is being generated with
+ different triplet constraints.
+ """
X, y = make_classification(n_samples=n_samples, n_classes=n_classes,
n_features=n_features, n_informative=n_features,
n_redundant=0, n_repeated=0)
X = StandardScaler().fit_transform(X)
-
- model = SCML_Supervised()
+ model = SCML_Supervised(n_basis=None) # Explicit n_basis=None
constraints = Constraints(y)
triplets = constraints.generate_knntriplets(X, model.k_genuine,
model.k_impostor)
- basis, n_basis = model._generate_bases_dist_diff(triplets, X)
+
+ msg = "As no value for `n_basis` was selected, "
+ with pytest.warns(UserWarning) as raised_warning:
+ basis, n_basis = model._generate_bases_dist_diff(triplets, X)
+ assert msg in str(raised_warning[0].message)
expected_n_basis = n_features * 80
assert n_basis == expected_n_basis
@@ -253,13 +265,21 @@ def test_triplet_diffs(self, n_samples, n_features, n_classes):
@pytest.mark.parametrize('n_features', [10, 50, 100])
@pytest.mark.parametrize('n_classes', [5, 10, 15])
def test_lda(self, n_samples, n_features, n_classes):
+ """
+ Test that when n_basis=None, the correct n_basis is generated,
+ for SCML_Supervised and different values of n_samples, n_features
+ and n_classes.
+ """
X, y = make_classification(n_samples=n_samples, n_classes=n_classes,
n_features=n_features, n_informative=n_features,
n_redundant=0, n_repeated=0)
X = StandardScaler().fit_transform(X)
- model = SCML_Supervised()
- basis, n_basis = model._generate_bases_LDA(X, y)
+ msg = "As no value for `n_basis` was selected, "
+ with pytest.warns(UserWarning) as raised_warning:
+ model = SCML_Supervised(n_basis=None) # Explicit n_basis=None
+ basis, n_basis = model._generate_bases_LDA(X, y)
+ assert msg in str(raised_warning[0].message)
num_eig = min(n_classes - 1, n_features)
expected_n_basis = min(20 * n_features, n_samples * 2 * num_eig - 1)
@@ -295,7 +315,7 @@ def test_int_inputs_supervised(self, name):
assert msg == raised_error.value.args[0]
def test_large_output_iter(self):
- scml = SCML(max_iter=1, output_iter=2)
+ scml = SCML(max_iter=1, output_iter=2, n_basis=33) # n_basis don't matter
triplets = np.array([[[0, 1], [2, 1], [0, 0]]])
msg = ("The value of output_iter must be equal or smaller than"
" max_iter.")
@@ -307,7 +327,7 @@ def test_large_output_iter(self):
class TestLSML(MetricTestCase):
def test_iris(self):
- lsml = LSML_Supervised(num_constraints=200)
+ lsml = LSML_Supervised(n_constraints=200)
lsml.fit(self.iris_points, self.iris_labels)
csep = class_separation(lsml.transform(self.iris_points), self.iris_labels)
@@ -316,7 +336,7 @@ def test_iris(self):
class TestITML(MetricTestCase):
def test_iris(self):
- itml = ITML_Supervised(num_constraints=200)
+ itml = ITML_Supervised(n_constraints=200)
itml.fit(self.iris_points, self.iris_labels)
csep = class_separation(itml.transform(self.iris_points), self.iris_labels)
@@ -362,7 +382,7 @@ def test_bounds_parameters_invalid(bounds):
class TestLMNN(MetricTestCase):
def test_iris(self):
- lmnn = LMNN(k=5, learn_rate=1e-6, verbose=False)
+ lmnn = LMNN(n_neighbors=5, learn_rate=1e-6, verbose=False)
lmnn.fit(self.iris_points, self.iris_labels)
csep = class_separation(lmnn.transform(self.iris_points),
@@ -379,7 +399,7 @@ def test_loss_grad_lbfgs(self):
L = rng.randn(rng.randint(1, X.shape[1] + 1), X.shape[1])
lmnn = LMNN()
- k = lmnn.k
+ k = lmnn.n_neighbors
reg = lmnn.regularization
X, y = lmnn._prepare_inputs(X, y, dtype=float,
@@ -555,9 +575,9 @@ def _loss_grad(self, X, L, dfG, k, reg, target_neighbors, label_inds):
def test_toy_ex_lmnn(X, y, loss):
"""Test that the loss give the right result on a toy example"""
L = np.array([[1]])
- lmnn = LMNN(k=1, regularization=0.5)
+ lmnn = LMNN(n_neighbors=1, regularization=0.5)
- k = lmnn.k
+ k = lmnn.n_neighbors
reg = lmnn.regularization
X, y = lmnn._prepare_inputs(X, y, dtype=float,
@@ -715,12 +735,12 @@ def test_raises_no_warning_installed_skggm(self):
pairs = np.array([[[-10., 0.], [10., 0.]], [[0., -55.], [0., -60]]])
y_pairs = [1, -1]
X, y = make_classification(random_state=42)
- with pytest.warns(None) as records:
+ with warnings.catch_warnings(record=True) as records:
sdml = SDML(prior='covariance')
sdml.fit(pairs, y_pairs)
for record in records:
assert record.category is not ConvergenceWarning
- with pytest.warns(None) as records:
+ with warnings.catch_warnings(record=True) as records:
sdml_supervised = SDML_Supervised(prior='identity', balance_param=1e-5)
sdml_supervised.fit(X, y)
for record in records:
@@ -731,7 +751,7 @@ def test_iris(self):
# TODO: un-flake it!
rs = np.random.RandomState(5555)
- sdml = SDML_Supervised(num_constraints=1500, prior='identity',
+ sdml = SDML_Supervised(n_constraints=1500, prior='identity',
balance_param=5e-5, random_state=rs)
sdml.fit(self.iris_points, self.iris_labels)
csep = class_separation(sdml.transform(self.iris_points),
@@ -929,7 +949,7 @@ def test_singleton_class(self):
X = X[[ind_0[0], ind_1[0], ind_2[0]]]
y = y[[ind_0[0], ind_1[0], ind_2[0]]]
- A = make_spd_matrix(X.shape[1], X.shape[1])
+ A = make_spd_matrix(n_dim=X.shape[1], random_state=X.shape[1])
nca = NCA(init=A, max_iter=30, n_components=X.shape[1])
nca.fit(X, y)
assert_array_equal(nca.components_, A)
@@ -940,7 +960,7 @@ def test_one_class(self):
X = self.iris_points[self.iris_labels == 0]
y = self.iris_labels[self.iris_labels == 0]
- A = make_spd_matrix(X.shape[1], X.shape[1])
+ A = make_spd_matrix(n_dim=X.shape[1], random_state=X.shape[1])
nca = NCA(init=A, max_iter=30, n_components=X.shape[1])
nca.fit(X, y)
assert_array_equal(nca.components_, A)
@@ -960,7 +980,7 @@ def test_iris(self):
class TestRCA(MetricTestCase):
def test_iris(self):
- rca = RCA_Supervised(n_components=2, num_chunks=30, chunk_size=2)
+ rca = RCA_Supervised(n_components=2, n_chunks=30, chunk_size=2)
rca.fit(self.iris_points, self.iris_labels)
csep = class_separation(rca.transform(self.iris_points), self.iris_labels)
self.assertLess(csep, 0.29)
@@ -980,21 +1000,21 @@ def test_rank_deficient_returns_warning(self):
'for instance using `sklearn.decomposition.PCA` as a '
'preprocessing step.')
- with pytest.warns(None) as raised_warnings:
+ with warnings.catch_warnings(record=True) as raised_warnings:
rca.fit(X, y)
assert any(str(w.message) == msg for w in raised_warnings)
def test_unknown_labels(self):
n = 200
- num_chunks = 50
+ n_chunks = 50
X, y = make_classification(random_state=42, n_samples=2 * n,
n_features=6, n_informative=6, n_redundant=0)
y2 = np.concatenate((y[:n], -np.ones(n)))
- rca = RCA_Supervised(num_chunks=num_chunks, random_state=42)
+ rca = RCA_Supervised(n_chunks=n_chunks, random_state=42)
rca.fit(X[:n], y[:n])
- rca2 = RCA_Supervised(num_chunks=num_chunks, random_state=42)
+ rca2 = RCA_Supervised(n_chunks=n_chunks, random_state=42)
rca2.fit(X, y2)
assert not np.any(np.isnan(rca.components_))
@@ -1004,18 +1024,18 @@ def test_unknown_labels(self):
def test_bad_parameters(self):
n = 200
- num_chunks = 3
+ n_chunks = 3
X, y = make_classification(random_state=42, n_samples=n,
n_features=6, n_informative=6, n_redundant=0)
- rca = RCA_Supervised(num_chunks=num_chunks, random_state=42)
+ rca = RCA_Supervised(n_chunks=n_chunks, random_state=42)
msg = ('Due to the parameters of RCA_Supervised, '
'the inner covariance matrix is not invertible, '
'so the transformation matrix will contain Nan values. '
'Increase the number or size of the chunks to correct '
'this problem.'
)
- with pytest.warns(None) as raised_warning:
+ with warnings.catch_warnings(record=True) as raised_warning:
rca.fit(X, y)
assert any(str(w.message) == msg for w in raised_warning)
@@ -1062,7 +1082,7 @@ def test_iris(self):
# Full metric
n_features = self.iris_points.shape[1]
- mmc = MMC(convergence_threshold=0.01, init=np.eye(n_features) / 10)
+ mmc = MMC(tol=0.01, init=np.eye(n_features) / 10)
mmc.fit(*wrap_pairs(self.iris_points, [a, b, c, d]))
expected = [[+0.000514, +0.000868, -0.001195, -0.001703],
[+0.000868, +0.001468, -0.002021, -0.002879],
@@ -1138,9 +1158,10 @@ def test_convergence_warning(dataset, algo_class):
X, y = dataset
model = algo_class(max_iter=2, verbose=True)
cls_name = model.__class__.__name__
- assert_warns_message(ConvergenceWarning,
- '[{}] {} did not converge'.format(cls_name, cls_name),
- model.fit, X, y)
+ msg = '[{}] {} did not converge'.format(cls_name, cls_name)
+ with pytest.warns(Warning) as raised_warning:
+ model.fit(X, y)
+ assert any([msg in str(warn.message) for warn in raised_warning])
if __name__ == '__main__':
diff --git a/test/test_base_metric.py b/test/test_base_metric.py
index fed9018a..b1e71020 100644
--- a/test/test_base_metric.py
+++ b/test/test_base_metric.py
@@ -1,74 +1,167 @@
+from numpy.core.numeric import array_equal
+import warnings
import pytest
import re
import unittest
import metric_learn
import numpy as np
from sklearn import clone
-from sklearn.utils.testing import set_random_state
from test.test_utils import ids_metric_learners, metric_learners, remove_y
+from metric_learn.sklearn_shims import set_random_state, SKLEARN_AT_LEAST_0_22
def remove_spaces(s):
return re.sub(r'\s+', '', s)
+def sk_repr_kwargs(def_kwargs, nndef_kwargs):
+ """Given the non-default arguments, and the default
+ keywords arguments, build the string that will appear
+ in the __repr__ of the estimator, depending on the
+ version of scikit-learn.
+ """
+ if SKLEARN_AT_LEAST_0_22:
+ def_kwargs = {}
+ def_kwargs.update(nndef_kwargs)
+ args_str = ",".join(f"{key}={repr(value)}"
+ for key, value in def_kwargs.items())
+ return args_str
+
+
class TestStringRepr(unittest.TestCase):
def test_covariance(self):
+ def_kwargs = {'preprocessor': None}
+ nndef_kwargs = {}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(remove_spaces(str(metric_learn.Covariance())),
- remove_spaces("Covariance()"))
+ remove_spaces(f"Covariance({merged_kwargs})"))
def test_lmnn(self):
+ def_kwargs = {'convergence_tol': 0.001, 'init': 'auto', 'n_neighbors': 3,
+ 'learn_rate': 1e-07, 'max_iter': 1000, 'min_iter': 50,
+ 'n_components': None, 'preprocessor': None,
+ 'random_state': None, 'regularization': 0.5,
+ 'verbose': False}
+ nndef_kwargs = {'convergence_tol': 0.01, 'n_neighbors': 6}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(
- remove_spaces(str(metric_learn.LMNN(convergence_tol=0.01, k=6))),
- remove_spaces("LMNN(convergence_tol=0.01, k=6)"))
+ remove_spaces(str(metric_learn.LMNN(convergence_tol=0.01,
+ n_neighbors=6))),
+ remove_spaces(f"LMNN({merged_kwargs})"))
def test_nca(self):
+ def_kwargs = {'init': 'auto', 'max_iter': 100, 'n_components': None,
+ 'preprocessor': None, 'random_state': None, 'tol': None,
+ 'verbose': False}
+ nndef_kwargs = {'max_iter': 42}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(remove_spaces(str(metric_learn.NCA(max_iter=42))),
- remove_spaces("NCA(max_iter=42)"))
+ remove_spaces(f"NCA({merged_kwargs})"))
def test_lfda(self):
+ def_kwargs = {'embedding_type': 'weighted', 'k': None,
+ 'n_components': None, 'preprocessor': None}
+ nndef_kwargs = {'k': 2}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(remove_spaces(str(metric_learn.LFDA(k=2))),
- remove_spaces("LFDA(k=2)"))
+ remove_spaces(f"LFDA({merged_kwargs})"))
def test_itml(self):
+ def_kwargs = {'tol': 0.001, 'gamma': 1.0,
+ 'max_iter': 1000, 'preprocessor': None,
+ 'prior': 'identity', 'random_state': None, 'verbose': False}
+ nndef_kwargs = {'gamma': 0.5}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(remove_spaces(str(metric_learn.ITML(gamma=0.5))),
- remove_spaces("ITML(gamma=0.5)"))
+ remove_spaces(f"ITML({merged_kwargs})"))
+ def_kwargs = {'tol': 0.001, 'gamma': 1.0,
+ 'max_iter': 1000, 'n_constraints': None,
+ 'preprocessor': None, 'prior': 'identity',
+ 'random_state': None, 'verbose': False}
+ nndef_kwargs = {'n_constraints': 7}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(
- remove_spaces(str(metric_learn.ITML_Supervised(num_constraints=7))),
- remove_spaces("ITML_Supervised(num_constraints=7)"))
+ remove_spaces(str(metric_learn.ITML_Supervised(n_constraints=7))),
+ remove_spaces(f"ITML_Supervised({merged_kwargs})"))
def test_lsml(self):
+ def_kwargs = {'max_iter': 1000, 'preprocessor': None, 'prior': 'identity',
+ 'random_state': None, 'tol': 0.001, 'verbose': False}
+ nndef_kwargs = {'tol': 0.1}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(remove_spaces(str(metric_learn.LSML(tol=0.1))),
- remove_spaces("LSML(tol=0.1)"))
+ remove_spaces(f"LSML({merged_kwargs})"))
+ def_kwargs = {'max_iter': 1000, 'n_constraints': None,
+ 'preprocessor': None, 'prior': 'identity',
+ 'random_state': None, 'tol': 0.001, 'verbose': False,
+ 'weights': None}
+ nndef_kwargs = {'verbose': True}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(
remove_spaces(str(metric_learn.LSML_Supervised(verbose=True))),
- remove_spaces("LSML_Supervised(verbose=True)"))
+ remove_spaces(f"LSML_Supervised({merged_kwargs})"))
def test_sdml(self):
+ def_kwargs = {'balance_param': 0.5, 'preprocessor': None,
+ 'prior': 'identity', 'random_state': None,
+ 'sparsity_param': 0.01, 'verbose': False}
+ nndef_kwargs = {'verbose': True}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(remove_spaces(str(metric_learn.SDML(verbose=True))),
- remove_spaces("SDML(verbose=True)"))
+ remove_spaces(f"SDML({merged_kwargs})"))
+ def_kwargs = {'balance_param': 0.5, 'n_constraints': None,
+ 'preprocessor': None, 'prior': 'identity',
+ 'random_state': None, 'sparsity_param': 0.01,
+ 'verbose': False}
+ nndef_kwargs = {'sparsity_param': 0.5}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(
remove_spaces(str(metric_learn.SDML_Supervised(sparsity_param=0.5))),
- remove_spaces("SDML_Supervised(sparsity_param=0.5)"))
+ remove_spaces(f"SDML_Supervised({merged_kwargs})"))
def test_rca(self):
+ def_kwargs = {'n_components': None, 'preprocessor': None}
+ nndef_kwargs = {'n_components': 3}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(remove_spaces(str(metric_learn.RCA(n_components=3))),
- remove_spaces("RCA(n_components=3)"))
+ remove_spaces(f"RCA({merged_kwargs})"))
+ def_kwargs = {'chunk_size': 2, 'n_components': None, 'n_chunks': 100,
+ 'preprocessor': None, 'random_state': None}
+ nndef_kwargs = {'n_chunks': 5}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(
- remove_spaces(str(metric_learn.RCA_Supervised(num_chunks=5))),
- remove_spaces("RCA_Supervised(num_chunks=5)"))
+ remove_spaces(str(metric_learn.RCA_Supervised(n_chunks=5))),
+ remove_spaces(f"RCA_Supervised({merged_kwargs})"))
def test_mlkr(self):
+ def_kwargs = {'init': 'auto', 'max_iter': 1000,
+ 'n_components': None, 'preprocessor': None,
+ 'random_state': None, 'tol': None, 'verbose': False}
+ nndef_kwargs = {'max_iter': 777}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(remove_spaces(str(metric_learn.MLKR(max_iter=777))),
- remove_spaces("MLKR(max_iter=777)"))
+ remove_spaces(f"MLKR({merged_kwargs})"))
def test_mmc(self):
+ def_kwargs = {'tol': 0.001, 'diagonal': False,
+ 'diagonal_c': 1.0, 'init': 'identity', 'max_iter': 100,
+ 'max_proj': 10000, 'preprocessor': None,
+ 'random_state': None, 'verbose': False}
+ nndef_kwargs = {'diagonal': True}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(remove_spaces(str(metric_learn.MMC(diagonal=True))),
- remove_spaces("MMC(diagonal=True)"))
+ remove_spaces(f"MMC({merged_kwargs})"))
+ def_kwargs = {'tol': 1e-06, 'diagonal': False,
+ 'diagonal_c': 1.0, 'init': 'identity', 'max_iter': 100,
+ 'max_proj': 10000, 'n_constraints': None,
+ 'preprocessor': None, 'random_state': None,
+ 'verbose': False}
+ nndef_kwargs = {'max_iter': 1}
+ merged_kwargs = sk_repr_kwargs(def_kwargs, nndef_kwargs)
self.assertEqual(
remove_spaces(str(metric_learn.MMC_Supervised(max_iter=1))),
- remove_spaces("MMC_Supervised(max_iter=1)"))
+ remove_spaces(f"MMC_Supervised({merged_kwargs})"))
@pytest.mark.parametrize('estimator, build_dataset', metric_learners,
@@ -134,7 +227,7 @@ def test_get_metric_works_does_not_raise(estimator, build_dataset):
(X[0][None], X[1][None])]
for u, v in list_test_get_metric_doesnt_raise:
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
metric(u, v)
assert len(record) == 0
@@ -142,7 +235,7 @@ def test_get_metric_works_does_not_raise(estimator, build_dataset):
model.components_ = np.array([3.1])
metric = model.get_metric()
for u, v in [(5, 6.7), ([5], [6.7]), ([[5]], [[6.7]])]:
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
metric(u, v)
assert len(record) == 0
@@ -184,5 +277,28 @@ def test_n_components(estimator, build_dataset):
'Invalid n_components, must be in [1, {}]'.format(X.shape[1]))
+@pytest.mark.parametrize('estimator, build_dataset', metric_learners,
+ ids=ids_metric_learners)
+def test_score_pairs_warning(estimator, build_dataset):
+ """Tests that score_pairs returns a FutureWarning regarding deprecation.
+ Also that score_pairs and pair_distance have the same behaviour"""
+ input_data, labels, _, X = build_dataset()
+ model = clone(estimator)
+ set_random_state(model)
+
+ # We fit the metric learner on it and then we call score_pairs on some
+ # points
+ model.fit(*remove_y(model, input_data, labels))
+
+ msg = ("score_pairs will be deprecated in release 0.7.0. "
+ "Use pair_score to compute similarity scores, or "
+ "pair_distances to compute distances.")
+ with pytest.warns(FutureWarning) as raised_warning:
+ score = model.score_pairs([[X[0], X[1]], ])
+ dist = model.pair_distance([[X[0], X[1]], ])
+ assert array_equal(score, dist)
+ assert any([str(warning.message) == msg for warning in raised_warning])
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/test/test_components_metric_conversion.py b/test/test_components_metric_conversion.py
index b9da87ed..c6113957 100644
--- a/test/test_components_metric_conversion.py
+++ b/test/test_components_metric_conversion.py
@@ -1,11 +1,10 @@
import unittest
import numpy as np
import pytest
-from numpy.linalg import LinAlgError
from scipy.stats import ortho_group
from sklearn.datasets import load_iris
from numpy.testing import assert_array_almost_equal, assert_allclose
-from sklearn.utils.testing import ignore_warnings
+from metric_learn.sklearn_shims import ignore_warnings
from metric_learn import (
LMNN, NCA, LFDA, Covariance, MLKR,
@@ -30,27 +29,27 @@ def test_cov(self):
def test_lsml_supervised(self):
seed = np.random.RandomState(1234)
- lsml = LSML_Supervised(num_constraints=200, random_state=seed)
+ lsml = LSML_Supervised(n_constraints=200, random_state=seed)
lsml.fit(self.X, self.y)
L = lsml.components_
assert_array_almost_equal(L.T.dot(L), lsml.get_mahalanobis_matrix())
def test_itml_supervised(self):
seed = np.random.RandomState(1234)
- itml = ITML_Supervised(num_constraints=200, random_state=seed)
+ itml = ITML_Supervised(n_constraints=200, random_state=seed)
itml.fit(self.X, self.y)
L = itml.components_
assert_array_almost_equal(L.T.dot(L), itml.get_mahalanobis_matrix())
def test_lmnn(self):
- lmnn = LMNN(k=5, learn_rate=1e-6, verbose=False)
+ lmnn = LMNN(n_neighbors=5, learn_rate=1e-6, verbose=False)
lmnn.fit(self.X, self.y)
L = lmnn.components_
assert_array_almost_equal(L.T.dot(L), lmnn.get_mahalanobis_matrix())
def test_sdml_supervised(self):
seed = np.random.RandomState(1234)
- sdml = SDML_Supervised(num_constraints=1500, prior='identity',
+ sdml = SDML_Supervised(n_constraints=1500, prior='identity',
balance_param=1e-5, random_state=seed)
sdml.fit(self.X, self.y)
L = sdml.components_
@@ -70,7 +69,7 @@ def test_lfda(self):
assert_array_almost_equal(L.T.dot(L), lfda.get_mahalanobis_matrix())
def test_rca_supervised(self):
- rca = RCA_Supervised(n_components=2, num_chunks=30, chunk_size=2)
+ rca = RCA_Supervised(n_components=2, n_chunks=30, chunk_size=2)
rca.fit(self.X, self.y)
L = rca.components_
assert_array_almost_equal(L.T.dot(L), rca.get_mahalanobis_matrix())
@@ -117,17 +116,14 @@ def test_components_from_metric_edge_cases(self):
L = components_from_metric(M)
assert_allclose(L.T.dot(L), M)
- # matrix with a determinant still high but which should be considered as a
- # non-definite matrix (to check we don't test the definiteness with the
- # determinant which is a bad strategy)
+ # matrix with a determinant still high but which is
+ # undefinite w.r.t to numpy standards
M = np.diag([1e5, 1e5, 1e5, 1e5, 1e5, 1e5, 1e-20])
M = P.dot(M).dot(P.T)
assert np.abs(np.linalg.det(M)) > 10
assert np.linalg.slogdet(M)[1] > 1 # (just to show that the computed
# determinant is far from null)
- with pytest.raises(LinAlgError) as err_msg:
- np.linalg.cholesky(M)
- assert str(err_msg.value) == 'Matrix is not positive definite'
+ assert np.linalg.matrix_rank(M) < M.shape[0]
# (just to show that this case is indeed considered by numpy as an
# indefinite case)
L = components_from_metric(M)
diff --git a/test/test_constraints.py b/test/test_constraints.py
index 92876779..3429d9cc 100644
--- a/test/test_constraints.py
+++ b/test/test_constraints.py
@@ -7,14 +7,14 @@
SEED = 42
-def gen_labels_for_chunks(num_chunks, chunk_size,
+def gen_labels_for_chunks(n_chunks, chunk_size,
n_classes=10, n_unknown_labels=5):
- """Generates num_chunks*chunk_size labels that split in num_chunks chunks,
+ """Generates n_chunks*chunk_size labels that split in n_chunks chunks,
that are homogeneous in the label."""
- assert min(num_chunks, chunk_size) > 0
+ assert min(n_chunks, chunk_size) > 0
classes = shuffle(np.arange(n_classes), random_state=SEED)
- n_per_class = chunk_size * (num_chunks // n_classes)
- n_maj_class = chunk_size * num_chunks - n_per_class * (n_classes - 1)
+ n_per_class = chunk_size * (n_chunks // n_classes)
+ n_maj_class = chunk_size * n_chunks - n_per_class * (n_classes - 1)
first_labels = classes[0] * np.ones(n_maj_class, dtype=int)
remaining_labels = np.concatenate([k * np.ones(n_per_class, dtype=int)
@@ -25,48 +25,48 @@ def gen_labels_for_chunks(num_chunks, chunk_size,
return shuffle(labels, random_state=SEED)
-@pytest.mark.parametrize("num_chunks, chunk_size", [(5, 10), (10, 50)])
-def test_exact_num_points_for_chunks(num_chunks, chunk_size):
+@pytest.mark.parametrize("n_chunks, chunk_size", [(5, 10), (10, 50)])
+def test_exact_num_points_for_chunks(n_chunks, chunk_size):
"""Checks that the chunk generation works well with just enough points."""
- labels = gen_labels_for_chunks(num_chunks, chunk_size)
+ labels = gen_labels_for_chunks(n_chunks, chunk_size)
constraints = Constraints(labels)
- chunks = constraints.chunks(num_chunks=num_chunks, chunk_size=chunk_size,
+ chunks = constraints.chunks(n_chunks=n_chunks, chunk_size=chunk_size,
random_state=SEED)
chunk_no, size_each_chunk = np.unique(chunks[chunks >= 0],
return_counts=True)
np.testing.assert_array_equal(size_each_chunk, chunk_size)
- assert chunk_no.shape[0] == num_chunks
+ assert chunk_no.shape[0] == n_chunks
-@pytest.mark.parametrize("num_chunks, chunk_size", [(5, 10), (10, 50)])
-def test_chunk_case_one_miss_point(num_chunks, chunk_size):
+@pytest.mark.parametrize("n_chunks, chunk_size", [(5, 10), (10, 50)])
+def test_chunk_case_one_miss_point(n_chunks, chunk_size):
"""Checks that the chunk generation breaks when one point is missing."""
- labels = gen_labels_for_chunks(num_chunks, chunk_size)
+ labels = gen_labels_for_chunks(n_chunks, chunk_size)
assert len(labels) >= 1
constraints = Constraints(labels[1:])
with pytest.raises(ValueError) as e:
- constraints.chunks(num_chunks=num_chunks, chunk_size=chunk_size,
+ constraints.chunks(n_chunks=n_chunks, chunk_size=chunk_size,
random_state=SEED)
expected_message = (('Not enough possible chunks of %d elements in each'
' class to form expected %d chunks - maximum number'
' of chunks is %d'
- ) % (chunk_size, num_chunks, num_chunks - 1))
+ ) % (chunk_size, n_chunks, n_chunks - 1))
assert str(e.value) == expected_message
-@pytest.mark.parametrize("num_chunks, chunk_size", [(5, 10), (10, 50)])
-def test_unknown_labels_not_in_chunks(num_chunks, chunk_size):
+@pytest.mark.parametrize("n_chunks, chunk_size", [(5, 10), (10, 50)])
+def test_unknown_labels_not_in_chunks(n_chunks, chunk_size):
"""Checks that unknown labels are not assigned to any chunk."""
- labels = gen_labels_for_chunks(num_chunks, chunk_size)
+ labels = gen_labels_for_chunks(n_chunks, chunk_size)
constraints = Constraints(labels)
- chunks = constraints.chunks(num_chunks=num_chunks, chunk_size=chunk_size,
+ chunks = constraints.chunks(n_chunks=n_chunks, chunk_size=chunk_size,
random_state=SEED)
assert np.all(chunks[labels < 0] < 0)
@@ -103,7 +103,7 @@ def test_generate_knntriplets_under_edge(k_genuine, k_impostor, T_test):
@pytest.mark.parametrize("k_genuine, k_impostor,",
- [(2, 3), (3, 3), (2, 4), (3, 4)])
+ [(3, 3), (2, 4), (3, 4), (10, 9), (144, 33)])
def test_generate_knntriplets(k_genuine, k_impostor):
"""Checks edge and over the edge cases of knn triplet construction with not
enough neighbors"""
@@ -118,8 +118,23 @@ def test_generate_knntriplets(k_genuine, k_impostor):
X = np.array([[0, 0], [2, 2], [4, 4], [8, 8], [16, 16], [32, 32], [33, 33]])
y = np.array([1, 1, 1, 2, 2, 2, -1])
- T = Constraints(y).generate_knntriplets(X, k_genuine, k_impostor)
-
+ msg1 = ("The class 1 has 3 elements, which is not sufficient to "
+ f"generate {k_genuine+1} genuine neighbors "
+ "as specified by k_genuine")
+ msg2 = ("The class 2 has 3 elements, which is not sufficient to "
+ f"generate {k_genuine+1} genuine neighbors "
+ "as specified by k_genuine")
+ msg3 = ("The class 1 has 3 elements of other classes, which is "
+ f"not sufficient to generate {k_impostor} impostor "
+ "neighbors as specified by k_impostor")
+ msg4 = ("The class 2 has 3 elements of other classes, which is "
+ f"not sufficient to generate {k_impostor} impostor "
+ "neighbors as specified by k_impostor")
+ msgs = [msg1, msg2, msg3, msg4]
+ with pytest.warns(UserWarning) as user_warning:
+ T = Constraints(y).generate_knntriplets(X, k_genuine, k_impostor)
+ assert any([[msg in str(warn.message) for msg in msgs]
+ for warn in user_warning])
assert np.array_equal(sorted(T.tolist()), T_test)
diff --git a/test/test_fit_transform.py b/test/test_fit_transform.py
index d4d4bfe0..246223b0 100644
--- a/test/test_fit_transform.py
+++ b/test/test_fit_transform.py
@@ -29,47 +29,47 @@ def test_cov(self):
def test_lsml_supervised(self):
seed = np.random.RandomState(1234)
- lsml = LSML_Supervised(num_constraints=200, random_state=seed)
+ lsml = LSML_Supervised(n_constraints=200, random_state=seed)
lsml.fit(self.X, self.y)
res_1 = lsml.transform(self.X)
seed = np.random.RandomState(1234)
- lsml = LSML_Supervised(num_constraints=200, random_state=seed)
+ lsml = LSML_Supervised(n_constraints=200, random_state=seed)
res_2 = lsml.fit_transform(self.X, self.y)
assert_array_almost_equal(res_1, res_2)
def test_itml_supervised(self):
seed = np.random.RandomState(1234)
- itml = ITML_Supervised(num_constraints=200, random_state=seed)
+ itml = ITML_Supervised(n_constraints=200, random_state=seed)
itml.fit(self.X, self.y)
res_1 = itml.transform(self.X)
seed = np.random.RandomState(1234)
- itml = ITML_Supervised(num_constraints=200, random_state=seed)
+ itml = ITML_Supervised(n_constraints=200, random_state=seed)
res_2 = itml.fit_transform(self.X, self.y)
assert_array_almost_equal(res_1, res_2)
def test_lmnn(self):
- lmnn = LMNN(k=5, learn_rate=1e-6, verbose=False)
+ lmnn = LMNN(n_neighbors=5, learn_rate=1e-6, verbose=False)
lmnn.fit(self.X, self.y)
res_1 = lmnn.transform(self.X)
- lmnn = LMNN(k=5, learn_rate=1e-6, verbose=False)
+ lmnn = LMNN(n_neighbors=5, learn_rate=1e-6, verbose=False)
res_2 = lmnn.fit_transform(self.X, self.y)
assert_array_almost_equal(res_1, res_2)
def test_sdml_supervised(self):
seed = np.random.RandomState(1234)
- sdml = SDML_Supervised(num_constraints=1500, balance_param=1e-5,
+ sdml = SDML_Supervised(n_constraints=1500, balance_param=1e-5,
prior='identity', random_state=seed)
sdml.fit(self.X, self.y)
res_1 = sdml.transform(self.X)
seed = np.random.RandomState(1234)
- sdml = SDML_Supervised(num_constraints=1500, balance_param=1e-5,
+ sdml = SDML_Supervised(n_constraints=1500, balance_param=1e-5,
prior='identity', random_state=seed)
res_2 = sdml.fit_transform(self.X, self.y)
@@ -99,13 +99,13 @@ def test_lfda(self):
def test_rca_supervised(self):
seed = np.random.RandomState(1234)
- rca = RCA_Supervised(n_components=2, num_chunks=30, chunk_size=2,
+ rca = RCA_Supervised(n_components=2, n_chunks=30, chunk_size=2,
random_state=seed)
rca.fit(self.X, self.y)
res_1 = rca.transform(self.X)
seed = np.random.RandomState(1234)
- rca = RCA_Supervised(n_components=2, num_chunks=30, chunk_size=2,
+ rca = RCA_Supervised(n_components=2, n_chunks=30, chunk_size=2,
random_state=seed)
res_2 = rca.fit_transform(self.X, self.y)
@@ -123,12 +123,12 @@ def test_mlkr(self):
def test_mmc_supervised(self):
seed = np.random.RandomState(1234)
- mmc = MMC_Supervised(num_constraints=200, random_state=seed)
+ mmc = MMC_Supervised(n_constraints=200, random_state=seed)
mmc.fit(self.X, self.y)
res_1 = mmc.transform(self.X)
seed = np.random.RandomState(1234)
- mmc = MMC_Supervised(num_constraints=200, random_state=seed)
+ mmc = MMC_Supervised(n_constraints=200, random_state=seed)
res_2 = mmc.fit_transform(self.X, self.y)
assert_array_almost_equal(res_1, res_2)
diff --git a/test/test_mahalanobis_mixin.py b/test/test_mahalanobis_mixin.py
index ab7e972d..9378ac60 100644
--- a/test/test_mahalanobis_mixin.py
+++ b/test/test_mahalanobis_mixin.py
@@ -3,7 +3,8 @@
import pytest
import numpy as np
from numpy.linalg import LinAlgError
-from numpy.testing import assert_array_almost_equal, assert_allclose
+from numpy.testing import assert_array_almost_equal, assert_allclose, \
+ assert_array_equal
from scipy.spatial.distance import pdist, squareform, mahalanobis
from scipy.stats import ortho_group
from sklearn import clone
@@ -11,9 +12,10 @@
from sklearn.datasets import make_spd_matrix, make_blobs
from sklearn.utils import check_random_state, shuffle
from sklearn.utils.multiclass import type_of_target
-from sklearn.utils.testing import set_random_state
+from metric_learn.sklearn_shims import set_random_state
from metric_learn._util import make_context, _initialize_metric_mahalanobis
+from metric_learn.sdml import _BaseSDML
from metric_learn.base_metric import (_QuadrupletsClassifierMixin,
_TripletsClassifierMixin,
_PairsClassifierMixin)
@@ -27,7 +29,27 @@
@pytest.mark.parametrize('estimator, build_dataset', metric_learners,
ids=ids_metric_learners)
-def test_score_pairs_pairwise(estimator, build_dataset):
+def test_pair_distance_pair_score_equivalent(estimator, build_dataset):
+ """
+ For Mahalanobis learners, pair_score should be equivalent to the
+ opposite of the pair_distance result.
+ """
+ input_data, labels, _, X = build_dataset()
+ n_samples = 20
+ X = X[:n_samples]
+ model = clone(estimator)
+ set_random_state(model)
+ model.fit(*remove_y(estimator, input_data, labels))
+
+ distances = model.pair_distance(np.array(list(product(X, X))))
+ scores = model.pair_score(np.array(list(product(X, X))))
+
+ assert_array_equal(distances, -1 * scores)
+
+
+@pytest.mark.parametrize('estimator, build_dataset', metric_learners,
+ ids=ids_metric_learners)
+def test_pair_distance_pairwise(estimator, build_dataset):
# Computing pairwise scores should return a euclidean distance matrix.
input_data, labels, _, X = build_dataset()
n_samples = 20
@@ -36,7 +58,7 @@ def test_score_pairs_pairwise(estimator, build_dataset):
set_random_state(model)
model.fit(*remove_y(estimator, input_data, labels))
- pairwise = model.score_pairs(np.array(list(product(X, X))))\
+ pairwise = model.pair_distance(np.array(list(product(X, X))))\
.reshape(n_samples, n_samples)
check_is_distance_matrix(pairwise)
@@ -51,8 +73,8 @@ def test_score_pairs_pairwise(estimator, build_dataset):
@pytest.mark.parametrize('estimator, build_dataset', metric_learners,
ids=ids_metric_learners)
-def test_score_pairs_toy_example(estimator, build_dataset):
- # Checks that score_pairs works on a toy example
+def test_pair_distance_toy_example(estimator, build_dataset):
+ # Checks that pair_distance works on a toy example
input_data, labels, _, X = build_dataset()
n_samples = 20
X = X[:n_samples]
@@ -64,24 +86,24 @@ def test_score_pairs_toy_example(estimator, build_dataset):
distances = np.sqrt(np.sum((embedded_pairs[:, 1] -
embedded_pairs[:, 0])**2,
axis=-1))
- assert_array_almost_equal(model.score_pairs(pairs), distances)
+ assert_array_almost_equal(model.pair_distance(pairs), distances)
@pytest.mark.parametrize('estimator, build_dataset', metric_learners,
ids=ids_metric_learners)
-def test_score_pairs_finite(estimator, build_dataset):
+def test_pair_distance_finite(estimator, build_dataset):
# tests that the score is finite
input_data, labels, _, X = build_dataset()
model = clone(estimator)
set_random_state(model)
model.fit(*remove_y(estimator, input_data, labels))
pairs = np.array(list(product(X, X)))
- assert np.isfinite(model.score_pairs(pairs)).all()
+ assert np.isfinite(model.pair_distance(pairs)).all()
@pytest.mark.parametrize('estimator, build_dataset', metric_learners,
ids=ids_metric_learners)
-def test_score_pairs_dim(estimator, build_dataset):
+def test_pair_distance_dim(estimator, build_dataset):
# scoring of 3D arrays should return 1D array (several tuples),
# and scoring of 2D arrays (one tuple) should return an error (like
# scikit-learn's error when scoring 1D arrays)
@@ -90,13 +112,13 @@ def test_score_pairs_dim(estimator, build_dataset):
set_random_state(model)
model.fit(*remove_y(estimator, input_data, labels))
tuples = np.array(list(product(X, X)))
- assert model.score_pairs(tuples).shape == (tuples.shape[0],)
+ assert model.pair_distance(tuples).shape == (tuples.shape[0],)
context = make_context(estimator)
msg = ("3D array of formed tuples expected{}. Found 2D array "
"instead:\ninput={}. Reshape your data and/or use a preprocessor.\n"
.format(context, tuples[1]))
with pytest.raises(ValueError) as raised_error:
- model.score_pairs(tuples[1])
+ model.pair_distance(tuples[1])
assert str(raised_error.value) == msg
@@ -140,7 +162,7 @@ def test_embed_dim(estimator, build_dataset):
"instead:\ninput={}. Reshape your data and/or use a "
"preprocessor.\n".format(context, X[0]))
with pytest.raises(ValueError) as raised_error:
- model.score_pairs(model.transform(X[0, :]))
+ model.pair_distance(model.transform(X[0, :]))
assert str(raised_error.value) == err_msg
# we test that the shape is also OK when doing dimensionality reduction
if hasattr(model, 'n_components'):
@@ -194,8 +216,7 @@ def test_get_metric_equivalent_to_explicit_mahalanobis(estimator,
metric = model.get_metric()
n_features = X.shape[1]
a, b = (rng.randn(n_features), rng.randn(n_features))
- expected_dist = mahalanobis(a[None], b[None],
- VI=model.get_mahalanobis_matrix())
+ expected_dist = mahalanobis(a, b, VI=model.get_mahalanobis_matrix())
assert_allclose(metric(a, b), expected_dist, rtol=1e-13)
@@ -270,8 +291,12 @@ def test_components_is_2D(estimator, build_dataset):
model.fit(*remove_y(estimator, input_data, labels))
assert model.components_.shape == (X.shape[1], X.shape[1])
- # test that it works for 1 feature
- trunc_data = input_data[..., :1]
+ if isinstance(estimator, _BaseSDML):
+ # SDML doesn't support running on a single feature.
+ return
+
+ # test that it works for 1 feature. Use 2nd dimension, to avoid border cases
+ trunc_data = input_data[..., 1:2]
# we drop duplicates that might have been formed, i.e. of the form
# aabc or abcc or aabb for quadruplets, and aa for pairs.
@@ -417,7 +442,7 @@ def test_auto_init_transformation(n_samples, n_features, n_classes,
random_state=rng)
# To make the test work for LMNN:
if 'LMNN' in model_base.__class__.__name__:
- model_base.set_params(k=1)
+ model_base.set_params(n_neighbors=1)
# To make the test faster for estimators that have a max_iter:
if hasattr(model_base, 'max_iter'):
model_base.set_params(max_iter=1)
@@ -503,12 +528,12 @@ def test_init_mahalanobis(estimator, build_dataset):
model.fit(input_data, labels)
# Initialize with a random spd matrix
- init = make_spd_matrix(X.shape[1], random_state=rng)
+ init = make_spd_matrix(n_dim=X.shape[1], random_state=rng)
model.set_params(**{param: init})
model.fit(input_data, labels)
# init.shape[1] must match X.shape[1]
- init = make_spd_matrix(X.shape[1] + 1, X.shape[1] + 1)
+ init = make_spd_matrix(n_dim=X.shape[1] + 1, random_state=rng)
model.set_params(**{param: init})
msg = ('The input dimensionality {} of the given '
'mahalanobis matrix `{}` must match the '
@@ -625,7 +650,7 @@ def test_singular_covariance_init_of_non_strict_pd(estimator, build_dataset):
'preprocessing step.')
with pytest.warns(UserWarning) as raised_warning:
model.fit(input_data, labels)
- assert np.any([str(warning.message) == msg for warning in raised_warning])
+ assert any([str(warning.message) == msg for warning in raised_warning])
M, _ = _initialize_metric_mahalanobis(X, init='covariance',
random_state=RNG,
return_inverse=True,
diff --git a/test/test_pairs_classifiers.py b/test/test_pairs_classifiers.py
index c5ca27f4..bfedefea 100644
--- a/test/test_pairs_classifiers.py
+++ b/test/test_pairs_classifiers.py
@@ -1,5 +1,6 @@
from functools import partial
+import warnings
import pytest
from numpy.testing import assert_array_equal
from scipy.spatial.distance import euclidean
@@ -11,7 +12,7 @@
from sklearn.model_selection import train_test_split
from test.test_utils import pairs_learners, ids_pairs_learners
-from sklearn.utils.testing import set_random_state
+from metric_learn.sklearn_shims import set_random_state
from sklearn import clone
import numpy as np
from itertools import product
@@ -49,14 +50,14 @@ def test_predict_monotonous(estimator, build_dataset,
pairs_train, pairs_test, y_train, y_test = train_test_split(input_data,
labels)
estimator.fit(pairs_train, y_train)
- distances = estimator.score_pairs(pairs_test)
+ scores = estimator.pair_score(pairs_test)
predictions = estimator.predict(pairs_test)
- min_dissimilar = np.min(distances[predictions == -1])
- max_similar = np.max(distances[predictions == 1])
- assert max_similar <= min_dissimilar
- separator = np.mean([min_dissimilar, max_similar])
- assert (predictions[distances > separator] == -1).all()
- assert (predictions[distances < separator] == 1).all()
+ max_dissimilar = np.max(scores[predictions == -1])
+ min_similar = np.min(scores[predictions == 1])
+ assert max_dissimilar <= min_similar
+ separator = np.mean([max_dissimilar, min_similar])
+ assert (predictions[scores < separator] == -1).all()
+ assert (predictions[scores > separator] == 1).all()
@pytest.mark.parametrize('with_preprocessor', [True, False])
@@ -65,15 +66,17 @@ def test_predict_monotonous(estimator, build_dataset,
def test_raise_not_fitted_error_if_not_fitted(estimator, build_dataset,
with_preprocessor):
"""Test that a NotFittedError is raised if someone tries to use
- score_pairs, decision_function, get_metric, transform or
+ pair_score, score_pairs, decision_function, get_metric, transform or
get_mahalanobis_matrix on input data and the metric learner
has not been fitted."""
input_data, labels, preprocessor, _ = build_dataset(with_preprocessor)
estimator = clone(estimator)
estimator.set_params(preprocessor=preprocessor)
set_random_state(estimator)
- with pytest.raises(NotFittedError):
+ with pytest.raises(NotFittedError): # Remove in 0.8.0
estimator.score_pairs(input_data)
+ with pytest.raises(NotFittedError):
+ estimator.pair_score(input_data)
with pytest.raises(NotFittedError):
estimator.decision_function(input_data)
with pytest.raises(NotFittedError):
@@ -134,7 +137,7 @@ def test_threshold_different_scores_is_finite(estimator, build_dataset,
estimator.set_params(preprocessor=preprocessor)
set_random_state(estimator)
estimator.fit(input_data, labels)
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
estimator.calibrate_threshold(input_data, labels, **kwargs)
assert len(record) == 0
@@ -178,6 +181,25 @@ def test_set_threshold():
assert identity_pairs_classifier.threshold_ == 0.5
+@pytest.mark.parametrize('value', ["ABC", None, [1, 2, 3], {'key': None},
+ (1, 2), set(),
+ np.array([[[0.], [1.]], [[1.], [3.]]])])
+def test_set_wrong_type_threshold(value):
+ """
+ Test that `set_threshold` indeed sets the threshold
+ and cannot accept nothing but float or integers, but
+ being permissive with boolean True=1.0 and False=0.0
+ """
+ model = IdentityPairsClassifier()
+ model.fit(np.array([[[0.], [1.]]]), np.array([1]))
+ msg = ('Parameter threshold must be a real number. '
+ 'Got {} instead.'.format(type(value)))
+
+ with pytest.raises(ValueError) as e: # String
+ model.set_threshold(value)
+ assert str(e.value).startswith(msg)
+
+
def test_f_beta_1_is_f_1():
# test that putting beta to 1 indeed finds the best threshold to optimize
# the f1_score
@@ -362,7 +384,7 @@ def test_calibrate_threshold_valid_parameters(valid_args):
pairs, y = rng.randn(20, 2, 5), rng.choice([-1, 1], size=20)
pairs_learner = IdentityPairsClassifier()
pairs_learner.fit(pairs, y)
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
pairs_learner.calibrate_threshold(pairs, y, **valid_args)
assert len(record) == 0
@@ -497,7 +519,7 @@ def test_validate_calibration_params_valid_parameters(
# test that no warning message is returned if valid arguments are given to
# _validate_calibration_params for all pairs metric learners, as well as
# a mocking example, and the class itself
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
estimator._validate_calibration_params(**valid_args)
assert len(record) == 0
diff --git a/test/test_quadruplets_classifiers.py b/test/test_quadruplets_classifiers.py
index efe10030..a8319961 100644
--- a/test/test_quadruplets_classifiers.py
+++ b/test/test_quadruplets_classifiers.py
@@ -3,7 +3,7 @@
from sklearn.model_selection import train_test_split
from test.test_utils import quadruplets_learners, ids_quadruplets_learners
-from sklearn.utils.testing import set_random_state
+from metric_learn.sklearn_shims import set_random_state
from sklearn import clone
import numpy as np
diff --git a/test/test_sklearn_compat.py b/test/test_sklearn_compat.py
index e18eb7f4..798d9036 100644
--- a/test/test_sklearn_compat.py
+++ b/test/test_sklearn_compat.py
@@ -4,10 +4,9 @@
from sklearn.base import TransformerMixin
from sklearn.pipeline import make_pipeline
from sklearn.utils import check_random_state
-from sklearn.utils.estimator_checks import is_public_parameter
-from sklearn.utils.testing import (assert_allclose_dense_sparse,
- set_random_state)
-
+from metric_learn.sklearn_shims import (assert_allclose_dense_sparse,
+ set_random_state, _get_args,
+ is_public_parameter, get_scorer)
from metric_learn import (Covariance, LFDA, LMNN, MLKR, NCA,
ITML_Supervised, LSML_Supervised,
MMC_Supervised, RCA_Supervised, SDML_Supervised,
@@ -16,8 +15,6 @@
import numpy as np
from sklearn.model_selection import (cross_val_score, cross_val_predict,
train_test_split, KFold)
-from sklearn.metrics.scorer import get_scorer
-from sklearn.utils.testing import _get_args
from test.test_utils import (metric_learners, ids_metric_learners,
mock_preprocessor, tuples_learners,
ids_tuples_learners, pairs_learners,
@@ -32,7 +29,7 @@ def __init__(self, n_components=None,
chunk_size=2, preprocessor=None, random_state=None):
# this init makes RCA stable for scikit-learn examples.
super(Stable_RCA_Supervised, self).__init__(
- num_chunks=2, n_components=n_components,
+ n_chunks=2, n_components=n_components,
chunk_size=chunk_size, preprocessor=preprocessor,
random_state=random_state)
@@ -40,49 +37,52 @@ def __init__(self, n_components=None,
class Stable_SDML_Supervised(SDML_Supervised):
def __init__(self, sparsity_param=0.01,
- num_constraints=None, verbose=False, preprocessor=None,
+ n_constraints=None, verbose=False, preprocessor=None,
random_state=None):
# this init makes SDML stable for scikit-learn examples.
super(Stable_SDML_Supervised, self).__init__(
sparsity_param=sparsity_param,
- num_constraints=num_constraints, verbose=verbose,
+ n_constraints=n_constraints, verbose=verbose,
preprocessor=preprocessor, balance_param=1e-5, prior='identity',
random_state=random_state)
class TestSklearnCompat(unittest.TestCase):
def test_covariance(self):
- check_estimator(Covariance)
+ check_estimator(Covariance())
def test_lmnn(self):
- check_estimator(LMNN)
+ check_estimator(LMNN())
def test_lfda(self):
- check_estimator(LFDA)
+ check_estimator(LFDA())
def test_mlkr(self):
- check_estimator(MLKR)
+ check_estimator(MLKR())
def test_nca(self):
- check_estimator(NCA)
+ check_estimator(NCA())
def test_lsml(self):
- check_estimator(LSML_Supervised)
+ check_estimator(LSML_Supervised())
def test_itml(self):
- check_estimator(ITML_Supervised)
+ check_estimator(ITML_Supervised())
def test_mmc(self):
- check_estimator(MMC_Supervised)
+ check_estimator(MMC_Supervised())
def test_sdml(self):
- check_estimator(Stable_SDML_Supervised)
+ check_estimator(Stable_SDML_Supervised())
def test_rca(self):
- check_estimator(Stable_RCA_Supervised)
+ check_estimator(Stable_RCA_Supervised())
def test_scml(self):
- check_estimator(SCML_Supervised)
+ msg = "As no value for `n_basis` was selected, "
+ with pytest.warns(UserWarning) as raised_warning:
+ check_estimator(SCML_Supervised())
+ assert msg in str(raised_warning[0].message)
RNG = check_random_state(0)
@@ -121,7 +121,8 @@ def test_array_like_inputs(estimator, build_dataset, with_preprocessor):
# we subsample the data for the test to be more efficient
input_data, _, labels, _ = train_test_split(input_data, labels,
- train_size=20)
+ train_size=40,
+ random_state=42)
X = X[:10]
estimator = clone(estimator)
@@ -149,8 +150,19 @@ def test_array_like_inputs(estimator, build_dataset, with_preprocessor):
pairs = np.array([[X[0], X[1]], [X[0], X[2]]])
pairs_variants, _ = generate_array_like(pairs)
+
+ not_implemented_msg = ""
+ # Todo in 0.7.0: Change 'not_implemented_msg' for the message that says
+ # "This learner does not have pair_distance"
+
for pairs_variant in pairs_variants:
- estimator.score_pairs(pairs_variant)
+ estimator.pair_score(pairs_variant) # All learners have pair_score
+
+ # But not all of them will have pair_distance
+ try:
+ estimator.pair_distance(pairs_variant)
+ except Exception as raised_exception:
+ assert raised_exception.value.args[0] == not_implemented_msg
@pytest.mark.parametrize('with_preprocessor', [True, False])
@@ -160,7 +172,7 @@ def test_various_scoring_on_tuples_learners(estimator, build_dataset,
with_preprocessor):
"""Tests that scikit-learn's scoring returns something finite,
for other scoring than default scoring. (List of scikit-learn's scores can be
- found in sklearn.metrics.scorer). For each type of output (predict,
+ found in sklearn.metrics._scorer). For each type of output (predict,
predict_proba, decision_function), we test a bunch of scores.
We only test on pairs learners because quadruplets don't have a y argument.
"""
@@ -226,7 +238,7 @@ def test_cross_validation_manual_vs_scikit(estimator, build_dataset,
n_splits = 3
kfold = KFold(shuffle=False, n_splits=n_splits)
n_samples = input_data.shape[0]
- fold_sizes = (n_samples // n_splits) * np.ones(n_splits, dtype=np.int)
+ fold_sizes = (n_samples // n_splits) * np.ones(n_splits, dtype=np.int64)
fold_sizes[:n_samples % n_splits] += 1
current = 0
scores, predictions = [], np.zeros(input_data.shape[0])
diff --git a/test/test_triplets_classifiers.py b/test/test_triplets_classifiers.py
index 10393919..515a0a33 100644
--- a/test/test_triplets_classifiers.py
+++ b/test/test_triplets_classifiers.py
@@ -2,10 +2,16 @@
from sklearn.exceptions import NotFittedError
from sklearn.model_selection import train_test_split
-from test.test_utils import triplets_learners, ids_triplets_learners
-from sklearn.utils.testing import set_random_state
+from metric_learn import SCML
+from test.test_utils import (
+ triplets_learners,
+ ids_triplets_learners,
+ build_triplets
+)
+from metric_learn.sklearn_shims import set_random_state
from sklearn import clone
import numpy as np
+from numpy.testing import assert_array_equal
@pytest.mark.parametrize('with_preprocessor', [True, False])
@@ -26,6 +32,49 @@ def test_predict_only_one_or_minus_one(estimator, build_dataset,
assert len(not_valid) == 0
+@pytest.mark.parametrize('estimator, build_dataset', triplets_learners,
+ ids=ids_triplets_learners)
+def test_no_zero_prediction(estimator, build_dataset):
+ """
+ Test that all predicted values are not zero, even when the
+ distance d(x,y) and d(x,z) is the same for a triplet of the
+ form (x, y, z). i.e border cases.
+ """
+ triplets, _, _, X = build_dataset(with_preprocessor=False)
+ # Force 3 dimentions only, to use cross product and get easy orthogonal vec.
+ triplets = np.array([[t[0][:3], t[1][:3], t[2][:3]] for t in triplets])
+ X = X[:, :3]
+ # Dummy fit
+ estimator = clone(estimator)
+ set_random_state(estimator)
+ estimator.fit(triplets)
+ # We force the transformation to be identity, to force euclidean distance
+ estimator.components_ = np.eye(X.shape[1])
+
+ # Get two orthogonal vectors in respect to X[1]
+ k = X[1] / np.linalg.norm(X[1]) # Normalize first vector
+ x = X[2] - X[2].dot(k) * k # Get random orthogonal vector
+ x /= np.linalg.norm(x) # Normalize
+ y = np.cross(k, x) # Get orthogonal vector to x
+ # Assert these orthogonal vectors are different
+ with pytest.raises(AssertionError):
+ assert_array_equal(X[1], x)
+ with pytest.raises(AssertionError):
+ assert_array_equal(X[1], y)
+ # Assert the distance is the same for both
+ assert estimator.get_metric()(X[1], x) == estimator.get_metric()(X[1], y)
+
+ # Form the three scenarios where predict() gives 0 with numpy.sign
+ triplets_test = np.array( # Critical examples
+ [[X[0], X[2], X[2]],
+ [X[1], X[1], X[1]],
+ [X[1], x, y]])
+ # Predict
+ predictions = estimator.predict(triplets_test)
+ # Check there are no zero values
+ assert np.sum(predictions == 0) == 0
+
+
@pytest.mark.parametrize('with_preprocessor', [True, False])
@pytest.mark.parametrize('estimator, build_dataset', triplets_learners,
ids=ids_triplets_learners)
@@ -63,3 +112,16 @@ def test_accuracy_toy_example(estimator, build_dataset):
# we force the transformation to be identity so that we control what it does
estimator.components_ = np.eye(X.shape[1])
assert estimator.score(triplets_test) == 0.25
+
+
+def test_raise_big_number_of_features():
+ triplets, _, _, X = build_triplets(with_preprocessor=False)
+ triplets = triplets[:3, :, :]
+ estimator = SCML(n_basis=320)
+ set_random_state(estimator)
+ with pytest.raises(ValueError) as exc_info:
+ estimator.fit(triplets)
+ assert exc_info.value.args[0] == \
+ "Number of features (4) is greater than the number of triplets(3)." \
+ "\nConsider using dimensionality reduction or using another basis " \
+ "generation scheme."
diff --git a/test/test_utils.py b/test/test_utils.py
index fdcb864a..c0383792 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -1,3 +1,4 @@
+import warnings
import pytest
from scipy.linalg import eigh, pinvh
from collections import namedtuple
@@ -5,7 +6,7 @@
from numpy.testing import assert_array_equal, assert_equal
from sklearn.model_selection import train_test_split
from sklearn.utils import check_random_state, shuffle
-from sklearn.utils.testing import set_random_state
+from metric_learn.sklearn_shims import set_random_state
from sklearn.base import clone
from metric_learn._util import (check_input, make_context, preprocess_tuples,
make_name, preprocess_points,
@@ -60,11 +61,11 @@ def build_regression(with_preprocessor=False):
def build_data():
input_data, labels = load_iris(return_X_y=True)
X, y = shuffle(input_data, labels, random_state=SEED)
- num_constraints = 50
+ n_constraints = 50
constraints = Constraints(y)
pairs = (
constraints
- .positive_negative_pairs(num_constraints, same_length=True,
+ .positive_negative_pairs(n_constraints, same_length=True,
random_state=check_random_state(SEED)))
return X, pairs
@@ -117,7 +118,7 @@ def build_quadruplets(with_preprocessor=False):
[learner for (learner, _) in
quadruplets_learners]))
-triplets_learners = [(SCML(), build_triplets)]
+triplets_learners = [(SCML(n_basis=320), build_triplets)]
ids_triplets_learners = list(map(lambda x: x.__class__.__name__,
[learner for (learner, _) in
triplets_learners]))
@@ -137,10 +138,10 @@ def build_quadruplets(with_preprocessor=False):
(ITML_Supervised(max_iter=5), build_classification),
(LSML_Supervised(), build_classification),
(MMC_Supervised(max_iter=5), build_classification),
- (RCA_Supervised(num_chunks=5), build_classification),
+ (RCA_Supervised(n_chunks=5), build_classification),
(SDML_Supervised(prior='identity', balance_param=1e-5),
build_classification),
- (SCML_Supervised(), build_classification)]
+ (SCML_Supervised(n_basis=80), build_classification)]
ids_classifiers = list(map(lambda x: x.__class__.__name__,
[learner for (learner, _) in
classifiers]))
@@ -353,7 +354,7 @@ def test_check_tuples_valid_tuple_size(tuple_size):
checks that checking the number of tuples (pairs, quadruplets, etc) raises
no warning if there is the right number of points in a tuple.
"""
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
check_input(tuples_prep(), type_of_inputs='tuples',
preprocessor=mock_preprocessor, tuple_size=tuple_size)
check_input(tuples_no_prep(), type_of_inputs='tuples', preprocessor=None,
@@ -378,7 +379,7 @@ def test_check_tuples_valid_tuple_size(tuple_size):
[[2.6, 2.3], [3.4, 5.0]]])])
def test_check_tuples_valid_with_preprocessor(tuples):
"""Test that valid inputs when using a preprocessor raises no warning"""
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
check_input(tuples, type_of_inputs='tuples',
preprocessor=mock_preprocessor)
assert len(record) == 0
@@ -399,7 +400,7 @@ def test_check_tuples_valid_with_preprocessor(tuples):
((3, 1), (4, 4), (29, 4)))])
def test_check_tuples_valid_without_preprocessor(tuples):
"""Test that valid inputs when using no preprocessor raises no warning"""
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
check_input(tuples, type_of_inputs='tuples', preprocessor=None)
assert len(record) == 0
@@ -408,12 +409,12 @@ def test_check_tuples_behaviour_auto_dtype():
"""Checks that check_tuples allows by default every type if using a
preprocessor, and numeric types if using no preprocessor"""
tuples_prep = [['img1.png', 'img2.png'], ['img3.png', 'img5.png']]
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
check_input(tuples_prep, type_of_inputs='tuples',
preprocessor=mock_preprocessor)
assert len(record) == 0
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
check_input(tuples_no_prep(), type_of_inputs='tuples') # numeric type
assert len(record) == 0
@@ -549,7 +550,7 @@ def test_check_classic_invalid_dtype_not_convertible(preprocessor, points):
[2.6, 2.3]])])
def test_check_classic_valid_with_preprocessor(points):
"""Test that valid inputs when using a preprocessor raises no warning"""
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
check_input(points, type_of_inputs='classic',
preprocessor=mock_preprocessor)
assert len(record) == 0
@@ -570,7 +571,7 @@ def test_check_classic_valid_with_preprocessor(points):
(3, 1, 4, 4, 29, 4))])
def test_check_classic_valid_without_preprocessor(points):
"""Test that valid inputs when using no preprocessor raises no warning"""
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
check_input(points, type_of_inputs='classic', preprocessor=None)
assert len(record) == 0
@@ -585,12 +586,12 @@ def test_check_classic_behaviour_auto_dtype():
"""Checks that check_input (for points) allows by default every type if
using a preprocessor, and numeric types if using no preprocessor"""
points_prep = ['img1.png', 'img2.png', 'img3.png', 'img5.png']
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
check_input(points_prep, type_of_inputs='classic',
preprocessor=mock_preprocessor)
assert len(record) == 0
- with pytest.warns(None) as record:
+ with warnings.catch_warnings(record=True) as record:
check_input(points_no_prep(), type_of_inputs='classic') # numeric type
assert len(record) == 0
@@ -834,9 +835,9 @@ def test_error_message_tuple_size(estimator, _):
@pytest.mark.parametrize('estimator, _', metric_learners,
ids=ids_metric_learners)
-def test_error_message_t_score_pairs(estimator, _):
- """tests that if you want to score_pairs on triplets for instance, it returns
- the right error message
+def test_error_message_t_pair_distance_or_score(estimator, _):
+ """Tests that if you want to pair_distance or pair_score on triplets
+ for instance, it returns the right error message
"""
estimator = clone(estimator)
set_random_state(estimator)
@@ -844,12 +845,22 @@ def test_error_message_t_score_pairs(estimator, _):
triplets = np.array([[[1.3, 6.3], [3., 6.8], [6.5, 4.4]],
[[1.9, 5.3], [1., 7.8], [3.2, 1.2]]])
with pytest.raises(ValueError) as raised_err:
- estimator.score_pairs(triplets)
+ estimator.pair_score(triplets)
expected_msg = ("Tuples of 2 element(s) expected{}. Got tuples of 3 "
"element(s) instead (shape=(2, 3, 2)):\ninput={}.\n"
.format(make_context(estimator), triplets))
assert str(raised_err.value) == expected_msg
+ not_implemented_msg = ""
+ # Todo in 0.7.0: Change 'not_implemented_msg' for the message that says
+ # "This learner does not have pair_distance"
+
+ # One exception will trigger for sure
+ with pytest.raises(Exception) as raised_exception:
+ estimator.pair_distance(triplets)
+ err_value = raised_exception.value.args[0]
+ assert err_value == expected_msg or err_value == not_implemented_msg
+
def test_preprocess_tuples_simple_example():
"""Test the preprocessor on a very simple example of tuples to ensure the
@@ -930,31 +941,59 @@ def test_same_with_or_without_preprocessor(estimator, build_dataset):
method)(formed_test)
assert np.array(output_with_prep == output_with_prep_formed).all()
- # test score_pairs
- output_with_prep = estimator_with_preprocessor.score_pairs(
- indicators_to_transform[[[[0, 2], [5, 3]]]])
- output_without_prep = estimator_without_preprocessor.score_pairs(
- formed_points_to_transform[[[[0, 2], [5, 3]]]])
+ # Test pair_score, all learners have it.
+ idx1 = np.array([[0, 2], [5, 3]], dtype=int)
+ output_with_prep = estimator_with_preprocessor.pair_score(
+ indicators_to_transform[idx1])
+ output_without_prep = estimator_without_preprocessor.pair_score(
+ formed_points_to_transform[idx1])
assert np.array(output_with_prep == output_without_prep).all()
- output_with_prep = estimator_with_preprocessor.score_pairs(
- indicators_to_transform[[[[0, 2], [5, 3]]]])
- output_without_prep = estimator_with_prep_formed.score_pairs(
- formed_points_to_transform[[[[0, 2], [5, 3]]]])
+ output_with_prep = estimator_with_preprocessor.pair_score(
+ indicators_to_transform[idx1])
+ output_without_prep = estimator_with_prep_formed.pair_score(
+ formed_points_to_transform[idx1])
assert np.array(output_with_prep == output_without_prep).all()
- # test transform
- output_with_prep = estimator_with_preprocessor.transform(
- indicators_to_transform)
- output_without_prep = estimator_without_preprocessor.transform(
- formed_points_to_transform)
- assert np.array(output_with_prep == output_without_prep).all()
-
- output_with_prep = estimator_with_preprocessor.transform(
- indicators_to_transform)
- output_without_prep = estimator_with_prep_formed.transform(
- formed_points_to_transform)
- assert np.array(output_with_prep == output_without_prep).all()
+ # Test pair_distance
+ not_implemented_msg = ""
+ # Todo in 0.7.0: Change 'not_implemented_msg' for the message that says
+ # "This learner does not have pair_distance"
+ try:
+ output_with_prep = estimator_with_preprocessor.pair_distance(
+ indicators_to_transform[idx1])
+ output_without_prep = estimator_without_preprocessor.pair_distance(
+ formed_points_to_transform[idx1])
+ assert np.array(output_with_prep == output_without_prep).all()
+
+ output_with_prep = estimator_with_preprocessor.pair_distance(
+ indicators_to_transform[idx1])
+ output_without_prep = estimator_with_prep_formed.pair_distance(
+ formed_points_to_transform[idx1])
+ assert np.array(output_with_prep == output_without_prep).all()
+
+ except Exception as raised_exception:
+ assert raised_exception.value.args[0] == not_implemented_msg
+
+ # Test transform
+ not_implemented_msg = ""
+ # Todo in 0.7.0: Change 'not_implemented_msg' for the message that says
+ # "This learner does not have transform"
+ try:
+ output_with_prep = estimator_with_preprocessor.transform(
+ indicators_to_transform)
+ output_without_prep = estimator_without_preprocessor.transform(
+ formed_points_to_transform)
+ assert np.array(output_with_prep == output_without_prep).all()
+
+ output_with_prep = estimator_with_preprocessor.transform(
+ indicators_to_transform)
+ output_without_prep = estimator_with_prep_formed.transform(
+ formed_points_to_transform)
+ assert np.array(output_with_prep == output_without_prep).all()
+
+ except Exception as raised_exception:
+ assert raised_exception.value.args[0] == not_implemented_msg
def test_check_collapsed_pairs_raises_no_error():
@@ -1055,6 +1094,53 @@ def test__check_sdp_from_eigen_returns_definiteness(w, is_definite):
assert _check_sdp_from_eigen(w) == is_definite
+@pytest.mark.unit
+@pytest.mark.parametrize('w, tol, is_definite',
+ [(np.array([5., 3.]), 2, True),
+ (np.array([5., 1.]), 2, False),
+ (np.array([5., -1.]), 2, False)])
+def test__check_sdp_from_eigen_tol_psd(w, tol, is_definite):
+ """Tests that _check_sdp_from_eigen, for PSD matrices, returns
+ False if an eigenvalue is lower than tol"""
+ assert _check_sdp_from_eigen(w, tol=tol) == is_definite
+
+
+@pytest.mark.unit
+@pytest.mark.parametrize('w, tol',
+ [(np.array([5., -3.]), 2),
+ (np.array([1., -3.]), 2)])
+def test__check_sdp_from_eigen_tol_non_psd(w, tol):
+ """Tests that _check_sdp_from_eigen raises a NonPSDError
+ when there is a negative value with abs value higher than tol"""
+ with pytest.raises(NonPSDError):
+ _check_sdp_from_eigen(w, tol=tol)
+
+
+@pytest.mark.unit
+@pytest.mark.parametrize('w, is_definite',
+ [(np.array([1e5, 1e5, 1e5, 1e5,
+ 1e5, 1e5, 1e-20]), False),
+ (np.array([1e-10, 1e-10]), True)])
+def test__check_sdp_from_eigen_tol_default_psd(w, is_definite):
+ """Tests that the default tol argument gives good results for edge cases
+ like even if the determinant is high but clearly one eigenvalue is low,
+ (undefinite so returns False) or when all eigenvalues are low (definite so
+ returns True)"""
+ assert _check_sdp_from_eigen(w, tol=None) == is_definite
+
+
+@pytest.mark.unit
+@pytest.mark.parametrize('w',
+ [np.array([1., -1.]),
+ np.array([-1e-10, 1e-10])])
+def test__check_sdp_from_eigen_tol_default_non_psd(w):
+ """Tests that the default tol argument is good for raising
+ NonPSDError, e.g. that when a value is clearly relatively
+ negative it raises such an error"""
+ with pytest.raises(NonPSDError):
+ _check_sdp_from_eigen(w, tol=None)
+
+
def test__check_n_components():
"""Checks that n_components returns what is expected
(including the errors)"""