Package topo :: Package analysis :: Module featureresponses
[hide private]
[frames] | no frames]

Source Code for Module topo.analysis.featureresponses

   1  """ 
   2  FeatureResponses and associated functions and classes. 
   3   
   4  These classes implement map and tuning curve measurement based 
   5  on measuring responses while varying features of an input pattern. 
   6   
   7  $Id: featureresponses.py 11316 2010-07-27 17:52:53Z ceball $ 
   8  """ 
   9  __version__='$Revision: 11316 $' 
  10   
  11   
  12  import copy 
  13   
  14  from math import pi 
  15  from colorsys import hsv_to_rgb 
  16   
  17  import numpy 
  18  from numpy import zeros, empty, object_, size, vectorize, fromfunction 
  19  from numpy.oldnumeric import Float 
  20   
  21  import param 
  22  from param.parameterized import ParameterizedFunction, ParamOverrides 
  23   
  24  import topo 
  25  import topo.base.sheetcoords 
  26  from topo.base.arrayutil import wrap 
  27  from topo.base.cf import CFSheet 
  28  from topo.base.functionfamily import PatternDrivenAnalysis  
  29  from topo.base.sheet import Sheet, activity_type 
  30  from topo.base.sheetview import SheetView 
  31  from topo.command.basic import pattern_present,restore_input_generators, save_input_generators 
  32  from topo.misc.distribution import Distribution 
  33  from topo.misc.util import cross_product, frange 
  34  from topo import pattern 
  35  from topo.pattern.basic import SineGrating, Gaussian, RawRectangle, Disk 
  36  from topo.plotting.plotgroup import plotgroups 
  37  from topo.sheet import GeneratorSheet 
