blob: 3a5a126a2cb816a0e6018b8db30cc208db3ed1a1 [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.ml.neuralnet.twod;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.io.Serializable;
import java.io.ObjectInputStream;
import org.apache.commons.math4.exception.MathInternalError;
import org.apache.commons.math4.exception.NumberIsTooSmallException;
import org.apache.commons.math4.exception.OutOfRangeException;
import org.apache.commons.math4.ml.neuralnet.FeatureInitializer;
import org.apache.commons.math4.ml.neuralnet.Network;
import org.apache.commons.math4.ml.neuralnet.Neuron;
import org.apache.commons.math4.ml.neuralnet.SquareNeighbourhood;
/**
* 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.ml.neuralnet.sofm
* @since 3.3
*/
public class NeuronSquareMesh2D
implements Iterable<Neuron>,
Serializable {
/** Serial version ID */
private static final long serialVersionUID = 1L;
/** 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,
}
/**
* Constructor with restricted access, solely used for deserialization.
*
* @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 NumberIsTooSmallException if {@code numRows < 2} or
* {@code numCols < 2}.
*/
NeuronSquareMesh2D(boolean wrapRowDim,
boolean wrapColDim,
SquareNeighbourhood neighbourhoodType,
double[][][] featuresList) {
numberOfRows = featuresList.length;
numberOfColumns = featuresList[0].length;
if (numberOfRows < 2) {
throw new NumberIsTooSmallException(numberOfRows, 2, true);
}
if (numberOfColumns < 2) {
throw new NumberIsTooSmallException(numberOfColumns, 2, true);
}
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 NumberIsTooSmallException if {@code numRows < 2} or
* {@code numCols < 2}.
*/
public NeuronSquareMesh2D(int numRows,
boolean wrapRowDim,
int numCols,
boolean wrapColDim,
SquareNeighbourhood neighbourhoodType,
FeatureInitializer[] featureInit) {
if (numRows < 2) {
throw new NumberIsTooSmallException(numRows, 2, true);
}
if (numCols < 2) {
throw new NumberIsTooSmallException(numCols, 2, true);
}
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 OutOfRangeException 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 OutOfRangeException(i, 0, numberOfRows - 1);
}
if (j < 0 ||
j >= numberOfColumns) {
throw new OutOfRangeException(j, 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 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 MathInternalError();
}
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 MathInternalError();
}
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 MathInternalError(); // Cannot happen.
}
final Neuron aNeuron = network.getNeuron(identifiers[i][j]);
for (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);
}
}
}
}
/**
* Prevents proxy bypass.
*
* @param in Input stream.
*/
private void readObject(ObjectInputStream in) {
throw new IllegalStateException();
}
/**
* Custom serialization.
*
* @return the proxy instance that will be actually serialized.
*/
private Object writeReplace() {
final double[][][] featuresList = new double[numberOfRows][numberOfColumns][];
for (int i = 0; i < numberOfRows; i++) {
for (int j = 0; j < numberOfColumns; j++) {
featuresList[i][j] = getNeuron(i, j).getFeatures();
}
}
return new SerializationProxy(wrapRows,
wrapColumns,
neighbourhood,
featuresList);
}
/**
* Serialization.
*/
private static class SerializationProxy implements Serializable {
/** Serializable. */
private static final long serialVersionUID = 20130226L;
/** Wrap. */
private final boolean wrapRows;
/** Wrap. */
private final boolean wrapColumns;
/** Neighbourhood type. */
private final SquareNeighbourhood neighbourhood;
/** Neurons' features. */
private final double[][][] featuresList;
/**
* @param wrapRows Whether the row dimension is wrapped.
* @param wrapColumns Whether the column dimension is wrapped.
* @param neighbourhood Neighbourhood type.
* @param featuresList List of neurons features.
* {@code neuronList}.
*/
SerializationProxy(boolean wrapRows,
boolean wrapColumns,
SquareNeighbourhood neighbourhood,
double[][][] featuresList) {
this.wrapRows = wrapRows;
this.wrapColumns = wrapColumns;
this.neighbourhood = neighbourhood;
this.featuresList = featuresList;
}
/**
* Custom serialization.
*
* @return the {@link Neuron} for which this instance is the proxy.
*/
private Object readResolve() {
return new NeuronSquareMesh2D(wrapRows,
wrapColumns,
neighbourhood,
featuresList);
}
}
}