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

Source Code for Module mvpa.mappers.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  """Data mapper""" 
 10   
 11  __docformat__ = 'restructuredtext' 
 12   
 13  import numpy as N 
 14   
 15  from mvpa.mappers.metric import Metric 
 16   
 17  from mvpa.misc.vproperty import VProperty 
 18  from mvpa.base.dochelpers import enhancedDocString 
 19   
 20  if __debug__: 
 21      from mvpa.base import warning 
 22      from mvpa.base import debug 
 23   
 24   
25 -class Mapper(object):
26 """Interface to provide mapping between two spaces: IN and OUT. 27 Methods are prefixed correspondingly. forward/reverse operate 28 on the entire dataset. get(In|Out)Id[s] operate per element:: 29 30 forward 31 ---------> 32 IN OUT 33 <--------/ 34 reverse 35 """
36 - def __init__(self, metric=None):
37 """ 38 :Parameters: 39 metric : Metric 40 Optional metric 41 """ 42 self.__metric = None 43 """Pylint happiness""" 44 self.setMetric(metric) 45 """Actually assign the metric"""
46 47 # 48 # The following methods are abstract and merely define the intended 49 # interface of a mapper and have to be implemented in derived classes. See 50 # the docstrings of the respective methods for details about what they 51 # should do. 52 # 53
54 - def forward(self, data):
55 """Map data from the IN dataspace into OUT space. 56 """ 57 raise NotImplementedError
58 59
60 - def reverse(self, data):
61 """Reverse map data from OUT space into the IN space. 62 """ 63 raise NotImplementedError
64 65
66 - def getInSize(self):
67 """Returns the size of the entity in input space""" 68 raise NotImplementedError
69 70
71 - def getOutSize(self):
72 """Returns the size of the entity in output space""" 73 raise NotImplementedError
74 75
76 - def selectOut(self, outIds):
77 """Limit the OUT space to a certain set of features. 78 79 :Parameters: 80 outIds: sequence 81 Subset of ids of the current feature in OUT space to keep. 82 """ 83 raise NotImplementedError
84 85
86 - def getInId(self, outId):
87 """Translate a feature id into a coordinate/index in input space. 88 89 Such a translation might not be meaningful or even possible for a 90 particular mapping algorithm and therefore cannot be relied upon. 91 """ 92 raise NotImplementedError
93 94 95 # 96 # The following methods are candidates for reimplementation in derived 97 # classes, in cases where the provided default behavior is not appropriate. 98 #
99 - def isValidOutId(self, outId):
100 """Validate feature id in OUT space. 101 102 Override if OUT space is not simly a 1D vector 103 """ 104 return(outId >= 0 and outId < self.getOutSize())
105 106
107 - def isValidInId(self, inId):
108 """Validate id in IN space. 109 110 Override if IN space is not simly a 1D vector 111 """ 112 return(inId >= 0 and inId < self.getInSize())
113 114
115 - def train(self, dataset):
116 """Perform training of the mapper. 117 118 This method is called to put the mapper in a state that allows it to 119 perform to intended mapping. 120 121 :Parameter: 122 dataset: Dataset or subclass 123 124 .. note:: 125 The default behavior of this method is to do nothing. 126 """ 127 pass
128 129
130 - def getNeighbor(self, outId, *args, **kwargs):
131 """Get feature neighbors in input space, given an id in output space. 132 133 This method has to be reimplemented whenever a derived class does not 134 provide an implementation for :meth:`~mvpa.mappers.base.Mapper.getInId`. 135 """ 136 if self.metric is None: 137 raise RuntimeError, "No metric was assigned to %s, thus no " \ 138 "neighboring information is present" % self 139 140 if self.isValidOutId(outId): 141 inId = self.getInId(outId) 142 for inId in self.getNeighborIn(inId, *args, **kwargs): 143 yield self.getOutId(inId)
144 145 146 # 147 # The following methods provide common functionality for all mappers 148 # and there should be no immediate need to reimplement them 149 #
150 - def getNeighborIn(self, inId, *args, **kwargs):
151 """Return the list of coordinates for the neighbors. 152 153 :Parameters: 154 inId 155 id (index) of an element in input dataspace. 156 *args, **kwargs 157 Any additional arguments are passed to the embedded metric of the 158 mapper. 159 160 XXX See TODO below: what to return -- list of arrays or list 161 of tuples? 162 """ 163 if self.metric is None: 164 raise RuntimeError, "No metric was assigned to %s, thus no " \ 165 "neighboring information is present" % self 166 167 isValidInId = self.isValidInId 168 if isValidInId(inId): 169 for neighbor in self.metric.getNeighbor(inId, *args, **kwargs): 170 if isValidInId(neighbor): 171 yield neighbor
172 173
174 - def getNeighbors(self, outId, *args, **kwargs):
175 """Return the list of coordinates for the neighbors. 176 177 By default it simply constructs the list based on 178 the generator returned by getNeighbor() 179 """ 180 return [ x for x in self.getNeighbor(outId, *args, **kwargs) ]
181 182
183 - def __repr__(self):
184 if self.__metric is not None: 185 s = "metric=%s" % repr(self.__metric) 186 else: 187 s = '' 188 return "%s(%s)" % (self.__class__.__name__, s)
189 190
191 - def __call__(self, data):
192 """Calls the mappers forward() method. 193 """ 194 return self.forward(data)
195 196
197 - def getMetric(self):
198 """To make pylint happy""" 199 return self.__metric
200 201
202 - def setMetric(self, metric):
203 """To make pylint happy""" 204 if metric is not None and not isinstance(metric, Metric): 205 raise ValueError, "metric for Mapper must be an " \ 206 "instance of a Metric class . Got %s" \ 207 % `type(metric)` 208 self.__metric = metric
209 210 211 metric = property(fget=getMetric, fset=setMetric) 212 nfeatures = VProperty(fget=getOutSize)
213 214 215
216 -class ProjectionMapper(Mapper):
217 """Mapper using a projection matrix to transform the data. 218 219 This class cannot be used directly. Sub-classes have to implement 220 the `_train()` method, which has to compute the projection matrix 221 given a dataset (see `_train()` docstring for more information). 222 223 Once the projection matrix is available, this class provides 224 functionality to perform forward and backwards mapping of data, the 225 latter using the hermitian (conjugate) transpose of the projection 226 matrix. Additionally, `ProjectionMapper` supports optional (but done 227 by default) demeaning of the data and selection of arbitrary 228 component (i.e. columns of the projection matrix) of the projection. 229 230 Forward and back-projection matrices (a.k.a. *projection* and 231 *reconstruction*) are available via the `proj` and `recon` 232 properties. the latter only after it has been computed (after first 233 call to `reverse`). 234 """ 235
236 - def __init__(self, selector=None, demean=True):
237 """Initialize the ProjectionMapper 238 239 :Parameters: 240 selector: None | list 241 Which components (i.e. columns of the projection matrix) 242 should be used for mapping. If `selector` is `None` all 243 components are used. If a list is provided, all list 244 elements are treated as component ids and the respective 245 components are selected (all others are discarded). 246 demean: bool 247 Either data should be demeaned while computing 248 projections and applied back while doing reverse() 249 250 """ 251 Mapper.__init__(self) 252 253 self._selector = selector 254 self._proj = None 255 """Forward projection matrix.""" 256 self._recon = None 257 """Reverse projection (reconstruction) matrix.""" 258 self._demean = demean 259 """Flag whether to demean the to be projected data, prior to projection. 260 """ 261 self._mean = None 262 """Data mean""" 263 self._mean_out = None 264 """Forward projected data mean."""
265 266 __doc__ = enhancedDocString('ProjectionMapper', locals(), Mapper) 267 268
269 - def train(self, dataset):
270 """Determine the projection matrix.""" 271 # store the feature wise mean 272 self._mean = dataset.samples.mean(axis=0) 273 # compute projection matrix with subclass logic 274 self._train(dataset) 275 276 # perform component selection 277 if self._selector is not None: 278 self.selectOut(self._selector)
279 280
281 - def _demeanData(self, data):
282 """Helper which optionally demeans 283 """ 284 if self._demean: 285 # demean the training data 286 data = data - self._mean 287 288 if __debug__ and "MAP_" in debug.active: 289 debug("MAP_", 290 "%s: Mean of data in input space %s was subtracted" % 291 (self.__class__.__name__, self._mean)) 292 return data
293 294
295 - def _train(self, dataset):
296 """Worker method. Needs to be implemented by subclass. 297 298 This method has to train the mapper and store the resulting 299 transformation matrix in `self._proj`. 300 """ 301 raise NotImplementedError
302 303
304 - def forward(self, data, demean=None):
305 """Perform forward projection. 306 307 :Parameters: 308 data: ndarray 309 Data array to map 310 demean: boolean | None 311 Override demean setting for this method call. 312 313 :Returns: 314 NumPy array 315 """ 316 # let arg overwrite instance flag 317 if demean is None: 318 demean = self._demean 319 320 if self._proj is None: 321 raise RuntimeError, "Mapper needs to be train before used." 322 if demean and self._mean is not None: 323 return ((N.asmatrix(data) - self._mean) * self._proj).A 324 else: 325 return (N.asmatrix(data) * self._proj).A
326 327
328 - def reverse(self, data):
329 """Reproject (reconstruct) data into the original feature space. 330 331 :Returns: 332 NumPy array 333 """ 334 if self._proj is None: 335 raise RuntimeError, "Mapper needs to be trained before used." 336 337 # get feature-wise mean in out-space 338 if self._demean and self._mean_out is None: 339 # forward project mean and cache result 340 self._mean_out = self.forward(self._mean, demean=False) 341 if __debug__: 342 debug("MAP_", 343 "Mean of data in input space %s became %s in " \ 344 "outspace" % (self._mean, self._mean_out)) 345 346 347 # (re)build reconstruction matrix 348 if self._recon is None: 349 self._recon = self._proj.H 350 351 if self._demean: 352 return ((N.asmatrix(data) + self._mean_out) * self._recon).A 353 else: 354 return ((N.asmatrix(data)) * self._recon).A
355 356
357 - def getInSize(self):
358 """Returns the number of original features.""" 359 return self._proj.shape[0]
360 361
362 - def getOutSize(self):
363 """Returns the number of components to project on.""" 364 return self._proj.shape[1]
365 366
367 - def selectOut(self, outIds):
368 """Choose a subset of components (and remove all others).""" 369 self._proj = self._proj[:, outIds] 370 # invalidate reconstruction matrix 371 self._recon = None 372 self._mean_out = None
373 374 375 proj = property(fget=lambda self: self._proj, doc="Projection matrix") 376 recon = property(fget=lambda self: self._recon, doc="Backprojection matrix")
377 378 379
380 -class CombinedMapper(Mapper):
381 """Meta mapper that combines several embedded mappers. 382 383 This mapper can be used the map from several input dataspaces into a common 384 output dataspace. When :meth:`~mvpa.mappers.base.CombinedMapper.forward` 385 is called with a sequence of data, each element in that sequence is passed 386 to the corresponding mapper, which in turned forward-maps the data. The 387 output of all mappers is finally stacked (horizontally or column or 388 feature-wise) into a single large 2D matrix (nsamples x nfeatures). 389 390 .. note:: 391 This mapper can only embbed mappers that transform data into a 2D 392 (nsamples x nfeatures) representation. For mappers not supporting this 393 transformation, consider wrapping them in a 394 :class:`~mvpa.mappers.base.ChainMapper` with an appropriate 395 post-processing mapper. 396 397 CombinedMapper fully supports forward and backward mapping, training, 398 runtime selection of a feature subset (in output dataspace) and retrieval 399 of neighborhood information. 400 """
401 - def __init__(self, mappers, **kwargs):
402 """ 403 :Parameters: 404 mappers: list of Mapper instances 405 The order of the mappers in the list is important, as it will define 406 the order in which data snippets have to be passed to 407 :meth:`~mvpa.mappers.base.CombinedMapper.forward`. 408 **kwargs 409 All additional arguments are passed to the base-class constructor. 410 """ 411 Mapper.__init__(self, **kwargs) 412 413 if not len(mappers): 414 raise ValueError, \ 415 'CombinedMapper needs at least one embedded mapper.' 416 417 self._mappers = mappers
418 419
420 - def forward(self, data):
421 """Map data from the IN spaces into to common OUT space. 422 423 :Parameter: 424 data: sequence 425 Each element in the `data` sequence is passed to the corresponding 426 embedded mapper and is mapped individually by it. The number of 427 elements in `data` has to match the number of embedded mappers. Each 428 element is `data` has to provide the same number of samples 429 (first dimension). 430 431 :Returns: 432 array: nsamples x nfeatures 433 Horizontally stacked array of all embedded mapper outputs. 434 """ 435 if not len(data) == len(self._mappers): 436 raise ValueError, \ 437 "CombinedMapper needs a sequence with data for each " \ 438 "Mapper" 439 440 # return a big array for the result of the forward mapped data 441 # of each embedded mapper 442 try: 443 return N.hstack( 444 [self._mappers[i].forward(d) for i, d in enumerate(data)]) 445 except ValueError: 446 raise ValueError, \ 447 "Embedded mappers do not generate same number of samples. " \ 448 "Check input data."
449 450
451 - def reverse(self, data):
452 """Reverse map data from OUT space into the IN spaces. 453 454 :Parameter: 455 data: array 456 Single data array to be reverse mapped into a sequence of data 457 snippets in their individual IN spaces. 458 459 :Returns: 460 list 461 """ 462 # assure array and transpose 463 # i.e. transpose of 1D does nothing, but of 2D puts features 464 # along first dimension 465 data = N.asanyarray(data).T 466 467 if not len(data) == self.getOutSize(): 468 raise ValueError, \ 469 "Data shape does match mapper reverse mapping properties." 470 471 result = [] 472 fsum = 0 473 for m in self._mappers: 474 # calculate upper border 475 fsum_new = fsum + m.getOutSize() 476 477 result.append(m.reverse(data[fsum:fsum_new].T)) 478 479 fsum = fsum_new 480 481 return result
482 483
484 - def train(self, dataset):
485 """Trains all embedded mappers. 486 487 The provided training dataset is splitted appropriately and the 488 corresponding pieces are passed to the 489 :meth:`~mvpa.mappers.base.Mapper.train` method of each embedded mapper. 490 491 :Parameter: 492 dataset: :class:`~mvpa.datasets.base.Dataset` or subclass 493 A dataset with the number of features matching the `outSize` of the 494 `CombinedMapper`. 495 """ 496 if dataset.nfeatures != self.getOutSize(): 497 raise ValueError, "Training dataset does not match the mapper " \ 498 "properties." 499 500 fsum = 0 501 for m in self._mappers: 502 # need to split the dataset 503 fsum_new = fsum + m.getOutSize() 504 m.train(dataset.selectFeatures(range(fsum, fsum_new))) 505 fsum = fsum_new
506 507
508 - def getInSize(self):
509 """Returns the size of the entity in input space""" 510 return N.sum(m.getInSize() for m in self._mappers)
511 512
513 - def getOutSize(self):
514 """Returns the size of the entity in output space""" 515 return N.sum(m.getOutSize() for m in self._mappers)
516 517
518 - def selectOut(self, outIds):
519 """Remove some elements and leave only ids in 'out'/feature space. 520 521 .. note:: 522 The subset selection is done inplace 523 524 :Parameter: 525 outIds: sequence 526 All output feature ids to be selected/kept. 527 """ 528 # determine which features belong to what mapper 529 # and call its selectOut() accordingly 530 ids = N.asanyarray(outIds) 531 fsum = 0 532 for m in self._mappers: 533 # bool which meta feature ids belongs to this mapper 534 selector = N.logical_and(ids < fsum + m.getOutSize(), ids >= fsum) 535 # make feature ids relative to this dataset 536 selected = ids[selector] - fsum 537 fsum += m.getOutSize() 538 # finally apply to mapper 539 m.selectOut(selected)
540 541
542 - def getNeighbor(self, outId, *args, **kwargs):
543 """Get the ids of the neighbors of a single feature in output dataspace. 544 545 :Parameters: 546 outId: int 547 Single id of a feature in output space, whos neighbors should be 548 determined. 549 *args, **kwargs 550 Additional arguments are passed to the metric of the embedded 551 mapper, that is responsible for the corresponding feature. 552 553 Returns a list of outIds 554 """ 555 fsum = 0 556 for m in self._mappers: 557 fsum_new = fsum + m.getOutSize() 558 if outId >= fsum and outId < fsum_new: 559 return m.getNeighbor(outId - fsum, *args, **kwargs) 560 fsum = fsum_new 561 562 raise ValueError, "Invalid outId passed to CombinedMapper.getNeighbor()"
563 564
565 - def __repr__(self):
566 s = Mapper.__repr__(self).rstrip(' )') 567 # beautify 568 if not s[-1] == '(': 569 s += ' ' 570 s += 'mappers=[%s])' % ', '.join([m.__repr__() for m in self._mappers]) 571 return s
572 573 574
575 -class ChainMapper(Mapper):
576 """Meta mapper that embedded a chain of other mappers. 577 578 Each mapper in the chain is called successively to perform forward or 579 reverse mapping. 580 581 .. note:: 582 583 In its current implementation the `ChainMapper` treats all but the last 584 mapper as simple pre-processing (in forward()) or post-processing (in 585 reverse()) steps. All other capabilities, e.g. training and neighbor 586 metrics are provided by or affect *only the last mapper in the chain*. 587 588 With respect to neighbor metrics this means that they are determined 589 based on the input space of the *last mapper* in the chain and *not* on 590 the input dataspace of the `ChainMapper` as a whole 591 """
592 - def __init__(self, mappers, **kwargs):
593 """ 594 :Parameters: 595 mappers: list of Mapper instances 596 **kwargs 597 All additional arguments are passed to the base-class constructor. 598 """ 599 Mapper.__init__(self, **kwargs) 600 601 if not len(mappers): 602 raise ValueError, 'ChainMapper needs at least one embedded mapper.' 603 604 self._mappers = mappers
605 606
607 - def forward(self, data):
608 """Calls all mappers in the chain successively. 609 610 :Parameter: 611 data 612 data to be chain-mapped. 613 """ 614 mp = data 615 for m in self._mappers: 616 mp = m.forward(mp) 617 618 return mp
619 620
621 - def reverse(self, data):
622 """Calls all mappers in the chain successively, in reversed order. 623 624 :Parameter: 625 data: array 626 data array to be reverse mapped into the orginal dataspace. 627 """ 628 mp = data 629 for m in reversed(self._mappers): 630 mp = m.reverse(mp) 631 632 return mp
633 634
635 - def train(self, dataset):
636 """Trains the *last* mapper in the chain. 637 638 :Parameter: 639 dataset: :class:`~mvpa.datasets.base.Dataset` or subclass 640 A dataset with the number of features matching the `outSize` of the 641 last mapper in the chain (which is identical to the one of the 642 `ChainMapper` itself). 643 """ 644 if dataset.nfeatures != self.getOutSize(): 645 raise ValueError, "Training dataset does not match the mapper " \ 646 "properties." 647 648 self._mappers[-1].train(dataset)
649 650
651 - def getInSize(self):
652 """Returns the size of the entity in input space""" 653 return self._mappers[0].getInSize()
654 655
656 - def getOutSize(self):
657 """Returns the size of the entity in output space""" 658 return self._mappers[-1].getOutSize()
659 660
661 - def selectOut(self, outIds):
662 """Remove some elements from the *last* mapper in the chain. 663 664 :Parameter: 665 outIds: sequence 666 All output feature ids to be selected/kept. 667 """ 668 self._mappers[-1].selectOut(outIds)
669 670
671 - def getNeighbor(self, outId, *args, **kwargs):
672 """Get the ids of the neighbors of a single feature in output dataspace. 673 674 .. note:: 675 676 The neighbors are determined based on the input space of the *last 677 mapper* in the chain and *not* on the input dataspace of the 678 `ChainMapper` as a whole! 679 680 :Parameters: 681 outId: int 682 Single id of a feature in output space, whos neighbors should be 683 determined. 684 *args, **kwargs 685 Additional arguments are passed to the metric of the embedded 686 mapper, that is responsible for the corresponding feature. 687 688 Returns a list of outIds 689 """ 690 return self._mappers[-1].getNeighbor(outId, *args, **kwargs)
691 692
693 - def __repr__(self):
694 s = Mapper.__repr__(self).rstrip(' )') 695 # beautify 696 if not s[-1] == '(': 697 s += ' ' 698 s += 'mappers=[%s])' % ', '.join([m.__repr__() for m in self._mappers]) 699 return s
700