From c5e1c4035856e96f648e53f207da5ea7bc6c5aab Mon Sep 17 00:00:00 2001 From: sbosse Date: Mon, 21 Jul 2025 23:07:32 +0200 Subject: [PATCH] Mon 21 Jul 22:43:21 CEST 2025 --- js/ml/svm.js | 534 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 534 insertions(+) create mode 100644 js/ml/svm.js diff --git a/js/ml/svm.js b/js/ml/svm.js new file mode 100644 index 0000000..768f958 --- /dev/null +++ b/js/ml/svm.js @@ -0,0 +1,534 @@ +/** + ** ============================== + ** 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