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}