/* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /* * CalibratedLabelRanking.java * Copyright (C) 2009-2010 Aristotle University of Thessaloniki, Thessaloniki, Greece */ package mulan.classifier.transformation; import java.util.Arrays; import mulan.classifier.MultiLabelOutput; import mulan.data.MultiLabelInstances; import mulan.transformations.RemoveAllLabels; import weka.classifiers.AbstractClassifier; import weka.classifiers.Classifier; import weka.core.Attribute; import weka.core.DenseInstance; import weka.core.Instance; import weka.core.Instances; import weka.core.SparseInstance; import weka.core.Utils; import weka.filters.Filter; import weka.filters.unsupervised.attribute.Reorder; /** * * <!-- globalinfo-start --> * * <pre> * Class implementing the Calibrated Label Ranking algorithm. * </pre> * * For more information: * * <pre> * Fuernkranz, J., Huellermeier, E., Loza Mencia, E., and Brinker, K. (2008) * Multilabel classification via calibrated label ranking. * Machine Learning 73(2), 133-153 * </pre> * * <!-- globalinfo-end --> * * <!-- technical-bibtex-start --> BibTeX: * * <pre> * @article{furnkranze+etal:2008, * author = {Fuernkranz, J. and Huellermeier, E. and Loza Mencia, E. and Brinker, K.}, * title = {Multilabel classification via calibrated label ranking}, * journal = {Machine Learning}, * volume = {73}, * number = {2}, * year = {2008}, * pages = {133--153}, * } * </pre> * * <p/> <!-- technical-bibtex-end --> * * @author Elise Rairat * @author Grigorios Tsoumakas * @author Sang-Hyeun Park * @version $Revision: 1.0 $ */ public class CalibratedLabelRanking extends TransformationBasedMultiLabelLearner { /** array holding the one vs one models */ protected Classifier[] oneVsOneModels; /** number of one vs one models */ protected int numModels; /** temporary training data for each one vs one model */ protected Instances trainingdata; /** headers of the training sets of the one vs one models */ protected Instances[] metaDataTest; /** binary relevance models for the virtual label */ protected BinaryRelevance virtualLabelModels; /** whether to use standard voting or the fast qweighted algorithm */ private boolean useStandardVoting = true; /** whether no data exist for one-vs-one learning */ protected boolean[] nodata; /** * Constructor that initializes the learner with a base algorithm * * @param classifier the binary classification algorithm to use */ public CalibratedLabelRanking(Classifier classifier) { super(classifier); } /** * Set Prediction to standard voting mode. * * @param standardVoting <code>true</code> if standard voting should be used */ public void setStandardVoting(boolean standardVoting) { useStandardVoting = standardVoting; } /** * Get whether standard voting is turned on. * * @return <code>true</code> if standard voting is on */ public boolean getStandardVoting() { return useStandardVoting; } @Override protected void buildInternal(MultiLabelInstances trainingSet) throws Exception { // Virtual label models debug("Building calibration label models"); virtualLabelModels = new BinaryRelevance(getBaseClassifier()); virtualLabelModels.setDebug(getDebug()); virtualLabelModels.build(trainingSet); // One-vs-one models numModels = ((numLabels) * (numLabels - 1)) / 2; oneVsOneModels = AbstractClassifier.makeCopies(getBaseClassifier(), numModels); nodata = new boolean[numModels]; metaDataTest = new Instances[numModels]; Instances trainingData = trainingSet.getDataSet(); int counter = 0; // Creation of one-vs-one models for (int label1 = 0; label1 < numLabels - 1; label1++) { // Attribute of label 1 Attribute attrLabel1 = trainingData.attribute(labelIndices[label1]); for (int label2 = label1 + 1; label2 < numLabels; label2++) { debug("Building one-vs-one model " + (counter + 1) + "/" + numModels); // Attribute of label 2 Attribute attrLabel2 = trainingData.attribute(labelIndices[label2]); // initialize training set Instances dataOneVsOne = new Instances(trainingData, 0); // filter out examples with no preference for (int i = 0; i < trainingData.numInstances(); i++) { Instance tempInstance; if (trainingData.instance(i) instanceof SparseInstance) { tempInstance = new SparseInstance(trainingData.instance(i)); } else { tempInstance = new DenseInstance(trainingData.instance(i)); } int nominalValueIndex; nominalValueIndex = (int) tempInstance.value(labelIndices[label1]); String value1 = attrLabel1.value(nominalValueIndex); nominalValueIndex = (int) tempInstance.value(labelIndices[label2]); String value2 = attrLabel2.value(nominalValueIndex); if (!value1.equals(value2)) { tempInstance.setValue(attrLabel1, value1); dataOneVsOne.add(tempInstance); } } // remove all labels apart from label1 and place it at the end Reorder filter = new Reorder(); int numPredictors = trainingData.numAttributes() - numLabels; int[] reorderedIndices = new int[numPredictors + 1]; for (int i = 0; i < numPredictors; i++) { reorderedIndices[i] = featureIndices[i]; } reorderedIndices[numPredictors] = labelIndices[label1]; filter.setAttributeIndicesArray(reorderedIndices); filter.setInputFormat(dataOneVsOne); dataOneVsOne = Filter.useFilter(dataOneVsOne, filter); //System.out.println(dataOneVsOne.toString()); dataOneVsOne.setClassIndex(numPredictors); // build model label1 vs label2 if (dataOneVsOne.size() > 0) { oneVsOneModels[counter].buildClassifier(dataOneVsOne); } else { nodata[counter] = true; } dataOneVsOne.delete(); metaDataTest[counter] = dataOneVsOne; counter++; } } } /** * This method does a prediction for an instance with the values of label missing * Temporary included to switch between standard voting and qweighted multilabel voting * @param instance * @return prediction * @throws java.lang.Exception */ protected MultiLabelOutput makePredictionInternal(Instance instance) throws Exception { if (useStandardVoting) { return makePredictionStandard(instance); } else { return makePredictionQW(instance); } } /** * This method does a prediction for an instance with the values of label missing * @param instance * @return prediction * @throws java.lang.Exception */ public MultiLabelOutput makePredictionStandard(Instance instance) throws Exception { boolean[] bipartition = new boolean[numLabels]; double[] confidences = new double[numLabels]; int[] voteLabel = new int[numLabels + 1]; //System.out.println("Instance:" + instance.toString()); // delete all labels and add a new atribute at the end Instance newInstance = RemoveAllLabels.transformInstance(instance, labelIndices); newInstance.insertAttributeAt(newInstance.numAttributes()); //initialize the array voteLabel Arrays.fill(voteLabel, 0); int counter = 0; for (int label1 = 0; label1 < numLabels - 1; label1++) { for (int label2 = label1 + 1; label2 < numLabels; label2++) { if (!nodata[counter]) { double distribution[] = new double[2]; try { newInstance.setDataset(metaDataTest[counter]); distribution = oneVsOneModels[counter].distributionForInstance(newInstance); } catch (Exception e) { System.out.println(e); return null; } int maxIndex = (distribution[0] > distribution[1]) ? 0 : 1; // Ensure correct predictions both for class values {0,1} and {1,0} Attribute classAttribute = metaDataTest[counter].classAttribute(); if (classAttribute.value(maxIndex).equals("1")) { voteLabel[label1]++; } else { voteLabel[label2]++; } } counter++; } } int voteVirtual = 0; MultiLabelOutput virtualMLO = virtualLabelModels.makePrediction(instance); boolean[] virtualBipartition = virtualMLO.getBipartition(); for (int i = 0; i < numLabels; i++) { if (virtualBipartition[i]) { voteLabel[i]++; } else { voteVirtual++; } } for (int i = 0; i < numLabels; i++) { if (voteLabel[i] >= voteVirtual) { bipartition[i] = true; } else { bipartition[i] = false; } confidences[i] = 1.0 * voteLabel[i] / numLabels; } MultiLabelOutput mlo = new MultiLabelOutput(bipartition, confidences); return mlo; } /** * This method does a prediction for an instance with the values of label missing * according to QWeighted algorithm for Multilabel Classification (QCMLPP2), which is * described in : * Loza Mencia, E., Park, S.-H., and Fuernkranz, J. (2009) * Efficient voting prediction for pairwise multilabel classification. * In Proceedings of 17th European Symposium on Artificial * Neural Networks (ESANN 2009), Bruges (Belgium), April 2009 * * This method reduces the number of classifier evaluations and guarantees the same * Multilabel Output as ordinary Voting. But: the estimated confidences are only * approximated. Therefore, ranking-based performances are worse than ordinary voting. * @param instance * @return prediction * @throws java.lang.Exception */ public MultiLabelOutput makePredictionQW(Instance instance) throws Exception { int[] voteLabel = new int[numLabels]; int[] played = new int[numLabels + 1]; int[][] playedMatrix = new int[numLabels + 1][numLabels + 1]; int[] sortarr = new int[numLabels + 1]; double[] limits = new double[numLabels]; boolean[] bipartition = new boolean[numLabels]; double[] confidences = new double[numLabels]; int voteVirtual = 0; double limitVirtual = 0.0; boolean allEqualClassesFound = false; // delete all labels and add a new atribute at the end Instance newInstance = RemoveAllLabels.transformInstance(instance, labelIndices); newInstance.insertAttributeAt(newInstance.numAttributes()); //initialize the array voteLabel Arrays.fill(voteLabel, 0); // evaluate all classifiers of the calibrated label beforehand, #numLabels 1 vs. A evaluations MultiLabelOutput virtualMLO = virtualLabelModels.makePrediction(instance); boolean[] virtualBipartition = virtualMLO.getBipartition(); for (int i = 0; i < numLabels; i++) { if (virtualBipartition[i]) { voteLabel[i]++; } else { voteVirtual++; } played[i]++; playedMatrix[i][numLabels] = 1; playedMatrix[numLabels][i] = 1; limits[i] = played[i] - voteLabel[i]; } limitVirtual = numLabels - voteVirtual; played[numLabels] = numLabels; // apply QWeighted iteratively to estimate all relevant labels until the // calibrated label is found boolean found = false; int pos = 0; int player1 = -1; int player2 = -1; while (!allEqualClassesFound && pos < numLabels) { while (!found) { // opponent selection process: pair best against second best w.r.t. to number of "lost games" // player1 = pick player with min(limits[player]) && player isn't ranked sortarr = Utils.sort(limits); player1 = sortarr[0]; player2 = -1; int i = 1; // can we found unplayed matches of player1 ? if (played[player1] < numLabels) { // search for best opponent while (player2 == -1 && i < sortarr.length) { // already played ?? if (playedMatrix[player1][sortarr[i]] == 0) { player2 = sortarr[i]; } i++; } // play found Pairing and update stats int modelIndex = getRRClassifierIndex(player1, player2); newInstance.setDataset(metaDataTest[modelIndex]); double[] distribution = oneVsOneModels[modelIndex].distributionForInstance(newInstance); int maxIndex = (distribution[0] > distribution[1]) ? 0 : 1; // Ensure correct predictions both for class values {0,1} and {1,0} Attribute classAttribute = metaDataTest[modelIndex].classAttribute(); if (classAttribute.value(maxIndex).equals("1")) { voteLabel[player1 > player2 ? player2 : player1]++; } else { voteLabel[player1 > player2 ? player1 : player2]++; } // update stats played[player1]++; played[player2]++; playedMatrix[player1][player2] = 1; playedMatrix[player2][player1] = 1; limits[player1] = played[player1] - voteLabel[player1]; limits[player2] = played[player2] - voteLabel[player2]; } // full played, there are no opponents left else { found = true; } } //arrange already as relevant validated labels at the end of possible opponents limits[player1] = Double.MAX_VALUE; //check for possible labels, which can still gain greater or equal votes as the calibrated label allEqualClassesFound = true; for (int i = 0; i < numLabels; i++) { if (limits[i] <= limitVirtual) { allEqualClassesFound = false; } } // search for next relevant label found = false; pos++; } //Generate Multilabel Output for (int i = 0; i < numLabels; i++) { if (voteLabel[i] >= voteVirtual) { bipartition[i] = true; } else { bipartition[i] = false; } confidences[i] = 1.0 * voteLabel[i] / numLabels; } MultiLabelOutput mlo = new MultiLabelOutput(bipartition, confidences); return mlo; } /** * a function to get the classifier index for label1 vs label2 (single Round-Robin) * in the array of classifiers, oneVsOneModels * @param label1 * @param label2 * @return index of classifier (label1 vs label2) */ private int getRRClassifierIndex(int label1, int label2) { int l1 = label1 > label2 ? label2 : label1; int l2 = label1 > label2 ? label1 : label2; if (l1 == 0) { return (l2 - 1); } else { int temp = 0; for (int i = l1; i > 0; i--) { temp += (numLabels - i); } temp += l2 - (l1 + 1); return temp; } } }