๐ฉปfMRI
3. Design of experiments
date
Aug 5, 2023
slug
fmri-glm-3
author
status
Public
tags
NI-edu
Dartbrain
summary
type
Post
thumbnail
category
๐ฉปfMRI
updatedAt
Aug 5, 2023 11:04 AM
Types of designs
Factorial designs
- ์ง๋ ์๊ฐ์ ๋ฐฐ์ ๋ t-test์์ ํ์ฉ๋๋ contrast vector๋ฅผ ์ธ์๋ณด๋ ์ฐ์ต์ ํด๋ณด์
- (1) red squares, (2) red circles, (3) green squares, (4) green circles๋ก ์๊ทน์ด ์ฃผ์ด์ก๋ค๊ณ ๊ฐ์ ํด๋ณด์
exp_length = 160
TR = 1 # TR์ด 1์ธ ๊ฒฝ์ฐ์๋ ๋ค์ด์ํ๋ง์ ๋ฌด์ํ ์ ์์
xmax = exp_length // TR
y, X = simulate_signal(
onsets=np.array(
[0, 40, 80, 120, # red squares
10, 50, 90, 130, # red circles
20, 60, 100, 140, # green squares
30, 70, 110, 150] # green circles
),
conditions=['rq'] * 4 + ['rc'] * 4 + ['gs'] * 4 + ['gc'] * 4,
TR=TR,
duration=exp_length,
icept=0,
params_canon=[1, 2, 3, 4],
rnd_seed=42,
plot=False
)
X = X[:, :5]
plt.figure(figsize=(15, 5))
plt.plot(X[:, 1], c='r', ls='--')
plt.plot(X[:, 2], c='r')
plt.plot(X[:, 3], c='g', ls='--')
plt.plot(X[:, 4], c='g')
plt.legend(['red squares', 'red circles', 'green squares', 'green circles'], frameon=False)
plt.plot(y)
plt.xlim(0, xmax)
simulate_signal code
def simulate_signal(onsets, conditions, TR=2, duration=None, icept=0, params_canon=None, params_tderiv=None,
params_deriv1=None, phi=None, std_noise=1, osf=100, rnd_seed=None, plot=True):
""" Simulates a somewhat realistic voxel signal with an associated design matrix. """
if rnd_seed is not None:
np.random.seed(rnd_seed)
conds = sorted(np.unique(conditions))
P = len(conds)
if duration is None:
duration = np.max(onsets) + 30
if params_canon is None:
params_canon = np.zeros(P)
# TMP FIX
params_tderiv = params_deriv1
if params_tderiv is None:
params_tderiv = np.zeros(P)
X = np.zeros((duration * osf, P))
for ons, con in zip(onsets, conditions):
X[int(ons * osf), conds.index(con)] = 1
hrf = glover_hrf(tr=1, oversampling=osf)
hrf_d = glover_time_derivative(tr=1, oversampling=osf)
t_orig = np.arange(0, duration, 1 / osf)
t_new = np.arange(0, duration, TR)
Xconv = np.zeros((t_orig.size, P * 2))
idx = 0
for i, this_hrf in enumerate([hrf, hrf_d]):
this_hrf /= this_hrf.max()
for ii in range(P):
Xconv[:, idx] = np.convolve(X[:, ii], this_hrf)[:t_orig.size]
idx += 1
Xconv = np.c_[np.ones(Xconv.shape[0]), Xconv]
params = np.r_[icept, params_canon, params_tderiv]
y = Xconv @ params
resampler = interp1d(t_orig, y)
y = resampler(t_new)
if phi is None:
noise_cov = std_noise ** 2 * np.eye(y.size)
else:
noise_cov = std_noise ** 2 * phi ** toeplitz(np.arange(y.size))
y = y + np.random.multivariate_normal(np.zeros(y.size), noise_cov)
Xconv_ds = np.zeros((t_new.size, Xconv.shape[1]))
for i in range(Xconv.shape[1]):
resampler = interp1d(t_orig, Xconv[:, i])
Xconv_ds[:, i] = resampler(t_new)
est_betas = np.linalg.lstsq(Xconv_ds, y, rcond=None)[0]
if plot:
plt.figure(figsize=(15, 5))
plt.plot(y)
plt.plot(Xconv_ds @ est_betas)
plt.xlim(0, y.size)
plt.legend(['y', 'y-hat'])
plt.show()
return y, Xconv_dsQuestions
Question 1.
GLM ๋ชจ๋ธ์ ๋ง๋ค์ด๋ผ.
print(y.shape, X.shape) # X: intercept ํฌํจ๋์ด ์์
from numpy.linalg import inv
betas_todo = inv(X.T @ X) @ X.T @ yย
Question 2.
๋นจ๊ฐ์ ์๊ทน์ด ๋
น์ ์๊ทน๋ณด๋ค ๋ ๋ง์ ํ๋์ ์ผ์ผํจ๋ค๋ ๊ฐ์ค์ ์ธ์ฐ๋ ค๊ณ ํ๋ค. ์ด์ ๋ง๋ contrast vector๋ฅผ ๋ง๋ค์ด๋ผ.
cvec_red_green = np.array([0, 1, 1, -1, -1])ย
Question 3.
์ ์๊ทน์ด ์ฌ๊ฐํ ์๊ทน๋ณด๋ค ๋ ๋ง์ ํ๋์ ์ผ์ผํจ๋ค๋ ๊ฐ์ค์ ์ธ์ฐ๋ ค๊ณ ํ๋ค. ์ด์ ๋ง๋ contrast vector๋ฅผ ๋ง๋ค์ด๋ผ.
cvec_circle_square = np.array([0, -1, 1, -1, 1])ย
Question 4.
๋นจ๊ฐ์ ์๊ทน์ด ๋
น์ ์๊ทน๋ณด๋ค ๋ ๋ง์ ํ๋์ ์ผ์ผํค๋ฉด์, ์ ์๊ทน์ด ์ฌ๊ฐํ ์๊ทน๋ณด๋ค ๋ ๋ง์ ํ๋์ ์ผ์ผํจ๋ค๋ ๊ฐ์ค์ ์ธ์ฐ๋ ค๊ณ ํ๋ค. ์ด์ ๋ง๋ contrast vector๋ฅผ ๋ง๋ค์ด๋ผ.
cvec_interaction = np.array([0, -1, 1, 1, -1])Parametric designs
- ๋ณ์๋ categorical๋ฟ๋ง ์๋๋ผ continuous, ordinalํ ์ ์์
- ์ด๋ ๊ฒ ์ง์์ ์ผ๋ก ์ํฅ์ ๋ฏธ์น๋ ๋ณ์๋ฅผ ๊ฐ์ง๊ณ ๊ฐ์ค๋ก ์ค์ ํ๋ ๊ฒ์ parametric design์ด๋ผ๊ณ ๋ถ๋ฆ
- parametric design์ ๋ ๊ฐ์ง ๋ฐฉ์์ผ๋ก ๋ณต์ ๋ฐ์์ ์ํฅ์ ๋ฏธ์น๋ค๊ณ ๊ฐ์ ํจ
- An โunmodulatedโ response
- A parametric modulation of the response

