/*
* Artificial Intelligence for Humans
* Volume 3: Deep Learning and Neural Networks
* Java Version
* http://www.aifh.org
* http://www.jeffheaton.com
*
* Code repository:
* https://github.com/jeffheaton/aifh
*
* Copyright 2014-2015 by Jeff Heaton
*
* Licensed 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.
*
* For more information on Heaton Research copyrights, licenses
* and trademarks visit:
* http://www.heatonresearch.com/copyright
*/
package com.heatonresearch.aifh.som.train;
import Jama.Matrix;
import com.heatonresearch.aifh.general.VectorAlgebra;
import com.heatonresearch.aifh.general.data.BasicData;
import com.heatonresearch.aifh.som.BestMatchingUnit;
import com.heatonresearch.aifh.som.SelfOrganizingMap;
import com.heatonresearch.aifh.som.neighborhood.NeighborhoodFunction;
import java.util.List;
/**
* This class implements competitive training, which would be used in a
* winner-take-all neural network, such as the self organizing map (SOM). This
* is an unsupervised training method, no ideal data is needed on the training
* set. If ideal data is provided, it will be ignored.
*
* Training is done by looping over all of the training elements and calculating
* a "best matching unit" (BMU). This BMU output neuron is then adjusted to
* better "learn" this pattern. Additionally, this training may be applied to
* other "nearby" output neurons. The degree to which nearby neurons are update
* is defined by the neighborhood function.
*
* A neighborhood function is required to determine the degree to which
* neighboring neurons (to the winning neuron) are updated by each training
* iteration.
*
* Because this is unsupervised training, calculating an error to measure
* progress by is difficult. The error is defined to be the "worst", or longest,
* Euclidean distance of any of the BMU's. This value should be minimized, as
* learning progresses.
*
* Because only the BMU neuron and its close neighbors are updated, you can end
* up with some output neurons that learn nothing. By default these neurons are
* not forced to win patterns that are not represented well. This spreads out
* the workload among all output neurons. This feature is not used by default,
* but can be enabled by setting the "forceWinner" property.
*
* @author jheaton
*
*/
public class BasicTrainSOM {
/**
* The neighborhood function to use to determine to what degree a neuron
* should be "trained".
*/
private final NeighborhoodFunction neighborhood;
/**
* The learning rate. To what degree should changes be applied.
*/
private double learningRate;
/**
* The network being trained.
*/
private final SelfOrganizingMap network;
/**
* How many neurons in the input layer.
*/
private final int inputNeuronCount;
/**
* How many neurons in the output layer.
*/
private final int outputNeuronCount;
/**
* Utility class used to determine the BMU.
*/
private final BestMatchingUnit bmuUtil;
/**
* Holds the corrections for any matrix being trained.
*/
private final Matrix correctionMatrix;
/**
* True is a winner is to be forced, see class description, or forceWinners
* method. By default, this is true.
*/
private boolean forceWinner;
/**
* When used with autodecay, this is the starting learning rate.
*/
private double startRate;
/**
* When used with autodecay, this is the ending learning rate.
*/
private double endRate;
/**
* When used with autodecay, this is the starting radius.
*/
private double startRadius;
/**
* When used with autodecay, this is the ending radius.
*/
private double endRadius;
/**
* This is the current autodecay learning rate.
*/
private double autoDecayRate;
/**
* This is the current autodecay radius.
*/
private double autoDecayRadius;
/**
* The current radius.
*/
private double radius;
private final List<BasicData> training;
private double error;
/**
* Create an instance of competitive training.
*
* @param network
* The network to train.
* @param learningRate
* The learning rate, how much to apply per iteration.
* @param training
* The training set (unsupervised).
* @param neighborhood
* The neighborhood function to use.
*/
public BasicTrainSOM(final SelfOrganizingMap network, final double learningRate,
final List<BasicData> training, final NeighborhoodFunction neighborhood) {
this.neighborhood = neighborhood;
this.training = training;
this.learningRate = learningRate;
this.network = network;
this.inputNeuronCount = network.getInputCount();
this.outputNeuronCount = network.getOutputCount();
this.forceWinner = false;
this.error = 0;
// setup the correction matrix
this.correctionMatrix = new Matrix(this.outputNeuronCount,this.inputNeuronCount);
// create the BMU class
this.bmuUtil = new BestMatchingUnit(network);
}
/**
* Loop over the synapses to be trained and apply any corrections that were
* determined by this training iteration.
*/
private void applyCorrection() {
for(int row=0;row<this.correctionMatrix.getRowDimension();row++) {
for(int col=0;col<this.correctionMatrix.getColumnDimension();col++) {
this.network.getWeights().set(row,col,this.correctionMatrix.get(row,col));
}
}
}
/**
* Should be called each iteration if autodecay is desired.
*/
public void autoDecay() {
if (this.radius > this.endRadius) {
this.radius += this.autoDecayRadius;
}
if (this.learningRate > this.endRate) {
this.learningRate += this.autoDecayRate;
}
getNeighborhood().setRadius(this.radius);
}
/**
* Copy the specified input pattern to the weight matrix. This causes an
* output neuron to learn this pattern "exactly". This is useful when a
* winner is to be forced.
*
* @param matrix
* The matrix that is the target of the copy.
* @param outputNeuron
* The output neuron to set.
* @param input
* The input pattern to copy.
*/
private void copyInputPattern(final Matrix matrix, final int outputNeuron,
final double[] input) {
for (int inputNeuron = 0; inputNeuron < this.inputNeuronCount; inputNeuron++) {
matrix.set(outputNeuron,inputNeuron, input[inputNeuron]);
}
}
/**
* Called to decay the learning rate and radius by the specified amount.
*
* @param d
* The percent to decay by.
*/
public void decay(final double d) {
this.radius *= (1.0 - d);
this.learningRate *= (1.0 - d);
}
/**
* Decay the learning rate and radius by the specified amount.
*
* @param decayRate
* The percent to decay the learning rate by.
* @param decayRadius
* The percent to decay the radius by.
*/
public void decay(final double decayRate, final double decayRadius) {
this.radius *= (1.0 - decayRadius);
this.learningRate *= (1.0 - decayRate);
getNeighborhood().setRadius(this.radius);
}
/**
* Determine the weight adjustment for a single neuron during a training
* iteration.
*
* @param weight
* The starting weight.
* @param input
* The input to this neuron.
* @param currentNeuron
* The neuron who's weight is being updated.
* @param bmu
* The neuron that "won", the best matching unit.
* @return The new weight value.
*/
private double determineNewWeight(final double weight, final double input,
final int currentNeuron, final int bmu) {
final double newWeight = weight
+ (this.neighborhood.function(currentNeuron, bmu)
* this.learningRate * (input - weight));
return newWeight;
}
/**
* Force any neurons that did not win to off-load patterns from overworked
* neurons.
*
* @param won
* An array that specifies how many times each output neuron has
* "won".
* @param leastRepresented
* The training pattern that is the least represented by this
* neural network.
* @param matrix
* The synapse to modify.
* @return True if a winner was forced.
*/
private boolean forceWinners(final Matrix matrix, final int[] won,
final double[] leastRepresented) {
double maxActivation = Double.MIN_VALUE;
int maxActivationNeuron = -1;
final double[] output = compute(this.network, leastRepresented);
// Loop over all of the output neurons. Consider any neurons that were
// not the BMU (winner) for any pattern. Track which of these
// non-winning neurons had the highest activation.
for (int outputNeuron = 0; outputNeuron < won.length; outputNeuron++) {
// Only consider neurons that did not "win".
if (won[outputNeuron] == 0) {
if ((maxActivationNeuron == -1)
|| (output[outputNeuron] > maxActivation)) {
maxActivation = output[outputNeuron];
maxActivationNeuron = outputNeuron;
}
}
}
// If a neurons was found that did not activate for any patterns, then
// force it to "win" the least represented pattern.
if (maxActivationNeuron != -1) {
copyInputPattern(matrix, maxActivationNeuron, leastRepresented);
return true;
} else {
return false;
}
}
/**
* @return The input neuron count.
*/
public int getInputNeuronCount() {
return this.inputNeuronCount;
}
/**
* @return The learning rate. This was set when the object was created.
*/
public double getLearningRate() {
return this.learningRate;
}
/**
* @return The network neighborhood function.
*/
public NeighborhoodFunction getNeighborhood() {
return this.neighborhood;
}
/**
* @return The output neuron count.
*/
public int getOutputNeuronCount() {
return this.outputNeuronCount;
}
/**
* @return Is a winner to be forced of neurons that do not learn. See class
* description for more info.
*/
public boolean isForceWinner() {
return this.forceWinner;
}
/**
* Perform one training iteration.
*/
public void iteration() {
// Reset the BMU and begin this iteration.
this.bmuUtil.reset();
final int[] won = new int[this.outputNeuronCount];
double leastRepresentedActivation = Double.MAX_VALUE;
double[] leastRepresented = null;
// Reset the correction matrix for this synapse and iteration.
//*this.correctionMatrix.clear();
// Determine the BMU for each training element.
for (final BasicData input : this.training) {
final int bmu = this.bmuUtil.calculateBMU(input.getInput());
won[bmu]++;
// If we are to force a winner each time, then track how many
// times each output neuron becomes the BMU (winner).
if (this.forceWinner) {
// Get the "output" from the network for this pattern. This
// gets the activation level of the BMU.
final double[] output = compute(this.network,input.getInput());
// Track which training entry produces the least BMU. This
// pattern is the least represented by the network.
if (output[bmu] < leastRepresentedActivation) {
leastRepresentedActivation = output[bmu];
leastRepresented = input.getInput();
}
}
train(bmu, this.network.getWeights(), input.getInput());
if (this.forceWinner) {
// force any non-winning neurons to share the burden somewhat\
if (!forceWinners(this.network.getWeights(), won,
leastRepresented)) {
applyCorrection();
}
} else {
applyCorrection();
}
}
// update the error
this.error = this.bmuUtil.getWorstDistance() / 100.0;
}
/**
* Setup autodecay. This will decrease the radius and learning rate from the
* start values to the end values.
*
* @param plannedIterations
* The number of iterations that are planned. This allows the
* decay rate to be determined.
* @param startRate
* The starting learning rate.
* @param endRate
* The ending learning rate.
* @param startRadius
* The starting radius.
* @param endRadius
* The ending radius.
*/
public void setAutoDecay(final int plannedIterations,
final double startRate, final double endRate,
final double startRadius, final double endRadius) {
this.startRate = startRate;
this.endRate = endRate;
this.startRadius = startRadius;
this.endRadius = endRadius;
this.autoDecayRadius = (endRadius - startRadius) / plannedIterations;
this.autoDecayRate = (endRate - startRate) / plannedIterations;
setParams(this.startRate, this.startRadius);
}
/**
* Determine if a winner is to be forced. See class description for more
* info.
*
* @param forceWinner
* True if a winner is to be forced.
*/
public void setForceWinner(final boolean forceWinner) {
this.forceWinner = forceWinner;
}
/**
* Set the learning rate. This is the rate at which the weights are changed.
*
* @param rate
* The learning rate.
*/
public void setLearningRate(final double rate) {
this.learningRate = rate;
}
/**
* Set the learning rate and radius.
*
* @param rate
* The new learning rate.
* @param radius
* The new radius.
*/
public void setParams(final double rate, final double radius) {
this.radius = radius;
this.learningRate = rate;
getNeighborhood().setRadius(radius);
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
String result = "Rate=" +
this.learningRate +
", Radius=" +
this.radius;
return result;
}
/**
* Train for the specified synapse and BMU.
*
* @param bmu
* The best matching unit for this input.
* @param matrix
* The synapse to train.
* @param input
* The input to train for.
*/
private void train(final int bmu, final Matrix matrix, final double[] input) {
// adjust the weight for the BMU and its neighborhood
for (int outputNeuron = 0; outputNeuron < this.outputNeuronCount; outputNeuron++) {
trainPattern(matrix, input, outputNeuron, bmu);
}
}
/**
* Train for the specified pattern.
*
* @param matrix
* The synapse to train.
* @param input
* The input pattern to train for.
* @param current
* The current output neuron being trained.
* @param bmu
* The best matching unit, or winning output neuron.
*/
private void trainPattern(final Matrix matrix, final double[] input,
final int current, final int bmu) {
for (int inputNeuron = 0; inputNeuron < this.inputNeuronCount; inputNeuron++) {
final double currentWeight = matrix.get(current,inputNeuron);
final double inputValue = input[inputNeuron];
final double newWeight = determineNewWeight(currentWeight,
inputValue, current, bmu);
this.correctionMatrix.set(current,inputNeuron,newWeight);
}
}
/**
* Train the specified pattern. Find a winning neuron and adjust all neurons
* according to the neighborhood function.
*
* @param pattern
* The pattern to train.
*/
public void trainPattern(final double[] pattern) {
final int bmu = this.bmuUtil.calculateBMU(pattern);
train(bmu, this.network.getWeights(), pattern);
applyCorrection();
}
/**
* Calculate the output of the SOM, for each output neuron. Typically,
* you will use the classify method instead of calling this method.
* @param input
* The input pattern.
* @return The output activation of each output neuron.
*/
private double[] compute(final SelfOrganizingMap som, final double[] input) {
final double[] result = new double[som.getOutputCount()];
for (int i = 0; i < som.getOutputCount(); i++) {
final double[] optr = som.getWeights().getArray()[i];
final Matrix matrixA = new Matrix(input.length,1);
for(int j=0;j<input.length;j++) {
matrixA.getArray()[0][j] = input[j];
}
final Matrix matrixB = new Matrix(1,input.length);
for(int j=0;j<optr.length;j++) {
matrixB.getArray()[0][j] = optr[j];
}
result[i] = VectorAlgebra.dotProduct(matrixA, matrixB);
}
return result;
}
}