/*
* RapidMiner
*
* Copyright (C) 2001-2011 by Rapid-I and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapid-i.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.operator.features.selection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import com.rapidminer.example.Attribute;
import com.rapidminer.example.AttributeWeights;
import com.rapidminer.example.Attributes;
import com.rapidminer.example.ExampleSet;
import com.rapidminer.operator.OperatorChain;
import com.rapidminer.operator.OperatorDescription;
import com.rapidminer.operator.OperatorException;
import com.rapidminer.operator.UserError;
import com.rapidminer.operator.ValueDouble;
import com.rapidminer.operator.ValueString;
import com.rapidminer.operator.performance.PerformanceCriterion;
import com.rapidminer.operator.performance.PerformanceVector;
import com.rapidminer.operator.ports.InputPort;
import com.rapidminer.operator.ports.OutputPort;
import com.rapidminer.operator.ports.metadata.SubprocessTransformRule;
import com.rapidminer.parameter.ParameterType;
import com.rapidminer.parameter.ParameterTypeBoolean;
import com.rapidminer.parameter.ParameterTypeCategory;
import com.rapidminer.parameter.ParameterTypeDouble;
import com.rapidminer.parameter.ParameterTypeInt;
import com.rapidminer.parameter.conditions.BooleanParameterCondition;
import com.rapidminer.parameter.conditions.EqualTypeCondition;
import com.rapidminer.tools.math.AnovaCalculator;
import com.rapidminer.tools.math.SignificanceCalculationException;
import com.rapidminer.tools.math.SignificanceTestResult;
/**
* This operator starts with an empty selection of attributes and, in each round, it adds each unused attribute of the
* given set of examples. For each added attribute, the performance is estimated using inner operators, e.g. a
* cross-validation. Only the attribute giving the highest increase of performance is added to the selection. Then a new
* round is started with the modified selection. This implementation will avoid any additional memory consumption beside
* the memory used originally for storing the data and the memory which might be needed for applying the inner
* operators.
* A parameter specifies when the iteration will be aborted. There are three different behaviors possible:
* <ul>
* <li><b>without increase</b>runs as long as there is any increase in performance</li>
* <li><b>without increase of at least</b> runs as long as the increase is at least as high as specified, either relative or absolute.</li>
* <li><b>without significant increase</b> stops as soon as the increase isn't significant to the specified level.</li>
* </ul>
*
* The parameter speculative_rounds defines how many rounds will be performed in a row, after a first time the stopping
* criterion was fulfilled. If the performance increases again during the speculative rounds, the selection will be continued.
* Otherwise all additionally selected attributes will be removed, as if no speculative rounds would have been executed.
* This might help to avoid getting stuck in local optima. A following backward elimination operator might remove unneeded
* attributes again.
*
* The operator provides a value for logging the performance in each round using a ProcessLog.
*
* @author Sebastian Land
*
*/
public class ForwardAttributeSelectionOperator extends OperatorChain {
public static final String PARAMETER_STOPPING_BEHAVIOR = "stopping_behavior";
public static final String PARAMETER_MAX_ATTRIBUTES = "maximal_number_of_attributes";
public static final String PARAMETER_MIN_RELATIVE_INCREASE = "minimal_relative_increase";
public static final String PARAMETER_MIN_ABSOLUT_INCREASE = "minimal_absolute_increase";
public static final String PARAMETER_USE_RELATIVE_INCREASE = "use_relative_increase";
public static final String PARAMETER_ALPHA = "alpha";
public static final String PARAMETER_ALLOWED_CONSECUTIVE_FAILS = "speculative_rounds";
public static final String[] STOPPING_BEHAVIORS = new String[] {
"without increase",
"without increase of at least",
"without significant increase"
};
public static final int WITHOUT_INCREASE = 0;
public static final int WITHOUT_INCREASE_OF_AT_LEAST = 1;
public static final int WITHOUT_INCREASE_SIGNIFICANT = 2;
private double currentNumberOfFeatures = 0;
private Attributes currentAttributes;
private InputPort exampleSetInput = getInputPorts().createPort("example set", ExampleSet.class);
private OutputPort innerExampleSetSource = getSubprocess(0).getInnerSources().createPort("example set");
private InputPort innerPerformanceSink = getSubprocess(0).getInnerSinks().createPort("performance", PerformanceVector.class);
private OutputPort exampleSetOutput = getOutputPorts().createPort("example set");
private OutputPort weightsOutput = getOutputPorts().createPort("attribute weights");
private OutputPort performanceOutput = getOutputPorts().createPort("performance");
public ForwardAttributeSelectionOperator(OperatorDescription description) {
super(description, "Learning Process");
getTransformer().addPassThroughRule(exampleSetInput, innerExampleSetSource);
getTransformer().addRule(new SubprocessTransformRule(getSubprocess(0)));
getTransformer().addPassThroughRule(exampleSetInput, exampleSetOutput);
getTransformer().addGenerationRule(performanceOutput, PerformanceVector.class);
getTransformer().addGenerationRule(weightsOutput, AttributeWeights.class);
addValue(new ValueDouble("number of attributes", "The current number of attributes.") {
@Override
public double getDoubleValue() {
return currentNumberOfFeatures;
}
});
addValue(new ValueString("feature_names", "A comma separated list of all features of this round.") {
@Override
public String getStringValue() {
if (currentAttributes == null)
return "This logging value is only available during execution of this operator's inner subprocess.";
StringBuffer buffer = new StringBuffer();
for (Attribute attribute: currentAttributes) {
if (buffer.length() > 0)
buffer.append(", ");
buffer.append(attribute.getName());
}
return buffer.toString();
}
});
}
@Override
public void doWork() throws OperatorException {
ExampleSet exampleSetOriginal = exampleSetInput.getData();
ExampleSet exampleSet = (ExampleSet) exampleSetOriginal.clone();
int numberOfAttributes = exampleSet.getAttributes().size();
Attributes attributes = exampleSet.getAttributes();
int maxNumberOfAttributes = Math.min(getParameterAsInt(PARAMETER_MAX_ATTRIBUTES), numberOfAttributes);
int maxNumberOfFails = getParameterAsInt(PARAMETER_ALLOWED_CONSECUTIVE_FAILS);
int behavior = getParameterAsInt(PARAMETER_STOPPING_BEHAVIOR);
boolean useRelativeIncrease = (behavior == WITHOUT_INCREASE_OF_AT_LEAST) ? getParameterAsBoolean(PARAMETER_USE_RELATIVE_INCREASE) : false;
double minimalIncrease = 0;
if (useRelativeIncrease)
minimalIncrease = useRelativeIncrease ? getParameterAsDouble(PARAMETER_MIN_RELATIVE_INCREASE) : getParameterAsDouble(PARAMETER_MIN_ABSOLUT_INCREASE);
double alpha = (behavior == WITHOUT_INCREASE_SIGNIFICANT)? getParameterAsDouble(PARAMETER_ALPHA) : 0d;
// remembering attributes and removing all from example set
Attribute[] attributeArray = new Attribute[numberOfAttributes];
int i = 0;
Iterator<Attribute> iterator = attributes.iterator();
while (iterator.hasNext()) {
Attribute attribute = iterator.next();
attributeArray[i] = attribute;
i++;
iterator.remove();
}
boolean[] selected = new boolean[numberOfAttributes];
boolean earlyAbort = false;
List<Integer> speculativeList = new ArrayList<Integer>(maxNumberOfFails);
int numberOfFails = maxNumberOfFails;
PerformanceVector lastPerformance = null;
PerformanceVector bestPerformanceEver = null;
for (i = 0; i < maxNumberOfAttributes && !earlyAbort; i++) {
// setting values for logging
currentNumberOfFeatures = i + 1;
// performing a round
int bestIndex = 0;
PerformanceVector currentBestPerformance = null;
for (int current = 0; current < numberOfAttributes; current++) {
if (!selected[current]) {
// switching on
attributes.addRegular(attributeArray[current]);
currentAttributes = attributes;
// evaluate performance
innerExampleSetSource.deliver(exampleSet);
getSubprocess(0).execute();
PerformanceVector performance = innerPerformanceSink.getData();
if (currentBestPerformance == null || performance.compareTo(currentBestPerformance) > 0) {
bestIndex = current;
currentBestPerformance = performance;
}
// switching off
attributes.remove(attributeArray[current]);
currentAttributes = null;
}
}
double currentFitness = currentBestPerformance.getMainCriterion().getFitness();
if (i != 0) {
double lastFitness = lastPerformance.getMainCriterion().getFitness();
// switch stopping behavior
switch (behavior) {
case WITHOUT_INCREASE:
if (lastFitness >= currentFitness)
earlyAbort = true;
break;
case WITHOUT_INCREASE_OF_AT_LEAST:
if (useRelativeIncrease) {
// relative increase testing
if (! (currentFitness > lastFitness + lastFitness * minimalIncrease))
earlyAbort = true;
} else {
// absolute increase testing
if (! (currentFitness > lastFitness + minimalIncrease))
earlyAbort = true;
}
break;
case WITHOUT_INCREASE_SIGNIFICANT:
AnovaCalculator calculator = new AnovaCalculator();
calculator.setAlpha(alpha);
PerformanceCriterion pc = currentBestPerformance.getMainCriterion();
calculator.addGroup(pc.getAverageCount(), pc.getAverage(), pc.getVariance());
pc = lastPerformance.getMainCriterion();
calculator.addGroup(pc.getAverageCount(), pc.getAverage(), pc.getVariance());
SignificanceTestResult result;
try {
result = calculator.performSignificanceTest();
} catch (SignificanceCalculationException e) {
throw new UserError(this, 920, e.getMessage());
}
if (lastFitness <= currentFitness || result.getProbability() < alpha)
earlyAbort = true;
}
}
if (earlyAbort) {
// check if there are some free tries left
if (numberOfFails == 0) {
break;
}
numberOfFails--;
speculativeList.add(bestIndex);
earlyAbort = false;
// needs performance increase compared to better performance of current and last!
if (currentBestPerformance.compareTo(lastPerformance) > 0)
lastPerformance = currentBestPerformance;
} else {
// resetting maximal number of fails.
numberOfFails = maxNumberOfFails;
speculativeList.clear();
lastPerformance = currentBestPerformance;
bestPerformanceEver = currentBestPerformance;
}
// switching best index on
attributes.addRegular(attributeArray[bestIndex]);
selected[bestIndex] = true;
}
// removing predictively added attributes: speculative execution did not yield good result
for (Integer removeIndex: speculativeList) {
selected[removeIndex] = false;
attributes.remove(attributeArray[removeIndex]);
}
AttributeWeights weights = new AttributeWeights();
i = 0;
for (Attribute attribute : attributeArray) {
if (selected[i])
weights.setWeight(attribute.getName(), 1d);
else
weights.setWeight(attribute.getName(), 0d);
i++;
}
exampleSetOutput.deliver(exampleSet);
performanceOutput.deliver(bestPerformanceEver);
weightsOutput.deliver(weights);
}
@Override
public List<ParameterType> getParameterTypes() {
List<ParameterType> types = super.getParameterTypes();
ParameterType type = new ParameterTypeInt(PARAMETER_MAX_ATTRIBUTES, "The maximal number of forward selection steps and hence the maximal number of attributes.", 1, Integer.MAX_VALUE, 10);
type.setExpert(false);
types.add(type);
type = new ParameterTypeInt(PARAMETER_ALLOWED_CONSECUTIVE_FAILS, "Defines the number of times, the stopping criterion might be consecutivly ignored before the selection is actually stopped. A number higher than one might help not to stack in the local optima.", 0, Integer.MAX_VALUE, 0);
type.setExpert(false);
types.add(type);
type = new ParameterTypeCategory(PARAMETER_STOPPING_BEHAVIOR, "Defines on what criterias the selection is stopped.", STOPPING_BEHAVIORS, 0);
type.setExpert(false);
types.add(type);
type = new ParameterTypeBoolean(PARAMETER_USE_RELATIVE_INCREASE, "If checked, the relative performance increase will be used as stopping criterion.", true);
type.setExpert(false);
type.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_STOPPING_BEHAVIOR, STOPPING_BEHAVIORS, false, WITHOUT_INCREASE_OF_AT_LEAST));
types.add(type);
type = new ParameterTypeDouble(PARAMETER_MIN_ABSOLUT_INCREASE, "If the absolut performance increase to the last step drops below this threshold, the selection will be stopped.", 0d, Double.POSITIVE_INFINITY, true);
type.setExpert(false);
type.registerDependencyCondition(new BooleanParameterCondition(this, PARAMETER_USE_RELATIVE_INCREASE, true, false));
type.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_STOPPING_BEHAVIOR, STOPPING_BEHAVIORS, false, WITHOUT_INCREASE_OF_AT_LEAST));
types.add(type);
type = new ParameterTypeDouble(PARAMETER_MIN_RELATIVE_INCREASE, "If the relative performance increase to the last step drops below this threshold, the selection will be stopped.", 0d, 1d, true);
type.setExpert(false);
type.registerDependencyCondition(new BooleanParameterCondition(this, PARAMETER_USE_RELATIVE_INCREASE, true, true));
type.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_STOPPING_BEHAVIOR, STOPPING_BEHAVIORS, false, WITHOUT_INCREASE_OF_AT_LEAST));
types.add(type);
type = new ParameterTypeDouble(PARAMETER_ALPHA, "The probability threshold which determines if differences are considered as significant.", 0.0d, 1.0d, 0.05d);
type.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_STOPPING_BEHAVIOR, STOPPING_BEHAVIORS, true, WITHOUT_INCREASE_SIGNIFICANT));
types.add(type);
return types;
}
}