- ์์ ์์์์ ๊ฐ์ค์ ๋ณด์ ๊ท๋ชจ์ ์ํ ๋ฐ์์ ๋ณด์ด๋ ๋ณต์ ์์ญ์ด ์๋์ง ์ฐพ๋ ๊ฒ์
np.random.seed(42)
exp_time = 60 * 10 # i.e., 14 minutes in seconds
n_trials = 30
reward_onsets = np.arange(4, exp_time, exp_time / n_trials).astype(int) # size: 30
reward_magnitudes = np.random.randint(1, 11, size=reward_onsets.size) / 10
plt.figure(figsize=(15, 5))
plt.plot(reward_magnitudes, marker='o')
- ์์ ๊ฐ์ค์ ๋ฐ๋ผ ์๊ทน์ด ์์ ๋ 0๊ณผ, ์๊ทน์ด ์์ ๋ 1์ ๊ฐ์ง ํ๊ท์์ ๋ง๋ค ๊ฒ์
- ์ด ๋, โunmodulatedโ response, parametric modulation of the response์ ๋ง์กฑํ๋ ์กฐ๊ฑด์ ๊ฐ์ง๋ ๋ ๊ฐ์ ํ๊ท์์ ๋ง๋ฆ
- regressor of โunmodulatedโ response
hrf = glover_hrf(tr=1, oversampling=1)
x_unmod = np.zeros(exp_time)
x_unmod[reward_onsets] = 1
x_unmod = np.convolve(x_unmod, hrf)[:exp_time] # โunmodulatedโ response
plt.figure(figsize=(15, 5))
plt.plot(x_unmod)
- ๋ณด์์ ํฌ๊ธฐ - ๋ณด์์ ํ๊ท ํฌ๊ธฐ๋ฅผ ํ์ฉํด์ ํ๊ท์์ ๋ง๋ฆ
- unmodulated regressor์์ modulated regressor๋งํผ decorrelate ํ๊ธฐ ๋๋ฌธ์
def parametric_modulation(reward_onsets, reward_magnitudes, exp_time, hrf, ans):
reward_magnitudes_ms = reward_magnitudes - reward_magnitudes.mean() # parametric modulation of the response
x_mod = np.zeros(exp_time)
x_mod[reward_onsets] = reward_magnitudes_ms
x_mod = np.convolve(x_mod, hrf)[:exp_time]- modulated regressor: ์๊ทน์ ๋ํ ์ผ๋ฐ์ ์ธ ๋ฐ์์ด ์๋๋ผ modulation of response์ ๋ํ ๋ถ์ฐ๋ง ๊ณ ๋ คํจ
Design variance/efficiency
Optimizing effects
Optimizing design variance
- design variance๊ฐ ๋์ผ๋ฉด ํจ์จ์ฑ์ด ๋ฎ๊ณ , design variance๊ฐ ๋ฎ์ผ๋ฉด ํจ์จ์ฑ์ด ๋์ (design variance๋ ๊ฐ ๋ณ์์ variance์ covariance๋ก ๊ตฌ์ฑ๋จ)
- ํจ์จ์ ์ธ ์ค๊ณ๋ ๋ค์๊ณผ ๊ฐ์ ์์ธ์ ๊ณ ๋ คํ๋ฉด ๋จ
- high varianceย (i.e. they should vary a lot relative to their mean)
- low covarianceย (i.e. they should not correlate between each other a lot)
- ์ฌ๊ธฐ์ ๋ ๋ถ์ฐ-๊ณต๋ถ์ฐ ํ๋ ฌ์, ๋ contrast vector๋ฅผ ์๋ฏธํจ
- ํฐ ํจ๊ณผ(t)๋ฅผ ์ป๊ธฐ ์ํด์๋ ์ธ๊ฐ์ง๊ฐ ํ์ํจ
- A large response/effect (ฮฒ)
- An efficient design (high variance, low covariance)
- Low noise/unexplained variance (lowย SSEย /ย DF)
def conceptual_tvalue_calculator(signal, noise, design_variance):
""" Calculate the effect (t-value) from the signal, noise, and design variance components.
Parameters
----------
signal : int/float
noise : int/float
design_variance : int/float
Returns
-------
effect : float
efficiency : float
"""
efficiency = 1 / design_variance
effect = signal / (noise * design_variance)
return effect, efficiency
SIGNAL = 0.5
NOISE = 2.8
DESIGN_VARIANCE = 0.02design_var = c @ inv(X.T @ X) @ c.T
efficiency = 1.0 / c @ inv(X.T @ X) @ c.TThe effect of predictor variance on design variance/efficiency
- ๋ณ์์ ๊ฐ์ด ํ๊ท ์์ ๋ ๋ง์ด ๋ฒ์ด๋ ์๋ก, ๋ถ์ฐ์ ์ปค์ง

