// MIT License // Andrej Karpathy var svmjs = (function(exports){ /* This is a binary SVM and is trained using the SMO algorithm. Reference: "The Simplified SMO Algorithm" (http://math.unt.edu/~hsp0009/smo.pdf) Simple usage example: svm = svmjs.SVM(); svm.train(data, labels); testlabels = svm.predict(testdata); */ var SVM = function(options) { } SVM.prototype = { // data is NxD array of floats. labels are 1 or -1. train: function(data, labels, options) { // we need these in helper functions this.data = data; this.labels = labels; // parameters options = options || {}; var C = options.C || 1.0; // C value. Decrease for more regularization var tol = options.tol || 1e-4; // numerical tolerance. Don't touch unless you're pro var alphatol = options.alphatol || 1e-7; // non-support vectors for space and time efficiency are truncated. To guarantee correct result set this to 0 to do no truncating. If you want to increase efficiency, experiment with setting this little higher, up to maybe 1e-4 or so. var maxiter = options.maxiter || 10000; // max number of iterations var numpasses = options.numpasses || 10; // how many passes over data with no change before we halt? Increase for more precision. // instantiate kernel according to options. kernel can be given as string or as a custom function var kernel = linearKernel; this.kernelType = "linear"; if("kernel" in options) { if(typeof options.kernel === "string") { // kernel was specified as a string. Handle these special cases appropriately if(options.kernel === "linear") { this.kernelType = "linear"; kernel = linearKernel; } if(options.kernel === "rbf") { var rbfSigma = options.rbfsigma || 0.5; this.rbfSigma = rbfSigma; // back this up this.kernelType = "rbf"; kernel = makeRbfKernel(rbfSigma); } } else { // assume kernel was specified as a function. Let's just use it this.kernelType = "custom"; kernel = options.kernel; } } // initializations this.kernel = kernel; this.N = data.length; var N = this.N; this.D = data[0].length; var D = this.D; this.alpha = zeros(N); this.b = 0.0; this.usew_ = false; // internal efficiency flag // Cache kernel computations to avoid expensive recomputation. // This could use too much memory if N is large. if (options.memoize) { this.kernelResults = new Array(N); for (var i=0;i tol && this.alpha[i] > 0) ){ // alpha_i needs updating! Pick a j to update it with var j = i; while(j === i) j= randi(0, this.N); var Ej= this.marginOne(data[j]) - labels[j]; // calculate L and H bounds for j to ensure we're in [0 C]x[0 C] box ai= this.alpha[i]; aj= this.alpha[j]; var L = 0; var H = C; if(labels[i] === labels[j]) { L = Math.max(0, ai+aj-C); H = Math.min(C, ai+aj); } else { L = Math.max(0, aj-ai); H = Math.min(C, C+aj-ai); } if(Math.abs(L - H) < 1e-4) continue; var eta = 2*this.kernelResult(i,j) - this.kernelResult(i,i) - this.kernelResult(j,j); if(eta >= 0) continue; // compute new alpha_j and clip it inside [0 C]x[0 C] box // then compute alpha_i based on it. var newaj = aj - labels[j]*(Ei-Ej) / eta; if(newaj>H) newaj = H; if(newaj 0 && newai < C) this.b= b1; if(newaj > 0 && newaj < C) this.b= b2; alphaChanged++; } // end alpha_i needed updating } // end for i=1..N iter++; //console.log("iter number %d, alphaChanged = %d", iter, alphaChanged); if(alphaChanged == 0) passes++; else passes= 0; } // end outer loop // if the user was using a linear kernel, lets also compute and store the // weights. This will speed up evaluations during testing time if(this.kernelType === "linear") { // compute weights and store them this.w = new Array(this.D); for(var j=0;j alphatol) { newdata.push(this.data[i]); newlabels.push(this.labels[i]); newalpha.push(this.alpha[i]); } } // store data and labels this.data = newdata; this.labels = newlabels; this.alpha = newalpha; this.N = this.data.length; //console.log("filtered training data from %d to %d support vectors.", data.length, this.data.length); } var trainstats = {}; trainstats.iters= iter; return trainstats; }, // inst is an array of length D. Returns margin of given example // this is the core prediction function. All others are for convenience mostly // and end up calling this one somehow. marginOne: function(inst) { var f = this.b; // if the linear kernel was used and w was computed and stored, // (i.e. the svm has fully finished training) // the internal class variable usew_ will be set to true. if(this.usew_) { // we can speed this up a lot by using the computed weights // we computed these during train(). This is significantly faster // than the version below for(var j=0;j 0 ? 1 : -1; }, // data is an NxD array. Returns array of margins. margins: function(data) { // go over support vectors and accumulate the prediction. var N = data.length; var margins = new Array(N); for(var i=0;i 0 ? 1 : -1; } return margs; }, // THIS FUNCTION IS NOW DEPRECATED. WORKS FINE BUT NO NEED TO USE ANYMORE. // LEAVING IT HERE JUST FOR BACKWARDS COMPATIBILITY FOR A WHILE. // if we trained a linear svm, it is possible to calculate just the weights and the offset // prediction is then yhat = sign(X * w + b) getWeights: function() { // DEPRECATED var w= new Array(this.D); for(var j=0;j