MATH-1547: Ranking of any number of the best matching units of a neural network.
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 2d82bf0..46e5b45 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -54,6 +54,9 @@
</release>
<release version="4.0" date="XXXX-XX-XX" description="">
+ <action dev="erans" type="update" issue="MATH-1547">
+ More flexible ranking of SOFM.
+ </action>
<action dev="erans" type="fix" issue="MATH-1537" due-to="Jin Xu">
Clean-up (typos and unused "import" statements).
</action>
diff --git a/src/main/java/org/apache/commons/math4/ml/neuralnet/MapRanking.java b/src/main/java/org/apache/commons/math4/ml/neuralnet/MapRanking.java
new file mode 100644
index 0000000..4994e8c
--- /dev/null
+++ b/src/main/java/org/apache/commons/math4/ml/neuralnet/MapRanking.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.math4.ml.neuralnet;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+import org.apache.commons.math4.exception.NotStrictlyPositiveException;
+import org.apache.commons.math4.ml.distance.DistanceMeasure;
+
+/**
+ * Utility for ranking the units (neurons) of a network.
+ *
+ * @since 4.0
+ */
+public class MapRanking {
+ /** List corresponding to the map passed to the constructor. */
+ private final List<Neuron> map = new ArrayList<>();
+ /** Distance function for sorting. */
+ private final DistanceMeasure distance;
+
+ /**
+ * @param neurons List to be ranked.
+ * No defensive copy is performed.
+ * The {@link #rank(double[],int) created list of units} will
+ * be sorted in increasing order of the {@code distance}.
+ * @param distance Distance function.
+ */
+ public MapRanking(Iterable<Neuron> neurons,
+ DistanceMeasure distance) {
+ this.distance = distance;
+
+ for (Neuron n : neurons) {
+ map.add(n); // No defensive copy.
+ }
+ }
+
+ /**
+ * Creates a list of the neurons whose features best correspond to the
+ * given {@code features}.
+ *
+ * @param features Data.
+ * @return the list of neurons sorted in decreasing order of distance to
+ * the given data.
+ * @throws org.apache.commons.math4.exception.DimensionMismatchException
+ * if the size of the input is not compatible with the neurons features
+ * size.
+ */
+ public List<Neuron> rank(double[] features) {
+ return rank(features, map.size());
+ }
+
+ /**
+ * Creates a list of the neurons whose features best correspond to the
+ * given {@code features}.
+ *
+ * @param features Data.
+ * @param max Maximum size of the returned list.
+ * @return the list of neurons sorted in decreasing order of distance to
+ * the given data.
+ * @throws org.apache.commons.math4.exception.DimensionMismatchException
+ * if the size of the input is not compatible with the neurons features
+ * size.
+ * @throws NotStrictlyPositiveException if {@code max <= 0}.
+ */
+ public List<Neuron> rank(double[] features,
+ int max) {
+ if (max <= 0) {
+ throw new NotStrictlyPositiveException(max);
+ }
+ final int m = max <= map.size() ?
+ max :
+ map.size();
+ final List<PairNeuronDouble> list = new ArrayList<>(m);
+
+ for (final Neuron n : map) {
+ final double d = distance.compute(n.getFeatures(), features);
+ final PairNeuronDouble p = new PairNeuronDouble(n, d);
+
+ if (list.size() < m) {
+ list.add(p);
+ if (list.size() > 1) {
+ // Sort if there is more than 1 element.
+ Collections.sort(list, PairNeuronDouble.COMPARATOR);
+ }
+ } else {
+ final int last = list.size() - 1;
+ if (PairNeuronDouble.COMPARATOR.compare(p, list.get(last)) < 0) {
+ list.set(last, p); // Replace worst entry.
+ if (last > 0) {
+ // Sort if there is more than 1 element.
+ Collections.sort(list, PairNeuronDouble.COMPARATOR);
+ }
+ }
+ }
+ }
+
+ final List<Neuron> result = new ArrayList<>(m);
+ for (PairNeuronDouble p : list) {
+ result.add(p.getNeuron());
+ }
+
+ return result;
+ }
+
+ /**
+ * Helper data structure holding a (Neuron, double) pair.
+ */
+ private static class PairNeuronDouble {
+ /** Comparator. */
+ static final Comparator<PairNeuronDouble> COMPARATOR
+ = new Comparator<PairNeuronDouble>() {
+ /** {@inheritDoc} */
+ @Override
+ public int compare(PairNeuronDouble o1,
+ PairNeuronDouble o2) {
+ return Double.compare(o1.value, o2.value);
+ }
+ };
+ /** Key. */
+ private final Neuron neuron;
+ /** Value. */
+ private final double value;
+
+ /**
+ * @param neuron Neuron.
+ * @param value Value.
+ */
+ PairNeuronDouble(Neuron neuron, double value) {
+ this.neuron = neuron;
+ this.value = value;
+ }
+
+ /** @return the neuron. */
+ public Neuron getNeuron() {
+ return neuron;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/math4/ml/neuralnet/MapUtils.java b/src/main/java/org/apache/commons/math4/ml/neuralnet/MapUtils.java
index a793fa0..4b1eb20 100644
--- a/src/main/java/org/apache/commons/math4/ml/neuralnet/MapUtils.java
+++ b/src/main/java/org/apache/commons/math4/ml/neuralnet/MapUtils.java
@@ -17,12 +17,9 @@
package org.apache.commons.math4.ml.neuralnet;
-import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
-import java.util.Comparator;
import org.apache.commons.math4.exception.NoDataException;
import org.apache.commons.math4.ml.distance.DistanceMeasure;
@@ -56,17 +53,7 @@
public static Neuron findBest(double[] features,
Iterable<Neuron> neurons,
DistanceMeasure distance) {
- Neuron best = null;
- double min = Double.POSITIVE_INFINITY;
- for (final Neuron n : neurons) {
- final double d = distance.compute(n.getFeatures(), features);
- if (d < min) {
- min = d;
- best = n;
- }
- }
-
- return best;
+ return new MapRanking(neurons, distance).rank(features, 1).get(0);
}
/**
@@ -85,27 +72,8 @@
public static Pair<Neuron, Neuron> findBestAndSecondBest(double[] features,
Iterable<Neuron> neurons,
DistanceMeasure distance) {
- Neuron[] best = { null, null };
- double[] min = { Double.POSITIVE_INFINITY,
- Double.POSITIVE_INFINITY };
- for (final Neuron n : neurons) {
- final double d = distance.compute(n.getFeatures(), features);
- if (d < min[0]) {
- // Replace second best with old best.
- min[1] = min[0];
- best[1] = best[0];
-
- // Store current as new best.
- min[0] = d;
- best[0] = n;
- } else if (d < min[1]) {
- // Replace old second best with current.
- min[1] = d;
- best[1] = n;
- }
- }
-
- return new Pair<>(best[0], best[1]);
+ final List<Neuron> list = new MapRanking(neurons, distance).rank(features, 2);
+ return new Pair<>(list.get(0), list.get(1));
}
/**
@@ -130,22 +98,7 @@
public static Neuron[] sort(double[] features,
Iterable<Neuron> neurons,
DistanceMeasure distance) {
- final List<PairNeuronDouble> list = new ArrayList<>();
-
- for (final Neuron n : neurons) {
- final double d = distance.compute(n.getFeatures(), features);
- list.add(new PairNeuronDouble(n, d));
- }
-
- Collections.sort(list, PairNeuronDouble.COMPARATOR);
-
- final int len = list.size();
- final Neuron[] sorted = new Neuron[len];
-
- for (int i = 0; i < len; i++) {
- sorted[i] = list.get(i).getNeuron();
- }
- return sorted;
+ return new MapRanking(neurons, distance).rank(features).toArray(new Neuron[0]);
}
/**
@@ -289,39 +242,4 @@
return ((double) notAdjacentCount) / count;
}
-
- /**
- * Helper data structure holding a (Neuron, double) pair.
- */
- private static class PairNeuronDouble {
- /** Comparator. */
- static final Comparator<PairNeuronDouble> COMPARATOR
- = new Comparator<PairNeuronDouble>() {
- /** {@inheritDoc} */
- @Override
- public int compare(PairNeuronDouble o1,
- PairNeuronDouble o2) {
- return Double.compare(o1.value, o2.value);
- }
- };
- /** Key. */
- private final Neuron neuron;
- /** Value. */
- private final double value;
-
- /**
- * @param neuron Neuron.
- * @param value Value.
- */
- PairNeuronDouble(Neuron neuron, double value) {
- this.neuron = neuron;
- this.value = value;
- }
-
- /** @return the neuron. */
- public Neuron getNeuron() {
- return neuron;
- }
-
- }
}
diff --git a/src/test/java/org/apache/commons/math4/ml/neuralnet/MapRankingTest.java b/src/test/java/org/apache/commons/math4/ml/neuralnet/MapRankingTest.java
new file mode 100644
index 0000000..d835088
--- /dev/null
+++ b/src/test/java/org/apache/commons/math4/ml/neuralnet/MapRankingTest.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.math4.ml.neuralnet;
+
+import java.util.Set;
+import java.util.HashSet;
+
+import org.apache.commons.math4.exception.NotStrictlyPositiveException;
+import org.apache.commons.math4.ml.distance.DistanceMeasure;
+import org.apache.commons.math4.ml.distance.EuclideanDistance;
+import org.apache.commons.math4.ml.neuralnet.FeatureInitializer;
+import org.apache.commons.math4.ml.neuralnet.FeatureInitializerFactory;
+import org.apache.commons.math4.ml.neuralnet.MapUtils;
+import org.apache.commons.math4.ml.neuralnet.Network;
+import org.apache.commons.math4.ml.neuralnet.Neuron;
+import org.apache.commons.math4.ml.neuralnet.oned.NeuronString;
+import org.junit.Test;
+import org.junit.Assert;
+
+/**
+ * Tests for {@link MapRanking} class.
+ */
+public class MapRankingTest {
+ /*
+ * Test assumes that the network is
+ *
+ * 0-----1-----2
+ */
+ @Test
+ public void testFindClosestNeuron() {
+ final FeatureInitializer init
+ = new OffsetFeatureInitializer(FeatureInitializerFactory.uniform(-0.1, 0.1));
+ final FeatureInitializer[] initArray = { init };
+
+ final MapRanking ranking = new MapRanking(new NeuronString(3, false, initArray).getNetwork(),
+ new EuclideanDistance());
+
+ final Set<Neuron> allBest = new HashSet<>();
+ final Set<Neuron> best = new HashSet<>();
+ double[][] features;
+
+ // The following tests ensures that
+ // 1. the same neuron is always selected when the input feature is
+ // in the range of the initializer,
+ // 2. different network's neuron have been selected by inputs features
+ // that belong to different ranges.
+
+ best.clear();
+ features = new double[][] {
+ { -1 },
+ { 0.4 },
+ };
+ for (double[] f : features) {
+ best.addAll(ranking.rank(f, 1));
+ }
+ Assert.assertEquals(1, best.size());
+ allBest.addAll(best);
+
+ best.clear();
+ features = new double[][] {
+ { 0.6 },
+ { 1.4 },
+ };
+ for (double[] f : features) {
+ best.addAll(ranking.rank(f, 1));
+ }
+ Assert.assertEquals(1, best.size());
+ allBest.addAll(best);
+
+ best.clear();
+ features = new double[][] {
+ { 1.6 },
+ { 3 },
+ };
+ for (double[] f : features) {
+ best.addAll(ranking.rank(f, 1));
+ }
+ Assert.assertEquals(1, best.size());
+ allBest.addAll(best);
+
+ Assert.assertEquals(3, allBest.size());
+ }
+
+ @Test(expected=NotStrictlyPositiveException.class)
+ public void testRankPrecondition() {
+ final FeatureInitializer init
+ = new OffsetFeatureInitializer(FeatureInitializerFactory.uniform(-0.1, 0.1));
+ final FeatureInitializer[] initArray = { init };
+
+ new MapRanking(new NeuronString(3, false, initArray).getNetwork(),
+ new EuclideanDistance()).rank(new double[] { -1 }, 0);
+ }
+}