38 39 40 # CB: having a class called DistributionMatrix with an attribute 41 # distribution_matrix to hold the distribution matrix seems silly. 42 # Either rename distribution_matrix or make DistributionMatrix into 43 # a matrix. 44 -class DistributionMatrix(param.Parameterized):
45 """ 46 Maintains a matrix of Distributions (each of which is a dictionary 47 of (feature value: activity) pairs). 48 49 The matrix contains one Distribution for each unit in a 50 rectangular matrix (given by the matrix_shape constructor 51 argument). The contents of each Distribution can be updated for a 52 given bin value all at once by providing a matrix of new values to 53 update(). 54 55 The results can then be accessed as a matrix of weighted averages 56 (which can be used as a preference map) and/or a selectivity 57 map (which measures the peakedness of each distribution). 58 """
59 - def __init__(self,matrix_shape,axis_range=(0.0,1.0), cyclic=False,keep_peak=True):
60 """Initialize the internal data structure: a matrix of Distribution objects.""" 61 self.axis_range=axis_range 62 new_distribution = vectorize(lambda x: Distribution(axis_range,cyclic,keep_peak), 63 doc="Return a Distribution instance for each element of x.") 64 self.distribution_matrix = new_distribution(empty(matrix_shape))
65 66 67
68 - def update(self, new_values, bin):
69 """Add a new matrix of histogram values for a given bin value.""" 70 ### JABHACKALERT! The Distribution class should override +=, 71 ### rather than + as used here, because this operation 72 ### actually modifies the distribution_matrix, but that has 73 ### not yet been done. Alternatively, it could use a different 74 ### function name altogether (e.g. update(x,y)). 75 self.distribution_matrix + fromfunction(vectorize(lambda i,j: {bin:new_values[i,j]}), 76 new_values.shape)
77 78
79 - def weighted_average(self):
80 """Return the weighted average of each Distribution as a matrix.""" 81 82 weighted_average_matrix=zeros(self.distribution_matrix.shape,Float) 83 84 for i in range(len(weighted_average_matrix)): 85 for j in range(len(weighted_average_matrix[i])): 86 weighted_average_matrix[i,j]=self.distribution_matrix[i,j].weighted_average() 87 # weighted_average_matrix[i,j]=self.distribution_matrix[i,j].estimated_maximum() 88 89 90 return weighted_average_matrix
91 92
93 - def max_value_bin(self):
94 """Return the bin with the max value of each Distribution as a matrix.""" 95 96 max_value_bin_matrix=zeros(self.distribution_matrix.shape,Float) 97 98 for i in range(len(max_value_bin_matrix)): 99 for j in range(len(max_value_bin_matrix[i])): 100 max_value_bin_matrix[i,j]=self.distribution_matrix[i,j].max_value_bin() 101 102 return max_value_bin_matrix
103 104 105
106 - def selectivity(self):
107 """Return the selectivity of each Distribution as a matrix.""" 108 109 selectivity_matrix=zeros(self.distribution_matrix.shape,Float) 110 111 for i in range(len(selectivity_matrix)): 112 for j in range(len(selectivity_matrix[i])): 113 selectivity_matrix[i,j]=self.distribution_matrix[i,j].selectivity() 114 115 return selectivity_matrix
116
117 118 119 -class FullMatrix(param.Parameterized):
120 """ 121 Records the output of every unit in a sheet, for every combination of feature values. 122 Useful for collecting data for later analysis while presenting many input patterns. 123 """ 124
125 - def __init__(self,matrix_shape,features):
126 self.matrix_shape = matrix_shape 127 self.features = features 128 self.dimensions = () 129 for f in features: 130 self.dimensions = self.dimensions + (size(f.values),) 131 self.full_matrix = empty(self.dimensions,object_)
132 133
134 - def update(self, new_values, feature_value_permutation):
135 """Add a new matrix of histogram values for a given bin value.""" 136 index = () 137 for f in self.features: 138 for ff,value in feature_value_permutation: 139 if(ff == f.name): 140 index = index + (f.values.index(value),) 141 self.full_matrix[index] = new_values
142
143 144 145 146 # CB: FeatureResponses and ReverseCorrelation need cleanup; I began but haven't finished. 147 # JABALERT: At least: 148 # - Move features out of __init__ and into measure_responses 149 # - Change measure_responses to __call__, as it's the only thing this 150 # class really does 151 # - Make the other methods private (_) since they are for internal use 152 # - Possibly -- make the __call__ methods have the same signature? 153 # - Clean up the inheritance hierarchy? 154 155 156 -class FeatureResponses(PatternDrivenAnalysis):
157 """ 158 Systematically vary input pattern feature values and collate the responses. 159 160 Each sheet has a DistributionMatrix for each feature that will be 161 tested. The DistributionMatrix stores the distribution of 162 activity values for each unit in the sheet for that feature. For 163 instance, if the features to be tested are orientation and phase, 164 we will create a DistributionMatrix for orientation and a 165 DistributionMatrix for phase for each sheet. The orientation and 166 phase of the input are then systematically varied (when 167 measure_responses is called), and the responses of each unit 168 to each pattern are collected into the DistributionMatrix. 169 170 The resulting data can then be used to plot feature maps and 171 tuning curves, or for similar types of feature-based analyses. 172 """ 173 174 # CEB: we might want to measure the map on a sheet due 175 # to a specific projection, rather than measure the map due 176 # to all projections. 177 178 repetitions = param.Integer(default=1,bounds=(1,None),doc=""" 179 How many times each stimulus will be presented. 180 181 Each stimulus is specified by a particular feature 182 combination, and need only be presented once if the network 183 has no other source of variability. If results differ for 184 each presentation of an identical stimulus (e.g. due to 185 intrinsic noise), then this parameter can be increased 186 so that results will be an average over the specified 187 number of repetitions.""") 188 189 _fullmatrix = {} 190
191 - def __init__(self,features,**params):
196
197 - def initialize_featureresponses(self,features):
198 """Create an empty DistributionMatrix for each feature and each sheet.""" 199 self._featureresponses = {} 200 self._activities = {} 201 FeatureResponses._fullmatrix = {} 202 for sheet in self.sheets_to_measure(): 203 self._featureresponses[sheet] = {} 204 self._activities[sheet]=zeros(sheet.shape) 205 for f in features: 206 self._featureresponses[sheet][f.name]=DistributionMatrix(sheet.shape,axis_range=f.range,cyclic=f.cyclic) 207 FeatureResponses._fullmatrix[sheet] = FullMatrix(sheet.shape,features)
208
209 - def sheets_to_measure(self):
210 """Return a list of the Sheets in the current simulation for which to collect responses.""" 211 return [x for x in topo.sim.objects(Sheet).values() 212 if hasattr(x,'measure_maps') and x.measure_maps]
213
214 - def measure_responses(self,pattern_presenter,param_dict,features,display):
215 """Present the given input patterns and collate the responses.""" 216 217 # Run hooks before the analysis session 218 for f in self.pre_analysis_session_hooks: f() 219 220 self.param_dict=param_dict 221 self.pattern_presenter = pattern_presenter 222 223 features_to_permute = [f for f in features if f.compute_fn is None] 224 self.features_to_compute = [f for f in features if f.compute_fn is not None] 225 226 self.feature_names=[f.name for f in features_to_permute] 227 values_lists=[f.values for f in features_to_permute] 228 self.permutations = cross_product(values_lists) 229 values_description=' * '.join(["%d %s" % (len(f.values),f.name) for f in features_to_permute]) 230 231 self.refresh_act_wins=False 232 if display: 233 if hasattr(topo,'guimain'): 234 self.refresh_act_wins=True 235 else: 236 self.warning("No GUI available for display.") 237 238 # CEBALERT: when there are multiple sheets, this can make it seem 239 # like topographica's stuck in a loop (because the counter goes 240 # to 100% lots of times...e.g. hierarchical's orientation tuning fullfield.) 241 242 timer = copy.copy(topo.sim.timer) 243 timer.func = self.present_permutation 244 245 if hasattr(topo,'guimain'): 246 topo.guimain.open_progress_window(timer) 247 else: 248 self.verbose("Presenting %d test patterns (%s)." % (len(self.permutations),values_description)) 249 250 timer.call_fixed_num_times(self.permutations) 251 252 # Run hooks after the analysis session 253 for f in self.post_analysis_session_hooks: f()
254
255 - def present_permutation(self,permutation):
256 """Present a pattern with the specified set of feature values.""" 257 for sheet in self.sheets_to_measure(): 258 self._activities[sheet]*=0 259 260 # Calculate complete set of settings 261 permuted_settings = zip(self.feature_names, permutation) 262 complete_settings = permuted_settings + \ 263 [(f.name,f.compute_fn(permuted_settings)) for f in self.features_to_compute] 264 265 266 for i in xrange(0,self.repetitions): 267 topo.sim.state_push() 268 269 # Run hooks before and after pattern presentation. 270 # Could use complete_settings here, to avoid some 271 # PatternPresenter special cases, but that might cause 272 # conflicts with the existing PatternPresenter code. 273 for f in self.pre_presentation_hooks: f() 274 #valstring = " ".join(["%s=%s" % (n,v) for n,v in complete_settings]) 275 #self.message("Presenting pattern %s" % valstring) 276 self.pattern_presenter(dict(permuted_settings),self.param_dict) 277 for f in self.post_presentation_hooks: f() 278 279 if self.refresh_act_wins:topo.guimain.refresh_activity_windows() 280 for sheet in self.sheets_to_measure(): 281 self._activities[sheet]+=sheet.activity 282 topo.sim.state_pop() 283 284 for sheet in self.sheets_to_measure(): 285 self._activities[sheet]=self._activities[sheet] / self.repetitions 286 287 self._update(complete_settings)
288
289 - def _update(self,current_values):
290 # Update each DistributionMatrix with (activity,bin) 291 for sheet in self.sheets_to_measure(): 292 for feature,value in current_values: 293 self._featureresponses[sheet][feature].update(self._activities[sheet], value) 294 FeatureResponses._fullmatrix[sheet].update(self._activities[sheet],current_values)
295
296 297 298 -class ReverseCorrelation(FeatureResponses):
299 """ 300 Calculate the receptive fields for all neurons using reverse correlation. 301 """ 302 # CB: Can't we have a better class hierarchy? 303 304 input_sheet = param.Parameter(default=None) 305 306 # JABALERT: Should _featureresponses be renamed here?; It's a different 307 # data structure using different indexing (r,c instead of feature).
308 - def initialize_featureresponses(self,features): # CB: doesn't need features!
309 310 self._featureresponses = {} 311 assert hasattr(self.input_sheet,'shape') 312 313 # surely there's a way to get an array of 0s for each element without 314 # looping? (probably had same question for distributionmatrix). 315 for sheet in self.sheets_to_measure(): 316 self._featureresponses[sheet]= numpy.ones(sheet.activity.shape,dtype=object) 317 rows,cols = sheet.activity.shape 318 for r in range(rows): 319 for c in range(cols): 320 self._featureresponses[sheet][r,c] = numpy.zeros(self.input_sheet.shape) # need to specify dtype?
321 322
323 - def collect_feature_responses(self,pattern_presenter,param_dict,display,feature_values):
324 self.measure_responses(pattern_presenter,param_dict,feature_values,display) 325 326 for sheet in self.sheets_to_measure(): 327 rows,cols = sheet.activity.shape 328 input_bounds = self.input_sheet.bounds 329 input_sheet_views = self.input_sheet.sheet_views 330 331 for ii in range(rows): 332 for jj in range(cols): 333 view = SheetView((self._featureresponses[sheet][ii,jj],input_bounds), 334 sheet.name,sheet.precedence,topo.sim.time(),sheet.row_precedence) 335 x,y = sheet.matrixidx2sheet(ii,jj) 336 key = ('RFs',sheet.name,x,y) 337 input_sheet_views[key]=view
338 339 340 341
342 - def measure_responses(self,pattern_presenter,param_dict,features,display):
343 """Present the given input patterns and collate the responses.""" 344 345 # Since input_sheet's not fixed, we have to call this. Means that there are 346 # normally duplicate calls (e.g. gets called by __init__ and then gets called 347 # here for no reason except maybe the input_sheet got changed). Would be better 348 # to have the input_sheet fixed. 349 self.initialize_featureresponses(features) 350 351 super(ReverseCorrelation,self).measure_responses(pattern_presenter,param_dict, 352 features,display)
353
354 - def present_permutation(self,permutation):
355 """Present a pattern with the specified set of feature values.""" 356 357 # Calculate complete set of settings 358 permuted_settings = zip(self.feature_names, permutation) 359 complete_settings = permuted_settings + \ 360 [(f.name,f.compute_fn(permuted_settings)) for f in self.features_to_compute] 361 362 topo.sim.state_push() 363 364 # Run hooks before and after pattern presentation. 365 # Could use complete_settings here, to avoid some 366 # PatternPresenter special cases, but that might cause 367 # conflicts with the existing PatternPresenter code. 368 for f in self.pre_presentation_hooks: f() 369 #valstring = " ".join(["%s=%s" % (n,v) for n,v in complete_settings]) 370 #self.message("Presenting pattern %s" % valstring) 371 self.pattern_presenter(dict(permuted_settings),self.param_dict) 372 for f in self.post_presentation_hooks: f() 373 374 if self.refresh_act_wins:topo.guimain.refresh_activity_windows() 375 376 self._update(complete_settings) 377 378 topo.sim.state_pop()
379 380 # Ignores current_values; they simply provide distinct patterns on the retina
381 - def _update(self,current_values):
382 for sheet in self.sheets_to_measure(): 383 rows,cols = sheet.activity.shape 384 for ii in range(rows): 385 for jj in range(cols): 386 self._featureresponses[sheet][ii,jj]+=sheet.activity[ii,jj]*self.input_sheet.activity
387
388 389 -class FeatureMaps(FeatureResponses):
390 """ 391 Measures and collects the responses to a set of features for calculating feature maps. 392 393 For each feature and each sheet, the results are stored as a 394 preference matrix and selectivity matrix in the sheet's 395 sheet_views; these can then be plotted as preference 396 or selectivity maps. 397 """ 398 399 selectivity_multiplier = param.Number(default=17,bounds=(0.0,None),doc=""" 400 Factor by which to multiply the calculated selectivity values 401 before plotting them. Usually set much greater than 1.0 to 402 highlight particularly unselective areas, especially when 403 combining selectivity with other plots as when using Confidence 404 subplots.""") 405 406 # CBENHANCEMENT: could allow full control over the generated names 407 # using a format parameter. The default would be 408 # ${prefix}${feature}${type} (where type is Preference or 409 # Selectivity) 410 sheet_views_prefix = param.String(default="",doc=""" 411 Prefix to add to the name under which results are stored in 412 sheet_views.""") 413
414 - def __init__(self,features,**params):
415 super(FeatureMaps,self).__init__(features,**params) 416 self.features=features
417
418 - def collect_feature_responses(self,pattern_presenter,param_dict,display,weighted_average=True):
419 """ 420 Present the given input patterns and collate the responses. 421 422 If weighted_average is True, the feature responses are 423 calculated from a weighted average of the values of each bin 424 in the distribution, rather than simply using the actual value 425 of the parameter for which response was maximal (the discrete 426 method). Such a computation will generally produce much more 427 precise maps using fewer test stimuli than the discrete 428 method. However, weighted_average methods generally require 429 uniform and full-range sampling, as described below, which is 430 not always feasible. 431 432 For measurements at evenly-spaced intervals over the full 433 range of possible parameter values, weighted_averages are a 434 good measure of the underlying continuous-valued parameter 435 preference, assuming that neurons are tuned broadly enough 436 (and/or sampled finely enough) that they respond to at least 437 two of the tested parameter values. This method will not 438 usually give good results when those criteria are not met, 439 i.e. if the sampling is too sparse, not at evenly-spaced 440 intervals, or does not cover the full range of possible 441 values. In such cases weighted_average should be set to 442 False, and the number of test patterns will usually need 443 to be increased instead. 444 """ 445 self.measure_responses(pattern_presenter,param_dict,self.features,display) 446 447 for sheet in self.sheets_to_measure(): 448 bounding_box = sheet.bounds 449 450 for feature in self._featureresponses[sheet].keys(): 451 ### JCHACKALERT! This is temporary to avoid the positionpref plot to shrink 452 ### Nevertheless we should think more about this (see alert in bitmap.py) 453 ### When passing a sheet_view that is not cropped to 1 in the parameter hue of hsv_to_rgb 454 ### it does not work... The normalization seems to be necessary in this case. 455 ### I guess it is always cyclic value that we will color with hue in an hsv plot 456 ### but still we should catch the error. 457 ### Also, what happens in case of negative values? 458 # CB: (see also ALERT by SheetView's norm_factor.) 459 #JL: Should be able to get rid of norm factor and incorporate with value_multiplier 460 #should also add similar method for selectivity_offset and selectivity_multiplier 461 cyclic = self._featureresponses[sheet][feature].distribution_matrix[0,0].cyclic 462 if cyclic: 463 norm_factor = self._featureresponses[sheet][feature].distribution_matrix[0,0].axis_range 464 else: 465 norm_factor = 1.0 466 467 468 value_offset = [f.value_offset for f in self.features if f.name==feature] 469 value_multiplier = [f.value_multiplier for f in self.features if f.name==feature] 470 471 472 if weighted_average: 473 474 preference_map = SheetView((((self._featureresponses[sheet][feature].weighted_average())+value_offset)*value_multiplier/norm_factor, 475 bounding_box), sheet.name, sheet.precedence, topo.sim.time()) 476 477 else: 478 preference_map = SheetView((((self._featureresponses[sheet][feature].max_value_bin())+value_offset)*value_multiplier/norm_factor, 479 bounding_box), sheet.name, sheet.precedence, topo.sim.time()) 480 481 preference_map.cyclic = cyclic 482 preference_map.norm_factor = norm_factor 483 484 485 sheet.sheet_views[self.sheet_views_prefix+feature.capitalize()+'Preference']=preference_map 486 487 selectivity_map = SheetView((self.selectivity_multiplier* 488 self._featureresponses[sheet][feature].selectivity(), 489 bounding_box), sheet.name , sheet.precedence, topo.sim.time(),sheet.row_precedence) 490 sheet.sheet_views[self.sheet_views_prefix+feature.capitalize()+'Selectivity']=selectivity_map
491
492 493 -class FeatureCurves(FeatureResponses):
494 """ 495 Measures and collects the responses to a set of features, for calculating tuning and similar curves. 496 497 These curves represent the response of a Sheet to patterns that 498 are controlled by a set of features. This class can collect data 499 for multiple curves, each with the same x axis. The x axis 500 represents the main feature value that is being varied, such as 501 orientation. Other feature values can also be varied, such as 502 contrast, which will result in multiple curves (one per unique 503 combination of other feature values). 504 505 The sheet responses used to construct the curves will be stored in 506 a dictionary curve_dict kept in the Sheet of interest. A 507 particular set of patterns is then constructed using a 508 user-specified PatternPresenter by adding the parameters 509 determining the curve (curve_param_dict) to a static list of 510 parameters (param_dict), and then varying the specified set of 511 features. The results can be accessed in the curve_dict, 512 indexed by the curve_label and feature value. 513 """ 514 post_collect_responses_hook = param.HookList(default=[],instantiate=False,doc=""" 515 List of callable objects to be run at the end of collect_feature_responses function. 516 The functions should accept three parameters: FullMatrix, curve label, sheet""") 517
518 - def __init__(self,features,sheet,x_axis):
519 super(FeatureCurves, self).__init__(features) 520 self.sheet=sheet 521 self.x_axis=x_axis 522 if hasattr(sheet, "curve_dict")==False: 523 sheet.curve_dict={} 524 sheet.curve_dict[x_axis]={}
525
526 - def sheets_to_measure(self):
527 return topo.sim.objects(CFSheet).values()
528
529 - def collect_feature_responses(self,features,pattern_presenter,param_dict,curve_label,display):
530 self.initialize_featureresponses(features) 531 rows,cols=self.sheet.shape 532 bounding_box = self.sheet.bounds 533 self.measure_responses(pattern_presenter,param_dict,features,display) 534 self.sheet.curve_dict[self.x_axis][curve_label]={} 535 for key in self._featureresponses[self.sheet][self.x_axis].distribution_matrix[0,0]._data.iterkeys(): 536 y_axis_values = zeros(self.sheet.shape,activity_type) 537 for i in range(rows): 538 for j in range(cols): 539 y_axis_values[i,j] = self._featureresponses[self.sheet][self.x_axis].distribution_matrix[i,j].get_value(key) 540 Response = SheetView((y_axis_values,bounding_box), self.sheet.