- ํฐ ๋ถ์ฐ์ ๊ฐ์ง ๋ฐ์ดํฐ๋ ํฐ MSE๋ฅผ ๊ฐ์ง๋ฉฐ, ๋์ t-value๋ฅผ ๊ฐ์ง
- ํฐ ๋ถ์ฐ์ ๊ฐ์ง ๋ฐ์ดํฐ๋ ์์ ๋ถ์ฐ์ ๊ฐ์ง ๋ฐ์ดํฐ๋ณด๋ค design variance๊ฐ ์ ๋ค๋ ๊ฒ์ ์๋ฏธํจ
ย
The effect of predictor covariance on design variance/efficiency
Multicollinearity
- ๋ณ์๋ค ๊ฐ์ ๊ณต๋ถ์ฐ์ multicollinearity๋ผ๊ณ ๋ถ๋ฆ
- ๋ณ์๋ค ๊ฐ์ ์ฐ๊ด์ฑ์ด ๋์ ๊ฒฝ์ฐ, ์ด๋ค ๋ณ์์ ์ํ ๋ณํ์ธ์ง ์ ํํ๊ฒ ์ธก์ ํ ์ ์์ด์ efficiency๊ฐ ๋ฎ์
- (if correlation is high, then the โambiguity, about the source of an effect is also high)
from scipy.stats import pearsonr
corr = pearsonr(X[:, 1], X[:, 2])[0]
eff = 1.0 / cvec.dot(np.linalg.inv(X.T.dot(X))).dot(cvec.T)Evaluating multiple contrasts
- ํ๋์ ๊ธฐ์ค์ด ์๋ ์ฌ๋ฌ ๊ธฐ์ค(A, B, A-B)์ ๋ํ ๋ณํ๋ฅผ ์ธก์ ํ๊ณ ์ถ์ ๋ ์ฌ์ฉํจ
- ์๋์ ์์๋ contrast vector๋ A, B, A-B์ ๋ํ ๋ณํ๋ฅผ ์ธก์ ํจ
def overall_eff(X):
cvecs = [np.array([0, 1, 0]), np.array([0, 0, 1]), np.array([0, 1, -1])]
overall_eff_ans = len(cvecs) / np.sum([cvec @ inv(X.T @ X) @ cvec.T for cvec in cvecs])How should we design our experiment to maximize efficiency?
Estimation vs. detection
1) [Majority research] You want to know whether different conditions activate voxel activity differently.
โ HRF-amplitude ์๋ง ์ง์คํ๋ฉด ๋จ (detection)
2) [Small portion] You want investigate how different conditions influence voxel activity not only by investigating the โamplitudeโ parameter of the HRF, but also parameters relating to other properties of the shape of the HRF (like width, lag, strenght of undershoot, etc.).
โ HRF ๋ชจ์์ด ๋์ด ๋ฑ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง ์ ์์
โ HRF ํจ์์์ ๋ค์ํ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ์ฉํด์ผ ํจ
Event-related vs. blocked designs
- Event-related designs: ๋ค๋ฅธ ์๊ทน์ด ๋ฌด์์๋ก ์ํ
- Blocked designs: ๊ฐ์ ์๊ทน์ด ๋ธ๋ก์ผ๋ก ๊ทธ๋ฃนํ๋์ด ์ํ
np.random.seed(2)
N = 180
dg_hrf = glover_hrf(tr=1, oversampling=1)
# Blocked designs
blocked_pred1_onsets = list(range(10, 30)) + list(range(90, 110))
blocked_pred2_onsets = list(range(50, 70)) + list(range(130, 150))
N_stim = len(blocked_pred1_onsets)
blocked_pred1, blocked_pred2 = np.zeros(N), np.zeros(N)
blocked_pred1[blocked_pred1_onsets] = 1
blocked_pred2[blocked_pred2_onsets] = 1
icept = np.ones((N, 1))
X_blocked = np.hstack((
icept,
np.convolve(blocked_pred1, dg_hrf)[:N, np.newaxis],
np.convolve(blocked_pred2, dg_hrf)[:N, np.newaxis]
))
# Event-related designs
er_stims = np.arange(N)
er_pred1_onsets = np.random.choice(er_stims, N_stim, replace=False)
er_stims_new = np.array([o for o in er_stims if o not in er_pred1_onsets])
er_pred2_onsets = np.random.choice(er_stims_new, N_stim, replace=False)
er_pred1, er_pred2 = np.zeros(N), np.zeros(N)
er_pred1[er_pred1_onsets] = 1
er_pred2[er_pred2_onsets] = 1
X_er = np.hstack((
icept,
np.convolve(er_pred1, dg_hrf)[:N, np.newaxis],
np.convolve(er_pred2, dg_hrf)[:N, np.newaxis]
))
- ์ฐ๋ฆฌ๊ฐ detection์ ํฌ์ปค์ฑ์ ํ๊ณ ์ถ๋ค๋ฉด, covariance๊ฐ ๋ฎ๊ณ (correlation), variance๊ฐ ๋์(spread) blocked design์ ์ฌ์ฉํด์ผ ํ๋ค.
corr_blocked = pearsonr(X_blocked[:, 1], X_blocked[:, 2])
corr_er = pearsonr(X_er[:, 1], X_er[:, 2])
print(corr_blocked, corr_er)
>>> -0.238, -0.268The โparadoxโ of efficiency
- ์ฐ๋ฆฌ๋ ์ด์์๋ ์ธ๊ฐ์ fMRI ๋ฐ์ดํฐ๋ฅผ scanํ๋ฏ๋ก, ํจ์จ์ ์ธ Blocked designs์๋ง ์ง์คํ๋ ๊ฒ์ ์ค๋ฅ๋ฅผ ๋ณ์ ์ ์์
- fMRI ์คํ ์์ฒด์ ํน์ง(์ง๊ฒจ์, ์์ธก๊ฐ๋ฅ์ฑ, ์ต๊ด, ๋ถํ์ํ ์์ง์ ๋ฑ)๋ค์ ์ํด์ ์ค๋ฅ์ ๋น ์ง ์ ์์
- ํนํ ๋ถํ์ํ ์์ง์์ผ๋ก ์ธํด LTI(Linear time-invariant)๊ฐ ์๊ฒจ BOLD ์ ํธ๋ฅผ ๋ฐฉํดํจ
- ์ฌ๋ฆฌ์ ์์ธ ๋ฑ์ ๊ฐ๋ฅ์ฑ์ ์ค์ด๊ธฐ ์ํด, semi-random event-related designs์ ํ์ฉํ ํ์๊ฐ ์์
- ๊ทธ๋ ๋ค๋ฉด, event-related designs์ ์ด๋ป๊ฒ ๋์์ธํด์ผ ์ต์ ํํ ์ ์์์ง ๊ณ ๋ฏผํด๋ณผ ํ์ ์์
Improving design efficiency for event-related designs using jittering
- ์๊ทน์ด ์๋ ๋์์ ์งง์ ๊ธฐ๊ฐ์ ISI(inter-stimulus interval)์ด๋ผ๊ณ ๋ถ๋ฆ
- ์๋ ์์๋ 8์ด์ ISI(์๊ทน์ ๊ฐ๊ฒฉ)๋ฅผ ๊ฐ์ง
def simulate_data_fixed_ISI(N=420):
dg_hrf = glover_hrf(tr=1, oversampling=1)
# Create indices in regularly spaced intervals (9 seconds, i.e. 1 sec stim + 8 ISI)
stim_onsets = np.arange(10, N - 15, 9)
stimcodes = np.repeat([1, 2], stim_onsets.size / 2) # create codes for two conditions
np.random.shuffle(stimcodes) # random shuffle
stim = np.zeros((N, 1))
c = np.array([[0, 1, 0], [0, 0, 1]])
# Fill stim array with codes at onsets
for i, stim_onset in enumerate(stim_onsets):
stim[stim_onset] = 1 if stimcodes[i] == 1 else 2
stims_A = (stim == 1).astype(int)
stims_B = (stim == 2).astype(int)
reg_A = np.convolve(stims_A.squeeze(), dg_hrf)[:N]
reg_B = np.convolve(stims_B.squeeze(), dg_hrf)[:N]
X = np.hstack((np.ones((reg_B.size, 1)), reg_A[:, np.newaxis], reg_B[:, np.newaxis]))
dvars = [(c[i, :].dot(np.linalg.inv(X.T.dot(X))).dot(c[i, :].T))
for i in range(c.shape[0])]
eff = c.shape[0] / np.sum(dvars)
return X, eff
X, eff = simulate_data_fixed_ISI()
ย
- ์์ ๋ฌ๋ฆฌ ISI๊ฐ ์๋ ๋๋ง๋ค ๋ค์ํด์ง ์ ์์ โ ์ด๋ฅผ โjitteringโ์ด๋ผ๊ณ ๋ถ๋ฆ
- jittering์ covariance๋ฅผ ์ค์ด๊ณ variance๋ฅผ ๋์ผ ์ ์์
- ์๋ ์์๋ 2-6์ด์ (๊ฐ๋ณ) ISI๋ฅผ ๊ฐ์ง
def simulate_data_jittered_ISI(N=420):
dg_hrf = glover_hrf(tr=1, oversampling=1)
stim_onsets = np.arange(10, N - 15, 9)
stimcodes = np.repeat([1, 2], stim_onsets.size / 2)
np.random.shuffle(stimcodes)
# Here, we pick some *deviations* from the standard ISI (i.e., 8),
# so possible ISIs are (8 - 2, 8 - 1, 8 - 0, 8 + 1, 8 + 2)
ISIs = np.repeat([-2, -1, 0, 1, 2], repeats=11)
np.random.shuffle(ISIs)
stim = np.zeros((N, 1))
c = np.array([[0, 1, 0], [0, 0, 1]])
for i, stim_onset in enumerate(stim_onsets):
# We subtract the stim-onset with -2, -1, 0, 1, or 2 (from ISIs)
# to simulate jittering
stim[stim_onset - ISIs[i]] = 1 if stimcodes[i] == 1 else 2
stims_A = (stim == 1).astype(int)
stims_B = (stim == 2).astype(int)
reg_A = np.convolve(stims_A.squeeze(), dg_hrf)[:N]
reg_B = np.convolve(stims_B.squeeze(), dg_hrf)[:N]
X = np.hstack((np.ones((reg_B.size, 1)), reg_A[:, np.newaxis], reg_B[:, np.newaxis]))
# Loop over the two contrasts
dvars = [(c[i, :].dot(np.linalg.inv(X.T.dot(X))).dot(c[i, :].T))
for i in range(c.shape[0])]
eff = c.shape[0] / np.sum(dvars)
return X, eff