# -*- mode: python;-*- """ GCAL: Gain Control, Adaptation, Laterally connected Simple but robust single-population V1 model orientation map from: Jean-Luc R. Stevens, Judith S. Law, Jan Antolik, and James A. Bednar. Mechanisms for stable, robust, and adaptive development of orientation maps in the primary visual cortex. Journal of Neuroscience, 33:15747-15766, 2013. Development of orientation maps in ferret and cat primary visual cortex (V1) has been shown to be stable, in that the earliest measurable maps are similar in form to the eventual adult map, robust, in that similar maps develop in both dark rearing and in a variety of normal visual environments, and yet adaptive, in that the final map pattern reflects the statistics of the specific visual environment. How can these three properties be reconciled? Using mechanistic models of the development of neural connectivity in V1, we show for the first time that realistic stable, robust, and adaptive map development can be achieved by including two low-level mechanisms originally motivated from single-neuron results. Specifically, contrast-gain control in the retinal ganglion cells and the lateral geniculate nucleus reduces variation in the presynaptic drive due to differences in input patterns, while homeostatic plasticity of V1 neuron excitability reduces the postsyn- aptic variability in firing rates. Together these two mechanisms, thought to be applicable across sensory systems in general, lead to biological maps that develop stably and robustly, yet adapt to the visual environment. The modeling results suggest that topographic map stability is a natural outcome of low-level processes of adaptation and normalization. The resulting model is more realistic, simpler, and far more robust, and is thus a good starting point for future studies of cortical map development. @article{Stevens02102013, author = {Stevens, Jean-Luc R. and Law, Judith S. and Antolik, Jan and Bednar, James A.}, title = {Mechanisms for Stable, Robust, and Adaptive Development of Orientation Maps in the Primary Visual Cortex}, journal = {The Journal of Neuroscience} volume = {33}, number = {40}, pages = {15747-15766}, year = {2013}, doi = {10.1523/JNEUROSCI.1037-13.2013}, url = {http://www.jneurosci.org/content/33/40/15747.full} } ================= Running the model ================= This model may be run with the Topographica simulator as follow: ./topographica -g gcal.ty A suitable version of the Topographica simulator may be obtained using git: git clone https://github.com/ioam/topographica.git cd topographica git submodule update --init git checkout GCALModelDB After 10000 iterations (~ 15 minutes on a quad-core 3Ghz machine), the orientation map corresponding to the condition shown in Figure 9F can be measured (GCAL model at 100% contrast). In the GUI, this may be viewed by opening the Orientation Preference window (Plots -> Preference Maps -> Orientation Preference). The default settings are appropriate for quick simulations, suitable for regular exploratory work. An exact match to Figure 9E requires a slower simulation: ./topographica -p cortex_density=98 -p area=1.5 -g gcal.ty The linear density of cortical units is doubled for a cortical area of 1.5 x 1.5. Making the cortical area 2.25 times larger allows border effects to be eliminated from the central 1.0 x 1.0 area. With four times more cortical units due to the higher density, the overall network is nine times larger. ================================= Reproducing all published figures ================================= An IPython Notebook detailing how to reproduce the entire paper (including all figures and simulations) may be viewed or run from here: http://topographica.org/_static/stevens_jn13.html """ from math import pi import os import numpy import param from topo import learningfn,numbergen,transferfn,pattern,projection,responsefn,sheet import topo.learningfn.optimized import topo.learningfn.projfn import topo.transferfn.optimized import topo.pattern.random import topo.pattern.image import topo.responsefn.optimized import topo.sheet.lissom import topo.sheet.optimized import topo.transferfn.misc from topo.base.arrayutil import DivideWithConstant from topo.transferfn.misc import HomeostaticResponse from topo.misc.commandline import global_params as p p.add( input_seed = param.Integer(default=102, doc=""" The random seed to set the input patterns"""), dataset=param.ObjectSelector(default='Gaussian',objects= ['Gaussian','Nature', 'VGR'],doc=""" Set of input patterns to use:: :'Gaussian': Two-dimensional Gaussians :'Nature': Shouval's 1999 monochrome 256x256 images. :'VGR': Simulated vertical goggle rearing (anisotropically blurred Shouval)"""), num_inputs=param.Integer(default=2,bounds=(1,None),doc=""" How many input patterns to present per unit area at each iteration, when using discrete patterns (e.g. Gaussians)."""), aff_strength=param.Number(default=1.5,bounds=(0.0,None),doc=""" Overall strength of the afferent projection to V1."""), exc_strength=param.Number(default=1.7,bounds=(0.0,None),doc=""" Overall strength of the lateral excitatory projection to V1."""), inh_strength=param.Number(default=1.4,bounds=(0.0,None),doc=""" Overall strength of the lateral inhibitory projection to V1."""), aff_lr=param.Number(default=0.1,bounds=(0.0,None),doc=""" Learning rate for the afferent projection to V1."""), exc_lr=param.Number(default=0.0,bounds=(0.0,None),doc=""" Learning rate for the lateral excitatory projection to V1."""), inh_lr=param.Number(default=0.3,bounds=(0.0,None),doc=""" Learning rate for the lateral inhibitory projection to V1."""), area=param.Number(default=1.0,bounds=(0,None), inclusive_bounds=(False,True),doc=""" Linear size of cortical area to simulate. 2.0 gives a 2.0x2.0 Sheet area in V1."""), retina_density=param.Number(default=24.0,bounds=(0,None), inclusive_bounds=(False,True),doc=""" The nominal_density to use for the retina."""), lgn_density=param.Number(default=24.0,bounds=(0,None), inclusive_bounds=(False,True),doc=""" The nominal_density to use for the LGN."""), cortex_density=param.Number(default=49.0,bounds=(0,None), inclusive_bounds=(False,True),doc=""" The nominal_density to use for V1."""), # Noisy disk parameters retinal_waves=param.Integer(default=0,bounds=(0,None),doc=""" How many retinal wave (noisy disk) presentations before the chosen dataset is displayed. Zero for no noisy disk presentations otherwise 6000 has been typically used.."""), percent_noise = param.Number(default=50, doc=""" The percentage of the noise strength to the disk strength for noisy disks"""), contrast=param.Number(default=100, bounds=(0,100), inclusive_bounds=(True,True),doc=""" Brightness of the input patterns as a contrast (percent)."""), # Toggle between the L, GCL, AL and GCAL models gain_control = param.Boolean(default=True, doc=""" Whether or not gain-control (divisive lateral inhibitory) is to be applied in the LGN"""), homeostasis = param.Boolean(default=True, doc=""" Whether or not the homeostatic adaption should be applied in V1"""), t_init = param.Number(default=0.20, doc=""" The initial V1 threshold value. This value is static in the L and GCL models and adaptive in the AL and GCAL models.""") ) ### Input patterns # Scale of 1.0 is equivalent to 100% contrast. contrast_scale = p.contrast / 100.0 if p.dataset=="Gaussian": total_num_inputs=int(p.num_inputs*p.area**2) inputs=[pattern.Gaussian(x=numbergen.UniformRandom(lbound=-(p.area/2.0+0.25), ubound= (p.area/2.0+0.25),seed=p.input_seed+12+i), y=numbergen.UniformRandom(lbound=-(p.area/2.0+0.25), ubound= (p.area/2.0+0.25),seed=p.input_seed+35+i), orientation=numbergen.UniformRandom(lbound=-pi,ubound=pi,seed=p.input_seed+21+i), size=0.088388, aspect_ratio=4.66667, scale=contrast_scale) for i in xrange(total_num_inputs)] combined_inputs = pattern.SeparatedComposite(min_separation=0,generators=inputs) if p.dataset in ["Nature", "VGR"]: # Do not randomly rotate GR patches (otherwise horizontal bias is meaningless) patch_orientation = 0.0 if p.dataset=="VGR" else numbergen.UniformRandom(lbound=-pi,ubound=pi,seed=p.input_seed+65) if p.dataset=="Nature": image_filenames= [param.resolve_path("images/shouval/combined%02d.png"%(i+1)) for i in xrange(25)] else: # Actual ordering used in the paper for historical reasons, # makes little difference if ordering=range(1,26) is used instead. ordering = [1,25,8,24,22,23,4,15,17,13,5,20,18,14,3,6,12,10,21,7,9,2,16,11,19] image_filenames = [param.resolve_path("images/VGR/combined%02d_VGR.png" % i) for i in ordering] inputs=[pattern.image.FileImage(filename=f, scale=contrast_scale, size=10.0, x=numbergen.UniformRandom(lbound=-0.75,ubound=0.75,seed=p.input_seed+12), y=numbergen.UniformRandom(lbound=-0.75,ubound=0.75,seed=p.input_seed+36), orientation=patch_orientation) for f in image_filenames] combined_inputs = pattern.Selector(generators=inputs) noise_ratio = (p.percent_noise / 100.0) disk_scale= 1.0 / (1.0 + noise_ratio) rand_scale= noise_ratio / (1.0 + noise_ratio) disks_inputs=[topo.pattern.Composite(operator=numpy.add, scale=contrast_scale, generators=[topo.pattern.Disk( x=numbergen.UniformRandom(lbound=-2.125,ubound=2.125, seed=p.input_seed+12), y=numbergen.UniformRandom(lbound=-2.125,ubound=2.125, seed=p.input_seed+36), size=2.0, aspect_ratio=1.0, scale = disk_scale, bounds=sheet.BoundingBox(radius=1.125),smoothing=0.1), topo.pattern.random.UniformRandom(scale=rand_scale)])] retina_inputs = topo.pattern.Selector(generators=disks_inputs) if p.retinal_waves == 0: retina_inputs = combined_inputs else: topo.sim.schedule_command(p.retinal_waves, 'topo.sim["Retina"].set_input_generator(combined_inputs, push_existing=False)') ### Specify weight initialization, response function, and learning function projection.CFProjection.cf_shape=pattern.Disk(smoothing=0.0) projection.CFProjection.response_fn=responsefn.optimized.CFPRF_DotProduct_opt() projection.CFProjection.learning_fn=learningfn.optimized.CFPLF_Hebbian_opt() projection.CFProjection.weights_output_fns=[transferfn.optimized.CFPOF_DivisiveNormalizeL1_opt()] projection.SharedWeightCFProjection.response_fn=responsefn.optimized.CFPRF_DotProduct_opt() topo.sim['Retina']=sheet.GeneratorSheet(nominal_density=p.retina_density, input_generator=retina_inputs, period=1.0, phase=0.05, nominal_bounds=sheet.BoundingBox(radius=p.area/2.0+0.25+0.375+0.5)) lgn_surroundg = pattern.Gaussian(size=0.25,aspect_ratio=1.0, output_fns=[transferfn.DivisiveNormalizeL1()]) # LGN has lateral connections for divisive normalization for GCL and GCAL models for s in ['LGNOn','LGNOff']: extra_kwargs = dict(tsettle=2,strict_tsettle=1) if p.gain_control else dict(tsettle=0,strict_tsettle=0) topo.sim[s]=sheet.optimized.LISSOM_Opt( nominal_density=p.lgn_density, nominal_bounds=sheet.BoundingBox(radius=p.area/2.0+0.25+0.5), output_fns=[transferfn.misc.HalfRectify()], measure_maps=False, **extra_kwargs) if p.gain_control: topo.sim.connect(s,s,delay=0.05, name='LateralGC', dest_port=("Activity"),activity_group=(0.6, DivideWithConstant(c=0.11)), connection_type=projection.SharedWeightCFProjection, strength=0.6,weights_generator=lgn_surroundg, nominal_bounds_template=sheet.BoundingBox(radius=0.25)) learning_rate = 0.01 if p.homeostasis else 0.0 topo.sim["V1"] = sheet.lissom.LISSOM(nominal_density=p.cortex_density, tsettle=16, plastic=True, nominal_bounds=sheet.BoundingBox(radius=p.area/2.0), output_fns = [HomeostaticResponse(t_init=p.t_init, learning_rate=learning_rate)]) topo.sim["V1"].joint_norm_fn=topo.sheet.optimized.compute_joint_norm_totals_opt centerg = pattern.Gaussian(size=0.07385,aspect_ratio=1.0, output_fns=[transferfn.DivisiveNormalizeL1()]) surroundg = pattern.Gaussian(size=0.29540,aspect_ratio=1.0, output_fns=[transferfn.DivisiveNormalizeL1()]) on_weights = pattern.Composite(generators=[centerg,surroundg],operator=numpy.subtract) off_weights = pattern.Composite(generators=[surroundg,centerg],operator=numpy.subtract) strength_factor = 6.0 topo.sim.connect( 'Retina','LGNOn',delay=0.05,strength=2.33*strength_factor, name='Afferent', connection_type=projection.SharedWeightCFProjection, nominal_bounds_template=sheet.BoundingBox(radius=0.375), weights_generator=on_weights) topo.sim.connect( 'Retina','LGNOff',delay=0.05,strength=2.33*strength_factor, name='Afferent', connection_type=projection.SharedWeightCFProjection, nominal_bounds_template=sheet.BoundingBox(radius=0.375), weights_generator=off_weights) "Center surround (difference-of-Gaussian) weights successfully generated" # Adjust feedforward delays to allow a common measurement protocol with and without gain control. LGN_V1_delay = 0.05 if p.gain_control else 0.10 topo.sim.connect( 'LGNOn','V1',delay=LGN_V1_delay,strength=p.aff_strength,name='LGNOnAfferent', dest_port=('Activity','JointNormalize','Afferent'), connection_type=projection.CFProjection,learning_rate=p.aff_lr, nominal_bounds_template=sheet.BoundingBox(radius=0.27083), weights_generator= pattern.random.GaussianCloud(gaussian_size=2*0.27083), learning_fn=learningfn.optimized.CFPLF_Hebbian_opt()) topo.sim.connect( 'LGNOff','V1',delay=LGN_V1_delay,strength=p.aff_strength,name='LGNOffAfferent', dest_port=('Activity','JointNormalize','Afferent'), connection_type=projection.CFProjection,learning_rate=p.aff_lr, nominal_bounds_template=sheet.BoundingBox(radius=0.27083), weights_generator= pattern.random.GaussianCloud(gaussian_size=2*0.27083), learning_fn=learningfn.optimized.CFPLF_Hebbian_opt()) "Afferent GaussianCloud weights successfully generated." lateral_excitatory_weights = pattern.Gaussian(aspect_ratio=1.0, size=0.05) topo.sim.connect( 'V1','V1',delay=0.05,strength=p.exc_strength,name='LateralExcitatory', connection_type=projection.CFProjection,learning_rate=p.exc_lr, nominal_bounds_template=sheet.BoundingBox(radius=0.104), weights_generator=lateral_excitatory_weights) "Lateral excitatory weights successfully generated" lateral_inhibitory_weights = pattern.random.GaussianCloud(gaussian_size=0.15) topo.sim.connect( 'V1','V1',delay=0.05,strength=-1.0*p.inh_strength,name='LateralInhibitory', connection_type=projection.CFProjection,learning_rate=p.inh_lr, nominal_bounds_template=sheet.BoundingBox(radius=0.22917), weights_generator=lateral_inhibitory_weights) "Lateral inhibitory weights successfully generated" ### Default locations for model editor topo.sim.grid_layout([[None, 'V1', None], ['LGNOn', None, 'LGNOff'], [None, 'Retina', None]], xstart=150,item_scale=0.8) import topo.analysis.featureresponses topo.analysis.featureresponses.FeatureMaps.selectivity_multiplier=2.0 contrasts = [{'contrast':c} for c in [100, 80, 60, 40, 20, 10]] topo.analysis.featureresponses.FeatureCurveCommand.curve_parameters= contrasts