Feature Selection Toolbox FST4
UTIA · Czech Academy of Sciences
Loading...
Searching...
No Matches
classifier_logreg.hpp
Go to the documentation of this file.
1#ifndef FSTCLASSIFIERLOGREG_H
2#define FSTCLASSIFIERLOGREG_H
3 /*
27=========================================================================
28Copyright and license:
29 * FST4 software (with the exception of any bundled or externally linked
30 third-party components) is Copyright (C) 2000-present Institute of
31 Information Theory and Automation of the Czech Academy of Sciences (UTIA).
32 * FST4 is free software: you can redistribute it and/or modify it under
33 the terms of the GNU Affero General Public License as published by the
34 Free Software Foundation, version 3. The full text of the license is in
35 the file LICENSE in the distribution root and at
36 https://www.gnu.org/licenses/agpl-3.0.html.
37 * FST4 is distributed in the hope that it will be useful, but WITHOUT ANY
38 WARRANTY; without even the implied warranty of MERCHANTABILITY or
39 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
40 License for more details.
41 * Closed-source use without the AGPL's copyleft obligations requires a
42 commercial license from UTIA; in case of interest write to
43 utia@utia.cas.cz. LICENSING.md in the distribution root explains both
44 options, the contributor license agreement that every contribution
45 requires, and the contacts (technical questions and pull requests:
46 somol@utia.cas.cz, with "FST" in the subject line).
47========================================================================== */
48
49#include <memory>
50#include <vector>
51#include <cassert>
52#include <cmath>
53#include <iostream>
54#include "error.hpp"
55#include "global.hpp"
56#include "classifier.hpp"
57#include "dense_train_tools.hpp"
58
59/*============== Template parameter type naming conventions ==============
60--------- Numeric types: -------------------------------------------------
61DATATYPE - data sample values - usually real numbers (but may be integers
62 in text processing etc.)
63REALTYPE - must be real numbers - for representing intermediate results of
64 calculations like mean, covariance etc.
65IDXTYPE - index values for enumeration of data samples - (nonnegative) integers,
66 extent depends on numbers of samples in data
67DIMTYPE - index values for enumeration of features (dimensions), or classes (not
68 class sizes) - (nonnegative) integers, usually lower extent than IDXTYPE,
69 but be aware of expressions like _classes*_features*_features !
70 in linearized representations of feature matrices for all classes
71BINTYPE - feature selection marker type - represents ca. <10 different feature
72 states (selected, deselected, sel./desel. temporarily 1st nested loop, 2nd...)
73RETURNTYPE - criterion value: real value, but may be extended in future to support
74 multiple values
75--------- Class types: ---------------------------------------------------
76SUBSET - class of class type Subset
77CLASSIFIER - class implementing interface defined in abstract class Classifier
78EVALUATOR - class implementing interface defined in abstract class Sequential_Step
79DISTANCE - class implementing interface defined in abstract class Distance
80DATAACCESSOR - class implementing interface defined in abstract class Data_Accessor
81INTERVALCONTAINER - class of class type Data_Intervaller
82CONTAINER - STL container of class type Data_Interval
83========================================================================== */
84
85namespace FST {
86
110template<class RETURNTYPE, typename DATATYPE, typename REALTYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR>
111class Classifier_Logistic_Regression : public Clonable_Impl<Classifier_Logistic_Regression<RETURNTYPE,DATATYPE,REALTYPE,IDXTYPE,DIMTYPE,SUBSET,DATAACCESSOR>,Classifier<RETURNTYPE,DIMTYPE,SUBSET,DATAACCESSOR> > {
112public:
114 typedef std::shared_ptr<DATAACCESSOR> PDataAccessor;
115 typedef std::shared_ptr<SUBSET> const PSubset;
116 typedef typename DATAACCESSOR::PPattern PPattern;
117 Classifier_Logistic_Regression() : _lambda(0.01), _max_iter(100), _tol(1e-6), _classes(0), _features(0), _subfeatures(0)
118 {notify("Classifier_Logistic_Regression constructor.");}
119 virtual ~Classifier_Logistic_Regression() {notify("Classifier_Logistic_Regression destructor.");}
120
121 void set_lambda(const REALTYPE lambda) {if(lambda<0.0) throw fst_error("Classifier_Logistic_Regression: lambda must be >=0"); _lambda=lambda;}
122 void set_max_iter(const unsigned int max_iter) {if(max_iter<1) throw fst_error("Classifier_Logistic_Regression: max_iter must be >=1"); _max_iter=max_iter;}
123 void set_tolerance(const REALTYPE tol) {if(!(tol>0.0)) throw fst_error("Classifier_Logistic_Regression: tolerance must be >0"); _tol=tol;}
124 REALTYPE get_lambda() const {return _lambda;}
125 unsigned int get_max_iter() const {return _max_iter;}
126 REALTYPE get_tolerance() const {return _tol;}
127
130 void initialize(const PDataAccessor da);
131
132 virtual bool classify(DIMTYPE &cls, const PPattern &pattern) override; // classifies pattern, returns the respective class index
133 virtual bool classify_scores(std::vector<RETURNTYPE> &scores, const PPattern &pattern) override; // per-class linear scores (log-odds up to a shared shift) for ranking metrics (AUC)
134 virtual bool train(const PDataAccessor da, const PSubset sub) override; // learns from designated training part of data
135 // test() is the accuracy loop inherited from the Classifier base
136
137 DIMTYPE get_n() const {return _features;}
138 DIMTYPE get_d() const {return _subfeatures;}
139
140 virtual std::ostream& print(std::ostream& os) const override {os << "Classifier_Logistic_Regression(lambda=" << _lambda << ", max_iter=" << _max_iter << ", tol=" << _tol << ")"; return os;}
141protected:
142 // knobs
143 REALTYPE _lambda;
144 unsigned int _max_iter;
145 REALTYPE _tol;
146
147 // data shape (set in initialize())
148 DIMTYPE _classes;
149 DIMTYPE _features;
150
151 // learned model (valid after train())
152 DIMTYPE _subfeatures;
153 std::vector<DIMTYPE> _index;
154 std::vector<REALTYPE> _mean;
155 std::vector<REALTYPE> _scale;
156 std::vector<REALTYPE> _W;
157
158 // scratch buffers - contents transient, capacity persists across train() calls
159 std::vector<REALTYPE> _Z;
160 std::vector<DIMTYPE> _y;
161 std::vector<REALTYPE> _P;
162 std::vector<REALTYPE> _H;
163 std::vector<REALTYPE> _grad;
164 std::vector<REALTYPE> _step;
165 std::vector<REALTYPE> _Wtry;
166 std::vector<REALTYPE> _sc;
167
168private:
172 REALTYPE eval_objective(const std::vector<REALTYPE> &W, const bool want_grad);
177 void assemble_hessian(const std::size_t m, const REALTYPE extra_ridge);
178 // Cholesky factor/solve of the Newton system live in dense_train_tools.hpp (shared with regressor_ridge)
179};
180
181template<class RETURNTYPE, typename DATATYPE, typename REALTYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR>
183{
184 notify("Classifier_Logistic_Regression::initialize().");
185 assert(da);
186 assert(da->getNoOfFeatures()>0);
187 if(da->getNoOfClasses()<2) throw fst_error("Classifier_Logistic_Regression requires at least 2 classes");
188 _classes=da->getNoOfClasses();
189 _features=da->getNoOfFeatures();
190 _index.resize(_features);
191 _subfeatures=0; // no model yet
192}
193
194template<class RETURNTYPE, typename DATATYPE, typename REALTYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR>
196{
197 const std::size_t d=_subfeatures, dp1=d+1, K=_classes, N=_y.size();
198 assert(N>0);
199 if(want_grad) {_grad.assign(K*dp1,0.0); _P.resize(N*K);}
200 REALTYPE J=0.0;
201 for(std::size_t i=0;i<N;i++)
202 {
203 const REALTYPE *z=&_Z[i*dp1];
204 REALTYPE smax; // assigned on the c==0 pass below; K>=2 is guaranteed by initialize()
205 for(std::size_t c=0;c<K;c++) {
206 REALTYPE s=0.0;
207 const REALTYPE *w=&W[c*dp1];
208 for(std::size_t f=0;f<dp1;f++) s+=w[f]*z[f];
209 _sc[c]=s;
210 if(c==0 || s>smax) smax=s;
211 }
212 REALTYPE sumexp=0.0;
213 for(std::size_t c=0;c<K;c++) {_sc[c]=std::exp(_sc[c]-smax); sumexp+=_sc[c];}
214 J-=std::log(_sc[_y[i]]/sumexp);
215 if(want_grad)
216 for(std::size_t c=0;c<K;c++) {
217 const REALTYPE p=_sc[c]/sumexp;
218 _P[i*K+c]=p;
219 const REALTYPE r=p-(c==(std::size_t)_y[i]?1.0:0.0);
220 REALTYPE *g=&_grad[c*dp1];
221 for(std::size_t f=0;f<dp1;f++) g[f]+=r*z[f];
222 }
223 }
224 J/=(REALTYPE)N;
225 REALTYPE penalty=0.0;
226 for(std::size_t c=0;c<K;c++)
227 for(std::size_t f=0;f<d;f++) penalty+=W[c*dp1+f]*W[c*dp1+f]; // biases (f==d) unpenalized
228 J+=0.5*_lambda*penalty;
229 if(want_grad)
230 for(std::size_t c=0;c<K;c++)
231 for(std::size_t f=0;f<dp1;f++) {
232 _grad[c*dp1+f]/=(REALTYPE)N;
233 if(f<d) _grad[c*dp1+f]+=_lambda*W[c*dp1+f];
234 }
235 return J;
236}
237
238template<class RETURNTYPE, typename DATATYPE, typename REALTYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR>
240{
241 const std::size_t d=_subfeatures, dp1=d+1, K=_classes, N=_y.size();
242 _H.assign(m*m,0.0);
243 for(std::size_t n=0;n<N;n++)
244 {
245 const REALTYPE *z=&_Z[n*dp1];
246 const REALTYPE *pr=&_P[n*K];
247 for(std::size_t c1=0;c1<K;c1++)
248 for(std::size_t c2=0;c2<=c1;c2++)
249 {
250 const REALTYPE coef=(c1==c2)?pr[c1]*(1.0-pr[c1]):-pr[c1]*pr[c2];
251 if(coef==0.0) continue;
252 REALTYPE *hblk=&_H[(c1*dp1)*m+c2*dp1];
253 for(std::size_t f1=0;f1<dp1;f1++)
254 {
255 const REALTYPE zc=coef*z[f1];
256 const std::size_t fend=(c1==c2)?f1+1:dp1; // diagonal block: lower triangle only
257 for(std::size_t f2=0;f2<fend;f2++) hblk[f1*m+f2]+=zc*z[f2];
258 }
259 }
260 }
261 for(std::size_t j=0;j<m*m;j++) _H[j]/=(REALTYPE)N; // only the lower triangle is meaningful, scaling all is harmless
262 for(std::size_t c=0;c<K;c++)
263 for(std::size_t f=0;f<dp1;f++)
264 {
265 const std::size_t j=c*dp1+f;
266 // weights get the L2 term; biases a tiny fixed ridge (softmax is
267 // shift-invariant in the biases, the gradient has no component in
268 // that null direction - the ridge only keeps Cholesky defined)
269 _H[j*m+j]+=((f<d)?_lambda:(REALTYPE)1e-8)+extra_ridge;
270 }
271}
272
273template<class RETURNTYPE, typename DATATYPE, typename REALTYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR>
275{
276 notify("Classifier_Logistic_Regression::train().");
277 assert(da);
278 assert(sub);
279 assert(sub->get_frozen_mode()==false);
280 assert(_classes==da->getNoOfClasses());
281 assert(_features==da->getNoOfFeatures());
282 assert(sub->get_n_raw()==da->getNoOfFeatures());
283
284 // buffer the feature subset index
285 _subfeatures=0;
286 for(DIMTYPE fsel : sub->selected_features()) {assert(_subfeatures<_features); _index[_subfeatures++]=fsel;}
287 assert(_subfeatures==sub->get_d_raw());
288 assert(_subfeatures>0);
289
290 const std::size_t d=_subfeatures, dp1=d+1, K=_classes, m=K*dp1;
291 // Hessian memory guard: m=8192 means a 512MB system - beyond it an OOM crash
292 // is likelier than a useful result, so fail with advice instead
293 if(m>8192) throw fst_error("Classifier_Logistic_Regression: Newton system too large (classes*(subset size+1) > 8192); use smaller subsets (e.g. a forward search bounded to a small target subset size)");
294
295 // pass 1 + standardize via the shared helper (dense_train_tools.hpp); the appended
296 // bias coordinate and the label ride along in the per-pattern callback
297 _y.clear();
299 [this](const auto *pat, const DIMTYPE c){(void)pat; _Z.push_back(1.0); _y.push_back(c);});
300 if(N==0) return false;
301
302 // damped Newton from W=0
303 _W.assign(m,0.0);
304 _sc.resize(K>dp1?K:dp1);
305 _step.resize(m); _Wtry.resize(m);
306 REALTYPE J=eval_objective(_W,true);
307 for(unsigned int iter=0;iter<_max_iter;iter++)
308 {
309 REALTYPE gmax=0.0;
310 for(std::size_t j=0;j<m;j++) {const REALTYPE a=std::fabs(_grad[j]); if(a>gmax) gmax=a;}
311 if(gmax<_tol) break;
312
313 // assemble and factor the Newton system, escalating the ridge on numerical failure
314 // (in-place factoring destroys _H, so each retry reassembles). A large ridge makes the
315 // Hessian approach a scaled identity, i.e. the "Newton" step degrades gracefully into a
316 // short gradient step - still a descent direction, which is all the line search needs.
317 REALTYPE ridge=0.0;
318 assemble_hessian(m,ridge);
319 bool factored=cholesky_factor(_H,m);
320 while(!factored && ridge<1.0)
321 {
322 ridge=(ridge==0.0)?(REALTYPE)1e-6:ridge*100.0;
323 assemble_hessian(m,ridge);
324 factored=cholesky_factor(_H,m);
325 }
326 if(!factored) return false; // pathological even with heavy ridge
327
328 cholesky_solve(_H,m,_grad.data(),true,_step.data()); // L L^T step = -grad
329
330 // step halving: guarantee monotone descent. The 60 halvings are a hard floor rather than
331 // a tuning knob - at t=2^-60 the trial weights differ from the current ones by less than
332 // double precision can represent, so no further halving could ever improve J.
333 REALTYPE t=1.0;
334 bool improved=false;
335 for(int half=0;half<60;half++)
336 {
337 for(std::size_t j=0;j<m;j++) _Wtry[j]=_W[j]+t*_step[j];
338 const REALTYPE Jtry=eval_objective(_Wtry,false);
339 if(Jtry<J && std::isfinite(Jtry)) {improved=true; _W.swap(_Wtry); J=Jtry; break;}
340 t*=0.5;
341 }
342 if(!improved) break; // FP floor - treat as converged
343 J=eval_objective(_W,true); // refresh gradient and posterior cache at the accepted point
344 }
345 return true;
346}
347
348template<class RETURNTYPE, typename DATATYPE, typename REALTYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR>
350{
351 notify("Classifier_Logistic_Regression::classify().");
352 assert(_subfeatures>0); // train() must have succeeded
353 const std::size_t d=_subfeatures, dp1=d+1, K=_classes;
354 assert(_W.size()==K*dp1);
355 // argmax of the linear scores == argmax of softmax posteriors (no exp needed)
356 REALTYPE best;
357 for(std::size_t c=0;c<K;c++)
358 {
359 const REALTYPE *w=&_W[c*dp1];
360 REALTYPE score=w[d]; // bias
361 for(std::size_t f=0;f<d;f++) score+=w[f]*(((REALTYPE)pattern[_index[f]]-_mean[f])/_scale[f]);
362 if(c==0 || score>best) {best=score; cls=(DIMTYPE)c;} // first maximum wins
363 }
364 return true;
365}
366
367template<class RETURNTYPE, typename DATATYPE, typename REALTYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR>
369{
370 // the classify() linear scores, exposed per class (monotone in the softmax posteriors)
371 notify("Classifier_Logistic_Regression::classify_scores().");
372 assert(_subfeatures>0); // train() must have succeeded
373 const std::size_t d=_subfeatures, dp1=d+1, K=_classes;
374 assert(_W.size()==K*dp1);
375 scores.resize(K);
376 for(std::size_t c=0;c<K;c++)
377 {
378 const REALTYPE *w=&_W[c*dp1];
379 REALTYPE score=w[d]; // bias
380 for(std::size_t f=0;f<d;f++) score+=w[f]*(((REALTYPE)pattern[_index[f]]-_mean[f])/_scale[f]);
381 scores[c]=(RETURNTYPE)score;
382 }
383 return true;
384}
385
386
387} // namespace
388#endif // FSTCLASSIFIERLOGREG_H ///:~
Implements multinomial (softmax) logistic regression classifier.
Definition classifier_logreg.hpp:111
DIMTYPE _features
raw (full) feature count, as recorded by initialize()
Definition classifier_logreg.hpp:149
REALTYPE get_lambda() const
current L2 weight penalty
Definition classifier_logreg.hpp:124
REALTYPE _tol
gradient infinity-norm stopping tolerance
Definition classifier_logreg.hpp:145
DIMTYPE _subfeatures
d = current subset size
Definition classifier_logreg.hpp:152
DIMTYPE get_d() const
current subset dimensionality, as recorded by the last train()
Definition classifier_logreg.hpp:138
virtual bool classify(DIMTYPE &cls, const PPattern &pattern) override
Definition classifier_logreg.hpp:349
virtual bool train(const PDataAccessor da, const PSubset sub) override
learns from designated training part of data
Definition classifier_logreg.hpp:274
std::vector< REALTYPE > _mean
standardization shift per subset position
Definition classifier_logreg.hpp:154
void set_lambda(const REALTYPE lambda)
sets the L2 weight penalty (must be >=0); default 0.01
Definition classifier_logreg.hpp:121
std::vector< REALTYPE > _Z
N x (d+1) standardized training data, last column fixed 1 (bias coordinate).
Definition classifier_logreg.hpp:159
std::vector< DIMTYPE > _y
N training labels.
Definition classifier_logreg.hpp:160
std::vector< REALTYPE > _step
Newton step solved from the Hessian system (see assemble_hessian()).
Definition classifier_logreg.hpp:164
std::vector< REALTYPE > _H
Newton system, m x m, m=classes*(d+1) (lower triangle used).
Definition classifier_logreg.hpp:162
void set_max_iter(const unsigned int max_iter)
sets the damped-Newton iteration cap (must be >=1); default 100
Definition classifier_logreg.hpp:122
DIMTYPE _classes
number of classes, as recorded by initialize()
Definition classifier_logreg.hpp:148
std::vector< REALTYPE > _W
classes x (d+1) row-major; column d = bias
Definition classifier_logreg.hpp:156
DATAACCESSOR::PPattern PPattern
one data sample as exposed by DATAACCESSOR (a raw pointer to its feature vector)
Definition classifier_logreg.hpp:116
Clonable_Impl< Classifier_Logistic_Regression, Classifier< RETURNTYPE, DIMTYPE, SUBSET, DATAACCESSOR > > parent_impl
implements clone() via the (implicit, member-wise) copy-constructor - all members are value-semantic
Definition classifier_logreg.hpp:113
REALTYPE eval_objective(const std::vector< REALTYPE > &W, const bool want_grad)
Definition classifier_logreg.hpp:195
REALTYPE _lambda
L2 weight penalty (biases unpenalized).
Definition classifier_logreg.hpp:143
std::shared_ptr< SUBSET > const PSubset
shared pointer to the (fixed, non-reassignable) feature subset being trained/classified on
Definition classifier_logreg.hpp:115
virtual bool classify_scores(std::vector< RETURNTYPE > &scores, const PPattern &pattern) override
Definition classifier_logreg.hpp:368
std::shared_ptr< DATAACCESSOR > PDataAccessor
shared pointer to the data accessor supplying train/test blocks
Definition classifier_logreg.hpp:114
std::vector< REALTYPE > _scale
standardization scale per subset position (1 for constant features)
Definition classifier_logreg.hpp:155
REALTYPE get_tolerance() const
current gradient infinity-norm stopping tolerance
Definition classifier_logreg.hpp:126
std::vector< REALTYPE > _Wtry
trial weights during step-halving line search in train()
Definition classifier_logreg.hpp:165
DIMTYPE get_n() const
raw (full) feature count, as recorded by initialize()
Definition classifier_logreg.hpp:137
void initialize(const PDataAccessor da)
Records data shape (classes, raw feature count) and resets to "no model yet". Must be called before t...
Definition classifier_logreg.hpp:182
void set_tolerance(const REALTYPE tol)
sets the gradient infinity-norm stopping tolerance (must be >0); default 1e-6
Definition classifier_logreg.hpp:123
unsigned int _max_iter
Newton iteration cap.
Definition classifier_logreg.hpp:144
std::vector< REALTYPE > _grad
objective gradient at the current Newton iterate (see eval_objective())
Definition classifier_logreg.hpp:163
virtual std::ostream & print(std::ostream &os) const override
writes a human-readable identification of the object; base default writes nothing
Definition classifier_logreg.hpp:140
void assemble_hessian(const std::size_t m, const REALTYPE extra_ridge)
Definition classifier_logreg.hpp:239
unsigned int get_max_iter() const
current Newton iteration cap
Definition classifier_logreg.hpp:125
std::vector< DIMTYPE > _index
subset position -> raw feature index (first d entries valid)
Definition classifier_logreg.hpp:153
std::vector< REALTYPE > _P
N x classes posterior cache for Hessian assembly.
Definition classifier_logreg.hpp:161
std::vector< REALTYPE > _sc
per-class softmax score scratch used by eval_objective() (sized max(classes,d+1))
Definition classifier_logreg.hpp:166
CRTP mixin implementing the Clonable protocol for a concrete class.
Definition clonable.hpp:184
FST4 specific error passing class.
Definition error.hpp:61
Defines interface for classifier implementations.
Shared dense-training helpers: standardized-design build and Cholesky factor/solve.
FST4 specific error passing class.
Global definitions.
The Feature Selection Toolbox 4 library: every library component – data access, criteria and classifi...
Definition data_accessor.hpp:80
void notify(const std::string msg)
Prints msg followed by a newline via syncout::print(); a no-op unless the preprocessor macro DEBUG is...
Definition global.hpp:112
std::size_t standardize_design(const std::shared_ptr< DATAACCESSOR > &da, const DIMTYPE classes, const std::size_t d, const std::size_t zstride, const std::vector< DIMTYPE > &index, std::vector< REALTYPE > &Z, std::vector< REALTYPE > &mean, std::vector< REALTYPE > &scale, AFTERPATTERN after_pattern)
Definition dense_train_tools.hpp:84
bool cholesky_factor(std::vector< REALTYPE > &A, const std::size_t m)
Definition dense_train_tools.hpp:133
void cholesky_solve(const std::vector< REALTYPE > &A, const std::size_t m, const REALTYPE *rhs, const bool negate_rhs, REALTYPE *out)
Definition dense_train_tools.hpp:156
@ f1
classification, two-class data: F1 of the positive class, which is Metric_Spec::f1_positive (class in...
Definition wrapper_metrics.hpp:63
Copyright © 2000–present ÚTIA, Institute of Information Theory and Automation of the Czech Academy of Sciences · Contacts · This site sets no cookies and uses no analytics.