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}