===== Introduction ===== Encoding models can be a powerful way to analyze data because they can reduce a high-dimensional stimulus space to a lower dimensional representation based on knowledge of how neural system encode stimuli and thus have more power to extract relevant information from cortical measurements. A channel encoding model, first introduced by [[https://www.jneurosci.org/content/29/44/13992.short|Brouwer & Heeger, 2009]] for the representation of color has been widely used to understand cortical representations of continuous stimulus variables like orientation, direction of motion and space. This tutorial will teach you how to simulate data for oriented stimuli that conform to the assumptions of the model, fit and invert the model. The basic idea is that each measurement unit (typically a voxel) is modeled to be the linear sum of underlying populations of neurons tuned for different orientations. So, for example if you present some orientation and get responses higher (red) or lower (blue) than the mean response, you would assume that higher responses are due to a stronger weighting of neural populations tuned for that orientation, and weaker responses due to stronger weighting of neural populations tuned for other orientations.
% some variables
iNeuron = 1;
orientations = 0:179;
k = 10;
% loop over each neuron tuning function
for orientPreference = 0:2:179
% compute the neural response as a Von Mises function
%Note the 2 here which makes it so that our 0 - 180 orientation
% space gets mapped to all 360 degrees
neuralResponse(iNeuron,:) = exp(k*cos(2*pi*(orientations-orientPreference)/180));
% normalize to a height of 1
neuralResponse(iNeuron,:) = neuralResponse(iNeuron,:) / max(neuralResponse(iNeuron,:));
% update counter
iNeuron = iNeuron + 1;
end
++++
If you got that working then, you should be able to plot a single neuron response function, let's try the 45th neuron - which should be tuned to 90 degees.
++++Matlab cheat sheet|
% plot the response of neuron 45
figure;
plot(orientations,neuralResponse(45,:));
xlabel('Orientation');
ylabel('Channel response (normalized units to 1)');
++++
Should look like the following.
++++Answer|
% make a random weighting of neurons on to each voxel
nNeurons = size(neuralResponse,1);
nVoxels = 250;
neuronToVoxelWeights = rand(nNeurons,nVoxels);
++++
Now, let's simulate an experiment in which we have nStimuli of different orientations. To keep things simple, we will have nStimuli=8 stimuli that are evenly spaced across orientations starting at 0. And we will have nRepeats (say 20) of each stimuli. In a real experiment, we would randomize the order of the stimuli so as to avoid adaptation and other non-stationary effects, but here we can just have them in a sequential order. We can start by making an array stimuli of length nStimuli x nRepeats with the stimulus values.
++++Matlab cheat sheet|
% make stimulus array
nStimuli = 8;
% evenly space stimuli
stimuli = 0:180/(nStimuli):179;
% number of repeats
nRepeats = 20;
stimuli = repmat(stimuli,1,nRepeats);
++++
A few simple things here - let's round all the stimulus values to the nearest integer degree (just for ease of calculation) and add one (because Matlab indexes starting with one and not zero) and make this into a column array
++++Matlab cheat sheet|
% round and make a column array
stimuli = round(stimuli(:))+1;
++++
ok, now we can compute the response to each stimulus. So we should make a voxelResponse matrix (dimensions nTrials = nStimuli x nRepeats by nVoxels).
++++Matlab cheat sheet|
% compute the voxelResponse
nTrials = nStimuli * nRepeats;
for iTrial = 1:nTrials
% get the neural response to this stimulus, by indexing the correct column of the neuralResponse matrix
thisNeuralResponse = neuralResponse(:,stimuli(iTrial));
% multiply this by the neuronToVoxelWeights to get the voxel response on this trial. Note that you need
% to get the matrix dimensions right, so transpose is needed on thisNeuralResponse
voxelResponse(iTrial,:) = thisNeuralResponse' * neuronToVoxelWeights;
end
++++
Great. let's see what we got. Plot the response to, say trial 7
++++Matlab cheat sheet|
% plot the voxelResponse for the 7th trial
figure;
plot(voxelResponse(7,:));
xlabel('Voxel (number)');
ylabel('Voxel response (fake measurement units)');
++++
++++Answer|
% plot another trial voxel response
figure;
plot(voxelResponse(7,:),'b-.');
hold on
plot(voxelResponse(7+nStimuli,:),'r-o');
xlabel('Voxel (number)');
ylabel('Voxel response (fake measurement units)');
++++
++++Answer|Yup, they are identical since we do not have any noise. Not very realistic, right?++++
Ok. Let's fix that by adding random gaussian noise to the voxelResponses (BTW - this is a a very simple noise model called IID - independent, identically distributed gaussian noise - in reality, noise is going to have some complex characteristics, for example, there might be more correlation between neighboring voxels than voxels spatially distant - or more correlation between voxels that receive similarly tuning - but for this purpose, let's go with this simple nosie model). Just to keep the scale understandable, let's normalize the voxel responses to have a mean of 1 and then add gaussian noise with a fixed noiseStandardDeviation of, say, 0.05.
++++Matlab cheat sheet|
% add noise to the voxel responses
noiseStandardDeviation = 0.05;
% normalize response
voxelResponse = voxelResponse / mean(voxelResponse(:));
% add gaussian noise
voxelResponse = voxelResponse + noiseStandardDeviation*randn(size(voxelResponse));
++++
Always check your work - now two responses should not be identical, but correlated. And not correlated with responses to very different orientations.
++++Matlab cheat sheet|
% check the voxelResponses
figure;
stim1 = 7;
stim2 = 3;
subplot(1,3,1);
plot(voxelResponse(stim1,:),'b-.');
hold on
plot(voxelResponse(stim1+nStimuli,:),'r-o');
xlabel('Voxel (number)');
ylabel('Voxel response (fake measurement units)');
subplot(1,3,2);
plot(voxelResponse(stim1,:),voxelResponse(stim1+nStimuli,:),'k.');
xlabel('Response to first presentation');
ylabel('Response to second presentation');
axis square
subplot(1,3,3);
plot(voxelResponse(stim1,:),voxelResponse(stim2,:),'k.');
xlabel(sprintf('Response to stimulus: %i deg',stimuli(stim1)));
ylabel(sprintf('Response to stimulus: %i',stimuli(stim2)));
axis square
++++
++++Answer|
% make channel basis functions
nChannels = 8;
exponent = 7;
orientations = 0:179;
prefOrientation = 0:180/nChannels:179;
% loop over each channel
for iChannel = 1:nChannels
% get sinusoid. Note the 2 here which makes it so that our 0 - 180 orientation
% space gets mapped to all 360 degrees
thisChannelBasis = cos(2*pi*(orientations-prefOrientation(iChannel))/180);
% rectify
thisChannelBasis(thisChannelBasis<0) = 0;
% raise to exponent
thisChannelBasis = thisChannelBasis.^exponent;
% keep in matrix
channelBasis(:,iChannel) = thisChannelBasis;
end
++++
Ok. If that worked, then we should have 8 channels that are tuned for different orientations. Let's plot it to make sure!
++++Matlab cheat sheet|
% plot channel basis functions
figure;
plot(orientations,channelBasis);
xlabel('Preferred orientation (deg)');
ylabel('Ideal channel response (normalized to 1)');
++++
Should look like the following:
++++Answer|
% compute the channelResponse for each trial
for iTrial = 1:nTrials
channelResponse(iTrial,:) = channelBasis(stimuli(iTrial),:);
end
++++
Easy, right? Now let's fit this model to the simulated data. Remember that the model that we have is:
channelResponses (nTrials x nChannels) x estimatedWeights (nChannels x nVoxels) = voxelResponses (nTrials x nVoxels)
You can solve this by doing your favorite least-squares estimation procedure (or if you want to be fancy, you could
do some robust regression technique). We'll just do the basic here.
++++Matlab cheat sheet|
% compute estimated weights
estimatedWeights = pinv(channelResponse) * voxelResponse;
++++
===== Model fit =====
Whenever you fit a model, it's always important to compute a measure of model fit - how well does the model actually fit the data. r2, amount of variance accounted for, is a good measure for this sort of data. So, compute that by seeing what the model predicts for the data (that's easy, that's just channelResponse x estimatedWeights and remove that from the data. Then compute 1 minus the residual variance / variance.
++++Matlab cheat sheet|
% compute model prediction
modelPrediction = channelResponse * estimatedWeights;
% compute residual
residualResponse = voxelResponse-modelPrediction;
% compute r2
r2 = 1-var(residualResponse(:))/var(voxelResponse(:))
++++
You should get a pretty good r2 value (like over 80-90% of variance accounted for). That would be terrific for real data - great working with a simulation, right?
===== Inverted encoding model =====
Ok. Now, let's invert the model to see what it predicts for channel responses. But, first we gotta talk about cross-validation. Always, always cross-validate. There, we are done talking about it. Here, let's do a simple version where we split the data into two (split-half cross-validation). Make two matrices from your voxelData where trainVoxelResponse is the first half trials and testVoxelResponse is the second half. Then compute estimatedWeights on the trainVoxelResponse.
++++Matlab cheat sheet|
% split half into train and test
firstHalf = 1:round(nTrials/2);
secondHalf = round(nTrials/2)+1:nTrials;
trainVoxelResponse = voxelResponse(firstHalf,:);
testVoxelResponse = voxelResponse(secondHalf:end,:);
% compute weights on train data
estimatedWeights = pinv(channelResponse(firstHalf,:))*trainVoxelResponse;
++++
Now on the second half of the data, compute the estimatedChannelResponse (look at above equations for channelResponses and use least-squares estimation
++++Matlab cheat sheet|
% compute channel response from textVoxelResponses
estimatedChannelResponse = testVoxelResponse * pinv(estimatedWeights);
++++
Ok. Let's see what we've got. Plot the mean estimatedChannelResponse for each stimulus type
++++Matlab cheat sheet|
% plot channel responses
figure;colors = hsv(nStimuli);
for iStimuli = 1:nStimuli
plot(prefOrientation,mean(estimatedChannelResponse(iStimuli:nStimuli:end,:),1),'-','Color',colors(iStimuli,:));
hold on
end
xlabel('Channel orientation preference (deg)');
ylabel('Estimated channel response (percentile of max)');
title(sprintf('r2 = %0.4f',r2));
++++
Should look like the following
++++Answer|
% Compute voxel response without noise
nTrials = nStimuli * nRepeats;
for iTrial = 1:nTrials
% get the neural response to this stimulus, by indexing the correct column of the neuralResponse matrix
thisNeuralResponse = neuralResponse(:,stimuli(iTrial));
% multiply this by the neuronToVoxelWeights to get the voxel response on this trial. Note that you need
% to get the matrix dimensions right, so transpose is needed on thisNeuralResponse
voxelResponseNoisy(iTrial,:) = thisNeuralResponse' * neuronToVoxelWeights;
end
% add noise
noiseStandardDeviation = 0.5;
% normalize response
voxelResponseNoisy = voxelResponseNoisy / mean(voxelResponseNoisy(:));
% add gaussian noise
voxelResponseNoisy = voxelResponseNoisy + noiseStandardDeviation*randn(size(voxelResponseNoisy));
% split into train and tes
trainVoxelResponseNoisy = voxelResponseNoisy(firstHalf,:);
testVoxelResponseNoisy = voxelResponseNoisy(secondHalf:end,:);
% compute weights on train data
estimatedWeights = pinv(channelResponse(firstHalf,:))*trainVoxelResponseNoisy;
% compute model prediction on test data
modelPrediction = channelResponse(secondHalf,:) * estimatedWeights;
% compute residual
residualResponse = testVoxelResponseNoisy-modelPrediction;
% compute r2
r2 = 1-var(residualResponse(:))/var(testVoxelResponseNoisy(:))
% invert model and compute channel response
estimatedChannelResponse = testVoxelResponseNoisy * pinv(estimatedWeights);
% plot estimated channel profiles
figure;colors = hsv(nStimuli);
for iStimuli = 1:nStimuli
plot(prefOrientation,mean(estimatedChannelResponse(iStimuli:nStimuli:end,:),1),'-','Color',colors(iStimuli,:));
hold on
end
xlabel('Channel orientation preference (deg)');
ylabel('Estimated channel response (percentile of max)');
title(sprintf('r2 = %0.4f',r2));
++++
This should make estimated channel response profiles that are more realistic
++++Answer|
% split half into train and test
firstHalf = 1:round(nTrials/2);
secondHalf = round(nTrials/2)+1:nTrials;
trainVoxelResponse = voxelResponse(firstHalf,:);
testVoxelResponse = voxelResponse(secondHalf:end,:);
% compute weights on train data
estimatedWeights = pinv(channelResponse(firstHalf,:))*trainVoxelResponse;
% compute weights on train data
estimatedWeights = pinv(channelResponse(firstHalf,:))*trainVoxelResponse;
% compute model prediction on test data
modelPrediction = channelResponse(secondHalf,:) * estimatedWeights;
% compute residual
residualResponse = testVoxelResponseNoisy-modelPrediction;
% compute residual variance, note that this is a scalar
residualVariance = var(residualResponse(:));
% make this into a covariance matrix in which the diagonal contains the variance for each voxel
% and off diagonals (in this case all 0) contain covariance between voxels
modelCovar = eye(nVoxels)*residualVariance;
++++
Ok, now we have our full model. The weights give you the mean response expected for any orientation
stimulus and the covariance matrix tells us the variation around that mean. So, now if we want
to figure out the probability of seeing any test response given any orientation, we simply compute
the probability as a multivariate gaussian probability distribution
++++Matlab cheat sheet|
% cycle over each trial
nTestTrials = size(testVoxelResponse,1);
for iTrial = 1:nTestTrials
% now cycle over all possible orientation
for iOrientation = 1:179
% compute the mean voxel response predicted by the channel encoding model
predictedResponse = channelBasis(iOrientation,:)*estimatedWeights;
% now use that mean response and the model covariance to estimate the probability
% of seeing this orientation given the response on this trial
likelihood(iTrial,iOrientation) = mvnpdf(testVoxelResponse(iTrial,:),predictedResponse,modelCovar);
end
end
% now plot the likelihood function averaged over repeats
figure
for iStimuli = 1:nStimuli
plot(1:179,mean(likelihood(iStimuli:nStimuli:end,:),1));
hold on
end
xlabel('stimulus orientation (deg)');
ylabel('probability given trial response');
++++
Should look like something like this:
++++Answer|
% reweight the channels
channelReweighting = [0 0.8 0.4 0 0 0 0.4 0.8]';
% make into a full matrix xform to transform the original channels
for iChannel = 1:nChannels
xform(iChannel,:) = circshift(channelReweighting,iChannel-1);
end
% and get new bimodal channels
bimodalChannelBasis = channelBasis * xform;
++++
Now, take a look at those channels
++++Matlab cheat sheet|
% display a figure with one of the channels
figure
plot(orientations,bimodalChannelBasis(:,5));
xlabel('orientation (deg)');
ylabel('Channel response (normalized to 1)');
++++
Should look like this
++++Answer|
% compute the channelResponse for each trial
for iTrial = 1:nTrials
channelResponse(iTrial,:) = bimodalChannelBasis(stimuli(iTrial),:);
end
% compute estimated weights
estimatedWeights = pinv(channelResponse) * voxelResponse;
% compute model prediction
modelPrediction = channelResponse * estimatedWeights;
% compute residual
residualResponse = voxelResponse-modelPrediction;
% compute r2
r2 = 1-var(residualResponse(:))/var(voxelResponse(:))
% compute estimated channel response profiles
estimatedChannelResponse = testVoxelResponse * pinv(estimatedWeights);
% and plot one of the channels averaged across all trials
figure;
plot(prefOrientation,mean(estimatedChannelResponse(5:nStimuli:end,:),1));
xlabel('Channel preferred orientation (deg)');
ylabel('Estimated channel response (percentile of full)');
title(sprintf('r2 = %0.4f',r2));
++++
Should look like the following
++++Answer|
% compute weights on train data
estimatedWeights = pinv(channelResponse(firstHalf,:))*trainVoxelResponse;
% compute model prediction on test data
modelPrediction = channelResponse(secondHalf,:) * estimatedWeights;
% compute residual
residualResponse = testVoxelResponseNoisy-modelPrediction;
% compute residual variance, note that this is a scalar
residualVariance = var(residualResponse(:));
% make this into a covariance matrix in which the diagonal contains the variance for each voxel
% and off diagonals (in this case all 0) contain covariance between voxels
modelCovar = eye(nVoxels)*residualVariance;
% cycle over each trial
nTestTrials = size(testVoxelResponse,1);
for iTrial = 1:nTestTrials
% now cycle over all possible orientation
for iOrientation = 1:179
% compute the mean voxel response predicted by the channel encoding model
predictedResponse = bimodalChannelBasis(iOrientation,:)*estimatedWeights;
% now use that mean response and the model covariance to estimate the probability
% of seeing this orientation given the response on this trial
likelihood(iTrial,iOrientation) = mvnpdf(testVoxelResponse(iTrial,:),predictedResponse,modelCovar);
end
end
% now plot the likelihood function averaged over repeats
figure
for iStimuli = 1:nStimuli
plot(1:179,mean(likelihood(iStimuli:nStimuli:end,:),1));
hold on
end
xlabel('stimulus orientation (deg)');
ylabel('probability given trial response');
++++
Should look like something like this:
++++Answer|