Package mvpa :: Package measures :: Module base
[hide private]
[frames] | no frames]

Source Code for Module mvpa.measures.base

  1  # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 
  2  # vi: set ft=python sts=4 ts=4 sw=4 et: 
  3  ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 
  4  # 
  5  #   See COPYING file distributed along with the PyMVPA package for the 
  6  #   copyright and license terms. 
  7  # 
  8  ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 
  9  """Base class for data measures: algorithms that quantify properties of 
 10  datasets. 
 11   
 12  Besides the `DatasetMeasure` base class this module also provides the 
 13  (abstract) `FeaturewiseDatasetMeasure` class. The difference between a general 
 14  measure and the output of the `FeaturewiseDatasetMeasure` is that the latter 
 15  returns a 1d map (one value per feature in the dataset). In contrast there are 
 16  no restrictions on the returned value of `DatasetMeasure` except for that it 
 17  has to be in some iterable container. 
 18   
 19  """ 
 20   
 21  __docformat__ = 'restructuredtext' 
 22   
 23  import numpy as N 
 24  import mvpa.support.copy as copy 
 25   
 26  from mvpa.misc.state import StateVariable, ClassWithCollections 
 27  from mvpa.misc.args import group_kwargs 
 28  from mvpa.misc.transformers import FirstAxisMean, SecondAxisSumOfAbs 
 29  from mvpa.base.dochelpers import enhancedDocString 
 30  from mvpa.base import externals, warning 
 31  from mvpa.clfs.stats import autoNullDist 
 32   
 33  if __debug__: 
 34      from mvpa.base import debug 
