/*
* 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.networks.training.propagation.resilient;
import java.util.Random;
import org.encog.mathutil.EncogMath;
import org.encog.ml.data.MLDataSet;
import org.encog.neural.networks.ContainsFlat;
import org.encog.neural.networks.training.TrainingError;
import org.encog.neural.networks.training.propagation.Propagation;
import org.encog.neural.networks.training.propagation.TrainingContinuation;
import org.encog.util.EngineArray;
/**
* One problem with the backpropagation algorithm is that the magnitude of the
* partial derivative is usually too large or too small. Further, the learning
* rate is a single value for the entire neural network. The resilient
* propagation learning algorithm uses a special update value(similar to the
* learning rate) for every neuron connection. Further these update values are
* automatically determined, unlike the learning rate of the backpropagation
* algorithm.
*
* For most training situations, we suggest that the resilient propagation
* algorithm (this class) be used for training.
*
* There are a total of three parameters that must be provided to the resilient
* training algorithm. Defaults are provided for each, and in nearly all cases,
* these defaults are acceptable. This makes the resilient propagation algorithm
* one of the easiest and most efficient training algorithms available.
*
* It is also important to note that RPROP does not work well with online training.
* You should always use a batch size bigger than one. Typically the larger the better.
* By default a batch size of zero is used, zero means to include the entire training
* set in the batch.
*
* The optional parameters are:
*
* zeroTolerance - How close to zero can a number be to be considered zero. The
* default is 0.00000000000000001.
*
* initialUpdate - What are the initial update values for each matrix value. The
* default is 0.1.
*
* maxStep - What is the largest amount that the update values can step. The
* default is 50.
*
*
* Usually you will not need to use these, and you should use the constructor
* that does not require them.
*
*
* @author jheaton
*
*/
public class ResilientPropagation extends Propagation {
/**
* The update values, for the weights and thresholds.
*/
private final double[] updateValues;
private final double[] lastDelta;
/**
* The zero tolerance.
*/
private final double zeroTolerance;
/**
* The maximum step value for rprop.
*/
private final double maxStep;
private static RPROPType rpropType = RPROPType.RPROPp;
private double[] lastWeightChange;
/**
* The value error at the beginning of the previous training iteration.
* This value is compared with the error at the beginning of the current
* iteration to determine if an improvement is occuring.
*/
private double lastError = Double.POSITIVE_INFINITY;
/**
* Continuation tag for the last gradients.
*/
public static final String LAST_GRADIENTS = "LAST_GRADIENTS";
/**
* Continuation tag for the last values.
*/
public static final String UPDATE_VALUES = "UPDATE_VALUES";
/**
* Denominator for ARPROP adaptive weight change
*/
private static double q = 1;
/**
* Construct an RPROP trainer, allows an OpenCL device to be specified. Use
* the defaults for all training parameters. Usually this is the constructor
* to use as the resilient training algorithm is designed for the default
* parameters to be acceptable for nearly all problems.
*
* @param network
* The network to train.
* @param training
* The training data to use.
*/
public ResilientPropagation(final ContainsFlat network,
final MLDataSet training) {
this(network, training, RPROPConst.DEFAULT_INITIAL_UPDATE,
RPROPConst.DEFAULT_MAX_STEP);
}
/**
* Construct a resilient training object, allow the training parameters to
* be specified. Usually the default parameters are acceptable for the
* resilient training algorithm. Therefore you should usually use the other
* constructor, that makes use of the default values.
*
* @param network
* The network to train.
* @param training
* The training set to use.
* @param initialUpdate
* The initial update values, this is the amount that the deltas
* are all initially set to.
* @param maxStep
* The maximum that a delta can reach.
*/
public ResilientPropagation(final ContainsFlat network,
final MLDataSet training, final double initialUpdate,
final double maxStep) {
super(network, training);
this.updateValues = new double[network.getFlat().getWeights().length];
this.lastDelta = new double[network.getFlat().getWeights().length];
this.lastWeightChange = new double[network.getFlat().getWeights().length];
this.zeroTolerance = RPROPConst.DEFAULT_ZERO_TOLERANCE;
this.maxStep = maxStep;
for (int i = 0; i < this.updateValues.length; i++) {
this.updateValues[i] = initialUpdate;
this.lastDelta[i] = 0;
}
}
/**
* @return True, as RPROP can continue.
*/
@Override
public boolean canContinue() {
return true;
}
/**
* Determine if the specified continuation object is valid to resume with.
*
* @param state
* The continuation object to check.
* @return True if the specified continuation object is valid for this
* training method and network.
*/
public boolean isValidResume(final TrainingContinuation state) {
if (!state.getContents().containsKey(
ResilientPropagation.LAST_GRADIENTS)
|| !state.getContents().containsKey(
ResilientPropagation.UPDATE_VALUES)) {
return false;
}
if (!state.getTrainingType().equals(getClass().getSimpleName())) {
return false;
}
final double[] d = (double[]) state
.get(ResilientPropagation.LAST_GRADIENTS);
return d.length == ((ContainsFlat) getMethod()).getFlat().getWeights().length;
}
/**
* Pause the training.
*
* @return A training continuation object to continue with.
*/
@Override
public TrainingContinuation pause() {
final TrainingContinuation result = new TrainingContinuation();
result.setTrainingType(this.getClass().getSimpleName());
result.set(ResilientPropagation.LAST_GRADIENTS,getLastGradient());
result.set(ResilientPropagation.UPDATE_VALUES,getUpdateValues());
return result;
}
/**
* Resume training.
*
* @param state
* The training state to return to.
*/
@Override
public void resume(final TrainingContinuation state) {
if (!isValidResume(state)) {
throw new TrainingError("Invalid training resume data length");
}
final double[] lastGradient = (double[]) state
.get(ResilientPropagation.LAST_GRADIENTS);
final double[] updateValues = (double[]) state
.get(ResilientPropagation.UPDATE_VALUES);
EngineArray.arrayCopy(lastGradient,getLastGradient());
EngineArray.arrayCopy(updateValues,getUpdateValues());
}
/**
* Set the type of RPROP to use. The default is RPROPp (RPROP+), or classic RPROP.
* @param t The type.
*/
public void setRPROPType(RPROPType t) {
this.rpropType = t;
}
/**
* @return The type of RPROP used.
*/
public RPROPType getRPROPType() {
return this.rpropType;
}
/**
* Perform training method specific init.
*/
public void initOthers() {
}
/**
* Calculate the amount to change the weight by.
*
* @param gradients
* The gradients.
* @param lastGradient
* The last gradients.
* @param index
* The index to update.
* @return The amount to change the weight by.
*/
@Override
public double updateWeight(final double[] gradients,
final double[] lastGradient, final int index) {
double weightChange = 0;
switch(this.rpropType) {
case RPROPp:
weightChange = updateWeightPlus(gradients,lastGradient,index);
break;
case RPROPm:
weightChange = updateWeightMinus(gradients,lastGradient,index);
break;
case iRPROPp:
weightChange = updateiWeightPlus(gradients,lastGradient,index);
break;
case iRPROPm:
weightChange = updateiWeightMinus(gradients,lastGradient,index);
break;
case ARPROP:
weightChange = updateJacobiWeight(gradients,lastGradient,index);
break;
default:
throw new TrainingError("Unknown RPROP type: " + this.rpropType);
}
this.lastWeightChange[index] = weightChange;
return weightChange;
}
/**
* Calculate the amount to change the weight by.
*
* @param gradients
* The gradients.
* @param lastGradient
* The last gradients.
* @param index
* The index to update.
* @return The amount to change the weight by.
*/
@Override
public double updateWeight(final double[] gradients,
final double[] lastGradient, final int index, double dropoutRate) {
double weightChange = 0;
if (dropoutRate > 0 && dropoutRandomSource.nextDouble() < dropoutRate) {
return 0;
};
switch(this.rpropType) {
case RPROPp:
weightChange = updateWeightPlus(gradients,lastGradient,index);
break;
case RPROPm:
weightChange = updateWeightMinus(gradients,lastGradient,index);
break;
case iRPROPp:
weightChange = updateiWeightPlus(gradients,lastGradient,index);
break;
case iRPROPm:
weightChange = updateiWeightMinus(gradients,lastGradient,index);
break;
case ARPROP:
weightChange = updateJacobiWeight(gradients,lastGradient,index);
break;
default:
throw new TrainingError("Unknown RPROP type: " + this.rpropType);
}
this.lastWeightChange[index] = weightChange;
return weightChange;
}
public double updateWeightPlus(final double[] gradients,
final double[] lastGradient, final int index) {
// multiply the current and previous gradient, and take the
// sign. We want to see if the gradient has changed its sign.
final int change = EncogMath.sign(gradients[index] * lastGradient[index]);
double weightChange = 0;
// if the gradient has retained its sign, then we increase the
// delta so that it will converge faster
if (change > 0) {
double delta = this.updateValues[index]
* RPROPConst.POSITIVE_ETA;
delta = Math.min(delta, this.maxStep);
weightChange = EncogMath.sign(gradients[index]) * delta;
this.updateValues[index] = delta;
lastGradient[index] = gradients[index];
} else if (change < 0) {
// if change<0, then the sign has changed, and the last
// delta was too big
double delta = this.updateValues[index]
* RPROPConst.NEGATIVE_ETA;
delta = Math.max(delta, RPROPConst.DELTA_MIN);
this.updateValues[index] = delta;
weightChange = -this.lastWeightChange[index];
// set the previous gradent to zero so that there will be no
// adjustment the next iteration
lastGradient[index] = 0;
} else if (change == 0) {
// if change==0 then there is no change to the delta
final double delta = this.updateValues[index];
weightChange = EncogMath.sign(gradients[index]) * delta;
lastGradient[index] = gradients[index];
}
// apply the weight change, if any
return weightChange;
}
public double updateWeightMinus(final double[] gradients,
final double[] lastGradient, final int index) {
// multiply the current and previous gradient, and take the
// sign. We want to see if the gradient has changed its sign.
final int change = EncogMath.sign(gradients[index] * lastGradient[index]);
double weightChange = 0;
double delta;
// if the gradient has retained its sign, then we increase the
// delta so that it will converge faster
if (change > 0) {
delta = this.lastDelta[index]
* RPROPConst.POSITIVE_ETA;
delta = Math.min(delta, this.maxStep);
} else {
// if change<0, then the sign has changed, and the last
// delta was too big
delta = this.lastDelta[index]
* RPROPConst.NEGATIVE_ETA;
delta = Math.max(delta, RPROPConst.DELTA_MIN);
}
lastGradient[index] = gradients[index];
weightChange = EncogMath.sign(gradients[index]) * delta;
this.lastDelta[index] = delta;
// apply the weight change, if any
return weightChange;
}
public double updateiWeightPlus(final double[] gradients,
final double[] lastGradient, final int index) {
// multiply the current and previous gradient, and take the
// sign. We want to see if the gradient has changed its sign.
final int change = EncogMath.sign(gradients[index] * lastGradient[index]);
double weightChange = 0;
// if the gradient has retained its sign, then we increase the
// delta so that it will converge faster
if (change > 0) {
double delta = this.updateValues[index]
* RPROPConst.POSITIVE_ETA;
delta = Math.min(delta, this.maxStep);
weightChange = EncogMath.sign(gradients[index]) * delta;
this.updateValues[index] = delta;
lastGradient[index] = gradients[index];
} else if (change < 0) {
// if change<0, then the sign has changed, and the last
// delta was too big
double delta = this.updateValues[index]
* RPROPConst.NEGATIVE_ETA;
delta = Math.max(delta, RPROPConst.DELTA_MIN);
this.updateValues[index] = delta;
if( this.getError()>this.lastError ) {
weightChange = -this.lastWeightChange[index];
}
// set the previous gradent to zero so that there will be no
// adjustment the next iteration
lastGradient[index] = 0;
} else if (change == 0) {
// if change==0 then there is no change to the delta
final double delta = this.updateValues[index];
weightChange = EncogMath.sign(gradients[index]) * delta;
lastGradient[index] = gradients[index];
}
// apply the weight change, if any
return weightChange;
}
public double updateiWeightMinus(final double[] gradients,
final double[] lastGradient, final int index) {
// multiply the current and previous gradient, and take the
// sign. We want to see if the gradient has changed its sign.
final int change = EncogMath.sign(gradients[index] * lastGradient[index]);
double weightChange = 0;
double delta;
// if the gradient has retained its sign, then we increase the
// delta so that it will converge faster
if (change > 0) {
delta = this.lastDelta[index]
* RPROPConst.POSITIVE_ETA;
delta = Math.min(delta, this.maxStep);
} else {
// if change<0, then the sign has changed, and the last
// delta was too big
delta = this.lastDelta[index]
* RPROPConst.NEGATIVE_ETA;
delta = Math.max(delta, RPROPConst.DELTA_MIN);
lastGradient[index] = 0;
}
lastGradient[index] = gradients[index];
weightChange = EncogMath.sign(gradients[index]) * delta;
this.lastDelta[index] = delta;
// apply the weight change, if any
return weightChange;
}
public double updateJacobiWeight(final double[] gradients,
final double[] lastGradient, final int index) {
// multiply the current and previous gradient, and take the
// sign. We want to see if the gradient has changed its sign.
final int change = EncogMath.sign(gradients[index] * lastGradient[index]);
double weightChange = 0;
// if the gradient has retained its sign, then we increase the
// delta so that it will converge faster
double delta = this.updateValues[index];
if (change > 0) {
delta = this.updateValues[index]
* RPROPConst.POSITIVE_ETA;
delta = Math.min(delta, this.maxStep);
weightChange = EncogMath.sign(gradients[index]) * delta;
this.updateValues[index] = delta;
lastGradient[index] = gradients[index];
} else if (change < 0) {
// if change<0, then the sign has changed, and the last
// delta was too big
delta = this.updateValues[index]
* RPROPConst.NEGATIVE_ETA;
delta = Math.max(delta, RPROPConst.DELTA_MIN);
this.updateValues[index] = delta;
weightChange = -this.lastWeightChange[index];
// set the previous gradient to zero so that there will be no
// adjustment the next iteration
lastGradient[index] = 0;
} else if (change == 0) {
// if change==0 then there is no change to the delta
delta = this.updateValues[index];
weightChange = EncogMath.sign(gradients[index]) * delta;
lastGradient[index] = gradients[index];
}
if(this.getError() > this.lastError) {
weightChange = (1 / (2*q)) * delta;
q++;
} else {
q = 1;
}
// apply the weight change, if any
return weightChange;
}
/**
* {@inheritDoc}
*/
@Override
public void postIteration() {
super.postIteration();
this.lastError = getError();
}
/**
* @return The RPROP update values.
*/
public double[] getUpdateValues() {
return updateValues;
}
}