blob: 4a6b1857d46a2fe4e82dc55b63ba7f752d94e024 [file] [log] [blame]
/*
* 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.neuralnet.twod;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Collection;
import org.apache.commons.math4.neuralnet.DistanceMeasure;
import org.apache.commons.math4.neuralnet.EuclideanDistance;
import org.apache.commons.math4.neuralnet.FeatureInitializer;
import org.apache.commons.math4.neuralnet.Network;
import org.apache.commons.math4.neuralnet.Neuron;
import org.apache.commons.math4.neuralnet.SquareNeighbourhood;
import org.apache.commons.math4.neuralnet.MapRanking;
import org.apache.commons.math4.neuralnet.internal.NeuralNetException;
import org.apache.commons.math4.neuralnet.twod.util.LocationFinder;
/**
* Neural network with the topology of a two-dimensional surface.
* Each neuron defines one surface element.
* <br>
* This network is primarily intended to represent a
* <a href="http://en.wikipedia.org/wiki/Kohonen">
* Self Organizing Feature Map</a>.
*
* @see org.apache.commons.math4.neuralnet.sofm
* @since 3.3
*/
public class NeuronSquareMesh2D
implements Iterable<Neuron> {
/** Minimal number of rows or columns. */
private static final int MIN_ROWS = 2;
/** Underlying network. */
private final Network network;
/** Number of rows. */
private final int numberOfRows;
/** Number of columns. */
private final int numberOfColumns;
/** Wrap. */
private final boolean wrapRows;
/** Wrap. */
private final boolean wrapColumns;
/** Neighbourhood type. */
private final SquareNeighbourhood neighbourhood;
/**
* Mapping of the 2D coordinates (in the rectangular mesh) to
* the neuron identifiers (attributed by the {@link #network}
* instance).
*/
private final long[][] identifiers;
/**
* Horizontal (along row) direction.
* @since 3.6
*/
public enum HorizontalDirection {
/** Column at the right of the current column. */
RIGHT,
/** Current column. */
CENTER,
/** Column at the left of the current column. */
LEFT,
}
/**
* Vertical (along column) direction.
* @since 3.6
*/
public enum VerticalDirection {
/** Row above the current row. */
UP,
/** Current row. */
CENTER,
/** Row below the current row. */
DOWN,
}
/**
* @param wrapRowDim Whether to wrap the first dimension (i.e the first
* and last neurons will be linked together).
* @param wrapColDim Whether to wrap the second dimension (i.e the first
* and last neurons will be linked together).
* @param neighbourhoodType Neighbourhood type.
* @param featuresList Arrays that will initialize the features sets of
* the network's neurons.
* @throws IllegalArgumentException if {@code numRows < 2} or
* {@code numCols < 2}.
*/
public NeuronSquareMesh2D(boolean wrapRowDim,
boolean wrapColDim,
SquareNeighbourhood neighbourhoodType,
double[][][] featuresList) {
numberOfRows = featuresList.length;
numberOfColumns = featuresList[0].length;
if (numberOfRows < MIN_ROWS) {
throw new NeuralNetException(NeuralNetException.TOO_SMALL, numberOfRows, MIN_ROWS);
}
if (numberOfColumns < MIN_ROWS) {
throw new NeuralNetException(NeuralNetException.TOO_SMALL, numberOfColumns, MIN_ROWS);
}
wrapRows = wrapRowDim;
wrapColumns = wrapColDim;
neighbourhood = neighbourhoodType;
final int fLen = featuresList[0][0].length;
network = new Network(0, fLen);
identifiers = new long[numberOfRows][numberOfColumns];
// Add neurons.
for (int i = 0; i < numberOfRows; i++) {
for (int j = 0; j < numberOfColumns; j++) {
identifiers[i][j] = network.createNeuron(featuresList[i][j]);
}
}
// Add links.
createLinks();
}
/**
* Creates a two-dimensional network composed of square cells:
* Each neuron not located on the border of the mesh has four
* neurons linked to it.
* <br>
* The links are bi-directional.
* <br>
* The topology of the network can also be a cylinder (if one
* of the dimensions is wrapped) or a torus (if both dimensions
* are wrapped).
*
* @param numRows Number of neurons in the first dimension.
* @param wrapRowDim Whether to wrap the first dimension (i.e the first
* and last neurons will be linked together).
* @param numCols Number of neurons in the second dimension.
* @param wrapColDim Whether to wrap the second dimension (i.e the first
* and last neurons will be linked together).
* @param neighbourhoodType Neighbourhood type.
* @param featureInit Array of functions that will initialize the
* corresponding element of the features set of each newly created
* neuron. In particular, the size of this array defines the size of
* feature set.
* @throws IllegalArgumentException if {@code numRows < 2} or
* {@code numCols < 2}.
*/
public NeuronSquareMesh2D(int numRows,
boolean wrapRowDim,
int numCols,
boolean wrapColDim,
SquareNeighbourhood neighbourhoodType,
FeatureInitializer[] featureInit) {
if (numRows < MIN_ROWS) {
throw new NeuralNetException(NeuralNetException.TOO_SMALL, numRows, MIN_ROWS);
}
if (numCols < MIN_ROWS) {
throw new NeuralNetException(NeuralNetException.TOO_SMALL, numCols, MIN_ROWS);
}
numberOfRows = numRows;
wrapRows = wrapRowDim;
numberOfColumns = numCols;
wrapColumns = wrapColDim;
neighbourhood = neighbourhoodType;
identifiers = new long[numberOfRows][numberOfColumns];
final int fLen = featureInit.length;
network = new Network(0, fLen);
// Add neurons.
for (int i = 0; i < numRows; i++) {
for (int j = 0; j < numCols; j++) {
final double[] features = new double[fLen];
for (int fIndex = 0; fIndex < fLen; fIndex++) {
features[fIndex] = featureInit[fIndex].value();
}
identifiers[i][j] = network.createNeuron(features);
}
}
// Add links.
createLinks();
}
/**
* Constructor with restricted access, solely used for making a
* {@link #copy() deep copy}.
*
* @param wrapRowDim Whether to wrap the first dimension (i.e the first
* and last neurons will be linked together).
* @param wrapColDim Whether to wrap the second dimension (i.e the first
* and last neurons will be linked together).
* @param neighbourhoodType Neighbourhood type.
* @param net Underlying network.
* @param idGrid Neuron identifiers.
*/
private NeuronSquareMesh2D(boolean wrapRowDim,
boolean wrapColDim,
SquareNeighbourhood neighbourhoodType,
Network net,
long[][] idGrid) {
numberOfRows = idGrid.length;
numberOfColumns = idGrid[0].length;
wrapRows = wrapRowDim;
wrapColumns = wrapColDim;
neighbourhood = neighbourhoodType;
network = net;
identifiers = idGrid;
}
/**
* Performs a deep copy of this instance.
* Upon return, the copied and original instances will be independent:
* Updating one will not affect the other.
*
* @return a new instance with the same state as this instance.
* @since 3.6
*/
public synchronized NeuronSquareMesh2D copy() {
final long[][] idGrid = new long[numberOfRows][numberOfColumns];
for (int r = 0; r < numberOfRows; r++) {
for (int c = 0; c < numberOfColumns; c++) {
idGrid[r][c] = identifiers[r][c];
}
}
return new NeuronSquareMesh2D(wrapRows,
wrapColumns,
neighbourhood,
network.copy(),
idGrid);
}
/** {@inheritDoc} */
@Override
public Iterator<Neuron> iterator() {
return network.iterator();
}
/**
* Retrieves the underlying network.
* A reference is returned (enabling, for example, the network to be
* trained).
* This also implies that calling methods that modify the {@link Network}
* topology may cause this class to become inconsistent.
*
* @return the network.
*/
public Network getNetwork() {
return network;
}
/**
* Gets the number of neurons in each row of this map.
*
* @return the number of rows.
*/
public int getNumberOfRows() {
return numberOfRows;
}
/**
* Gets the number of neurons in each column of this map.
*
* @return the number of column.
*/
public int getNumberOfColumns() {
return numberOfColumns;
}
/**
* Retrieves the neuron at location {@code (i, j)} in the map.
* The neuron at position {@code (0, 0)} is located at the upper-left
* corner of the map.
*
* @param i Row index.
* @param j Column index.
* @return the neuron at {@code (i, j)}.
* @throws IllegalArgumentException if {@code i} or {@code j} is
* out of range.
*
* @see #getNeuron(int,int,HorizontalDirection,VerticalDirection)
*/
public Neuron getNeuron(int i,
int j) {
if (i < 0 ||
i >= numberOfRows) {
throw new NeuralNetException(NeuralNetException.OUT_OF_RANGE,
i, 0, numberOfRows - 1);
}
if (j < 0 ||
j >= numberOfColumns) {
throw new NeuralNetException(NeuralNetException.OUT_OF_RANGE,
i, 0, numberOfColumns - 1);
}
return network.getNeuron(identifiers[i][j]);
}
/**
* Retrieves the requested neuron relative to the given {@code (row, col)}
* position.
* The neuron at position {@code (0, 0)} is located at the upper-left
* corner of the map.
*
* @param row Row index.
* @param col Column index.
* @param alongRowDir Direction along the given {@code row} (i.e. an
* offset will be added to the given <em>column</em> index.
* @param alongColDir Direction along the given {@code col} (i.e. an
* offset will be added to the given <em>row</em> index.
* @return the neuron at the requested location, or {@code null} if
* the location is not on the map.
*
* @see #getNeuron(int,int)
*/
public Neuron getNeuron(int row,
int col,
HorizontalDirection alongRowDir,
VerticalDirection alongColDir) {
final int[] location = getLocation(row, col, alongRowDir, alongColDir);
return location == null ? null : getNeuron(location[0], location[1]);
}
/**
* Computes various {@link DataVisualization indicators} of the quality
* of the representation of the given {@code data} by this map.
*
* @param data Features.
* @return a new instance holding quality indicators.
*/
public DataVisualization computeQualityIndicators(Iterable<double[]> data) {
return DataVisualization.from(copy(), data);
}
/**
* Computes the location of a neighbouring neuron.
* Returns {@code null} if the resulting location is not part
* of the map.
* Position {@code (0, 0)} is at the upper-left corner of the map.
*
* @param row Row index.
* @param col Column index.
* @param alongRowDir Direction along the given {@code row} (i.e. an
* offset will be added to the given <em>column</em> index.
* @param alongColDir Direction along the given {@code col} (i.e. an
* offset will be added to the given <em>row</em> index.
* @return an array of length 2 containing the indices of the requested
* location, or {@code null} if that location is not part of the map.
*
* @see #getNeuron(int,int)
*/
private int[] getLocation(int row,
int col,
HorizontalDirection alongRowDir,
VerticalDirection alongColDir) {
final int colOffset;
switch (alongRowDir) {
case LEFT:
colOffset = -1;
break;
case RIGHT:
colOffset = 1;
break;
case CENTER:
colOffset = 0;
break;
default:
// Should never happen.
throw new IllegalStateException();
}
int colIndex = col + colOffset;
if (wrapColumns) {
if (colIndex < 0) {
colIndex += numberOfColumns;
} else {
colIndex %= numberOfColumns;
}
}
final int rowOffset;
switch (alongColDir) {
case UP:
rowOffset = -1;
break;
case DOWN:
rowOffset = 1;
break;
case CENTER:
rowOffset = 0;
break;
default:
// Should never happen.
throw new IllegalStateException();
}
int rowIndex = row + rowOffset;
if (wrapRows) {
if (rowIndex < 0) {
rowIndex += numberOfRows;
} else {
rowIndex %= numberOfRows;
}
}
if (rowIndex < 0 ||
rowIndex >= numberOfRows ||
colIndex < 0 ||
colIndex >= numberOfColumns) {
return null;
} else {
return new int[] {rowIndex, colIndex};
}
}
/**
* Creates the neighbour relationships between neurons.
*/
private void createLinks() {
// "linkEnd" will store the identifiers of the "neighbours".
final List<Long> linkEnd = new ArrayList<>();
final int iLast = numberOfRows - 1;
final int jLast = numberOfColumns - 1;
for (int i = 0; i < numberOfRows; i++) {
for (int j = 0; j < numberOfColumns; j++) {
linkEnd.clear();
switch (neighbourhood) {
case MOORE:
// Add links to "diagonal" neighbours.
if (i > 0) {
if (j > 0) {
linkEnd.add(identifiers[i - 1][j - 1]);
}
if (j < jLast) {
linkEnd.add(identifiers[i - 1][j + 1]);
}
}
if (i < iLast) {
if (j > 0) {
linkEnd.add(identifiers[i + 1][j - 1]);
}
if (j < jLast) {
linkEnd.add(identifiers[i + 1][j + 1]);
}
}
if (wrapRows) {
if (i == 0) {
if (j > 0) {
linkEnd.add(identifiers[iLast][j - 1]);
}
if (j < jLast) {
linkEnd.add(identifiers[iLast][j + 1]);
}
} else if (i == iLast) {
if (j > 0) {
linkEnd.add(identifiers[0][j - 1]);
}
if (j < jLast) {
linkEnd.add(identifiers[0][j + 1]);
}
}
}
if (wrapColumns) {
if (j == 0) {
if (i > 0) {
linkEnd.add(identifiers[i - 1][jLast]);
}
if (i < iLast) {
linkEnd.add(identifiers[i + 1][jLast]);
}
} else if (j == jLast) {
if (i > 0) {
linkEnd.add(identifiers[i - 1][0]);
}
if (i < iLast) {
linkEnd.add(identifiers[i + 1][0]);
}
}
}
if (wrapRows &&
wrapColumns) {
if (i == 0 &&
j == 0) {
linkEnd.add(identifiers[iLast][jLast]);
} else if (i == 0 &&
j == jLast) {
linkEnd.add(identifiers[iLast][0]);
} else if (i == iLast &&
j == 0) {
linkEnd.add(identifiers[0][jLast]);
} else if (i == iLast &&
j == jLast) {
linkEnd.add(identifiers[0][0]);
}
}
// Case falls through since the "Moore" neighbourhood
// also contains the neurons that belong to the "Von
// Neumann" neighbourhood.
// fallthru (CheckStyle)
case VON_NEUMANN:
// Links to preceding and following "row".
if (i > 0) {
linkEnd.add(identifiers[i - 1][j]);
}
if (i < iLast) {
linkEnd.add(identifiers[i + 1][j]);
}
if (wrapRows) {
if (i == 0) {
linkEnd.add(identifiers[iLast][j]);
} else if (i == iLast) {
linkEnd.add(identifiers[0][j]);
}
}
// Links to preceding and following "column".
if (j > 0) {
linkEnd.add(identifiers[i][j - 1]);
}
if (j < jLast) {
linkEnd.add(identifiers[i][j + 1]);
}
if (wrapColumns) {
if (j == 0) {
linkEnd.add(identifiers[i][jLast]);
} else if (j == jLast) {
linkEnd.add(identifiers[i][0]);
}
}
break;
default:
throw new IllegalStateException(); // Cannot happen.
}
final Neuron aNeuron = network.getNeuron(identifiers[i][j]);
for (final long b : linkEnd) {
final Neuron bNeuron = network.getNeuron(b);
// Link to all neighbours.
// The reverse links will be added as the loop proceeds.
network.addLink(aNeuron, bNeuron);
}
}
}
}
/**
* Miscellaneous indicators of the map quality.
* <ul>
* <li>Hit histogram</li>
* <li>Quantization error</li>
* <li>Topographic error</li>
* <li>Unified distance matrix</li>
* </ul>
*/
public static final class DataVisualization {
/** Distance function. */
private static final DistanceMeasure DISTANCE = new EuclideanDistance();
/** Total number of samples. */
private final int numberOfSamples;
/** Hit histogram. */
private final double[][] hitHistogram;
/** Quantization error. */
private final double[][] quantizationError;
/** Mean quantization error. */
private final double meanQuantizationError;
/** Topographic error. */
private final double[][] topographicError;
/** Mean topographic error. */
private final double meanTopographicError;
/** U-matrix. */
private final double[][] uMatrix;
/**
* @param numberOfSamples Number of samples.
* @param hitHistogram Hit histogram.
* @param quantizationError Quantization error.
* @param topographicError Topographic error.
* @param uMatrix U-matrix.
*/
private DataVisualization(int numberOfSamples,
double[][] hitHistogram,
double[][] quantizationError,
double[][] topographicError,
double[][] uMatrix) {
this.numberOfSamples = numberOfSamples;
this.hitHistogram = hitHistogram;
this.quantizationError = quantizationError;
meanQuantizationError = hitWeightedMean(quantizationError, hitHistogram);
this.topographicError = topographicError;
meanTopographicError = hitWeightedMean(topographicError, hitHistogram);
this.uMatrix = uMatrix;
}
/**
* @param map Map
* @param data Data.
* @return the metrics.
*/
static DataVisualization from(NeuronSquareMesh2D map,
Iterable<double[]> data) {
final LocationFinder finder = new LocationFinder(map);
final MapRanking rank = new MapRanking(map, DISTANCE);
final Network net = map.getNetwork();
final int nR = map.getNumberOfRows();
final int nC = map.getNumberOfColumns();
// Hit bins.
final int[][] hitCounter = new int[nR][nC];
// Hit bins.
final double[][] hitHistogram = new double[nR][nC];
// Quantization error bins.
final double[][] quantizationError = new double[nR][nC];
// Topographic error bins.
final double[][] topographicError = new double[nR][nC];
// U-matrix.
final double[][] uMatrix = new double[nR][nC];
int numSamples = 0;
for (final double[] sample : data) {
++numSamples;
final List<Neuron> winners = rank.rank(sample, 2);
final Neuron best = winners.get(0);
final Neuron secondBest = winners.get(1);
final LocationFinder.Location locBest = finder.getLocation(best);
final int rowBest = locBest.getRow();
final int colBest = locBest.getColumn();
// Increment hit counter.
hitCounter[rowBest][colBest] += 1;
// Aggregate quantization error.
quantizationError[rowBest][colBest] += DISTANCE.applyAsDouble(sample, best.getFeatures());
// Aggregate topographic error.
if (!net.getNeighbours(best).contains(secondBest)) {
// Increment count if first and second best matching units
// are not neighbours.
topographicError[rowBest][colBest] += 1;
}
}
for (int r = 0; r < nR; r++) {
for (int c = 0; c < nC; c++) {
final Neuron neuron = map.getNeuron(r, c);
final Collection<Neuron> neighbours = net.getNeighbours(neuron);
final double[] features = neuron.getFeatures();
double uDistance = 0;
int neighbourCount = 0;
for (final Neuron n : neighbours) {
++neighbourCount;
uDistance += DISTANCE.applyAsDouble(features, n.getFeatures());
}
final int hitCount = hitCounter[r][c];
if (hitCount != 0) {
hitHistogram[r][c] = hitCount / (double) numSamples;
quantizationError[r][c] /= hitCount;
topographicError[r][c] /= hitCount;
}
uMatrix[r][c] = uDistance / neighbourCount;
}
}
return new DataVisualization(numSamples,
hitHistogram,
quantizationError,
topographicError,
uMatrix);
}
/**
* @return the total number of samples.
*/
public int getNumberOfSamples() {
return numberOfSamples;
}
/**
* @return the quantization error.
* Each bin will contain the average of the distances between samples
* mapped to the corresponding unit and the weight vector of that unit.
* @see #getMeanQuantizationError()
*/
public double[][] getQuantizationError() {
return copy(quantizationError);
}
/**
* @return the topographic error.
* Each bin will contain the number of data for which the first and
* second best matching units are not adjacent in the map.
* @see #getMeanTopographicError()
*/
public double[][] getTopographicError() {
return copy(topographicError);
}
/**
* @return the hits histogram (normalized).
* Each bin will contain the number of data for which the corresponding
* neuron is the best matching unit.
*/
public double[][] getNormalizedHits() {
return copy(hitHistogram);
}
/**
* @return the U-matrix.
* Each bin will contain the average distance between a unit and all its
* neighbours will be computed (and stored in the pixel corresponding to
* that unit of the 2D-map). The number of neighbours taken into account
* depends on the network {@link org.apache.commons.math4.neuralnet.SquareNeighbourhood
* neighbourhood type}.
*/
public double[][] getUMatrix() {
return copy(uMatrix);
}
/**
* @return the mean (hit-weighted) quantization error.
* @see #getQuantizationError()
*/
public double getMeanQuantizationError() {
return meanQuantizationError;
}
/**
* @return the mean (hit-weighted) topographic error.
* @see #getTopographicError()
*/
public double getMeanTopographicError() {
return meanTopographicError;
}
/**
* @param orig Source.
* @return a deep copy of the original array.
*/
private static double[][] copy(double[][] orig) {
final double[][] copy = new double[orig.length][];
for (int i = 0; i < orig.length; i++) {
copy[i] = orig[i].clone();
}
return copy;
}
/**
* @param metrics Metrics.
* @param normalizedHits Hits histogram (normalized).
* @return the hit-weighted mean of the given {@code metrics}.
*/
private static double hitWeightedMean(double[][] metrics,
double[][] normalizedHits) {
double mean = 0;
final int rows = metrics.length;
final int cols = metrics[0].length;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
mean += normalizedHits[i][j] * metrics[i][j];
}
}
return mean;
}
}
}