35 36 37 -class DatasetMeasure(ClassWithCollections):
38 """A measure computed from a `Dataset` 39 40 All dataset measures support arbitrary transformation of the measure 41 after it has been computed. Transformation are done by processing the 42 measure with a functor that is specified via the `transformer` keyword 43 argument of the constructor. Upon request, the raw measure (before 44 transformations are applied) is stored in the `raw_results` state variable. 45 46 Additionally all dataset measures support the estimation of the 47 probabilit(y,ies) of a measure under some distribution. Typically this will 48 be the NULL distribution (no signal), that can be estimated with 49 permutation tests. If a distribution estimator instance is passed to the 50 `null_dist` keyword argument of the constructor the respective 51 probabilities are automatically computed and stored in the `null_prob` 52 state variable. 53 54 .. note:: 55 For developers: All subclasses shall get all necessary parameters via 56 their constructor, so it is possible to get the same type of measure for 57 multiple datasets by passing them to the __call__() method successively. 58 """ 59 60 raw_results = StateVariable(enabled=False, 61 doc="Computed results before applying any " + 62 "transformation algorithm") 63 null_prob = StateVariable(enabled=True) 64 """Stores the probability of a measure under the NULL hypothesis""" 65 null_t = StateVariable(enabled=False) 66 """Stores the t-score corresponding to null_prob under assumption 67 of Normal distribution""" 68
69 - def __init__(self, transformer=None, null_dist=None, **kwargs):
70 """Does nothing special. 71 72 :Parameters: 73 transformer: Functor 74 This functor is called in `__call__()` to perform a final 75 processing step on the to be returned dataset measure. If None, 76 nothing is called 77 null_dist: instance of distribution estimator 78 The estimated distribution is used to assign a probability for a 79 certain value of the computed measure. 80 """ 81 ClassWithCollections.__init__(self, **kwargs) 82 83 self.__transformer = transformer 84 """Functor to be called in return statement of all subclass __call__() 85 methods.""" 86 null_dist_ = autoNullDist(null_dist) 87 if __debug__: 88 debug('SA', 'Assigning null_dist %s whenever original given was %s' 89 % (null_dist_, null_dist)) 90 self.__null_dist = null_dist_
91 92 93 __doc__ = enhancedDocString('DatasetMeasure', locals(), ClassWithCollections) 94 95
96 - def __call__(self, dataset):
97 """Compute measure on a given `Dataset`. 98 99 Each implementation has to handle a single arguments: the source 100 dataset. 101 102 Returns the computed measure in some iterable (list-like) 103 container applying transformer if such is defined 104 """ 105 result = self._call(dataset) 106 result = self._postcall(dataset, result) 107 return result
108 109
110 - def _call(self, dataset):
111 """Actually compute measure on a given `Dataset`. 112 113 Each implementation has to handle a single arguments: the source 114 dataset. 115 116 Returns the computed measure in some iterable (list-like) container. 117 """ 118 raise NotImplemented
119 120
121 - def _postcall(self, dataset, result):
122 """Some postprocessing on the result 123 """ 124 self.states.raw_results = result 125 if not self.__transformer is None: 126 if __debug__: 127 debug("SA_", "Applying transformer %s" % self.__transformer) 128 result = self.__transformer(result) 129 130 # estimate the NULL distribution when functor is given 131 if not self.__null_dist is None: 132 if __debug__: 133 debug("SA_", "Estimating NULL distribution using %s" 134 % self.__null_dist) 135 136 # we need a matching datameasure instance, but we have to disable 137 # the estimation of the null distribution in that child to prevent 138 # infinite looping. 139 measure = copy.copy(self) 140 measure.__null_dist = None 141 self.__null_dist.fit(measure, dataset) 142 143 if self.states.isEnabled('null_t'): 144 # get probability under NULL hyp, but also request 145 # either it belong to the right tail 146 null_prob, null_right_tail = \ 147 self.__null_dist.p(result, return_tails=True) 148 self.states.null_prob = null_prob 149 150 externals.exists('scipy', raiseException=True) 151 from scipy.stats import norm 152 153 # TODO: following logic should appear in NullDist, 154 # not here 155 tail = self.null_dist.tail 156 if tail == 'left': 157 acdf = N.abs(null_prob) 158 elif tail == 'right': 159 acdf = 1.0 - N.abs(null_prob) 160 elif tail in ['any', 'both']: 161 acdf = 1.0 - N.clip(N.abs(null_prob), 0, 0.5) 162 else: 163 raise RuntimeError, 'Unhandled tail %s' % tail 164 # We need to clip to avoid non-informative inf's ;-) 165 # that happens due to lack of precision in mantissa 166 # which is 11 bits in double. We could clip values 167 # around 0 at as low as 1e-100 (correspond to z~=21), 168 # but for consistency lets clip at 1e-16 which leads 169 # to distinguishable value around p=1 and max z=8.2. 170 # Should be sufficient range of z-values ;-) 171 clip = 1e-16 172 null_t = norm.ppf(N.clip(acdf, clip, 1.0 - clip)) 173 # assure that we deal with arrays: 174 null_t = N.array(null_t, ndmin=1, copy=False) 175 null_t[~null_right_tail] *= -1.0 # revert sign for negatives 176 self.states.null_t = null_t # store 177 else: 178 # get probability of result under NULL hypothesis if available 179 # and don't request tail information 180 self.null_prob = self.__null_dist.p(result) 181 182 return result
183 184
185 - def __repr__(self, prefixes=[]):
186 """String representation of DatasetMeasure 187 188 Includes only arguments which differ from default ones 189 """ 190 prefixes = prefixes[:] 191 if self.__transformer is not None: 192 prefixes.append("transformer=%s" % self.__transformer) 193 if self.__null_dist is not None: 194 prefixes.append("null_dist=%s" % self.__null_dist) 195 return super(DatasetMeasure, self).__repr__(prefixes=prefixes)
196
197 - def untrain(self):
198 """'Untraining' Measure 199 200 Some derived classes might used classifiers, so we need to 201 untrain those 202 """ 203 pass
204 205 @property
206 - def null_dist(self):
207 """Return Null Distribution estimator""" 208 return self.__null_dist
209 210 @property
211 - def transformer(self):
212 """Return transformer""" 213 return self.__transformer
214
215 216 -class FeaturewiseDatasetMeasure(DatasetMeasure):
217 """A per-feature-measure computed from a `Dataset` (base class). 218 219 Should behave like a DatasetMeasure. 220 """ 221 222 base_sensitivities = StateVariable(enabled=False, 223 doc="Stores basic sensitivities if the sensitivity " + 224 "relies on combining multiple ones") 225 226 # XXX should we may be default to combiner=None to avoid 227 # unexpected results? Also rethink if we need combiner here at 228 # all... May be combiners should be 'adjoint' with transformer 229 # YYY in comparison to CombinedSensitivityAnalyzer here default 230 # value for combiner is worse than anywhere. From now on, 231 # default combiners should be provided "in place", ie 232 # in SMLR it makes sense to have SecondAxisMaxOfAbs, 233 # in SVM (pair-wise) only for not-binary should be 234 # SecondAxisSumOfAbs, though could be Max as well... uff 235 # YOH: started to do so, but still have issues... thus 236 # reverting back for now 237 # MH: Full ack -- voting for no default combiners!
238 - def __init__(self, combiner=SecondAxisSumOfAbs, **kwargs): # SecondAxisSumOfAbs
239 """Initialize 240 241 :Parameters: 242 combiner : Functor 243 The combiner is only applied if the computed featurewise dataset 244 measure is more than one-dimensional. This is different from a 245 `transformer`, which is always applied. By default, the sum of 246 absolute values along the second axis is computed. 247 """ 248 DatasetMeasure.__init__(self, **kwargs) 249 250 self.__combiner = combiner
251
252 - def __repr__(self, prefixes=None):
253 if prefixes is None: 254 prefixes = [] 255 if self.__combiner != SecondAxisSumOfAbs: 256 prefixes.append("combiner=%s" % self.__combiner) 257 return \ 258 super(FeaturewiseDatasetMeasure, self).__repr__(prefixes=prefixes)
259 260
261 - def _call(self, dataset):
262 """Computes a per-feature-measure on a given `Dataset`. 263 264 Behaves like a `DatasetMeasure`, but computes and returns a 1d ndarray 265 with one value per feature. 266 """ 267 raise NotImplementedError
268 269
270 - def _postcall(self, dataset, result):
271 """Adjusts per-feature-measure for computed `result` 272 273 274 TODO: overlaps in what it does heavily with 275 CombinedSensitivityAnalyzer, thus this one might make use of 276 CombinedSensitivityAnalyzer yoh thinks, and here 277 base_sensitivities doesn't sound appropriate. 278 MH: There is indeed some overlap, but also significant differences. 279 This one operates on a single sensana and combines over second 280 axis, CombinedFeaturewiseDatasetMeasure uses first axis. 281 Additionally, 'Sensitivity' base class is 282 FeaturewiseDatasetMeasures which would have to be changed to 283 CombinedFeaturewiseDatasetMeasure to deal with stuff like 284 SMLRWeights that return multiple sensitivity values by default. 285 Not sure if unification of both (and/or removal of functionality 286 here does not lead to an overall more complicated situation, 287 without any real gain -- after all this one works ;-) 288 """ 289 # !!! This is not stupid -- it is intended -- some times we might get 290 # scalars as input. 291 result = N.atleast_1d(result) 292 result_sq = result.squeeze() 293 # Assure that we have some iterable (could be a scalar if it 294 # was just a single value in 1D array) 295 result_sq = N.atleast_1d(result_sq) 296 297 if len(result_sq.shape)>1: 298 n_base = result.shape[1] 299 """Number of base sensitivities""" 300 if self.states.isEnabled('base_sensitivities'): 301 b_sensitivities = [] 302 if not self.states.isKnown('biases'): 303 biases = None 304 else: 305 biases = self.states.biases 306 if len(self.states.biases) != n_base: 307 warning("Number of biases %d differs from number " 308 "of base sensitivities %d which could happen " 309 "when measure is collided across labels." 310 % (len(self.states.biases), n_base)) 311 for i in xrange(n_base): 312 if not biases is None: 313 if n_base > 1 and len(biases) == 1: 314 # The same bias for all bases 315 bias = biases[0] 316 else: 317 bias = biases[i] 318 else: 319 bias = None 320 b_sensitivities = StaticDatasetMeasure( 321 measure = result[:,i], 322 bias = bias) 323 self.states.base_sensitivities = b_sensitivities 324 325 # After we stored each sensitivity separately, 326 # we can apply combiner 327 if self.__combiner is not None: 328 result = self.__combiner(result) 329 else: 330 # remove bogus dimensions 331 # XXX we might need to come up with smth better. May be some naive 332 # combiner? :-) 333 result = result_sq 334 335 # call base class postcall 336 result = DatasetMeasure._postcall(self, dataset, result) 337 338 return result
339 340 @property
341 - def combiner(self):
342 """Return combiner""" 343 return self.__combiner
344
345 346 347 -class StaticDatasetMeasure(DatasetMeasure):
348 """A static (assigned) sensitivity measure. 349 350 Since implementation is generic it might be per feature or 351 per whole dataset 352 """ 353
354 - def __init__(self, measure=None, bias=None, *args, **kwargs):
355 """Initialize. 356 357 :Parameters: 358 measure 359 actual sensitivity to be returned 360 bias 361 optionally available bias 362 """ 363 DatasetMeasure.__init__(self, *args, **kwargs) 364 if measure is None: 365 raise ValueError, "Sensitivity measure has to be provided" 366 self.__measure = measure 367 self.__bias = bias
368
369 - def _call(self, dataset):
370 """Returns assigned sensitivity 371 """ 372 return self.__measure
373 374 #XXX Might need to move into StateVariable? 375 bias = property(fget=lambda self:self.__bias)
376
377 378 379 # 380 # Flavored implementations of FeaturewiseDatasetMeasures 381 382 -class Sensitivity(FeaturewiseDatasetMeasure):
383 384 _LEGAL_CLFS = [] 385 """If Sensitivity is classifier specific, classes of classifiers 386 should be listed in the list 387 """ 388
389 - def __init__(self, clf, force_training=True, **kwargs):
390 """Initialize the analyzer with the classifier it shall use. 391 392 :Parameters: 393 clf : :class:`Classifier` 394 classifier to use. 395 force_training : Bool 396 if classifier was already trained -- do not retrain 397 """ 398 399 """Does nothing special.""" 400 FeaturewiseDatasetMeasure.__init__(self, **kwargs) 401 402 _LEGAL_CLFS = self._LEGAL_CLFS 403 if len(_LEGAL_CLFS) > 0: 404 found = False 405 for clf_class in _LEGAL_CLFS: 406 if isinstance(clf, clf_class): 407 found = True 408 break 409 if not found: 410 raise ValueError, \ 411 "Classifier %s has to be of allowed class (%s), but is %s" \ 412 % (clf, _LEGAL_CLFS, `type(clf)`) 413 414 self.__clf = clf 415 """Classifier used to computed sensitivity""" 416 417 self._force_training = force_training 418 """Either to force it to train"""
419
420 - def __repr__(self, prefixes=None):
421 if prefixes is None: 422 prefixes = [] 423 prefixes.append("clf=%s" % repr(self.clf)) 424 if not self._force_training: 425 prefixes.append("force_training=%s" % self._force_training) 426 return super(Sensitivity, self).__repr__(prefixes=prefixes)
427 428
429 - def __call__(self, dataset=None):
430 """Train classifier on `dataset` and then compute actual sensitivity. 431 432 If the classifier is already trained it is possible to extract the 433 sensitivities without passing a dataset. 434 """ 435 # local bindings 436 clf = self.__clf 437 if not clf.trained or self._force_training: 438 if dataset is None: 439 raise ValueError, \ 440 "Training classifier to compute sensitivities requires " \ 441 "a dataset." 442 if __debug__: 443 debug("SA", "Training classifier %s %s" % 444 (`clf`, 445 {False: "since it wasn't yet trained", 446 True: "although it was trained previousely"} 447 [clf.trained])) 448 clf.train(dataset) 449 450 return FeaturewiseDatasetMeasure.__call__(self, dataset)
451 452
453 - def _setClassifier(self, clf):
454 self.__clf = clf
455 456
457 - def untrain(self):
458 """Untrain corresponding classifier for Sensitivity 459 """ 460 if self.__clf is not None: 461 self.__clf.untrain()
462 463 @property
464 - def feature_ids(self):
465 """Return feature_ids used by the underlying classifier 466 """ 467 return self.__clf._getFeatureIds()
468 469 470 clf = property(fget=lambda self:self.__clf, 471 fset=_setClassifier)
472
473 474 475 -class CombinedFeaturewiseDatasetMeasure(FeaturewiseDatasetMeasure):
476 """Set sensitivity analyzers to be merged into a single output""" 477 478 sensitivities = StateVariable(enabled=False, 479 doc="Sensitivities produced by each analyzer") 480 481 # XXX think again about combiners... now we have it in here and as 482 # well as in the parent -- FeaturewiseDatasetMeasure 483 # YYY because we don't use parent's _call. Needs RF
484 - def __init__(self, analyzers=None, # XXX should become actually 'measures' 485 combiner=None, #FirstAxisMean, 486 **kwargs):
487 """Initialize CombinedFeaturewiseDatasetMeasure 488 489 :Parameters: 490 analyzers : list or None 491 List of analyzers to be used. There is no logic to populate 492 such a list in __call__, so it must be either provided to 493 the constructor or assigned to .analyzers prior calling 494 """ 495 if analyzers is None: 496 analyzers = [] 497 498 FeaturewiseDatasetMeasure.__init__(self, **kwargs) 499 self.__analyzers = analyzers 500 """List of analyzers to use""" 501 502 self.__combiner = combiner 503 """Which functor to use to combine all sensitivities"""
504 505
506 - def _call(self, dataset):
507 sensitivities = [] 508 for ind,analyzer in enumerate(self.__analyzers): 509 if __debug__: 510 debug("SA", "Computing sensitivity for SA#%d:%s" % 511 (ind, analyzer)) 512 sensitivity = analyzer(dataset) 513 sensitivities.append(sensitivity) 514 515 self.states.sensitivities = sensitivities 516 if __debug__: 517 debug("SA", 518 "Returning combined using %s sensitivity across %d items" % 519 (self.__combiner, len(sensitivities))) 520 521 if self.__combiner is not None: 522 sensitivities = self.__combiner(sensitivities) 523 else: 524 # assure that we have an ndarray on output 525 sensitivities = N.asarray(sensitivities) 526 return sensitivities
527 528
529 - def untrain(self):
530 """Untrain CombinedFDM 531 """ 532 if self.__analyzers is not None: 533 for anal in self.__analyzers: 534 anal.untrain()
535
536 - def _setAnalyzers(self, analyzers):
537 """Set the analyzers 538 """ 539 self.__analyzers = analyzers 540 """Analyzers to use"""
541 542 analyzers = property(fget=lambda x:x.__analyzers, 543 fset=_setAnalyzers, 544 doc="Used analyzers")
545
546 547 # XXX Why did we come to name everything analyzer? inputs of regular 548 # things like CombinedFeaturewiseDatasetMeasure can be simple 549 # measures.... 550 551 -class SplitFeaturewiseDatasetMeasure(FeaturewiseDatasetMeasure):
552 """Compute measures across splits for a specific analyzer""" 553 554 # XXX This beast is created based on code of 555 # CombinedFeaturewiseDatasetMeasure, thus another reason to refactor 556 557 sensitivities = StateVariable(enabled=False, 558 doc="Sensitivities produced for each split") 559 560 splits = StateVariable(enabled=False, doc= 561 """Store the actual splits of the data. Can be memory expensive""") 562
563 - def __init__(self, splitter, analyzer, 564 insplit_index=0, combiner=None, **kwargs):
565 """Initialize SplitFeaturewiseDatasetMeasure 566 567 :Parameters: 568 splitter : Splitter 569 Splitter to use to split the dataset 570 analyzer : DatasetMeasure 571 Measure to be used. Could be analyzer as well (XXX) 572 insplit_index : int 573 splitter generates tuples of dataset on each iteration 574 (usually 0th for training, 1st for testing). 575 On what split index in that tuple to operate. 576 """ 577 578 # XXX might want to extend insplit_index to handle 'all', so we store 579 # sensitivities for all parts of the splits... not sure if it is needed 580 581 # XXX We really think through whole transformer/combiners pipelining 582 583 # Here we provide combiner None since if needs to be combined 584 # within each sensitivity, it better be done within analyzer 585 FeaturewiseDatasetMeasure.__init__(self, combiner=None, **kwargs) 586 587 self.__analyzer = analyzer 588 """Analyzer to use per split""" 589 590 self.__combiner = combiner 591 """Which functor to use to combine all sensitivities""" 592 593 self.__splitter = splitter 594 """Splitter to be used on the dataset""" 595 596 self.__insplit_index = insplit_index
597 598
599 - def untrain(self):
600 """Untrain SplitFeaturewiseDatasetMeasure 601 """ 602 if self.__analyzer is not None: 603 self.__analyzer.untrain()
604 605
606 - def _call(self, dataset):
607 # local bindings 608 analyzer = self.__analyzer 609 insplit_index = self.__insplit_index 610 611 sensitivities = [] 612 self.states.splits = splits = [] 613 store_splits = self.states.isEnabled("splits") 614 615 for ind,split in enumerate(self.__splitter(dataset)): 616 ds = split[insplit_index] 617 if __debug__ and "SA" in debug.active: 618 debug("SA", "Computing sensitivity for split %d on " 619 "dataset %s using %s" % (ind, ds, analyzer)) 620 sensitivity = analyzer(ds) 621 sensitivities.append(sensitivity) 622 if store_splits: splits.append(split) 623 624 self.states.sensitivities = sensitivities 625 if __debug__: 626 debug("SA", 627 "Returning sensitivities combined using %s across %d items " 628 "generated by splitter %s" % 629 (self.__combiner, len(sensitivities), self.__splitter)) 630 631 if self.__combiner is not None: 632 sensitivities = self.__combiner(sensitivities) 633 else: 634 # assure that we have an ndarray on output 635 sensitivities = N.asarray(sensitivities) 636 return sensitivities
637
638 639 -class BoostedClassifierSensitivityAnalyzer(Sensitivity):
640 """Set sensitivity analyzers to be merged into a single output""" 641 642 643 # XXX we might like to pass parameters also for combined_analyzer 644 @group_kwargs(prefixes=['slave_'], assign=True)
645 - def __init__(self, 646 clf, 647 analyzer=None, 648 combined_analyzer=None, 649 slave_kwargs={}, 650 **kwargs):
651 """Initialize Sensitivity Analyzer for `BoostedClassifier` 652 653 :Parameters: 654 clf : `BoostedClassifier` 655 Classifier to be used 656 analyzer : analyzer 657 Is used to populate combined_analyzer 658 slave_* 659 Arguments to pass to created analyzer if analyzer is None 660 """ 661 Sensitivity.__init__(self, clf, **kwargs) 662 if combined_analyzer is None: 663 # sanitarize kwargs 664 kwargs.pop('force_training', None) 665 combined_analyzer = CombinedFeaturewiseDatasetMeasure(**kwargs) 666 self.__combined_analyzer = combined_analyzer 667 """Combined analyzer to use""" 668 669 if analyzer is not None and len(self._slave_kwargs): 670 raise ValueError, \ 671 "Provide either analyzer of slave_* arguments, not both" 672 self.__analyzer = analyzer 673 """Analyzer to use for basic classifiers within boosted classifier"""
674 675
676 - def untrain(self):
677 """Untrain BoostedClassifierSensitivityAnalyzer 678 """ 679 if self.__analyzer is not None: 680 self.__analyzer.untrain() 681 self.__combined_analyzer.untrain()
682 683
684 - def _call(self, dataset):
685 analyzers = [] 686 # create analyzers 687 for clf in self.clf.clfs: 688 if self.__analyzer is None: 689 analyzer = clf.getSensitivityAnalyzer(**(self._slave_kwargs)) 690 if analyzer is None: 691 raise ValueError, \ 692 "Wasn't able to figure basic analyzer for clf %s" % \ 693 `clf` 694 if __debug__: 695 debug("SA", "Selected analyzer %s for clf %s" % \ 696 (`analyzer`, `clf`)) 697 else: 698 # XXX shallow copy should be enough... 699 analyzer = copy.copy(self.__analyzer) 700 701 # assign corresponding classifier 702 analyzer.clf = clf 703 # if clf was trained already - don't train again 704 if clf.trained: 705 analyzer._force_training = False 706 analyzers.append(analyzer) 707 708 self.__combined_analyzer.analyzers = analyzers 709 710 # XXX not sure if we don't want to call directly ._call(dataset) to avoid 711 # double application of transformers/combiners, after all we are just 712 # 'proxying' here to combined_analyzer... 713 # YOH: decided -- lets call ._call 714 return self.__combined_analyzer._call(dataset)
715 716 combined_analyzer = property(fget=lambda x:x.__combined_analyzer)
717
718 719 -class ProxyClassifierSensitivityAnalyzer(Sensitivity):
720 """Set sensitivity analyzer output just to pass through""" 721 722 clf_sensitivities = StateVariable(enabled=False, 723 doc="Stores sensitivities of the proxied classifier") 724 725 726 @group_kwargs(prefixes=['slave_'], assign=True)
727 - def __init__(self, 728 clf, 729 analyzer=None, 730 **kwargs):
731 """Initialize Sensitivity Analyzer for `BoostedClassifier` 732 """ 733 Sensitivity.__init__(self, clf, **kwargs) 734 735 if analyzer is not None and len(self._slave_kwargs): 736 raise ValueError, \ 737 "Provide either analyzer of slave_* arguments, not both" 738 739 self.__analyzer = analyzer 740 """Analyzer to use for basic classifiers within boosted classifier"""
741 742
743 - def untrain(self):
744 super(ProxyClassifierSensitivityAnalyzer, self).untrain() 745 if self.__analyzer is not None: 746 self.__analyzer.untrain()
747 748
749 - def _call(self, dataset):
750 # OPT: local bindings 751 clfclf = self.clf.clf 752 analyzer = self.__analyzer 753 754 if analyzer is None: 755 analyzer = clfclf.getSensitivityAnalyzer( 756 **(self._slave_kwargs)) 757 if analyzer is None: 758 raise ValueError, \ 759 "Wasn't able to figure basic analyzer for clf %s" % \ 760 `clfclf` 761 if __debug__: 762 debug("SA", "Selected analyzer %s for clf %s" % \ 763 (analyzer, clfclf)) 764 # bind to the instance finally 765 self.__analyzer = analyzer 766 767 # TODO "remove" unnecessary things below on each call... 768 # assign corresponding classifier 769 analyzer.clf = clfclf 770 771 # if clf was trained already - don't train again 772 if clfclf.trained: 773 analyzer._force_training = False 774 775 result = analyzer._call(dataset) 776 self.states.clf_sensitivities = result 777 778 return result
779 780 analyzer = property(fget=lambda x:x.__analyzer)
781
782 783 -class MappedClassifierSensitivityAnalyzer(ProxyClassifierSensitivityAnalyzer):
784 """Set sensitivity analyzer output be reverse mapped using mapper of the 785 slave classifier""" 786
787 - def _call(self, dataset):
788 sens = super(MappedClassifierSensitivityAnalyzer, self)._call(dataset) 789 # So we have here the case that some sensitivities are given 790 # as nfeatures x nclasses, thus we need to take .T for the 791 # mapper and revert back afterwards 792 # devguide's TODO lists this point to 'disguss' 793 sens_mapped = self.clf.mapper.reverse(sens.T) 794 return sens_mapped.T
795
796 797 -class FeatureSelectionClassifierSensitivityAnalyzer(ProxyClassifierSensitivityAnalyzer):
798 """Set sensitivity analyzer output be reverse mapped using mapper of the 799 slave classifier""" 800
801 - def _call(self, dataset):
802 sens = super(FeatureSelectionClassifierSensitivityAnalyzer, self)._call(dataset) 803 # So we have here the case that some sensitivities are given 804 # as nfeatures x nclasses, thus we need to take .T for the 805 # mapper and revert back afterwards 806 # devguide's TODO lists this point to 'disguss' 807 sens_mapped = self.clf.maskclf.mapper.reverse(sens.T) 808 return sens_mapped.T
809