/** ** ============================== ** O O O OOOO ** O O O O O O ** O O O O O O ** OOOO OOOO O OOO OOOO ** O O O O O O O ** O O O O O O O ** OOOO OOOO O O OOOO ** ============================== ** Dr. Stefan Bosse http://www.bsslab.de ** ** COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED ** BY THE AUTHOR(S). ** THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED, ** MODIFIED, OR OTHERWISE USED IN A CONTEXT ** OUTSIDE OF THE SOFTWARE SYSTEM. ** ** $AUTHORS: joonkukang, Stefan Bosse ** $INITIAL: (C) 2014, joonkukang ** $MODIFIED: (C) 2006-2018 bLAB by sbosse ** $VERSION: 1.1.3 ** ** $INFO: ** ** Support Vector Machine Algrotihm ** ** 1. References : http://cs229.stanford.edu/materials/smo.pdf . simplified smo algorithm ** 2. https://github.com/karpathy/svmjs ** ** Portable model ** ** $ENDOFINFO */ var math = Require('ml/math'); /** * type options = {x: number [] [], y: number []} */ var SVM = function (options) { var L = {}; L.x = options.x; L.y = options.y; return L }; SVM.code = { train : function (L,options) { var self = L; var C = options.C || 1.0; var tol = options.tol || 1e-4; var maxPasses = options.max_passes || 20; var alphatol = options.alpha_tol || 1e-5; L.options={kernel:options.kernel,iterations:maxPasses,alpha_tol:alphatol, C:C, tol:tol }; self.kernel = getKernel(options.kernel); self.alphas = math.zeroVec(self.x.length); self.b = 0; var passes = 0, i; var count=0; while(passes < maxPasses) { var numChangedAlphas = 0; for(i=0; i tol && self.alphas[i] >0)) { // Randomly selects j (i != j) var j = math.randInt(0,self.x.length-1); if(i==j) j = (j+1) % self.x.length; var E_j = SVM.code.f(self,self.x[j]) - self.y[j]; var alpha_i_old = self.alphas[i], alpha_j_old = self.alphas[j]; // Compute L,H var L,H; if(self.y[i] !== self.y[j]) { L = Math.max(0, self.alphas[j] - self.alphas[i]); H = Math.min(C, C + self.alphas[j] - self.alphas[i]); } else { L = Math.max(0, self.alphas[j] + self.alphas[i] - C); H = Math.min(C, self.alphas[j] + self.alphas[i]); } if(L === H) continue; // Compute ETA var ETA = 2 * self.kernel(self.x[i],self.x[j]) - self.kernel(self.x[i],self.x[i]) - self.kernel(self.x[j],self.x[j]); if(ETA >= 0) continue; // Clip new value to alpha_j self.alphas[j] -= 1.*self.y[j] * (E_i - E_j) / ETA; if(self.alphas[j] > H) self.alphas[j] = H; else if(self.alphas[j] < L) self.alphas[j] = L; if(Math.abs(self.alphas[j] - alpha_j_old) < alphatol) continue; // Clip new value to alpha_i self.alphas[i] += self.y[i] * self.y[j] * (alpha_j_old - self.alphas[j]); // update b var b1 = self.b - E_i - self.y[i] * (self.alphas[i] - alpha_i_old) * self.kernel(self.x[i],self.x[i]) - self.y[j] * (self.alphas[j] - alpha_j_old) * self.kernel(self.x[i],self.x[j]); var b2 = self.b - E_j - self.y[i] * (self.alphas[i] - alpha_i_old) * self.kernel(self.x[i],self.x[j]) - self.y[j] * (self.alphas[j] - alpha_j_old) * self.kernel(self.x[j],self.x[j]); if(0 < self.alphas[i] && self.alphas[i] < C) self.b = b1; else if(0 < self.alphas[j] && self.alphas[j] < C) self.b = b2; else self.b = (b1+b2)/2.0; numChangedAlphas ++ ; } // end-if } // end-for if(numChangedAlphas == 0) passes++; else passes = 0; } }, predict : function(L,x) { var self = L; this.kernel = getKernel(L.options.kernel); // update kernel if(SVM.code.f(L,x) >= 0) return 1; else return -1; }, f : function(L,x) { var self = L; var f = 0, j; for(j=0; j tol && L.alpha[i] > 0) ){ // alpha_i needs updating! Pick a j to update it with var j = i; while(j === i) j= randi(0, L.N); var Ej= SVM2.code.marginOne(L, data[j]) - labels[j]; // calculate L and H bounds for j to ensure we're in [0 C]x[0 C] box ai= L.alpha[i]; aj= L.alpha[j]; var Lb = 0; var Hb = C; if(labels[i] === labels[j]) { Lb = Math.max(0, ai+aj-C); Hb = Math.min(C, ai+aj); } else { Lb = Math.max(0, aj-ai); Hb = Math.min(C, C+aj-ai); } if(Math.abs(Lb - Hb) < 1e-4) continue; var eta = 2*SVM2.code.kernelResult(L, i,j) - SVM2.code.kernelResult(L, i,i) - SVM2.code.kernelResult(L, 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>Hb) newaj = Hb; if(newaj 0 && newai < C) L.b= b1; if(newaj > 0 && newaj < C) L.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(L.kernelType === "linear") { // compute weights and store them L.w = new Array(L.D); for(var j=0;j alphatol) { newdata.push(L.data[i]); newlabels.push(L.labels[i]); newalpha.push(L.alpha[i]); } } // store data and labels L.data = newdata; L.labels = newlabels; L.alpha = newalpha; L.N = L.data.length; // console.log("filtered training data from %d to %d support vectors.", data.length, L.data.length); } var trainstats = {}; trainstats.iters= iter; trainstats.passes= passes; 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(L,inst) { var f = L.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(L.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 L.threshold ? 1 : -1; }, // data is an NxD array. Returns array of margins. margins: function(L,data) { // go over support vectors and accumulate the prediction. var N = data.length; var margins = new Array(N); for(var i=0;i L.threshold ? 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(L) { // DEPRECATED var w= new Array(L.D); for(var j=0;j