Skip to content

Commit

Permalink
tests/Distance: Consolidate
Browse files Browse the repository at this point in the history
Add DOT_PRODUCT testing.
Drop references to PEARSON and ANGLE, the implementations are no longer
available.
Drop 'fail' parameter, it is no longer needed.
Convert test data to a list of pytest.param-s including ids.
Use reduced precision only for the COSINE metric or fp32 mode.

Signed-off-by: Jan Vesely <[email protected]>
  • Loading branch information
jvesely committed Aug 13, 2023
1 parent 693c336 commit 2c59d26
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 52 deletions.
1 change: 1 addition & 0 deletions psyneulink/core/globals/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ def is_distance_metric(s):
DistanceMetricLiteral = Literal[
'max_abs_diff',
'difference',
'dot_product',
'normed_L0_similarity',
'euclidean',
'angle',
Expand Down
80 changes: 28 additions & 52 deletions tests/functions/test_distance.py
Original file line number Diff line number Diff line change
@@ -1,82 +1,58 @@
import numpy as np
import psyneulink.core.llvm as pnlvm
import psyneulink.core.components.functions.function as Function
import psyneulink.core.components.functions.nonstateful.objectivefunctions as Functions
import psyneulink.core.components.functions as Functions
import psyneulink.core.globals.keywords as kw
import pytest

SIZE=1000
# Some metrics (CROSS_ENTROPY) don't like 0s
test_var = np.random.rand(2, SIZE) + Function.EPSILON
test_var = np.random.rand(2, SIZE) + Functions.EPSILON
v1 = test_var[0]
v2 = test_var[1]
norm = len(test_var[0])

def correlation(v1,v2):
def correlation(v1, v2):
v1_norm = v1 - np.mean(v1)
v2_norm = v2 - np.mean(v2)
return np.sum(v1_norm * v2_norm) / np.sqrt(np.sum(v1_norm**2) * np.sum(v2_norm**2))


test_data = [
(kw.MAX_ABS_DIFF, False, None, np.max(abs(v1 - v2))),
(kw.MAX_ABS_DIFF, True, None, np.max(abs(v1 - v2))),
(kw.DIFFERENCE, False, None, np.sum(np.abs(v1 - v2))),
(kw.DIFFERENCE, True, None, np.sum(np.abs(v1 - v2)) / norm),
(kw.COSINE, False, None, 1 - np.abs(np.sum(v1 * v2) / (
np.sqrt(np.sum(v1**2)) *
np.sqrt(np.sum(v2**2))) )),
(kw.NORMED_L0_SIMILARITY, False, None, 1 - np.sum(np.abs(v1 - v2) / 4)),
(kw.NORMED_L0_SIMILARITY, True, None, (1 - np.sum(np.abs(v1 - v2) / 4)) / norm),
(kw.EUCLIDEAN, False, None, np.linalg.norm(v1 - v2)),
(kw.EUCLIDEAN, True, None, np.linalg.norm(v1 - v2) / norm),
(kw.ANGLE, False, "Needs sci-py", 0),
(kw.ANGLE, True, "Needs sci-py", 0 / norm),
(kw.CORRELATION, False, None, 1 - np.abs(correlation(v1,v2))),
(kw.CORRELATION, True, None, 1 - np.abs(correlation(v1,v2))),
(kw.CROSS_ENTROPY, False, None, -np.sum(v1 * np.log(v2))),
(kw.CROSS_ENTROPY, True, None, -np.sum(v1 * np.log(v2)) / norm),
(kw.ENERGY, False, None, -np.sum(v1 * v2) / 2),
(kw.ENERGY, True, None, (-np.sum(v1 * v2) / 2) / norm**2),
]

# use list, naming function produces ugly names
names = [
"MAX_ABS_DIFF",
"MAX_ABS_DIFF NORMALIZED",
"DIFFERENCE",
"DIFFERENCE NORMALIZED",
"COSINE",
"NORMED_L0_SIMILARITY",
"NORMED_L0_SIMILARITY NORMALIZED",
"EUCLIDEAN",
"EUCLIDEAN NORMALIZED",
"ANGLE",
"ANGLE NORMALIZED",
"CORRELATION",
"CORRELATION NORMALIZED",
# "PEARSON",
# "PEARSON NORMALIZED",
"CROSS_ENTROPY",
"CROSS_ENTROPY NORMALIZED",
"ENERGY",
"ENERGY NORMALIZED",
pytest.param(kw.MAX_ABS_DIFF, False, np.max(abs(v1 - v2)), id="MAX_ABS_DIFF"),
pytest.param(kw.MAX_ABS_DIFF, True, np.max(abs(v1 - v2)), id="MAX_ABS_DIFF NORMALIZED"),
pytest.param(kw.DIFFERENCE, False, np.sum(np.abs(v1 - v2)), id="DIFFERENCE"),
pytest.param(kw.DIFFERENCE, True, np.sum(np.abs(v1 - v2)) / norm, id="DIFFERENCE NORMALIZED"),
pytest.param(kw.COSINE, False, 1 - np.abs(np.sum(v1 * v2) / (np.sqrt(np.sum(v1 ** 2)) * np.sqrt(np.sum(v2 ** 2)))), id="COSINE"),
pytest.param(kw.COSINE, True, 1 - np.abs(np.sum(v1 * v2) / (np.sqrt(np.sum(v1 ** 2)) * np.sqrt(np.sum(v2 ** 2)))), id="COSINE NORMALIZED"),
pytest.param(kw.NORMED_L0_SIMILARITY, False, 1 - np.sum(np.abs(v1 - v2) / 4), id="NORMED_L0_SIMILARITY"),
pytest.param(kw.NORMED_L0_SIMILARITY, True, (1 - np.sum(np.abs(v1 - v2) / 4)) / norm, id="NORMED_L0_SIMILARITY NORMALIZED"),
pytest.param(kw.EUCLIDEAN, False, np.linalg.norm(v1 - v2), id="EUCLIDEAN"),
pytest.param(kw.EUCLIDEAN, True, np.linalg.norm(v1 - v2) / norm, id="EUCLIDEAN NORMALIZED"),
pytest.param(kw.CORRELATION, False, 1 - np.abs(correlation(v1, v2)), id="CORRELATION"),
pytest.param(kw.CORRELATION, True, 1 - np.abs(correlation(v1, v2)), id="CORRELATION NORMALIZED"),
pytest.param(kw.CROSS_ENTROPY, False, -np.sum(v1 * np.log(v2)), id="CROSS_ENTROPY"),
pytest.param(kw.CROSS_ENTROPY, True, -np.sum(v1 * np.log(v2)) / norm, id="CROSS_ENTROPY NORMALIZED"),
pytest.param(kw.ENERGY, False, -np.sum(v1 * v2) / 2, id="ENERGY"),
pytest.param(kw.ENERGY, True, (-np.sum(v1 * v2) / 2) / norm ** 2, id="ENERGY NORMALIZED"),
pytest.param(kw.DOT_PRODUCT, False, np.dot(v1, v2), id="DOT_PRODUCT"),
pytest.param(kw.DOT_PRODUCT, True, np.dot(v1, v2) / norm, id="DOT_PRODUCT NORMALIZED"),
]

@pytest.mark.function
@pytest.mark.distance_function
@pytest.mark.benchmark
@pytest.mark.parametrize("metric, normalize, fail, expected", test_data, ids=names)
@pytest.mark.parametrize("metric, normalize, expected", test_data)
@pytest.mark.parametrize("variable", [test_var, test_var.astype(np.float32), test_var.tolist()], ids=["np.default", "np.float32", "list"])
def test_basic(variable, metric, normalize, fail, expected, benchmark, func_mode):
if fail is not None:
pytest.xfail(fail)
def test_basic(variable, metric, normalize, expected, benchmark, func_mode):

benchmark.group = "DistanceFunction " + metric + ("-normalized" if normalize else "")
f = Functions.Distance(default_variable=variable, metric=metric, normalize=normalize)
EX = pytest.helpers.get_func_execution(f, func_mode)

res = benchmark(EX, variable)

np.testing.assert_allclose(res, expected, rtol=1e-5, atol=1e-8)
assert np.isscalar(res) or len(res) == 1 or (metric == kw.PEARSON and res.size == 4)
# FIXME: Python calculation of COSINE using fp32 inputs are not accurate.
# LLVM calculations of most metrics using fp32 are not accurate.
tol = {'rtol':1e-5, 'atol':1e-8} if metric == kw.COSINE or pytest.helpers.llvm_current_fp_precision() == 'fp32' else {}
np.testing.assert_allclose(res, expected, **tol)
assert np.isscalar(res) or len(res) == 1

0 comments on commit 2c59d26

Please sign in to comment.