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.kernel;
031
032import org.openimaj.citation.annotation.Reference;
033import org.openimaj.citation.annotation.ReferenceType;
034import org.openimaj.citation.annotation.References;
035import org.openimaj.feature.DoubleFV;
036import org.openimaj.feature.FeatureExtractor;
037import org.openimaj.feature.FeatureVector;
038import org.openimaj.math.util.MathUtils;
039import org.openimaj.math.util.MathUtils.ExponentAndMantissa;
040
041/**
042 * Implementation of the Homogeneous Kernel Map. The Homogeneous Kernel Map
043 * transforms data into a compact linear representation such that applying a
044 * linear SVM can approximate to a high degree of accuracy the application of a
045 * non-linear SVM to the original data. Additive kernels including Chi2,
046 * intersection, and Jensen-Shannon are supported.
047 * <p>
048 * This implementation is based directly on the VLFeat implementation written by
049 * Andrea Verdaldi, although it has been refactored to better fit with Java
050 * conventions.
051 * 
052 * @see "http://www.vlfeat.org/api/homkermap.html"
053 * @see "http://www.robots.ox.ac.uk/~vgg/software/homkermap/"
054 * 
055 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
056 * @author Based on code originally written by Andrea Verdaldi
057 */
058@References(
059                references = {
060                                @Reference(
061                                                type = ReferenceType.Article,
062                                                author = { "Vedaldi, A.", "Zisserman, A." },
063                                                title = "Efficient Additive Kernels via Explicit Feature Maps",
064                                                year = "2012",
065                                                journal = "Pattern Analysis and Machine Intelligence, IEEE Transactions on",
066                                                pages = { "480", "492" },
067                                                number = "3",
068                                                volume = "34",
069                                                customData = {
070                                                                "keywords", "approximation theory;computer vision;data handling;feature extraction;learning (artificial intelligence);spectral analysis;support vector machines;Nystrom approximation;additive homogeneous kernels;approximate finite-dimensional feature maps;approximation error;computer vision;data dependency;explicit feature maps;exponential decay;large scale nonlinear support vector machines;linear SVM;spectral analysis;Additives;Approximation methods;Histograms;Kernel;Measurement;Support vector machines;Training;Kernel methods;feature map;large scale learning;object detection.;object recognition",
071                                                                "doi", "10.1109/TPAMI.2011.153",
072                                                                "ISSN", "0162-8828"
073                                                }
074                                ),
075                                @Reference(
076                                                type = ReferenceType.Inproceedings,
077                                                author = { "A. Vedaldi", "A. Zisserman" },
078                                                title = "Efficient Additive Kernels via Explicit Feature Maps",
079                                                year = "2010",
080                                                booktitle = "Proceedings of the IEEE Conf. on Computer Vision and Pattern Recognition (CVPR)"
081                                )
082                })
083public class HomogeneousKernelMap {
084        /**
085         * Types of supported kernel for the {@link HomogeneousKernelMap}
086         * 
087         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
088         * 
089         */
090        public enum KernelType {
091                /**
092                 * Intersection kernel
093                 * 
094                 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
095                 * 
096                 */
097                Intersection {
098                        @Override
099                        protected double getSpectrum(double omega) {
100                                return (2.0 / Math.PI) / (1 + 4 * omega * omega);
101                        }
102                },
103                /**
104                 * Chi^2 kernel
105                 * 
106                 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
107                 * 
108                 */
109                Chi2 {
110                        @Override
111                        protected double getSpectrum(double omega) {
112                                return 2.0 / (Math.exp(Math.PI * omega) + Math.exp(-Math.PI * omega));
113                        }
114                },
115                /**
116                 * Jenson-Shannon Kernel
117                 * 
118                 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
119                 * 
120                 */
121                JensonShannon {
122                        @Override
123                        protected double getSpectrum(double omega) {
124                                return (2.0 / Math.log(4.0)) * 2.0 / (Math.exp(Math.PI * omega) + Math.exp(-Math.PI * omega))
125                                                / (1 + 4 * omega * omega);
126                        }
127                };
128
129                protected abstract double getSpectrum(double omega);
130        }
131
132        /**
133         * Types of window supported by the {@link HomogeneousKernelMap}.
134         * 
135         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
136         * 
137         */
138        public enum WindowType {
139                /**
140                 * Uniform window
141                 * 
142                 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
143                 * 
144                 */
145                Uniform {
146                        @Override
147                        double computeKappaHat(KernelType type, double omega, HomogeneousKernelMap map) {
148                                return type.getSpectrum(omega);
149                        }
150                },
151                /**
152                 * Rectangular window
153                 * 
154                 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
155                 * 
156                 */
157                Rectangular {
158                        @Override
159                        double computeKappaHat(KernelType type, double omega, HomogeneousKernelMap map) {
160                                double kappa_hat = 0;
161                                final double epsilon = 1e-2;
162                                final double omegaRange = 2.0 / (map.period * epsilon);
163                                final double domega = 2 * omegaRange / (2 * 1024.0 + 1);
164
165                                for (double omegap = -omegaRange; omegap <= omegaRange; omegap += domega) {
166                                        double win = sinc((map.period / 2.0) * omegap);
167                                        win *= (map.period / (2.0 * Math.PI));
168                                        kappa_hat += win * type.getSpectrum(omegap + omega);
169                                }
170
171                                kappa_hat *= domega;
172
173                                // project on the positive orthant (see PAMI)
174                                kappa_hat = Math.max(kappa_hat, 0.0);
175
176                                return kappa_hat;
177                        }
178
179                        private double sinc(double x)
180                        {
181                                if (x == 0.0)
182                                        return 1.0;
183                                return Math.sin(x) / x;
184                        }
185                };
186
187                abstract double computeKappaHat(KernelType type, double omega, HomogeneousKernelMap map);
188
189                protected double getSmoothSpectrum(double omega, HomogeneousKernelMap map) {
190                        return computeKappaHat(map.kernelType, omega, map);
191                }
192        }
193
194        /**
195         * Helper implementation of a {@link FeatureExtractor} that wraps another
196         * {@link FeatureExtractor} and then applies the
197         * {@link HomogeneousKernelMap} to the output before returning the vector.
198         * 
199         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
200         * 
201         * @param <T>
202         *            Type of object that features can be extracted from
203         */
204        public static class ExtractorWrapper<T> implements FeatureExtractor<DoubleFV, T> {
205                private FeatureExtractor<? extends FeatureVector, T> inner;
206                private HomogeneousKernelMap map;
207
208                /**
209                 * Construct with the given internal extractor and homogeneous kernel
210                 * map.
211                 * 
212                 * @param inner
213                 *            the internal extractor
214                 * @param map
215                 *            the homogeneous kernel map
216                 */
217                public ExtractorWrapper(FeatureExtractor<? extends FeatureVector, T> inner, HomogeneousKernelMap map) {
218                        this.inner = inner;
219                        this.map = map;
220                }
221
222                @Override
223                public DoubleFV extractFeature(T object) {
224                        return map.evaluate(inner.extractFeature(object).asDoubleFV());
225                }
226        }
227
228        private KernelType kernelType;
229        private double period;
230        private double gamma;
231        private int order;
232        private int numSubdivisions;
233        private double subdivision;
234        private int minExponent;
235        private int maxExponent;
236        private double[] table;
237
238        /**
239         * Construct with the given kernel and window. The Gamma and order values
240         * are set at their defaults of 1. The period is computed automatically.
241         * 
242         * @param kernelType
243         *            the type of kernel
244         * @param windowType
245         *            the type of window (use {@link WindowType#Rectangular} if
246         *            unsure)
247         */
248        public HomogeneousKernelMap(KernelType kernelType, WindowType windowType) {
249                this(kernelType, 1, 1, -1, windowType);
250        }
251
252        /**
253         * Construct with the given kernel, gamma and window. The period is computed
254         * automatically and the approximation order is set to 1.
255         * 
256         * @param kernelType
257         *            the type of kernel
258         * @param gamma
259         *            the gamma value. the standard kernels are 1-homogeneous, but
260         *            smaller values can work better in practice.
261         * @param windowType
262         *            the type of window (use {@link WindowType#Rectangular} if
263         *            unsure)
264         */
265        public HomogeneousKernelMap(KernelType kernelType,
266                        double gamma,
267                        WindowType windowType)
268        {
269                this(kernelType, gamma, 1, -1, windowType);
270        }
271
272        /**
273         * Construct with the given kernel, gamma, order and window. The period is
274         * computed automatically.
275         * 
276         * @param kernelType
277         *            the type of kernel
278         * @param gamma
279         *            the gamma value. the standard kernels are 1-homogeneous, but
280         *            smaller values can work better in practice.
281         * @param order
282         *            the approximation order (usually 1 is enough)
283         * @param windowType
284         *            the type of window (use {@link WindowType#Rectangular} if
285         *            unsure)
286         */
287        public HomogeneousKernelMap(KernelType kernelType,
288                        double gamma,
289                        int order,
290                        WindowType windowType)
291        {
292                this(kernelType, gamma, order, -1, windowType);
293        }
294
295        /**
296         * Construct with the given kernel, gamma, order, period and window. If the
297         * period is negative, it will be replaced by the default.
298         * 
299         * @param kernelType
300         *            the type of kernel
301         * @param gamma
302         *            the gamma value. the standard kernels are 1-homogeneous, but
303         *            smaller values can work better in practice.
304         * @param order
305         *            the approximation order (usually 1 is enough)
306         * @param period
307         *            the periodicity of the kernel spectrum
308         * @param windowType
309         *            the type of window (use {@link WindowType#Rectangular} if
310         *            unsure)
311         */
312        public HomogeneousKernelMap(KernelType kernelType,
313                        double gamma,
314                        int order,
315                        double period,
316                        WindowType windowType)
317        {
318                if (gamma <= 0)
319                        throw new IllegalArgumentException("Gamma must be > 0");
320                final int tableWidth, tableHeight;
321
322                if (period < 0) {
323                        period = computeDefaultPeriod(windowType, kernelType);
324                }
325
326                this.kernelType = kernelType;
327                this.gamma = gamma;
328                this.order = order;
329                this.period = period;
330                this.numSubdivisions = 8 + 8 * order;
331                this.subdivision = 1.0 / this.numSubdivisions;
332                this.minExponent = -20;
333                this.maxExponent = 8;
334
335                tableHeight = 2 * this.order + 1;
336                tableWidth = this.numSubdivisions * (this.maxExponent - this.minExponent + 1);
337                this.table = new double[tableHeight * tableWidth + 2 * (1 + this.order)];
338
339                int tableOffset = 0;
340                final int kappaOffset = tableHeight * tableWidth;
341                final int freqOffset = kappaOffset + 1 + order;
342                final double L = 2.0 * Math.PI / this.period;
343
344                /* precompute the sampled periodicized spectrum */
345                int j = 0;
346                int i = 0;
347                while (i <= this.order) {
348                        table[freqOffset + i] = j;
349                        table[kappaOffset + i] = windowType.getSmoothSpectrum(j * L, this);
350                        j++;
351                        if (table[kappaOffset + i] > 0 || j >= 3 * i)
352                                i++;
353                }
354
355                /* fill table */
356                for (int exponent = minExponent; exponent <= maxExponent; exponent++) {
357                        double x, Lxgamma, Llogx, xgamma;
358                        double sqrt2kappaLxgamma;
359                        double mantissa = 1.0;
360
361                        for (i = 0; i < numSubdivisions; i++, mantissa += subdivision) {
362                                x = mantissa * Math.pow(2, exponent);
363                                xgamma = Math.pow(x, this.gamma);
364                                Lxgamma = L * xgamma;
365                                Llogx = L * Math.log(x);
366
367                                table[tableOffset++] = Math.sqrt(Lxgamma * table[kappaOffset]);
368                                for (j = 1; j <= this.order; ++j) {
369                                        sqrt2kappaLxgamma = Math.sqrt(2.0 * Lxgamma * table[kappaOffset + j]);
370                                        table[tableOffset++] = sqrt2kappaLxgamma * Math.cos(table[freqOffset + j] * Llogx);
371                                        table[tableOffset++] = sqrt2kappaLxgamma * Math.sin(table[freqOffset + j] * Llogx);
372                                }
373                        }
374                }
375        }
376
377        private double computeDefaultPeriod(WindowType windowType, KernelType kernelType) {
378                double period = 0;
379
380                // compute default period
381                switch (windowType) {
382                case Uniform:
383                        switch (kernelType) {
384                        case Chi2:
385                                period = 5.86 * Math.sqrt(order + 0) + 3.65;
386                                break;
387                        case JensonShannon:
388                                period = 6.64 * Math.sqrt(order + 0) + 7.24;
389                                break;
390                        case Intersection:
391                                period = 2.38 * Math.log(order + 0.8) + 5.6;
392                                break;
393                        }
394                        break;
395                case Rectangular:
396                        switch (kernelType) {
397                        case Chi2:
398                                period = 8.80 * Math.sqrt(order + 4.44) - 12.6;
399                                break;
400                        case JensonShannon:
401                                period = 9.63 * Math.sqrt(order + 1.00) - 2.93;
402                                break;
403                        case Intersection:
404                                period = 2.00 * Math.log(order + 0.99) + 3.52;
405                                break;
406                        }
407                        break;
408                }
409                return Math.max(period, 1.0);
410        }
411
412        /**
413         * Evaluate the kernel for the given <code>x</code> value. The output values
414         * will be written into the destination array at
415         * <code>offset + j*stride</code> intervals where <code>j</code> is between
416         * 0 and <code>2 * order + 1</code>.
417         * 
418         * @param destination
419         *            the destination array
420         * @param stride
421         *            the stride
422         * @param offset
423         *            the offset
424         * @param x
425         *            the value to compute the kernel approximation for
426         */
427        public void evaluate(double[] destination, int stride, int offset, double x) {
428                final ExponentAndMantissa em = MathUtils.frexp(x);
429
430                double mantissa = em.mantissa;
431                int exponent = em.exponent;
432                final double sign = (mantissa >= 0.0) ? +1.0 : -1.0;
433                mantissa *= 2 * sign;
434                exponent--;
435
436                if (mantissa == 0 || exponent <= minExponent || exponent >= maxExponent) {
437                        for (int j = 0; j <= order; j++) {
438                                destination[offset + j * stride] = 0.0;
439                        }
440                        return;
441                }
442
443                final int featureDimension = 2 * order + 1;
444                int v1offset = (exponent - minExponent) * numSubdivisions * featureDimension;
445
446                mantissa -= 1.0;
447                while (mantissa >= subdivision) {
448                        mantissa -= subdivision;
449                        v1offset += featureDimension;
450                }
451
452                int v2offset = v1offset + featureDimension;
453                for (int j = 0; j < featureDimension; j++) {
454                        final double f1 = table[v1offset++];
455                        final double f2 = table[v2offset++];
456
457                        destination[offset + j * stride] = sign * ((f2 - f1) * (numSubdivisions * mantissa) + f1);
458                }
459        }
460
461        /**
462         * Compute the Homogeneous Kernel Map approximation of the given feature
463         * vector
464         * 
465         * @param in
466         *            the feature vector
467         * @return the expanded feature vector
468         */
469        public DoubleFV evaluate(DoubleFV in) {
470                final int step = (2 * order + 1);
471                final DoubleFV out = new DoubleFV(step * in.length());
472
473                for (int i = 0; i < in.length(); i++) {
474                        evaluate(out.values, 1, i * step, in.values[i]);
475                }
476
477                return out;
478        }
479
480        /**
481         * Construct a new {@link ExtractorWrapper} that applies the map to features
482         * extracted by an internal extractor.
483         * 
484         * @param inner
485         *            the internal extractor
486         * @return the wrapped {@link FeatureExtractor}
487         * @param <T>
488         *            Type of object that features can be extracted from
489         */
490        public <T> FeatureExtractor<DoubleFV, T> createWrappedExtractor(FeatureExtractor<? extends FeatureVector, T> inner) {
491                return new ExtractorWrapper<T>(inner, this);
492        }
493}