001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package org.openimaj.ml.annotation.linear; 031 032import java.util.ArrayList; 033import java.util.HashSet; 034import java.util.List; 035import java.util.Set; 036 037import org.openimaj.citation.annotation.Reference; 038import org.openimaj.citation.annotation.ReferenceType; 039import org.openimaj.data.dataset.GroupedDataset; 040import org.openimaj.data.dataset.ListDataset; 041import org.openimaj.feature.FeatureExtractor; 042import org.openimaj.feature.FeatureVector; 043import org.openimaj.ml.annotation.Annotated; 044import org.openimaj.ml.annotation.AnnotatedObject; 045import org.openimaj.ml.annotation.BatchAnnotator; 046import org.openimaj.ml.annotation.ScoredAnnotation; 047import org.openimaj.ml.annotation.utils.AnnotatedListHelper; 048import org.openimaj.ml.annotation.utils.LiblinearHelper; 049 050import de.bwaldvogel.liblinear.DenseLinear; 051import de.bwaldvogel.liblinear.DenseProblem; 052import de.bwaldvogel.liblinear.Feature; 053import de.bwaldvogel.liblinear.Linear; 054import de.bwaldvogel.liblinear.Model; 055import de.bwaldvogel.liblinear.Parameter; 056import de.bwaldvogel.liblinear.Problem; 057import de.bwaldvogel.liblinear.SolverType; 058 059/** 060 * Annotator based on linear classifiers learned using Liblinear (see 061 * {@link Linear}) or {@link DenseLinear} depending on the density of the 062 * features. Two modes of operation are available depending on whether the 063 * problem is multiclass or multilabel. Binary classification can be achieved 064 * with either mode, although multiclass mode is more efficient in this case. 065 * 066 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 067 * 068 * @param <OBJECT> 069 * Type of object being annotated 070 * @param <ANNOTATION> 071 * Type of annotation 072 */ 073@Reference( 074 type = ReferenceType.Article, 075 author = { "Fan, Rong-En", "Chang, Kai-Wei", "Hsieh, Cho-Jui", "Wang, Xiang-Rui", "Lin, Chih-Jen" }, 076 title = "LIBLINEAR: A Library for Large Linear Classification", 077 year = "2008", 078 journal = "J. Mach. Learn. Res.", 079 pages = { "1871", "", "1874" }, 080 url = "http://dl.acm.org/citation.cfm?id=1390681.1442794", 081 month = "june", 082 publisher = "JMLR.org", 083 volume = "9", 084 customData = { 085 "date", "6/1/2008", 086 "issn", "1532-4435", 087 "numpages", "4", 088 "acmid", "1442794" 089 }) 090public class LiblinearAnnotator<OBJECT, ANNOTATION> 091 extends 092 BatchAnnotator<OBJECT, ANNOTATION> 093{ 094 /** 095 * The classifier mode; either multiclass or multilabel. Multiclass mode 096 * will use liblinear's internal multiclass support, whereas multilabel mode 097 * will create a set of one-versus-all (OvA) classifiers for each class. 098 * 099 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 100 * 101 */ 102 public enum Mode { 103 /** 104 * Multiclass mode using liblinear's internal multiclass support 105 */ 106 MULTICLASS, 107 /** 108 * Multilabel mode, using an ensemble of one-versus-all binary 109 * classifiers (class/not-class) which are used to determine the labels 110 */ 111 MULTILABEL; 112 } 113 114 static abstract class InternalModel<OBJECT, ANNOTATION> { 115 ArrayList<ANNOTATION> annotationsList; 116 FeatureExtractor<? extends FeatureVector, OBJECT> extractor; 117 boolean dense; 118 double bias = -1; 119 boolean estimateProbabilities = true; 120 121 public abstract void train(List<? extends Annotated<OBJECT, ANNOTATION>> data); 122 123 public abstract void train(GroupedDataset<ANNOTATION, ? extends ListDataset<OBJECT>, OBJECT> dataset); 124 125 public abstract List<ScoredAnnotation<ANNOTATION>> annotate(OBJECT object); 126 127 Feature[] computeFeature(OBJECT object) { 128 final FeatureVector feature = extractor.extractFeature(object); 129 130 return LiblinearHelper.convert(feature, bias); 131 } 132 133 double[] computeFeatureDense(OBJECT object) { 134 final FeatureVector feature = extractor.extractFeature(object); 135 136 return LiblinearHelper.convertDense(feature, bias); 137 } 138 139 void computeProbabilities(double[] prob_estimates) { 140 if (!estimateProbabilities) 141 return; 142 143 final int nr_class = prob_estimates.length; 144 int nr_w; 145 if (nr_class == 2) 146 nr_w = 1; 147 else 148 nr_w = nr_class; 149 150 for (int i = 0; i < nr_w; i++) 151 prob_estimates[i] = 1 / (1 + Math.exp(-prob_estimates[i])); 152 153 if (nr_class == 2) // for binary classification 154 prob_estimates[1] = 1. - prob_estimates[0]; 155 else { 156 double sum = 0; 157 for (int i = 0; i < nr_class; i++) 158 sum += prob_estimates[i]; 159 160 for (int i = 0; i < nr_class; i++) 161 prob_estimates[i] = prob_estimates[i] / sum; 162 } 163 } 164 } 165 166 static class Multiclass<OBJECT, ANNOTATION> extends InternalModel<OBJECT, ANNOTATION> { 167 private Parameter parameter; 168 private Model model; 169 170 public Multiclass(SolverType solver, double C, double eps, double bias, boolean dense) { 171 parameter = new Parameter(solver, C, eps); 172 this.dense = dense; 173 this.bias = bias; 174 } 175 176 @Override 177 public void train(GroupedDataset<ANNOTATION, ? extends ListDataset<OBJECT>, OBJECT> dataset) { 178 annotationsList = new ArrayList<ANNOTATION>(dataset.getGroups()); 179 180 final int nItems = dataset.numInstances(); 181 final int featureLength = extractor.extractFeature(dataset.getRandomInstance()).length(); 182 183 if (dense) { 184 final DenseProblem problem = new DenseProblem(); 185 problem.l = nItems; 186 problem.n = featureLength + (bias >= 0 ? 1 : 0); 187 problem.bias = bias; 188 problem.x = new double[nItems][]; 189 problem.y = new double[nItems]; 190 191 int i = 0; 192 for (final ANNOTATION annotation : dataset.getGroups()) { 193 for (final OBJECT object : dataset.get(annotation)) { 194 problem.y[i] = annotationsList.indexOf(annotation) + 1; 195 problem.x[i] = computeFeatureDense(object); 196 i++; 197 } 198 } 199 200 model = DenseLinear.train(problem, parameter); 201 } else { 202 final Problem problem = new Problem(); 203 problem.l = nItems; 204 problem.n = featureLength + (bias >= 0 ? 1 : 0); 205 problem.bias = bias; 206 problem.x = new Feature[nItems][]; 207 problem.y = new double[nItems]; 208 209 int i = 0; 210 for (final ANNOTATION annotation : dataset.getGroups()) { 211 for (final OBJECT object : dataset.get(annotation)) { 212 problem.y[i] = annotationsList.indexOf(annotation) + 1; 213 problem.x[i] = computeFeature(object); 214 i++; 215 } 216 } 217 218 model = Linear.train(problem, parameter); 219 } 220 } 221 222 @Override 223 public void train(List<? extends Annotated<OBJECT, ANNOTATION>> data) { 224 final AnnotatedListHelper<OBJECT, ANNOTATION> helper = new AnnotatedListHelper<OBJECT, ANNOTATION>(data); 225 final Set<ANNOTATION> annotations = helper.getAnnotations(); 226 annotationsList = new ArrayList<ANNOTATION>(annotations); 227 228 final int nItems = data.size(); 229 final int featureLength = extractor.extractFeature(data.get(0).getObject()).length(); 230 231 if (dense) { 232 final DenseProblem problem = new DenseProblem(); 233 problem.l = nItems; 234 problem.n = featureLength + (bias >= 0 ? 1 : 0); 235 problem.bias = bias; 236 problem.x = new double[nItems][]; 237 problem.y = new double[nItems]; 238 239 for (int i = 0; i < nItems; i++) { 240 final Annotated<OBJECT, ANNOTATION> object = data.get(i); 241 242 if (object.getAnnotations().size() != 1) 243 throw new IllegalArgumentException( 244 "A multiclass problem cannot have more than one class per instance"); 245 246 final ANNOTATION annotation = object.getAnnotations().iterator().next(); 247 248 problem.y[i] = annotationsList.indexOf(annotation) + 1; 249 problem.x[i] = computeFeatureDense(object.getObject()); 250 } 251 252 model = DenseLinear.train(problem, parameter); 253 } else { 254 final Problem problem = new Problem(); 255 problem.l = nItems; 256 problem.n = featureLength + (bias >= 0 ? 1 : 0); 257 problem.bias = bias; 258 problem.x = new Feature[nItems][]; 259 problem.y = new double[nItems]; 260 261 for (int i = 0; i < nItems; i++) { 262 final Annotated<OBJECT, ANNOTATION> object = data.get(i); 263 264 if (object.getAnnotations().size() != 1) 265 throw new IllegalArgumentException( 266 "A multiclass problem cannot have more than one class per instance"); 267 268 final ANNOTATION annotation = object.getAnnotations().iterator().next(); 269 270 problem.y[i] = annotationsList.indexOf(annotation) + 1; 271 problem.x[i] = computeFeature(object.getObject()); 272 } 273 274 model = Linear.train(problem, parameter); 275 } 276 } 277 278 @Override 279 public List<ScoredAnnotation<ANNOTATION>> annotate(OBJECT object) { 280 final double clz; 281 final double prob; 282 283 if (dense) { 284 final double[] feature = computeFeatureDense(object); 285 286 if (parameter.getSolverType().isLogisticRegressionSolver()) { 287 final double[] probs = new double[annotationsList.size()]; 288 clz = DenseLinear.predictProbability(model, feature, probs) - 1; 289 prob = probs[(int) clz]; 290 } else { 291 // clz = DenseLinear.predict(model, feature) - 1; 292 final double[] prob_estimates = new double[annotationsList.size()]; 293 clz = DenseLinear.predictValues(model, feature, prob_estimates) - 1; 294 computeProbabilities(prob_estimates); 295 prob = prob_estimates[(int) clz]; 296 } 297 } else { 298 final Feature[] feature = computeFeature(object); 299 300 if (parameter.getSolverType().isLogisticRegressionSolver()) { 301 final double[] probs = new double[annotationsList.size()]; 302 clz = Linear.predictProbability(model, feature, probs) - 1; 303 prob = probs[(int) clz]; 304 } else { 305 // clz = Linear.predict(model, feature) - 1; 306 final double[] prob_estimates = new double[annotationsList.size()]; 307 clz = Linear.predictValues(model, feature, prob_estimates) - 1; 308 computeProbabilities(prob_estimates); 309 prob = prob_estimates[(int) clz]; 310 } 311 } 312 313 final List<ScoredAnnotation<ANNOTATION>> result = new ArrayList<ScoredAnnotation<ANNOTATION>>(1); 314 result.add(new ScoredAnnotation<ANNOTATION>(annotationsList.get((int) clz), (float) prob)); 315 return result; 316 } 317 } 318 319 /** 320 * Multi-label classifier built from multiple binary classifiers. 321 * 322 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 323 * 324 */ 325 static class Multilabel<OBJECT, ANNOTATION> extends InternalModel<OBJECT, ANNOTATION> { 326 private Parameter parameter; 327 private Model[] models; 328 329 private static final int NEGATIVE_CLASS = 1; 330 private static final int POSTIVE_CLASS = 2; 331 332 public Multilabel(SolverType solver, double C, double eps, double bias, boolean dense) { 333 parameter = new Parameter(solver, C, eps); 334 this.dense = dense; 335 this.bias = bias; 336 } 337 338 @Override 339 public void train(List<? extends Annotated<OBJECT, ANNOTATION>> data) { 340 final AnnotatedListHelper<OBJECT, ANNOTATION> helper = new AnnotatedListHelper<OBJECT, ANNOTATION>(data); 341 final Set<ANNOTATION> annotations = helper.getAnnotations(); 342 annotationsList = new ArrayList<ANNOTATION>(annotations); 343 344 final int featureLength = extractor.extractFeature(data.get(0).getObject()).length(); 345 346 models = new Model[annotationsList.size()]; 347 348 for (int i = 0; i < annotationsList.size(); i++) { 349 final ANNOTATION annotation = annotationsList.get(i); 350 final List<? extends FeatureVector> positive = helper.extractFeatures(annotation, extractor); 351 final List<? extends FeatureVector> negative = helper.extractFeaturesExclude(annotation, extractor); 352 353 if (dense) { 354 final DenseProblem problem = new DenseProblem(); 355 problem.l = positive.size() + negative.size(); 356 problem.n = featureLength + (bias >= 0 ? 1 : 0); 357 problem.bias = bias; 358 problem.x = new double[problem.l][]; 359 problem.y = new double[problem.l]; 360 361 for (int j = 0; j < negative.size(); j++) { 362 problem.x[j] = LiblinearHelper.convertDense(negative.get(j), bias); 363 problem.y[j] = NEGATIVE_CLASS; 364 } 365 366 for (int j = negative.size(), k = 0; k < positive.size(); j++, k++) { 367 problem.x[j] = LiblinearHelper.convertDense(positive.get(k), bias); 368 problem.y[j] = POSTIVE_CLASS; 369 } 370 371 models[i] = DenseLinear.train(problem, parameter); 372 } else { 373 final Problem problem = new Problem(); 374 problem.l = positive.size() + negative.size(); 375 problem.n = featureLength + (bias >= 0 ? 1 : 0); 376 problem.bias = bias; 377 problem.x = new Feature[problem.l][]; 378 problem.y = new double[problem.l]; 379 380 for (int j = 0; j < negative.size(); j++) { 381 problem.x[j] = LiblinearHelper.convert(negative.get(j), bias); 382 problem.y[j] = NEGATIVE_CLASS; 383 } 384 385 for (int j = negative.size(), k = 0; k < positive.size(); j++, k++) { 386 problem.x[j] = LiblinearHelper.convert(positive.get(k), bias); 387 problem.y[j] = POSTIVE_CLASS; 388 } 389 390 models[i] = Linear.train(problem, parameter); 391 } 392 } 393 } 394 395 @Override 396 public List<ScoredAnnotation<ANNOTATION>> annotate(OBJECT object) { 397 final List<ScoredAnnotation<ANNOTATION>> result = new ArrayList<ScoredAnnotation<ANNOTATION>>(); 398 399 if (dense) { 400 final double[] feature = computeFeatureDense(object); 401 402 for (int i = 0; i < annotationsList.size(); i++) { 403 final double clz; 404 final double prob; 405 if (parameter.getSolverType().isLogisticRegressionSolver()) { 406 final double[] probs = new double[annotationsList.size()]; 407 clz = DenseLinear.predictProbability(models[i], feature, probs); 408 prob = probs[(int) clz - 1]; 409 } else { 410 final double[] prob_estimates = new double[2]; 411 clz = DenseLinear.predictValues(models[i], feature, prob_estimates); 412 computeProbabilities(prob_estimates); 413 prob = prob_estimates[(int) clz - 1]; 414 } 415 416 if (clz == POSTIVE_CLASS) { 417 result.add(new ScoredAnnotation<ANNOTATION>(annotationsList.get(i), (float) prob)); 418 } 419 } 420 } else { 421 final Feature[] feature = computeFeature(object); 422 423 for (int i = 0; i < annotationsList.size(); i++) { 424 final double clz; 425 final double prob; 426 if (parameter.getSolverType().isLogisticRegressionSolver()) { 427 final double[] probs = new double[annotationsList.size()]; 428 clz = Linear.predictProbability(models[i], feature, probs); 429 prob = probs[(int) clz - 1]; 430 } else { 431 final double[] prob_estimates = new double[2]; 432 clz = Linear.predictValues(models[i], feature, prob_estimates); 433 computeProbabilities(prob_estimates); 434 prob = prob_estimates[(int) clz - 1]; 435 } 436 437 if (clz == POSTIVE_CLASS) { 438 result.add(new ScoredAnnotation<ANNOTATION>(annotationsList.get(i), (float) prob)); 439 } 440 } 441 } 442 443 return result; 444 } 445 446 @Override 447 public void train(GroupedDataset<ANNOTATION, ? extends ListDataset<OBJECT>, OBJECT> dataset) { 448 train(AnnotatedObject.createList(dataset)); 449 } 450 } 451 452 InternalModel<OBJECT, ANNOTATION> internal; 453 454 /** 455 * Default constructor. Assumes sparse features. 456 * 457 * @param extractor 458 * the feature extractor 459 * @param mode 460 * the mode 461 * @param solver 462 * the liblinear solver 463 * @param C 464 * the C parameter (usually 1 or larger) 465 * @param eps 466 * the epsilon value 467 */ 468 public LiblinearAnnotator(FeatureExtractor<? extends FeatureVector, OBJECT> extractor, Mode mode, SolverType solver, 469 double C, double eps) 470 { 471 this(extractor, mode, solver, C, eps, -1, false); 472 } 473 474 /** 475 * Default constructor. 476 * 477 * @param extractor 478 * the feature extractor 479 * @param mode 480 * the mode 481 * @param solver 482 * the liblinear solver 483 * @param C 484 * the C parameter (usually 1 or larger) 485 * @param eps 486 * the epsilon value 487 * @param bias 488 * the bias 489 * @param dense 490 * are the features dense? If so the dense variant of liblinear 491 * will be used to drastically reduce memory usage 492 */ 493 public LiblinearAnnotator(FeatureExtractor<? extends FeatureVector, OBJECT> extractor, Mode mode, SolverType solver, 494 double C, double eps, double bias, boolean dense) 495 { 496 switch (mode) { 497 case MULTICLASS: 498 this.internal = new Multiclass<OBJECT, ANNOTATION>(solver, C, eps, bias, dense); 499 break; 500 case MULTILABEL: 501 this.internal = new Multilabel<OBJECT, ANNOTATION>(solver, C, eps, bias, dense); 502 break; 503 default: 504 throw new RuntimeException("Unhandled mode"); 505 } 506 507 this.internal.extractor = extractor; 508 } 509 510 @Override 511 public void train(List<? extends Annotated<OBJECT, ANNOTATION>> data) { 512 internal.train(data); 513 } 514 515 @Override 516 public Set<ANNOTATION> getAnnotations() { 517 return new HashSet<ANNOTATION>(internal.annotationsList); 518 } 519 520 @Override 521 public List<ScoredAnnotation<ANNOTATION>> annotate(OBJECT object) { 522 return internal.annotate(object); 523 } 524 525 @Override 526 public void train(GroupedDataset<ANNOTATION, ? extends ListDataset<OBJECT>, OBJECT> dataset) { 527 internal.train(dataset); 528 } 529}