/* * 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. */ /* * BRkNN.java * Copyright (C) 2009-2010 Aristotle University of Thessaloniki, Thessaloniki, Greece */ package mulan.classifier.lazy; import java.util.ArrayList; import java.util.Random; import mulan.classifier.MultiLabelOutput; import mulan.core.Util; import mulan.data.MultiLabelInstances; import weka.classifiers.lazy.IBk; import weka.core.Instance; import weka.core.Instances; import weka.core.TechnicalInformation; import weka.core.Utils; import weka.core.TechnicalInformation.Field; import weka.core.TechnicalInformation.Type; /** * Simple BR implementation of the KNN algorithm <!-- globalinfo-start --> * * <pre> * Class implementing the base BRkNN algorithm and its 2 extensions BRkNN-a and BRkNN-b. * </pre> * * For more information: * * <pre> * E. Spyromitros, G. Tsoumakas, I. Vlahavas, An Empirical Study of Lazy Multilabel Classification Algorithms, * Proc. 5th Hellenic Conference on Artificial Intelligence (SETN 2008), Springer, Syros, Greece, 2008. * http://mlkd.csd.auth.gr/multilabel.html * </pre> * * <!-- globalinfo-end --> * * <!-- technical-bibtex-start --> BibTeX: * * <pre> * @inproceedings{1428385, * author = {Spyromitros, Eleftherios and Tsoumakas, Grigorios and Vlahavas, Ioannis}, * title = {An Empirical Study of Lazy Multilabel Classification Algorithms}, * booktitle = {SETN '08: Proceedings of the 5th Hellenic conference on Artificial Intelligence}, * year = {2008}, * isbn = {978-3-540-87880-3}, * pages = {401--406}, * doi = {http://dx.doi.org/10.1007/978-3-540-87881-0_40}, * publisher = {Springer-Verlag}, * address = {Berlin, Heidelberg}, * } * * </pre> * * <!-- technical-bibtex-end --> * * @author Eleftherios Spyromitros-Xioufis ( espyromi@csd.auth.gr ) * */ @SuppressWarnings("serial") public class BRkNN extends MultiLabelKNN { Random random; /** * Stores the average number of labels among the knn for each instance Used * in BRkNN-b extension */ int avgPredictedLabels; /** * The value of kNN provided by the user. This may differ from * numOfNeighbors if cross-validation is being used. */ private int cvMaxK; /** * Whether to select k by cross validation. */ private boolean cvkSelection = false; /** * The two types of extensions */ public enum ExtensionType { /** * Standard BR */ NONE, /** * Predict top ranked label in case of empty prediction set */ EXTA, /** * Predict top n ranked labels based on size of labelset in neighbors */ EXTB }; /** * The type of extension to be used */ private ExtensionType extension = ExtensionType.NONE; /** * The default constructor * * @param numOfNeighbors */ public BRkNN(int numOfNeighbors) { this(numOfNeighbors, ExtensionType.NONE); } /** * Constructor giving the option to select an extension of the base version * * @param numOfNeighbors * @param ext the extension to use (see {@link ExtensionType}) * */ public BRkNN(int numOfNeighbors, ExtensionType ext) { super(numOfNeighbors); random = new Random(1); extension = ext; distanceWeighting = WEIGHT_NONE; // weight none } /** * Returns an instance of a TechnicalInformation object, containing detailed * information about the technical background of this class, e.g., paper * reference or book this class is based on. * * @return the technical information about this class */ @Override public TechnicalInformation getTechnicalInformation() { TechnicalInformation result = new TechnicalInformation( Type.INPROCEEDINGS); result.setValue(Field.AUTHOR, "Eleftherios Spyromitros, Grigorios Tsoumakas, Ioannis Vlahavas"); result.setValue(Field.TITLE, "An Empirical Study of Lazy Multilabel Classification Algorithms"); result.setValue(Field.BOOKTITLE, "Proc. 5th Hellenic Conference on Artificial Intelligence (SETN 2008)"); result.setValue(Field.LOCATION, "Syros, Greece"); result.setValue(Field.YEAR, "2008"); return result; } @Override protected void buildInternal(MultiLabelInstances aTrain) throws Exception { super.buildInternal(aTrain); if (cvkSelection == true) { crossValidate(); } } /** * * @param flag * if true the k is selected via cross-validation */ public void setkSelectionViaCV(boolean flag) { cvkSelection = flag; } /** * Select the best value for k by hold-one-out cross-validation. Hamming * Loss is minimized * * @throws Exception */ protected void crossValidate() throws Exception { try { // the performance for each different k double[] hammingLoss = new double[cvMaxK]; for (int i = 0; i < cvMaxK; i++) { hammingLoss[i] = 0; } Instances dataSet = train; Instance instance; // the hold out instance Instances neighbours; // the neighboring instances double[] origDistances, convertedDistances; for (int i = 0; i < dataSet.numInstances(); i++) { if (getDebug() && (i % 50 == 0)) { debug("Cross validating " + i + "/" + dataSet.numInstances() + "\r"); } instance = dataSet.instance(i); neighbours = lnn.kNearestNeighbours(instance, cvMaxK); origDistances = lnn.getDistances(); // gathering the true labels for the instance boolean[] trueLabels = new boolean[numLabels]; for (int counter = 0; counter < numLabels; counter++) { int classIdx = labelIndices[counter]; String classValue = instance.attribute(classIdx).value( (int) instance.value(classIdx)); trueLabels[counter] = classValue.equals("1"); } // calculate the performance metric for each different k for (int j = cvMaxK; j > 0; j--) { convertedDistances = new double[origDistances.length]; System.arraycopy(origDistances, 0, convertedDistances, 0, origDistances.length); double[] confidences = this.getConfidences(neighbours, convertedDistances); boolean[] bipartition = null; switch (extension) { case NONE: // BRknn MultiLabelOutput results; results = new MultiLabelOutput(confidences, 0.5); bipartition = results.getBipartition(); break; case EXTA: // BRknn-a bipartition = labelsFromConfidences2(confidences); break; case EXTB: // BRknn-b bipartition = labelsFromConfidences3(confidences); break; } double symmetricDifference = 0; // |Y xor Z| for (int labelIndex = 0; labelIndex < numLabels; labelIndex++) { boolean actual = trueLabels[labelIndex]; boolean predicted = bipartition[labelIndex]; if (predicted != actual) { symmetricDifference++; } } hammingLoss[j - 1] += (symmetricDifference / numLabels); neighbours = new IBk().pruneToK(neighbours, convertedDistances, j - 1); } } // Display the results of the cross-validation if (getDebug()) { for (int i = cvMaxK; i > 0; i--) { debug("Hold-one-out performance of " + (i) + " neighbors "); debug("(Hamming Loss) = " + hammingLoss[i - 1] / dataSet.numInstances()); } } // Check through the performance stats and select the best // k value (or the lowest k if more than one best) double[] searchStats = hammingLoss; double bestPerformance = Double.NaN; int bestK = 1; for (int i = 0; i < cvMaxK; i++) { if (Double.isNaN(bestPerformance) || (bestPerformance > searchStats[i])) { bestPerformance = searchStats[i]; bestK = i + 1; } } numOfNeighbors = bestK; if (getDebug()) { System.err.println("Selected k = " + bestK); } } catch (Exception ex) { throw new Error("Couldn't optimize by cross-validation: " + ex.getMessage()); } } /** * weka Ibk style prediction * * @throws Exception if nearest neighbours search fails */ protected MultiLabelOutput makePredictionInternal(Instance instance) throws Exception { Instances knn = lnn.kNearestNeighbours(instance, numOfNeighbors); double[] distances = lnn.getDistances(); double[] confidences = getConfidences(knn, distances); boolean[] bipartition; MultiLabelOutput results = null; switch (extension) { case NONE: // BRknn results = new MultiLabelOutput(confidences, 0.5); break; case EXTA: // BRknn-a bipartition = labelsFromConfidences2(confidences); results = new MultiLabelOutput(bipartition, confidences); break; case EXTB: // BRknn-b bipartition = labelsFromConfidences3(confidences); results = new MultiLabelOutput(bipartition, confidences); break; } return results; } /** * Calculates the confidences of the labels, based on the neighboring * instances * * @param neighbours * the list of nearest neighboring instances * @param distances * the distances of the neighbors * @return the confidences of the labels */ private double[] getConfidences(Instances neighbours, double[] distances) { double total = 0, weight; double neighborLabels = 0; double[] confidences = new double[numLabels]; // Set up a correction to the estimator for (int i = 0; i < numLabels; i++) { confidences[i] = 1.0 / Math.max(1, train.numInstances()); } total = (double) numLabels / Math.max(1, train.numInstances()); for (int i = 0; i < neighbours.numInstances(); i++) { // Collect class counts Instance current = neighbours.instance(i); distances[i] = distances[i] * distances[i]; distances[i] = Math.sqrt(distances[i] / (train.numAttributes() - numLabels)); switch (distanceWeighting) { case WEIGHT_INVERSE: weight = 1.0 / (distances[i] + 0.001); // to avoid division by // zero break; case WEIGHT_SIMILARITY: weight = 1.0 - distances[i]; break; default: // WEIGHT_NONE: weight = 1.0; break; } weight *= current.weight(); for (int j = 0; j < numLabels; j++) { double value = Double.parseDouble(current.attribute( labelIndices[j]).value( (int) current.value(labelIndices[j]))); if (Utils.eq(value, 1.0)) { confidences[j] += weight; neighborLabels += weight; } } total += weight; } avgPredictedLabels = (int) Math.round(neighborLabels / total); // Normalise distribution if (total > 0) { Utils.normalize(confidences, total); } return confidences; } /** * used for BRknn-a * * @param confidences the probabilities for each label * @return a bipartition */ protected boolean[] labelsFromConfidences2(double[] confidences) { boolean[] bipartition = new boolean[numLabels]; boolean flag = false; // check the case that no label is true for (int i = 0; i < numLabels; i++) { if (confidences[i] >= 0.5) { bipartition[i] = true; flag = true; } } // assign the class with the greater confidence if (flag == false) { int index = Util.RandomIndexOfMax(confidences, random); bipartition[index] = true; } return bipartition; } /** * used for BRkNN-b (break ties arbitrarily) * * @param confidences the probabilities for each label * @return a bipartition */ protected boolean[] labelsFromConfidences3(double[] confidences) { boolean[] bipartition = new boolean[numLabels]; int[] indices = Utils.stableSort(confidences); ArrayList<Integer> lastindices = new ArrayList<Integer>(); int counter = 0; int i = numLabels - 1; while (i > 0) { if (confidences[indices[i]] > confidences[indices[numLabels - avgPredictedLabels]]) { bipartition[indices[i]] = true; counter++; } else if (confidences[indices[i]] == confidences[indices[numLabels - avgPredictedLabels]]) { lastindices.add(indices[i]); } else { break; } i--; } int size = lastindices.size(); int j = avgPredictedLabels - counter; while (j > 0) { int next = random.nextInt(size); if (bipartition[lastindices.get(next)] != true) { bipartition[lastindices.get(next)] = true; j--; } } return bipartition; } /** * set the maximum number of neighbors to be evaluated via cross-validation * * @param cvMaxK */ public void setCvMaxK(int cvMaxK) { this.cvMaxK = cvMaxK; } }