/*
* Encog(tm) Core v3.4 - Java Version
* http://www.heatonresearch.com/encog/
* https://github.com/encog/encog-java-core
* Copyright 2008-2016 Heaton Research, Inc.
*
* 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 org.encog.neural.som.training.basic;
import org.encog.mathutil.matrices.Matrix;
import org.encog.mathutil.matrices.MatrixMath;
import org.encog.ml.MLMethod;
import org.encog.ml.TrainingImplementationType;
import org.encog.ml.data.MLData;
import org.encog.ml.data.MLDataPair;
import org.encog.ml.data.MLDataSet;
import org.encog.ml.data.basic.BasicMLData;
import org.encog.ml.train.BasicTraining;
import org.encog.neural.networks.training.LearningRate;
import org.encog.neural.networks.training.propagation.TrainingContinuation;
import org.encog.neural.som.SOM;
import org.encog.neural.som.training.basic.neighborhood.NeighborhoodFunction;
import org.encog.util.Format;
import org.encog.util.logging.EncogLogging;
/**
* 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 extends BasicTraining implements LearningRate {
/**
* 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 SOM 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;
/**
* 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 SOM network, final double learningRate,
final MLDataSet training, final NeighborhoodFunction neighborhood) {
super(TrainingImplementationType.Iterative);
this.neighborhood = neighborhood;
setTraining(training);
this.learningRate = learningRate;
this.network = network;
this.inputNeuronCount = network.getInputCount();
this.outputNeuronCount = network.getOutputCount();
this.forceWinner = false;
setError(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() {
this.network.getWeights().set(this.correctionMatrix);
}
/**
* 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);
}
/**
* {@inheritDoc}
*/
@Override
public boolean canContinue() {
return false;
}
/**
* 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 MLData input) {
for (int inputNeuron = 0; inputNeuron < this.inputNeuronCount; inputNeuron++) {
matrix.set(outputNeuron, inputNeuron, input.getData(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 MLData leastRepresented) {
double maxActivation = Double.MIN_VALUE;
int maxActivationNeuron = -1;
final MLData 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.getData(outputNeuron) > maxActivation)) {
maxActivation = output.getData(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.
*/
@Override
public double getLearningRate() {
return this.learningRate;
}
/**
* {@inheritDoc}
*/
@Override
public MLMethod getMethod() {
return this.network;
}
/**
* @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.
*/
@Override
public void iteration() {
EncogLogging.log(EncogLogging.LEVEL_INFO,
"Performing SOM Training iteration.");
preIteration();
// Reset the BMU and begin this iteration.
this.bmuUtil.reset();
final int[] won = new int[this.outputNeuronCount];
double leastRepresentedActivation = Double.MAX_VALUE;
MLData leastRepresented = null;
// Reset the correction matrix for this synapse and iteration.
this.correctionMatrix.clear();
// Determine the BMU for each training element.
for (final MLDataPair pair : getTraining()) {
final MLData input = pair.getInput();
final int bmu = this.bmuUtil.calculateBMU(input);
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 MLData output = compute(this.network,pair.getInput());
// Track which training entry produces the least BMU. This
// pattern is the least represented by the network.
if (output.getData(bmu) < leastRepresentedActivation) {
leastRepresentedActivation = output.getData(bmu);
leastRepresented = pair.getInput();
}
}
train(bmu, this.network.getWeights(), input);
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
setError(this.bmuUtil.getWorstDistance() / 100.0);
postIteration();
}
/**
* {@inheritDoc}
*/
@Override
public TrainingContinuation pause() {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void resume(final TrainingContinuation state) {
}
/**
* 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.
*/
@Override
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() {
final StringBuilder result = new StringBuilder();
result.append("Rate=");
result.append(Format.formatPercent(this.learningRate));
result.append(", Radius=");
result.append(Format.formatDouble(this.radius, 2));
return result.toString();
}
/**
* 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 MLData 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 MLData 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.getData(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 MLData pattern) {
final MLData input = pattern;
final int bmu = this.bmuUtil.calculateBMU(input);
train(bmu, this.network.getWeights(), input);
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 MLData compute(final SOM som, final MLData input) {
final MLData result = new BasicMLData(som.getOutputCount());
for (int i = 0; i < som.getOutputCount(); i++) {
final Matrix optr = som.getWeights().getRow(i);
final Matrix inputMatrix = Matrix.createRowMatrix(input.getData());
result.setData(i, MatrixMath.dotProduct(inputMatrix, optr));
}
return result;
}
}