- Bosking et al 1997
The visual system is organized into a hierarchy of different interconnected areas of the brain. As signals propagate across the hierarchy of the visual system, the information represented in the signal becomes more complex. For example, the output cells of the primate retina, retinal ganglion cells, primarily transmit luminance contrast and chromatic contrast to the cortex. However, the response properties of neurons in V1 (the first visual cortical area) build upon this to represent the orientation of contrast edges.
The following code will visualize some responses from V1 cortical neurons from a data set made publicly available by the Bethge Lab. The neurons were recorded during a experiments where drifting sinusoid gratings were presented at different orientation angles. The following code will visualize the responses to all grating orientations for a subset of the neurons. To facilitate comparison between conditions, the response for each orientation will be smoothed using loess (locally weighted regression) smoothing. The image below shows a few examples of sinusoidal contrast gratings of different orientations similar to the stimuli used in this experiment.
Let’s begin by getting a feel for the data set. The data set holds recordings from electrode arrays in V1 and has different experimental sessions organized in different structures:
sSlength <- length( sortedSpikes$data )
sSlength## [1] 27
This set contains neural data from 27 separate experimental recordings. Each recording holds the binned spike data for a number of isolated neurons, or single units. For example, we can see the number of single units recorded for the first experiment session:
r1dim <- dim( sortedSpikes$data[[1]][[1]][[6]] )
r1dim_dt <- data.table( 'single_units' = r1dim[1], 'conditions' = r1dim[2],
'time_bins' = r1dim[3], 'repetitions' = r1dim[4] )
r1dim_dt## single_units conditions time_bins repetitions
## 1: 38 16 90 13
From the output above and referencing the data documentation, we see that the first recording session has data for 38 single units recorded over 16 different conditions. The data is binned into 90 time bins (each 10ms in duration). There were 13 trials for this experiment.
Let’s find the recording session with the most number of single units recorded and the most trials (repetitions):
num_units <- c()
num_trials <- c()
for ( sunit in 1:sSlength ) {
rdim <- dim( sortedSpikes$data[[1]][[1]][[6]] )
num_units[ sunit ] <- rdim[1]
num_trials[ sunit ] <- rdim[4]
}
exp_info_df <- data.frame( 'num_units' = num_units, 'num_trials' = num_trials )
distinct( exp_info_df )## num_units num_trials
## 1 38 13
Data was collected for the same number of units and trials for each recording session. Hmmmmm, that’s unusual in the wild…this data must have been cleaned up beforehand.
From the documentation that accompanied the dataset, we know fields contain the following information:
- date - date and time stamp when the session was recorded
- subject - identifies the monkey used in the session
- conditions - specifies the orientation and contrast used
- contamination - contamination of the single units, for details see paper; for both studies only units with a contamination value < 0.05 were used
- tetrode - specifies the tetrode a single unit was recorded on; for tetrode grid layout, see supplementary material of Ecker et al. (2010)
- spikes - contains binned spikes single units x conditions x time bins x repetitions
- times - times aligned to bin centers
Let’s explore the data further to learn about a few of these fields for a given recording session. Starting with the conditions. The following are the different stimuli conditions data was collected for (13 trials each condition):
session1 <- sortedSpikes$data[[1]][[1]]
session1[[3]]## , , 1
##
## [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12]
## contrast 100 100 100 100 100 100 100 100 100 100 100 100
## direction 0 22.5 45 67.5 90 112.5 135 157.5 180 202.5 225 247.5
## [,13] [,14] [,15] [,16]
## contrast 100 100 100 100
## direction 270 292.5 315 337.5
Data was collected for 16 different grating orientations. The goal of the following data visualization is to demonstrate that V1 cortical neurons have preferred orientation tunings. To illustrate this, we will be fitting smoothed non-parametric functions (method = loess) to the spike histograms constructed from the responses to each orientation condition. Only one contrast setting was used for this experiment, so we will exclude this feature from further analysis.
The following function will format the binned spike data for one of the (38) neurons recorded during this experimental session:
makeNeuronResponseDF <- function( array, session, neuron ){
session_dat <- array$data[[session]][[1]]
# feature columns with stimulus contrast and orientation information
stimmat <- matrix( unlist( session_dat[[3]] ), nrow = 2, byrow = F )
conditions_df <- data.frame( 'contrasts' = stimmat[1,], 'orientation' = stimmat[2,] )
columns <- c()
for(col in 1:dim(stimmat)[2]){
columns[ col ] <- toString( paste0( 'Con_', stimmat[ 1,col ], '_Deg_', stimmat[ 2,col ] ) )
}
binnedSpikes_df <- data.frame( matrix(ncol = length( columns ), nrow = 90))
colnames( binnedSpikes_df ) <- columns
# sum the binned spike data along the 3rd dimension.
# this will combine information from all trials for each condition
binnedSpikes <- rowSums( session_dat[[6]], dims = 3 )
numTrials <- dim(session_dat[[6]])[4]
# take the average response by diving by number of trials
for(col in 1:dim(stimmat)[2]){
binnedSpikes_df[ columns[ col ] ] <- binnedSpikes[neuron, col , 1:dim(session_dat[[6]])[3] ] *(100/13)
}
binnedSpikes_df$timebins <- session_dat[[7]][1:dim(session_dat[[6]])[3]]
return( list( conditions_df, binnedSpikes_df ) )
} The following function will visualize the responses formatted by makeNeuronResponseDF:
plotOrientationTuning <- function( neuronResponse_List ){
conditions <- neuronResponse_List[[1]]
responses <- neuronResponse_List[[2]]
names <- colnames( responses )
#make data long to plot by factor (condition)
long_dat <- responses %>%
pivot_longer( cols = !timebins, names_to = 'condition', values_to = 'responses' )
#specify the order of the factor
long_dat$condition <- factor( long_dat$condition, levels = names )
#custom color scale
colfunc <- colorRampPalette(c("red", "yellow", "green", "blue"))
plot <- ggplot( data = long_dat ) +
geom_vline( xintercept = 0, color = 'black' ) +
stat_smooth( aes( y= responses, x = timebins, col = condition ), se = F, span = 0.50, method = 'loess' ) +
scale_color_manual( name = 'condition', values = colfunc( 16 )) +
ylab( 'mean firing rate (spikes/ms)' ) +
theme_classic() +
ggtitle('Smoothed Response')
return( plot )
}Let’s use these functions to visualize the responses of the first neuron from the first recording session:
neuron1 <- makeNeuronResponseDF( sortedSpikes, 1, 1 )
plotOrientationTuning( neuron1 ) The figure above shows the smoothed binned response histograms for this particular neuron to drifting sinusoid gratings presented at 16 different orientations. The vertical solid black line at time = 0 marks the moment the stimulus was turned on. There is an approximately 50ms time period after stimulus onset while the response of the cell ramps up. There are fluctuations (likely owing to the periodicity of the sinusoid), but overall the response is sustained for the rest of the 500ms that the grating was presented. It is very obvious from the figure that the response is not equal for all orientations of the grating. Rather, there are some orientations where the stimulus is not much greater than baseline (i.e. 315 & 337.5 deg). On the other hand, there are other orientations that give a very strong response. For this cell 0 & 180 deg (same orientation but different direction of motion) give very strong responses. However, there are other orientations that the cells responds somewhat strongly to, therefore, we would say that this neuron has a relatively broad orientation tuning.
Let’s now use the functions we wrote to observe the preferred orientations of a few other neurons from the data set:
neuron2 <- makeNeuronResponseDF( sortedSpikes, 20, 15 )
plotOrientationTuning( neuron2 )Looking at the smoothed responses for this neuron, we see that there are two orientations, 67 & 247 deg, with very strong responses while the remaining orientations were much lower. 67 & 247 deg are 180 deg and therefore the same orientation but drifting in opposite directions. Because this cell really only responds to one orientation we would say that it has a narrow orientation tuning compared to the first neuron.
Now, let’s visualize several cells together to get a feel for the variety of responses:
neuron3 <- makeNeuronResponseDF( sortedSpikes, 27, 2 )
p1 <- plotOrientationTuning( neuron3 )
neuron3 <- makeNeuronResponseDF( sortedSpikes, 27, 3 )
p2 <- plotOrientationTuning( neuron3 )
neuron3 <- makeNeuronResponseDF( sortedSpikes, 27, 4 )
p3 <- plotOrientationTuning( neuron3 )
neuron3 <- makeNeuronResponseDF( sortedSpikes, 27, 5 )
p4 <- plotOrientationTuning( neuron3 )
neuron3 <- makeNeuronResponseDF( sortedSpikes, 27, 6 )
p5 <- plotOrientationTuning( neuron3 )
neuron3 <- makeNeuronResponseDF( sortedSpikes, 27, 7 )
p6 <- plotOrientationTuning( neuron3 )
grid.arrange( arrangeGrob( p1 + theme(legend.position="none"),
p2 + theme(legend.position="none"),
p3 + theme(legend.position="none"),
p4 + theme(legend.position="none"),
p5 + theme(legend.position="none"),
p6 + theme(legend.position="none"), nrow = 2 ) )This data set hosts the orientation responses of 1026 neurons, so the figures shown here only show a handful of the data. In a future blog post we will explore ways to summarize the orientation tuning properties for all recorded neurons.