/*
* 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.learner.functions;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import liblinear.FeatureNode;
import liblinear.Linear;
import liblinear.Parameter;
import liblinear.Problem;
import liblinear.SolverType;
import com.rapidminer.example.Attribute;
import com.rapidminer.example.Example;
import com.rapidminer.example.ExampleSet;
import com.rapidminer.example.FastExample2SparseTransform;
import com.rapidminer.operator.Model;
import com.rapidminer.operator.OperatorCapability;
import com.rapidminer.operator.OperatorDescription;
import com.rapidminer.operator.OperatorException;
import com.rapidminer.operator.UserError;
import com.rapidminer.operator.learner.AbstractLearner;
import com.rapidminer.operator.learner.PredictionModel;
import com.rapidminer.parameter.ParameterType;
import com.rapidminer.parameter.ParameterTypeBoolean;
import com.rapidminer.parameter.ParameterTypeCategory;
import com.rapidminer.parameter.ParameterTypeDouble;
import com.rapidminer.parameter.ParameterTypeList;
import com.rapidminer.parameter.ParameterTypeString;
/**
* Applies a fast margin learner based on the linear support vector learning scheme proposed by R.-E. Fan, K.-W. Chang,
* C.-J. Hsieh, X.-R. Wang, and C.-J. Lin. Although the result is similar to those delivered by classical SVM or
* logistic regression implementations, this linear classifier is able to work on data set with millions of examples and
* attributes.
*
* @rapidminer.index SVM
* @author Ingo Mierswa
*/
public class FastLargeMargin extends AbstractLearner {
public static final String PARAMETER_SOLVER = "solver";
/** The parameter name for "The cost parameter C for c_svc, epsilon_svr, and nu_svr." */
public static final String PARAMETER_C = "C";
/** The parameter name for "Tolerance of termination criterion." */
public static final String PARAMETER_EPSILON = "epsilon";
/**
* The parameter name for "The weights w for all classes (first column: class name, second column: weight),
* i.e. set the parameters C of each class w * C (empty: using 1 for all classes where the weight was not
* defined)."
*/
public static final String PARAMETER_CLASS_WEIGHTS = "class_weights";
public static final String PARAMETER_USE_BIAS = "use_bias";
/*
* What to do for a new LibLinear version (current version 1.33):
* -------------------------------------------------------------- - set field DEBUG_OUTPUT in class Linear to false
* - remove some additional system.out statements - make some fields public of Model (nr_class, l, nSV, label etc.)
*/
/** The different SVM types implemented by the LibSVM package. */
public static final String[] SOLVER = { "L2 SVM Dual", "L2 SVM Primal", "L2 Logistic Regression", "L1 SVM Dual" };
public static final int SOLVER_L2_SVM_DUAL = 0;
public static final int SOLVER_L2_SVM_PRIMAL = 1;
public static final int SOLVER_L2_LR = 2;
public static final int SOLVER_L1_SVM_DUAL = 3;
public FastLargeMargin(OperatorDescription description) {
super(description);
}
public boolean supportsCapability(OperatorCapability lc) {
switch (lc) {
case NUMERICAL_ATTRIBUTES:
case BINOMINAL_LABEL:
return true;
default:
return false;
}
}
/**
* Creates a data node row for the LibSVM (sparse format, i.e. each node keeps the index and the value if not
* default).
*/
public static FeatureNode[] makeNodes(Example e, FastExample2SparseTransform ripper, boolean useBias) {
int[] nonDefaultIndices = ripper.getNonDefaultAttributeIndices(e);
double[] nonDefaultValues = ripper.getNonDefaultAttributeValues(e, nonDefaultIndices);
int offset = 0;
if (useBias)
offset = 1;
FeatureNode[] nodeArray = new FeatureNode[nonDefaultIndices.length + offset];
for (int a = 0; a < nonDefaultIndices.length; a++) {
FeatureNode node = new FeatureNode(nonDefaultIndices[a] + 1, nonDefaultValues[a]);
nodeArray[a] = node;
}
if (useBias)
nodeArray[nodeArray.length - 1] = new FeatureNode(nodeArray.length, 1);
return nodeArray;
}
/**
* Creates a support vector problem for the LibSVM.
*
* @throws UserError
*/
private Problem getProblem(ExampleSet exampleSet) throws UserError {
log("Creating LibLinear problem.");
FastExample2SparseTransform ripper = new FastExample2SparseTransform(exampleSet);
int nodeCount = 0;
Problem problem = new Problem();
problem.l = exampleSet.size();
boolean useBias = getParameterAsBoolean(PARAMETER_USE_BIAS);
if (useBias) {
problem.n = exampleSet.getAttributes().size() + 1;
} else {
problem.n = exampleSet.getAttributes().size();
}
problem.y = new int[exampleSet.size()];
problem.x = new FeatureNode[exampleSet.size()][];
Iterator<Example> i = exampleSet.iterator();
Attribute label = exampleSet.getAttributes().getLabel();
int j = 0;
int firstIndex = label.getMapping().getNegativeIndex();
while (i.hasNext()) {
Example e = i.next();
problem.x[j] = makeNodes(e, ripper, useBias);
problem.y[j] = (int) e.getValue(label) == firstIndex ? 0 : 1;
nodeCount += problem.x[j].length;
j++;
}
log("Created " + nodeCount + " nodes for " + j + " examples.");
return problem;
}
/**
* Creates a LibSVM parameter object based on the user defined parameters. If gamma is set to zero, it will be
* overwritten by 1 divided by the number of attributes.
*/
private Parameter getParameters(ExampleSet exampleSet) throws OperatorException {
SolverType solverType = null;
int solverTypeParameter = getParameterAsInt(PARAMETER_SOLVER);
switch (solverTypeParameter) {
case SOLVER_L2_SVM_DUAL:
solverType = SolverType.L2LOSS_SVM_DUAL;
break;
case SOLVER_L2_SVM_PRIMAL:
solverType = SolverType.L2LOSS_SVM;
break;
case SOLVER_L2_LR:
solverType = SolverType.L2_LR;
break;
case SOLVER_L1_SVM_DUAL:
solverType = SolverType.L1LOSS_SVM_DUAL;
break;
default:
solverType = SolverType.L2LOSS_SVM_DUAL;
break;
}
double c = getParameterAsDouble(PARAMETER_C);
double epsilon = getParameterAsDouble(PARAMETER_EPSILON);
Parameter parameter = new Parameter(solverType, c, epsilon);
// class weights (if set)
if (isParameterSet(PARAMETER_CLASS_WEIGHTS)) {
double[] weights = new double[2];
int[] weightLabelIndices = new int[2];
for (int i = 0; i < weights.length; i++) {
weights[i] = 1.0d;
weightLabelIndices[i] = i;
}
List<String[]> classWeights = getParameterList(PARAMETER_CLASS_WEIGHTS);
Iterator<String[]> i = classWeights.iterator();
Attribute label = exampleSet.getAttributes().getLabel();
while (i.hasNext()) {
String[] classWeightArray = i.next();
String className = classWeightArray[0];
double classWeight = Double.valueOf(classWeightArray[1]);
int index = label.getMapping().getIndex(className);
if ((index >= 0) && (index < weights.length))
weights[index] = classWeight;
}
// logging
List<Double> weightList = new LinkedList<Double>();
for (double d : weights)
weightList.add(d);
log(getName() + ": used class weights --> " + weightList);
parameter.setWeights(weights, weightLabelIndices);
}
return parameter;
}
/** Learns a new SVM model with the LibSVM package. */
public Model learn(ExampleSet exampleSet) throws OperatorException {
Parameter params = getParameters(exampleSet);
if (exampleSet.size() < 2) {
throw new UserError(this, 110, 2);
}
Linear.resetRandom();
Linear.disableDebugOutput();
Problem problem = getProblem(exampleSet);
liblinear.Model model = Linear.train(problem, params);
return new FastMarginModel(exampleSet, model, getParameterAsBoolean(PARAMETER_USE_BIAS));
}
@Override
public Class<? extends PredictionModel> getModelClass() {
return FastMarginModel.class;
}
@Override
public List<ParameterType> getParameterTypes() {
List<ParameterType> types = super.getParameterTypes();
ParameterType type = new ParameterTypeCategory(PARAMETER_SOLVER, "The solver type for this fast margin method.", SOLVER, SOLVER_L2_SVM_DUAL);
type.setExpert(false);
types.add(type);
type = new ParameterTypeDouble(PARAMETER_C, "The cost parameter C for c_svc, epsilon_svr, and nu_svr.", 0, Double.POSITIVE_INFINITY, 1);
type.setExpert(false);
types.add(type);
types.add(new ParameterTypeDouble(PARAMETER_EPSILON, "Tolerance of termination criterion.", Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, 0.01));
types.add(new ParameterTypeList(PARAMETER_CLASS_WEIGHTS, "The weights w for all classes, i.e. set the parameters C of each class w * C (empty: using 1 for all classes where the weight was not defined).", new ParameterTypeString("class_name", "The class name (possible value of your label attribute)."), new ParameterTypeDouble("weight",
"The weight for this class.", 0.0d, Double.POSITIVE_INFINITY, 1.0d)));
types.add(new ParameterTypeBoolean(PARAMETER_USE_BIAS, "Indicates if an intercept value should be calculated.", true));
return types;
}
}