24 Lab 24: AI as a Microscope for the Brain
24.1 Learning Objectives
- Understand core concepts from Chapter 24
- Implement key algorithms and techniques
- Apply methods to practical problems
- Analyze results and draw insights
- Connect to broader NeuroAI themes
24.2 Prerequisites
- Reading: Chapter 24: AI for Neuroscience Discovery
- Libraries: NumPy, Matplotlib, relevant frameworks
- Concepts: ML for neural data
24.3 Setup
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
plt.rcParams['figure.figsize'] = (12, 8)24.4 Part 1: Fundamentals
Core concepts and theory from AI for Neuroscience Discovery.
## Example implementation
def example_function():
'''Placeholder for main implementation.'''
pass24.5 Part 2: Hands-On Implementation
Practical exercises exploring ML for neural data.
24.6 Part 3: Analysis
Analyze and visualize results.
24.7 Exercises
24.7.1 Exercise 1
Implement the basic algorithm.
24.7.2 Exercise 2
Explore parameter variations.
24.7.3 Exercise 3
Apply to real or simulated data.
24.7.4 Exercise 4
Compare different approaches.
24.7.5 Exercise 5
Discuss NeuroAI connections.
24.8 Challenge Problems
24.8.1 Challenge 1
Advanced implementation.
24.8.2 Challenge 2
Reproduce research findings.
24.8.3 Challenge 3
Novel application.
24.9 Discussion Questions
24.9.1 Question 1
How does this relate to neuroscience?
24.9.2 Question 2
What are the AI implications?
24.9.3 Question 3
Future research directions?
24.10 Summary
Explored ML for neural data through theory and practice.
Key Takeaways: - Core concepts mastered - Practical implementation skills developed - NeuroAI connections understood
24.11 Code Examples from Chapter 24 (Part 1)
24.11.1 Code Block 1: Neural Speech Decoder (CNN+LSTM for ECoG Data)
# Neural speech decoder using CNN+LSTM for ECoG data
import torch
import torch.nn as nn
class SpeechDecoder(nn.Module):
def __init__(self, n_channels, n_classes):
super().__init__()
# A convolutional and recurrent architecture to process spatio-temporal ECoG data
self.conv1 = nn.Conv1d(n_channels, 128, kernel_size=3)
self.lstm = nn.LSTM(128, 256, batch_first=True)
self.fc = nn.Linear(256, n_classes) # n_classes = vocabulary size
def forward(self, ecog_data):
# ecog_data shape: (batch, channels, time)
x = torch.relu(self.conv1(ecog_data))
x = x.transpose(1, 2) # Switch to (batch, time, features) for LSTM
lstm_out, _ = self.lstm(x)
# Take the output of the last time step
last_step_out = lstm_out[:, -1, :]
logits = self.fc(last_step_out)
return logits24.11.2 Code Block 2: AlphaFold-style Contact Prediction Map
# AlphaFold-style contact prediction map visualization
import numpy as np
import matplotlib.pyplot as plt
# Simulate a contact map showing predicted amino acid interactions
np.random.seed(42)
sequence_length = 100
# Create a realistic-looking contact map
contact_map = np.zeros((sequence_length, sequence_length))
# Add diagonal (sequential contacts)
for i in range(sequence_length-1):
contact_map[i, i+1] = 0.9
contact_map[i+1, i] = 0.9
# Add secondary structure patterns (alpha helices and beta sheets)
# Alpha helix pattern (i, i+3 and i, i+4 contacts)
for i in range(20, 40):
if i+3 < sequence_length:
contact_map[i, i+3] = 0.7
contact_map[i+3, i] = 0.7
if i+4 < sequence_length:
contact_map[i, i+4] = 0.6
contact_map[i+4, i] = 0.6
# Beta sheet pattern (long-range contacts)
for i in range(60, 70):
for j in range(80, 90):
if abs(i-j) > 10:
contact_map[i, j] = 0.5 + 0.2 * np.random.rand()
contact_map[j, i] = contact_map[i, j]
# Add some noise
contact_map += np.random.rand(sequence_length, sequence_length) * 0.1
contact_map = np.clip(contact_map, 0, 1)
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
im = ax.imshow(contact_map, cmap='RdPu', origin='lower')
ax.set_xlabel('Amino Acid Position', fontsize=12)
ax.set_ylabel('Amino Acid Position', fontsize=12)
ax.set_title('AlphaFold-style Contact Prediction Map', fontsize=14, fontweight='bold')
plt.colorbar(im, ax=ax, label='Contact Probability')
plt.tight_layout()
plt.show()
print("Contact map shows predicted spatial proximity between amino acids")
print("Diagonal: Sequential contacts | Patterns: Secondary structures (helices, sheets)")24.11.3 Code Block 3: AlphaFold Impact on Structural Biology
# AlphaFold impact on structural biology - growth visualization
import pandas as pd
# Historical data on protein structures
years = np.array([2000, 2005, 2010, 2015, 2018, 2020, 2021, 2022, 2023])
experimental = np.array([15000, 30000, 65000, 110000, 145000, 170000, 180000, 190000, 200000])
predicted = np.array([0, 0, 0, 0, 0, 0, 365000, 550000, 800000]) # AlphaFold launched 2020
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(years, experimental, 'o-', linewidth=2, markersize=8,
label='Experimental Structures (PDB)', color='#0066cc')
ax.plot(years, predicted, 's-', linewidth=2, markersize=8,
label='AI-Predicted Structures', color='#cc0000')
ax.axvline(x=2020, color='gray', linestyle='--', alpha=0.7, label='AlphaFold 2 Release')
ax.set_xlabel('Year', fontsize=12)
ax.set_ylabel('Number of Protein Structures', fontsize=12)
ax.set_title('The AlphaFold Revolution in Structural Biology', fontsize=14, fontweight='bold')
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("AlphaFold expanded known protein structures by orders of magnitude")
print("Impact: From ~170K experimental to ~800K+ total structures in 3 years")24.11.4 Code Block 4: Calcium Imaging Cell Detection (Suite2p)
# Calcium imaging cell detection and ROI segmentation (Suite2p)
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage
from matplotlib.patches import Ellipse
np.random.seed(42)
# Create synthetic calcium imaging frame
img_size = 256
n_cells = 45
# Generate background (neuropil)
background = np.random.rand(img_size, img_size) * 0.3
# Add cell bodies
cell_image = background.copy()
cell_centers = []
for i in range(n_cells):
x = np.random.randint(15, img_size-15)
y = np.random.randint(15, img_size-15)
cell_centers.append((x, y))
# Create Gaussian cell body
y_grid, x_grid = np.ogrid[:img_size, :img_size]
radius = np.random.uniform(3, 6)
intensity = np.random.uniform(0.7, 1.0)
mask = ((x_grid - x)**2 + (y_grid - y)**2) <= radius**2
cell_image[mask] += intensity
# Add noise
cell_image += np.random.randn(img_size, img_size) * 0.1
cell_image = np.clip(cell_image, 0, 1.5)
# Create "detected" ROIs
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# Panel 1: Raw calcium imaging data
axes[0].imshow(cell_image, cmap='gray', interpolation='nearest')
axes[0].set_title('Raw Calcium Imaging Frame', fontsize=12, fontweight='bold')
axes[0].axis('off')
# Panel 2: Detected cells (Suite2p style)
axes[1].imshow(cell_image, cmap='gray', interpolation='nearest')
for x, y in cell_centers:
circle = plt.Circle((x, y), 5, color='#cc0000', fill=False, linewidth=1.5, alpha=0.7)
axes[1].add_patch(circle)
axes[1].set_title('Suite2p: Automated Cell Detection', fontsize=12, fontweight='bold')
axes[1].axis('off')
# Panel 3: Extracted ROIs colored by identity
roi_image = np.zeros((img_size, img_size, 3))
roi_image[:, :, 0] = cell_image # Red channel: raw image
colors = plt.cm.Set3(np.linspace(0, 1, n_cells))
for idx, (x, y) in enumerate(cell_centers):
y_grid, x_grid = np.ogrid[:img_size, :img_size]
radius = 5
mask = ((x_grid - x)**2 + (y_grid - y)**2) <= radius**2
for c in range(3):
roi_image[mask, c] = colors[idx, c]
axes[2].imshow(roi_image)
axes[2].set_title('Segmented ROIs (Color-coded)', fontsize=12, fontweight='bold')
axes[2].axis('off')
plt.tight_layout()
plt.show()
print(f"Detected {n_cells} neurons automatically")
print("Suite2p reduces analysis time from days to minutes")24.11.5 Code Block 5: Spike Inference from Calcium Traces
# Spike inference from calcium traces with performance metrics
time = np.linspace(0, 10, 1000) # 10 seconds
dt = time[1] - time[0]
# Generate ground truth spikes (Poisson process)
np.random.seed(42)
spike_rate = 5 # Hz
spike_prob = spike_rate * dt
spikes = np.random.rand(len(time)) < spike_prob
# Simulate calcium dynamics (convolution with exponential kernel)
tau_rise = 0.05 # 50 ms rise
tau_decay = 0.3 # 300 ms decay
t_kernel = np.arange(0, 1, dt)
kernel = (1 - np.exp(-t_kernel/tau_rise)) * np.exp(-t_kernel/tau_decay)
kernel /= kernel.sum()
calcium = np.convolve(spikes.astype(float), kernel, mode='same')
calcium += np.random.randn(len(time)) * 0.05 # Add noise
# Simulate deep learning spike inference
# Simple threshold-based inference for demonstration
calcium_smooth = ndimage.gaussian_filter1d(calcium, sigma=5)
inferred_spikes = np.zeros_like(spikes)
threshold = np.percentile(calcium_smooth, 75)
inferred_spikes[calcium_smooth > threshold] = 1
fig, axes = plt.subplots(3, 1, figsize=(12, 8), sharex=True)
# Ground truth spikes
axes[0].eventplot([time[spikes]], lineoffsets=0.5, linelengths=0.8, colors='black', linewidths=2)
axes[0].set_ylabel('Spikes', fontsize=11)
axes[0].set_title('Ground Truth: Action Potentials', fontsize=12, fontweight='bold')
axes[0].set_ylim([0, 1])
axes[0].set_yticks([])
# Calcium trace
axes[1].plot(time, calcium, linewidth=1.5, color='#0066cc')
axes[1].set_ylabel('ΔF/F', fontsize=11)
axes[1].set_title('Measured: Calcium Fluorescence', fontsize=12, fontweight='bold')
axes[1].grid(True, alpha=0.3)
# Inferred spikes
axes[2].eventplot([time[inferred_spikes.astype(bool)]], lineoffsets=0.5,
linelengths=0.8, colors='#cc0000', linewidths=2)
axes[2].set_ylabel('Inferred', fontsize=11)
axes[2].set_xlabel('Time (s)', fontsize=11)
axes[2].set_title('Deep Learning Output: Inferred Spikes', fontsize=12, fontweight='bold')
axes[2].set_ylim([0, 1])
axes[2].set_yticks([])
plt.tight_layout()
plt.show()
# Calculate performance metrics
true_positive = np.sum(spikes & inferred_spikes.astype(bool))
false_positive = np.sum(~spikes & inferred_spikes.astype(bool))
false_negative = np.sum(spikes & ~inferred_spikes.astype(bool))
precision = true_positive / (true_positive + false_positive) if (true_positive + false_positive) > 0 else 0
recall = true_positive / (true_positive + false_negative) if (true_positive + false_negative) > 0 else 0
print(f"Spike Inference Performance:")
print(f" Precision: {precision:.2f} | Recall: {recall:.2f}")
print(f" True Positive: {true_positive} | False Positive: {false_positive} | False Negative: {false_negative}")24.12 Code Examples from Chapter 24 (Part 3: Lines 1000-1500)
24.12.1 Code Block 1: CNN Decoder Training Visualization
# CNN decoder training visualization - loss curves, accuracy comparison, confusion matrix
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Panel 1: Training curves
ax = axes[0, 0]
ax.plot(train_losses, label='Training Loss', linewidth=2, color='#cc0000')
ax.set_xlabel('Epoch', fontsize=11)
ax.set_ylabel('Loss', fontsize=11)
ax.set_title('CNN Training Loss', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Panel 2: Accuracy curves
ax = axes[0, 1]
ax.plot(train_accs, label='Train Accuracy', linewidth=2, color='#0066cc')
ax.plot(test_accs, label='Test Accuracy', linewidth=2, color='#cc0000')
ax.axhline(y=linear_accuracy, color='gray', linestyle='--', label='Linear Baseline', linewidth=2)
ax.set_xlabel('Epoch', fontsize=11)
ax.set_ylabel('Accuracy', fontsize=11)
ax.set_title('CNN Learning Curves', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Panel 3: CNN confusion matrix
model.eval()
with torch.no_grad():
outputs = model(X_test_pt)
_, y_pred_cnn = torch.max(outputs.data, 1)
y_pred_cnn = y_pred_cnn.numpy()
cm_cnn = confusion_matrix(y_test, y_pred_cnn)
ax = axes[1, 0]
sns.heatmap(cm_cnn, annot=True, fmt='d', cmap='Greens', ax=ax, cbar_kws={'label': 'Count'})
ax.set_xlabel('Predicted Stimulus', fontsize=11)
ax.set_ylabel('True Stimulus', fontsize=11)
ax.set_title(f'CNN Decoder Confusion Matrix - Accuracy: {test_accs[-1]:.3f}',
fontsize=12, fontweight='bold')
# Panel 4: Comparison
ax = axes[1, 1]
methods = ['Linear - Decoder', 'CNN - Decoder']
accuracies = [linear_accuracy, test_accs[-1]]
colors = ['#0066cc', '#cc0000']
bars = ax.bar(methods, accuracies, color=colors, alpha=0.7, edgecolor='black', linewidth=2)
# Add value labels on bars
for bar, acc in zip(bars, accuracies):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height,
f'{acc:.3f}',
ha='center', va='bottom', fontsize=12, fontweight='bold')
ax.set_ylabel('Decoding Accuracy', fontsize=11)
ax.set_title('Decoder Performance Comparison', fontsize=12, fontweight='bold')
ax.set_ylim([0, 1.0])
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.show()
print(); print("Key Findings:")
print(f"• Linear decoder: {linear_accuracy:.1%} accuracy")
print(f"• CNN decoder: {test_accs[-1]:.1%} accuracy")
print(f"• Deep learning captures nonlinear temporal dynamics")
print(f"• Improvement is most pronounced for confusable stimuli")24.12.2 Code Block 2: CNN Feature Visualization
# CNN feature visualization - learned filters, activation patterns, neuron importance, latent space
# Extract learned convolutional filters
conv1_weights = model.conv1.weight.data.cpu().numpy() # Shape: (64, n_neurons, 3)
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Panel 1: First layer filters (sample)
ax = axes[0, 0]
n_filters_show = 16
filters_to_show = conv1_weights[:n_filters_show, :, 1] # Middle of kernel
im = ax.imshow(filters_to_show, aspect='auto', cmap='RdBu_r', interpolation='nearest')
ax.set_xlabel('Neuron #', fontsize=11)
ax.set_ylabel('Filter #', fontsize=11)
ax.set_title('Learned Conv1 Filters (Spatial Patterns)', fontsize=12, fontweight='bold')
plt.colorbar(im, ax=ax, label='Weight')
# Panel 2: Activation patterns for different stimuli
ax = axes[0, 1]
model.eval()
# Get activations for example stimuli
example_stimuli = [0, 2, 4, 6]
with torch.no_grad():
for stim in example_stimuli:
stim_data = X_test_pt[y_test_pt == stim][:5] # First 5 trials
# Forward through first conv layer
activations = torch.relu(model.bn1(model.conv1(stim_data)))
mean_activation = activations.mean(dim=(0, 2)).numpy() # Average over trials and time
ax.plot(mean_activation, label=f'Stimulus {stim}', linewidth=2)
ax.set_xlabel('Filter #', fontsize=11)
ax.set_ylabel('Mean Activation', fontsize=11)
ax.set_title('Filter Activations by Stimulus', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Panel 3: Decoding confidence across neurons
ax = axes[1, 0]
# Measure which neurons contribute most to decoding
neuron_importance = np.zeros(n_neurons)
for neuron in range(n_neurons):
# Shuffle this neuron's activity
X_test_shuffled = X_test_pt.clone()
shuffle_idx = torch.randperm(X_test_shuffled.shape[0])
X_test_shuffled[:, neuron, :] = X_test_shuffled[shuffle_idx, neuron, :]
# Measure accuracy drop
with torch.no_grad():
outputs_shuffled = model(X_test_shuffled)
_, pred_shuffled = torch.max(outputs_shuffled.data, 1)
acc_shuffled = (pred_shuffled == y_test_pt).sum().item() / len(y_test_pt)
neuron_importance[neuron] = test_accs[-1] - acc_shuffled # Drop in accuracy
ax.bar(range(n_neurons), neuron_importance, color='#9966cc', alpha=0.7)
ax.set_xlabel('Neuron #', fontsize=11)
ax.set_ylabel('Importance - (Accuracy Drop When Shuffled)', fontsize=11)
ax.set_title('Neuron Importance for Decoding', fontsize=12, fontweight='bold')
ax.grid(True, alpha=0.3, axis='y')
# Panel 4: Decision boundary in latent space
ax = axes[1, 1]
# Get latent representations (before final classification layer)
model.eval()
with torch.no_grad():
# Forward to second-to-last layer
x = X_test_pt
x = torch.relu(model.bn1(model.conv1(x)))
x = model.pool(torch.relu(model.bn2(model.conv2(x))))
x = x.view(x.size(0), -1)
latent = torch.relu(model.fc1(x)).numpy()
# PCA on latent space
pca_latent = PCA(n_components=2)
latent_pca = pca_latent.fit_transform(latent)
colors = plt.cm.rainbow(np.linspace(0, 1, n_stimuli))
for stim in range(n_stimuli):
mask = y_test == stim
ax.scatter(latent_pca[mask, 0], latent_pca[mask, 1], c=[colors[stim]],
label=f'Stim {stim}', alpha=0.6, s=30)
ax.set_xlabel(f'PC1 ({pca_latent.explained_variance_ratio_[0]:.1%})', fontsize=11)
ax.set_ylabel(f'PC2 ({pca_latent.explained_variance_ratio_[1]:.1%})', fontsize=11)
ax.set_title('CNN Latent Space (PCA)', fontsize=12, fontweight='bold')
ax.legend(fontsize=8, ncol=2)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(); print("Visualization Insights:")
print("• Conv filters learn to detect specific neuron-time patterns")
print("• Different stimuli activate different filter combinations")
print("• Neurons with broad tuning are most important for decoding")
print("• Latent space shows better stimulus separation than raw activity")24.12.3 Code Block 3: Simulating Neural Trajectories for Decision-Making
# Simulating neural trajectories for decision-making task with latent dynamics
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
from mpl_toolkits.mplot3d import Axes3D
np.random.seed(42)
# Simulate a decision-making task with neural trajectories
n_neurons = 80
n_trials = 200
n_timepoints = 50
n_conditions = 2 # Two choice conditions
def generate_decision_trajectories(n_neurons, n_trials_per_condition, n_timepoints, condition):
"""
Generate neural trajectories during a decision-making task.
Neural activity follows a low-dimensional trajectory from stimulus onset to choice.
"""
trajectories = np.zeros((n_trials_per_condition, n_neurons, n_timepoints))
# Define a low-dimensional trajectory in "latent space"
# Three dimensions: stimulus encoding, decision process, motor preparation
for trial in range(n_trials_per_condition):
# Latent trajectory parameters
stimulus_strength = np.random.uniform(0.5, 1.5)
decision_speed = np.random.uniform(0.8, 1.2)
motor_noise = np.random.randn()
# Time-varying latent states
t_normalized = np.linspace(0, 1, n_timepoints)
# Dimension 1: Stimulus encoding (rises then plateaus)
latent_1 = stimulus_strength * (1 - np.exp(-5 * t_normalized))
# Dimension 2: Decision variable (S-shaped accumulation, condition-dependent)
if condition == 0:
latent_2 = 1.0 / (1 + np.exp(-10 * (t_normalized - 0.5)))
else:
latent_2 = -1.0 / (1 + np.exp(-10 * (t_normalized - 0.5)))
# Dimension 3: Motor preparation (late ramp)
latent_3 = np.maximum(0, decision_speed * (t_normalized - 0.6)) + 0.1 * motor_noise
# Create random projection from latent space to neural space
# Each neuron is a random linear combination of latent dimensions plus noise
if trial == 0: # Create mixing matrix once per condition
mixing_matrix = np.random.randn(n_neurons, 3)
# Normalize
mixing_matrix = mixing_matrix / np.linalg.norm(mixing_matrix, axis=1, keepdims=True)
# Project latent trajectory to neural space
latent_trajectory = np.vstack([latent_1, latent_2, latent_3]) # (3, n_timepoints)
neural_trajectory = mixing_matrix @ latent_trajectory # (n_neurons, n_timepoints)
# Add noise and ensure non-negativity (firing rates)
neural_trajectory += np.random.randn(n_neurons, n_timepoints) * 0.3
neural_trajectory = np.maximum(neural_trajectory, 0)
# Add baseline firing rate
neural_trajectory += 2.0
trajectories[trial, :, :] = neural_trajectory
return trajectories
# Generate data for both conditions
print("Generating neural trajectories for decision-making task...")
trials_per_condition = n_trials // n_conditions
trajectories_cond0 = generate_decision_trajectories(n_neurons, trials_per_condition, n_timepoints, condition=0)
trajectories_cond1 = generate_decision_trajectories(n_neurons, trials_per_condition, n_timepoints, condition=1)
# Combine
X_traj = np.vstack([trajectories_cond0, trajectories_cond1])
conditions = np.array([0]*trials_per_condition + [1]*trials_per_condition)
print(f"Data shape: {X_traj.shape} (trials, neurons, time)")
# Visualize raw neural data
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Panel 1: Single trial activity
ax = axes[0, 0]
trial_idx = 0
im = ax.imshow(X_traj[trial_idx, :, :], aspect='auto', cmap='hot', interpolation='nearest')
ax.set_xlabel('Time Step', fontsize=11)
ax.set_ylabel('Neuron #', fontsize=11)
ax.set_title(f'Single Trial Neural Activity (Condition {conditions[trial_idx]})',
fontsize=12, fontweight='bold')
plt.colorbar(im, ax=ax, label='Firing Rate')
# Panel 2: Average activity across trials for each condition
ax = axes[0, 1]
avg_cond0 = trajectories_cond0.mean(axis=0)
avg_cond1 = trajectories_cond1.mean(axis=0)
im = ax.imshow(avg_cond0 - avg_cond1, aspect='auto', cmap='RdBu_r',
interpolation='nearest', vmin=-1, vmax=1)
ax.set_xlabel('Time Step', fontsize=11)
ax.set_ylabel('Neuron #', fontsize=11)
ax.set_title('Differential Activity: Condition 0 - Condition 1', fontsize=12, fontweight='bold')
plt.colorbar(im, ax=ax, label='ΔFiring Rate')
# Panel 3: Example neurons over time
ax = axes[1, 0]
example_neurons = [5, 20, 35, 50]
time = np.arange(n_timepoints)
for neuron in example_neurons:
mean_0 = trajectories_cond0[:, neuron, :].mean(axis=0)
mean_1 = trajectories_cond1[:, neuron, :].mean(axis=0)
ax.plot(time, mean_0, label=f'Neuron {neuron} (Cond 0)', linewidth=2)
ax.plot(time, mean_1, '--', label=f'Neuron {neuron} (Cond 1)', linewidth=2)
ax.set_xlabel('Time Step', fontsize=11)
ax.set_ylabel('Firing Rate', fontsize=11)
ax.set_title('Example Neuron Trajectories', fontsize=12, fontweight='bold')
ax.legend(fontsize=8, ncol=2)
ax.grid(True, alpha=0.3)
# Panel 4: Population variance over time
ax = axes[1, 1]
variance_cond0 = trajectories_cond0.var(axis=0).mean(axis=0) # Avg variance across neurons
variance_cond1 = trajectories_cond1.var(axis=0).mean(axis=0)
ax.plot(time, variance_cond0, label='Condition 0', linewidth=2, color='#0066cc')
ax.plot(time, variance_cond1, label='Condition 1', linewidth=2, color='#cc0000')
ax.set_xlabel('Time Step', fontsize=11)
ax.set_ylabel('Population Variance', fontsize=11)
ax.set_title('Neural Variability Over Time', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()24.12.4 Code Block 4: PCA for Neural Manifolds
# PCA for neural manifolds - explained variance, trajectories in PC space, 3D manifold
# Reshape data for PCA: (trials * time, neurons)
X_flat = X_traj.reshape(-1, n_neurons)
# Apply PCA
pca = PCA(n_components=10)
X_pca = pca.fit_transform(X_flat)
# Reshape back to (trials, time, components)
X_pca = X_pca.reshape(n_trials, n_timepoints, -1)
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Panel 1: Explained variance
ax = axes[0, 0]
explained_var = pca.explained_variance_ratio_
cumulative_var = np.cumsum(explained_var)
ax.bar(range(1, 11), explained_var, alpha=0.7, color='#0066cc', label='Individual')
ax.plot(range(1, 11), cumulative_var, 'o-', color='#cc0000', linewidth=2,
markersize=8, label='Cumulative')
ax.set_xlabel('Principal Component', fontsize=11)
ax.set_ylabel('Explained Variance Ratio', fontsize=11)
ax.set_title('PCA: Explained Variance', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
print(f"Top 3 PCs explain {cumulative_var[2]:.1%} of variance")
# Panel 2: Neural trajectories in PC space (2D)
ax = axes[0, 1]
colors_cond0 = plt.cm.Blues(np.linspace(0.3, 1, n_timepoints))
colors_cond1 = plt.cm.Reds(np.linspace(0.3, 1, n_timepoints))
# Plot a few example trials
n_trials_plot = 5
for trial in range(n_trials_plot):
if conditions[trial] == 0:
ax.plot(X_pca[trial, :, 0], X_pca[trial, :, 1], 'o-',
color='#0066cc', alpha=0.4, linewidth=1, markersize=2)
else:
trial_idx = trials_per_condition + trial
ax.plot(X_pca[trial_idx, :, 0], X_pca[trial_idx, :, 1], 's-',
color='#cc0000', alpha=0.4, linewidth=1, markersize=2)
# Mark start and end
for cond in [0, 1]:
mask = conditions == cond
start_mean = X_pca[mask, 0, :2].mean(axis=0)
end_mean = X_pca[mask, -1, :2].mean(axis=0)
color = '#0066cc' if cond == 0 else '#cc0000'
ax.plot(start_mean[0], start_mean[1], 'o', markersize=15, color=color,
markeredgecolor='black', markeredgewidth=2, label=f'Cond {cond} Start')
ax.plot(end_mean[0], end_mean[1], '*', markersize=20, color=color,
markeredgecolor='black', markeredgewidth=2, label=f'Cond {cond} End')
ax.set_xlabel(f'PC1 ({explained_var[0]:.1%})', fontsize=11)
ax.set_ylabel(f'PC2 ({explained_var[1]:.1%})', fontsize=11)
ax.set_title('Neural Trajectories in PC Space', fontsize=12, fontweight='bold')
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
# Panel 3: 3D trajectories
ax = fig.add_subplot(2, 2, 3, projection='3d')
for trial in range(n_trials_plot):
if conditions[trial] == 0:
ax.plot(X_pca[trial, :, 0], X_pca[trial, :, 1], X_pca[trial, :, 2],
color='#0066cc', alpha=0.4, linewidth=1.5)
else:
trial_idx = trials_per_condition + trial
ax.plot(X_pca[trial_idx, :, 0], X_pca[trial_idx, :, 1], X_pca[trial_idx, :, 2],
color='#cc0000', alpha=0.4, linewidth=1.5)
ax.set_xlabel(f'PC1 ({explained_var[0]:.1%})', fontsize=10)
ax.set_ylabel(f'PC2 ({explained_var[1]:.1%})', fontsize=10)
ax.set_zlabel(f'PC3 ({explained_var[2]:.1%})', fontsize=10)
ax.set_title('3D Neural Manifold (PCA)', fontsize=12, fontweight='bold')
# Panel 4: PC loadings (which neurons contribute to each PC)
ax = axes[1, 1]
loadings = pca.components_[:3, :] # Top 3 PCs
im = ax.imshow(loadings, aspect='auto', cmap='RdBu_r', interpolation='nearest')
ax.set_xlabel('Neuron #', fontsize=11)
ax.set_ylabel('Principal Component', fontsize=11)
ax.set_title('PC Loadings: Neuron Contributions', fontsize=12, fontweight='bold')
ax.set_yticks([0, 1, 2])
ax.set_yticklabels(['PC1', 'PC2', 'PC3'])
plt.colorbar(im, ax=ax, label='Loading')
plt.tight_layout()
plt.show()24.13 Code Examples from Chapter 24 (Part 2: Lines 500-1000)
24.13.1 Code Block 1: Connectomics Project Scale and AI Contribution Visualization
# Visualize the scale and accuracy of connectomics projects
import matplotlib.pyplot as plt
import numpy as np
projects = ['C. elegans (1986)', 'Larval Fly (2020)', 'Adult Fly (2020)',
'FlyWire (2022)', 'Mouse Retina', 'Mouse Cortex']
neurons = [302, 3000, 25000, 130000, 80000, 100000]
synapses = [7000, 500000, 20000000, 54500000, 30000000, 1000000000]
year_completed = [1986, 2020, 2020, 2022, 2019, 2025]
ai_percentage = [0, 50, 90, 95, 85, 98] # Estimated AI contribution
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Panel 1: Neurons and synapses mapped
ax1 = axes[0, 0]
x_pos = np.arange(len(projects))
width = 0.35
ax1.bar(x_pos - width/2, np.array(neurons)/1000, width, label='Neurons (thousands)', color='#0066cc')
ax1_twin = ax1.twinx()
ax1_twin.bar(x_pos + width/2, np.array(synapses)/1000000, width, label='Synapses (millions)', color='#cc0000')
ax1.set_xlabel('Project', fontsize=11)
ax1.set_ylabel('Neurons (thousands)', fontsize=11, color='#0066cc')
ax1_twin.set_ylabel('Synapses (millions)', fontsize=11, color='#cc0000')
ax1.set_title('Scale of Connectomics Projects', fontsize=12, fontweight='bold')
ax1.set_xticks(x_pos)
ax1.set_xticklabels(projects, rotation=45, ha='right')
ax1.set_yscale('log')
ax1_twin.set_yscale('log')
ax1.grid(True, alpha=0.3, axis='y')
# Panel 2: AI contribution over time
ax2 = axes[0, 1]
ax2.plot(year_completed, ai_percentage, 'o-', linewidth=2, markersize=10, color='#9966cc')
for i, project in enumerate(projects):
ax2.annotate(project.split(' - ')[0], (year_completed[i], ai_percentage[i]),
textcoords="offset points", xytext=(0,10), ha='center', fontsize=8)
ax2.set_xlabel('Year', fontsize=11)
ax2.set_ylabel('AI Contribution (%)', fontsize=11)
ax2.set_title('Increasing Role of AI in Connectomics', fontsize=12, fontweight='bold')
ax2.grid(True, alpha=0.3)
ax2.set_ylim([0, 105])
# Panel 3: Time to complete (estimated)
ax3 = axes[1, 0]
manual_years = [30, 500, 3000, 15000, 2000, 100000] # Estimated if done manually
actual_years = [15, 3, 3, 2, 4, 5] # Actual project duration
speedup = np.array(manual_years) / np.array(actual_years)
x_pos = np.arange(len(projects))
bars = ax3.barh(x_pos, speedup, color='#cc9900', alpha=0.7)
ax3.set_yticks(x_pos)
ax3.set_yticklabels(projects)
ax3.set_xlabel('Speedup Factor (log scale)', fontsize=11)
ax3.set_title('AI Acceleration of Connectomics', fontsize=12, fontweight='bold')
ax3.set_xscale('log')
ax3.grid(True, alpha=0.3, axis='x')
# Add annotations
for i, (bar, speed) in enumerate(zip(bars, speedup)):
ax3.text(speed, bar.get_y() + bar.get_height()/2, f'{speed:.0f}x',
va='center', ha='left', fontweight='bold', fontsize=9)
# Panel 4: Data size and processing time
ax4 = axes[1, 1]
data_sizes = [0.001, 0.1, 26, 95, 10, 1000] # TB
processing_times = [5, 2, 1.5, 1, 1.2, 0.8] # years
scatter = ax4.scatter(data_sizes, processing_times, s=np.array(neurons)/200,
c=ai_percentage, cmap='RdYlGn', alpha=0.6, edgecolors='black', linewidth=1.5)
for i, project in enumerate(projects):
ax4.annotate(project.split(' - ')[0], (data_sizes[i], processing_times[i]),
textcoords="offset points", xytext=(5,5), ha='left', fontsize=8)
ax4.set_xlabel('Dataset Size (TB, log scale)', fontsize=11)
ax4.set_ylabel('Processing Time (years)', fontsize=11)
ax4.set_title('Efficiency Gains: Data Size vs Processing Time', fontsize=12, fontweight='bold')
ax4.set_xscale('log')
ax4.grid(True, alpha=0.3)
cbar = plt.colorbar(scatter, ax=ax4, label='AI Contribution (%)')
plt.tight_layout()
plt.show()
print("Key Insights:")
print("- AI enables 100-10,000x speedup in connectome reconstruction")
print("- Modern projects process PB-scale data in years, not centuries")
print("- Hybrid AI+human approaches achieve highest accuracy")24.13.2 Code Block 2: Simulating Neural Population Responses with Tuning Curves
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix
import seaborn as sns
np.random.seed(42)
torch.manual_seed(42)
# Simulation parameters
n_neurons = 100 # Population size
n_stimuli = 8 # Number of different stimuli (e.g., oriented gratings)
n_trials = 50 # Trials per stimulus
n_timepoints = 20 # Time bins per trial
# Create tuning curves for neurons
# Each neuron has a preferred stimulus orientation
preferred_stimuli = np.random.randint(0, n_stimuli, n_neurons)
tuning_width = 2.0 # Width of tuning curves
def generate_neural_responses(stimulus_id, n_trials, n_neurons, n_timepoints):
"""Generate realistic neural population responses to a stimulus."""
responses = np.zeros((n_trials, n_neurons, n_timepoints))
for trial in range(n_trials):
for neuron in range(n_neurons):
# Tuning curve: Gaussian centered on preferred stimulus
pref = preferred_stimuli[neuron]
# Circular distance on stimulus space
dist = min(abs(stimulus_id - pref), n_stimuli - abs(stimulus_id - pref))
tuning_response = np.exp(-(dist**2) / (2 * tuning_width**2))
# Base firing rate + tuned response + noise
base_rate = 5.0 # Hz
max_response = 30.0 # Hz
mean_rate = base_rate + max_response * tuning_response
# Generate Poisson spike counts with temporal dynamics
for t in range(n_timepoints):
# Add temporal modulation (onset transient)
temporal_mod = 1.0 + 0.5 * np.exp(-t / 5.0)
rate = mean_rate * temporal_mod
responses[trial, neuron, t] = np.random.poisson(rate * 0.05) # 50ms bins
return responses
# Generate dataset
X = [] # Neural activity
y = [] # Stimulus labels
print("Generating neural population data...")
for stim in range(n_stimuli):
responses = generate_neural_responses(stim, n_trials, n_neurons, n_timepoints)
X.append(responses)
y.extend([stim] * n_trials)
X = np.vstack(X) # Shape: (n_stimuli * n_trials, n_neurons, n_timepoints)
y = np.array(y)
print(f"Dataset shape: {X.shape}")
print(f"Labels shape: {y.shape}")
# Visualize population response to one stimulus
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Panel 1: Raster plot for one trial
trial_idx = 0
ax = axes[0, 0]
for neuron in range(min(50, n_neurons)):
spike_times = np.where(X[trial_idx, neuron, :] > 0)[0]
spike_counts = X[trial_idx, neuron, spike_times]
for t, count in zip(spike_times, spike_counts):
ax.plot([t]*int(count), [neuron]*int(count), 'k.', markersize=2)
ax.set_xlabel('Time Bin', fontsize=11)
ax.set_ylabel('Neuron #', fontsize=11)
ax.set_title(f'Raster Plot: Stimulus {y[trial_idx]}', fontsize=12, fontweight='bold')
# Panel 2: Population response heatmap
ax = axes[0, 1]
trial_avg = X[y == 0].mean(axis=0) # Average across trials for stimulus 0
im = ax.imshow(trial_avg, aspect='auto', cmap='hot', interpolation='nearest')
ax.set_xlabel('Time Bin', fontsize=11)
ax.set_ylabel('Neuron #', fontsize=11)
ax.set_title('Population Activity Heatmap (Stimulus 0)', fontsize=12, fontweight='bold')
plt.colorbar(im, ax=ax, label='Firing Rate (Hz)')
# Panel 3: Tuning curves of example neurons
ax = axes[1, 0]
example_neurons = [0, 25, 50, 75]
for neuron in example_neurons:
mean_responses = []
for stim in range(n_stimuli):
stim_trials = X[y == stim, neuron, :].mean(axis=1) # Average across time
mean_responses.append(stim_trials.mean()) # Average across trials
ax.plot(range(n_stimuli), mean_responses, 'o-', label=f'Neuron {neuron}', linewidth=2)
ax.set_xlabel('Stimulus ID', fontsize=11)
ax.set_ylabel('Mean Firing Rate (Hz)', fontsize=11)
ax.set_title('Tuning Curves: Example Neurons', fontsize=12, fontweight='bold')
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
# Panel 4: Population decoding potential
ax = axes[1, 1]
# Show how well each stimulus is separated in neural space (first 2 PCs)
from sklearn.decomposition import PCA
X_flat = X.reshape(X.shape[0], -1) # Flatten for PCA
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_flat)
colors = plt.cm.rainbow(np.linspace(0, 1, n_stimuli))
for stim in range(n_stimuli):
mask = y == stim
ax.scatter(X_pca[mask, 0], X_pca[mask, 1], c=[colors[stim]],
label=f'Stim {stim}', alpha=0.6, s=30)
ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%} var)', fontsize=11)
ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%} var)', fontsize=11)
ax.set_title('Neural Population Space (PCA)', fontsize=12, fontweight='bold')
ax.legend(fontsize=8, ncol=2)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"PC1+PC2 explain {(pca.explained_variance_ratio_[:2].sum()):.1%} of variance")24.13.3 Code Block 3: Linear Decoder Baseline Using Logistic Regression
# Prepare data for sklearn
X_flat = X.reshape(X.shape[0], -1) # Flatten time and neurons
X_train, X_test, y_train, y_test = train_test_split(X_flat, y, test_size=0.3, random_state=42)
# Train linear decoder
print("Training linear decoder (Logistic Regression)...")
linear_decoder = LogisticRegression(max_iter=1000, multi_class='multinomial', random_state=42)
linear_decoder.fit(X_train, y_train)
# Evaluate
y_pred_linear = linear_decoder.predict(X_test)
linear_accuracy = accuracy_score(y_test, y_pred_linear)
print(f"Linear Decoder Accuracy: {linear_accuracy:.3f}")
# Confusion matrix
cm_linear = confusion_matrix(y_test, y_pred_linear)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Plot confusion matrix
ax = axes[0]
sns.heatmap(cm_linear, annot=True, fmt='d', cmap='Blues', ax=ax, cbar_kws={'label': 'Count'})
ax.set_xlabel('Predicted Stimulus', fontsize=11)
ax.set_ylabel('True Stimulus', fontsize=11)
ax.set_title(f'Linear Decoder Confusion Matrix - Accuracy: {linear_accuracy:.3f}',
fontsize=12, fontweight='bold')
# Plot decoder weights
ax = axes[1]
weights = linear_decoder.coef_ # Shape: (n_stimuli, n_features)
weights_reshaped = weights.reshape(n_stimuli, n_neurons, n_timepoints)
mean_weights = np.abs(weights_reshaped).mean(axis=2) # Average across time
im = ax.imshow(mean_weights, aspect='auto', cmap='RdBu_r', interpolation='nearest')
ax.set_xlabel('Neuron #', fontsize=11)
ax.set_ylabel('Stimulus #', fontsize=11)
ax.set_title('Linear Decoder Weights - (Absolute Mean Across Time)', fontsize=12, fontweight='bold')
plt.colorbar(im, ax=ax, label='Weight Magnitude')
plt.tight_layout()
plt.show()24.13.4 Code Block 4: CNN-Based Deep Learning Decoder for Neural Data
# Prepare data for PyTorch
X_train_pt = torch.FloatTensor(X_train.reshape(-1, n_neurons, n_timepoints))
X_test_pt = torch.FloatTensor(X_test.reshape(-1, n_neurons, n_timepoints))
y_train_pt = torch.LongTensor(y_train)
y_test_pt = torch.LongTensor(y_test)
# Create data loaders
from torch.utils.data import TensorDataset, DataLoader
train_dataset = TensorDataset(X_train_pt, y_train_pt)
test_dataset = TensorDataset(X_test_pt, y_test_pt)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
# Define CNN decoder architecture
class CNNDecoder(nn.Module):
def __init__(self, n_neurons, n_timepoints, n_classes):
super(CNNDecoder, self).__init__()
# Convolutional layers to process spatiotemporal patterns
self.conv1 = nn.Conv1d(n_neurons, 64, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm1d(64)
self.conv2 = nn.Conv1d(64, 128, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm1d(128)
self.pool = nn.MaxPool1d(2)
# Calculate size after convolutions
conv_output_size = 128 * (n_timepoints // 2)
# Fully connected layers
self.fc1 = nn.Linear(conv_output_size, 256)
self.dropout = nn.Dropout(0.5)
self.fc2 = nn.Linear(256, n_classes)
def forward(self, x):
# x shape: (batch, neurons, time)
x = torch.relu(self.bn1(self.conv1(x)))
x = self.pool(torch.relu(self.bn2(self.conv2(x))))
# Flatten
x = x.view(x.size(0), -1)
# Fully connected
x = torch.relu(self.fc1(x))
x = self.dropout(x)
x = self.fc2(x)
return x
# Initialize model
model = CNNDecoder(n_neurons, n_timepoints, n_stimuli)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# Train the model
print(); print("Training CNN decoder...")
n_epochs = 50
train_losses = []
train_accs = []
test_accs = []
for epoch in range(n_epochs):
model.train()
epoch_loss = 0
correct = 0
total = 0
for batch_X, batch_y in train_loader:
optimizer.zero_grad()
outputs = model(batch_X)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
total += batch_y.size(0)
correct += (predicted == batch_y).sum().item()
train_loss = epoch_loss / len(train_loader)
train_acc = correct / total
train_losses.append(train_loss)
train_accs.append(train_acc)
# Evaluate on test set
model.eval()
correct = 0
total = 0
with torch.no_grad():
for batch_X, batch_y in test_loader:
outputs = model(batch_X)
_, predicted = torch.max(outputs.data, 1)
total += batch_y.size(0)
correct += (predicted == batch_y).sum().item()
test_acc = correct / total
test_accs.append(test_acc)
if (epoch + 1) % 10 == 0:
print(f'Epoch [{epoch+1}/{n_epochs}], Loss: {train_loss:.4f}, '
f'Train Acc: {train_acc:.3f}, Test Acc: {test_acc:.3f}')
print(f"Final CNN Decoder Accuracy: {test_accs[-1]:.3f}")
print(f"Improvement over linear: {(test_accs[-1] - linear_accuracy):.3f} "
f"({((test_accs[-1] - linear_accuracy)/linear_accuracy * 100):.1f}%)")24.13.5 Code Block 5: t-SNE Nonlinear Dimensionality Reduction for Neural Manifolds
# Apply t-SNE to time-averaged activity per trial
X_trial_avg = X_traj.mean(axis=2) # Average over time: (trials, neurons)
print("Running t-SNE (this may take a minute)...")
tsne = TSNE(n_components=2, random_state=42, perplexity=30)
X_tsne = tsne.fit_transform(X_trial_avg)
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# Panel 1: t-SNE embedding colored by condition
ax = axes[0]
scatter0 = ax.scatter(X_tsne[conditions==0, 0], X_tsne[conditions==0, 1],
c='#0066cc', s=50, alpha=0.6, label='Condition 0', edgecolors='black', linewidth=0.5)
scatter1 = ax.scatter(X_tsne[conditions==1, 0], X_tsne[conditions==1, 1],
c='#cc0000', s=50, alpha=0.6, label='Condition 1', edgecolors='black', linewidth=0.5)
ax.set_xlabel('t-SNE Dimension 1', fontsize=11)
ax.set_ylabel('t-SNE Dimension 2', fontsize=11)
ax.set_title('t-SNE Embedding: Trial-Averaged Activity', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Panel 2: Compare PCA vs t-SNE separation
ax = axes[1]
# Calculate silhouette score (measure of cluster separation)
from sklearn.metrics import silhouette_score
# PCA separation (using first 2 PCs of trial-averaged data)
X_trial_avg_pca = pca.transform(X_trial_avg)[:, :2]
silhouette_pca = silhouette_score(X_trial_avg_pca, conditions)
# t-SNE separation
silhouette_tsne = silhouette_score(X_tsne, conditions)
methods = ['PCA', 't-SNE']
silhouettes = [silhouette_pca, silhouette_tsne]
colors = ['#0066cc', '#cc0000']
bars = ax.bar(methods, silhouettes, color=colors, alpha=0.7, edgecolor='black', linewidth=2)
for bar, score in zip(bars, silhouettes):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height,
f'{score:.3f}',
ha='center', va='bottom', fontsize=12, fontweight='bold')
ax.set_ylabel('Silhouette Score - (Condition Separation)', fontsize=11)
ax.set_title('PCA vs t-SNE: Cluster Separation', fontsize=12, fontweight='bold')
ax.set_ylim([0, 1])
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.show()
print(f"PCA Silhouette Score: {silhouette_pca:.3f}")
print(f"t-SNE Silhouette Score: {silhouette_tsne:.3f}")
print(f"t-SNE provides {((silhouette_tsne - silhouette_pca)/silhouette_pca * 100):.1f}% better separation")24.13.6 Code Block 6: UMAP Dimensionality Reduction and Method Comparison
# Note: UMAP requires installation: pip install umap-learn
try:
import umap
print("Running UMAP...")
umap_model = umap.UMAP(n_components=2, random_state=42, n_neighbors=15)
X_umap = umap_model.fit_transform(X_trial_avg)
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# Panel 1: UMAP embedding
ax = axes[0]
ax.scatter(X_umap[conditions==0, 0], X_umap[conditions==0, 1],
c='#0066cc', s=50, alpha=0.6, label='Condition 0', edgecolors='black', linewidth=0.5)
ax.scatter(X_umap[conditions==1, 0], X_umap[conditions==1, 1],
c='#cc0000', s=50, alpha=0.6, label='Condition 1', edgecolors='black', linewidth=0.5)
ax.set_xlabel('UMAP Dimension 1', fontsize=11)
ax.set_ylabel('UMAP Dimension 2', fontsize=11)
ax.set_title('UMAP Embedding', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Panel 2: All three methods side by side
ax = axes[1]
ax.scatter(X_trial_avg_pca[conditions==0, 0], X_trial_avg_pca[conditions==0, 1],
c='#0066cc', s=30, alpha=0.5, label='Condition 0', marker='o')
ax.scatter(X_trial_avg_pca[conditions==1, 0], X_trial_avg_pca[conditions==1, 1],
c='#cc0000', s=30, alpha=0.5, label='Condition 1', marker='s')
ax.set_xlabel('Dimension 1', fontsize=11)
ax.set_ylabel('Dimension 2', fontsize=11)
ax.set_title('PCA (Linear)', fontsize=12, fontweight='bold')
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
# Panel 3: Comparison metrics
ax = axes[2]
silhouette_umap = silhouette_score(X_umap, conditions)
methods = ['PCA', 't-SNE', 'UMAP']
silhouettes = [silhouette_pca, silhouette_tsne, silhouette_umap]
colors = ['#0066cc', '#cc0000', '#9966cc']
bars = ax.bar(methods, silhouettes, color=colors, alpha=0.7, edgecolor='black', linewidth=2)
for bar, score in zip(bars, silhouettes):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height,
f'{score:.3f}',
ha='center', va='bottom', fontsize=11, fontweight='bold')
ax.set_ylabel('Silhouette Score', fontsize=11)
ax.set_title('Method Comparison', fontsize=12, fontweight='bold')
ax.set_ylim([0, 1])
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.show()
print(f"UMAP Silhouette Score: {silhouette_umap:.3f}")
print("UMAP often provides best balance of local and global structure")
except ImportError:
print("UMAP not installed. Install with: pip install umap-learn")
print("Skipping UMAP analysis...")24.14 Code Examples from Chapter 24 (Part 4: Lines 1500-End)
24.14.1 Code Block 1: CEBRA-Style Contrastive Learning for Neural Embeddings
# Conceptual illustration of CEBRA-style contrastive learning
import numpy as np
import matplotlib.pyplot as plt
from sklearn.manifold import MDS
np.random.seed(42)
# Simulate neural activity during 4 distinct behavioral states
n_neurons = 50
n_samples_per_state = 30
n_states = 4
# Generate neural patterns for each state
neural_data = []
labels = []
for state in range(n_states):
# Each state has a characteristic population pattern
base_pattern = np.random.randn(n_neurons) * 2
for sample in range(n_samples_per_state):
# Add trial-to-trial variability
pattern = base_pattern + np.random.randn(n_neurons) * 0.5
neural_data.append(pattern)
labels.append(state)
neural_data = np.array(neural_data)
labels = np.array(labels)
# Simulate CEBRA embedding (using MDS as approximation)
mds = MDS(n_components=2, random_state=42)
embeddings = mds.fit_transform(neural_data)
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# Panel 1: High-dimensional neural space (projected to 2D for visualization)
ax = axes[0]
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
neural_pca = pca.fit_transform(neural_data)
colors = ['#0066cc', '#cc0000', '#00cc66', '#cc9900']
for state in range(n_states):
mask = labels == state
ax.scatter(neural_pca[mask, 0], neural_pca[mask, 1],
c=colors[state], label=f'Behavior {state+1}',
alpha=0.6, s=50, edgecolors='black', linewidth=0.5)
ax.set_xlabel('PC1', fontsize=11)
ax.set_ylabel('PC2', fontsize=11)
ax.set_title('Raw Neural Activity (PCA)', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Panel 2: CEBRA embedding (better separation)
ax = axes[1]
for state in range(n_states):
mask = labels == state
ax.scatter(embeddings[mask, 0], embeddings[mask, 1],
c=colors[state], label=f'Behavior {state+1}',
alpha=0.6, s=50, edgecolors='black', linewidth=0.5)
ax.set_xlabel('CEBRA Dimension 1', fontsize=11)
ax.set_ylabel('CEBRA Dimension 2', fontsize=11)
ax.set_title('CEBRA Embedding (Contrastive Learning)', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Panel 3: Quantify separation improvement
from sklearn.metrics import silhouette_score
silhouette_pca = silhouette_score(neural_pca, labels)
silhouette_cebra = silhouette_score(embeddings, labels)
ax = axes[2]
methods = ['PCA', 'CEBRA']
scores = [silhouette_pca, silhouette_cebra]
bars = ax.bar(methods, scores, color=['#0066cc', '#cc0000'],
alpha=0.7, edgecolor='black', linewidth=2)
for bar, score in zip(bars, scores):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height,
f'{score:.3f}',
ha='center', va='bottom', fontsize=12, fontweight='bold')
ax.set_ylabel('Behavioral Clustering - (Silhouette Score)', fontsize=11)
ax.set_title('Separation Quality', fontsize=12, fontweight='bold')
ax.set_ylim([0, 1])
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.show()
print(f"PCA Silhouette: {silhouette_pca:.3f}")
print(f"CEBRA Silhouette: {silhouette_cebra:.3f}")
print(f"Improvement: {((silhouette_cebra - silhouette_pca) / silhouette_pca * 100):.1f}%")24.14.2 Code Block 2: Brain-Score Benchmarking
# Visualize Brain-Score concept: comparing model representations to neural data
import numpy as np
import matplotlib.pyplot as plt
# Simulate neural responses and model predictions
n_images = 100
n_neurons = 50
np.random.seed(42)
# True neural responses to images
neural_responses = np.random.randn(n_images, n_neurons)
neural_responses = np.cumsum(neural_responses, axis=0) # Add structure
# Simulate different model predictions
model_predictions = {
'Random CNN': neural_responses + np.random.randn(n_images, n_neurons) * 3,
'ImageNet CNN': neural_responses + np.random.randn(n_images, n_neurons) * 1.5,
'Self-Supervised': neural_responses + np.random.randn(n_images, n_neurons) * 0.8,
'Vision Transformer': neural_responses + np.random.randn(n_images, n_neurons) * 0.5,
}
# Calculate correlation for each model
correlations = {}
for model_name, predictions in model_predictions.items():
corrs = []
for neuron in range(n_neurons):
corr = np.corrcoef(neural_responses[:, neuron], predictions[:, neuron])[0, 1]
corrs.append(corr)
correlations[model_name] = np.mean(corrs)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Panel 1: Example predictions vs actual
ax = axes[0]
example_neuron = 10
ax.scatter(neural_responses[:, example_neuron],
model_predictions['Random CNN'][:, example_neuron],
alpha=0.5, s=30, label='Random CNN', color='#cccccc')
ax.scatter(neural_responses[:, example_neuron],
model_predictions['Vision Transformer'][:, example_neuron],
alpha=0.5, s=30, label='Vision Transformer', color='#cc0000')
ax.plot([neural_responses[:, example_neuron].min(), neural_responses[:, example_neuron].max()],
[neural_responses[:, example_neuron].min(), neural_responses[:, example_neuron].max()],
'k--', linewidth=2, label='Perfect Match')
ax.set_xlabel('True Neural Response', fontsize=11)
ax.set_ylabel('Model Prediction', fontsize=11)
ax.set_title(f'Prediction vs Reality: Example Neuron #{example_neuron}', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Panel 2: Brain-Score comparison
ax = axes[1]
models = list(correlations.keys())
scores = list(correlations.values())
colors = ['#cccccc', '#0066cc', '#9966cc', '#cc0000']
bars = ax.barh(models, scores, color=colors, alpha=0.7, edgecolor='black', linewidth=1.5)
for bar, score in zip(bars, scores):
width = bar.get_width()
ax.text(width, bar.get_y() + bar.get_height()/2.,
f'{score:.3f}',
ha='left', va='center', fontsize=11, fontweight='bold', color='black')
ax.set_xlabel('Neural Predictivity (Correlation)', fontsize=11)
ax.set_title('Brain-Score: Model Performance', fontsize=12, fontweight='bold')
ax.set_xlim([0, 1])
ax.grid(True, alpha=0.3, axis='x')
plt.tight_layout()
plt.show()
print(); print("Brain-Score Rankings:")
for i, (model, score) in enumerate(sorted(correlations.items(), key=lambda x: x[1], reverse=True), 1):
print(f"{i}. {model}: {score:.3f}")24.14.3 Code Block 3: Transfer Learning Efficiency
# Illustrate transfer learning benefit
import numpy as np
import matplotlib.pyplot as plt
# Simulate data requirements and performance
n_samples = np.array([10, 20, 50, 100, 200, 500, 1000, 2000])
# Performance with training from scratch
perf_scratch = 0.5 + 0.4 * (1 - np.exp(-n_samples / 500))
# Performance with transfer learning (pre-trained model)
perf_transfer = 0.65 + 0.3 * (1 - np.exp(-n_samples / 100))
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Panel 1: Learning curves
ax = axes[0]
ax.plot(n_samples, perf_scratch, 'o-', linewidth=2, markersize=8,
color='#0066cc', label='Train from Scratch')
ax.plot(n_samples, perf_transfer, 's-', linewidth=2, markersize=8,
color='#cc0000', label='Transfer Learning')
ax.axhline(y=0.9, color='gray', linestyle='--', linewidth=1.5, alpha=0.7, label='Target Performance')
# Highlight data efficiency
target_perf = 0.85
scratch_needed = n_samples[np.argmin(np.abs(perf_scratch - target_perf))]
transfer_needed = n_samples[np.argmin(np.abs(perf_transfer - target_perf))]
ax.axvline(x=scratch_needed, color='#0066cc', linestyle=':', alpha=0.5)
ax.axvline(x=transfer_needed, color='#cc0000', linestyle=':', alpha=0.5)
ax.set_xlabel('Training Samples', fontsize=11)
ax.set_ylabel('Decoding Performance', fontsize=11)
ax.set_title('Transfer Learning Efficiency', fontsize=12, fontweight='bold')
ax.set_xscale('log')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Panel 2: Data efficiency gain
ax = axes[1]
data_reduction = []
for target in np.linspace(0.6, 0.9, 10):
if np.max(perf_scratch) >= target and np.max(perf_transfer) >= target:
scratch_n = n_samples[np.argmin(np.abs(perf_scratch - target))]
transfer_n = n_samples[np.argmin(np.abs(perf_transfer - target))]
reduction = scratch_n / transfer_n
data_reduction.append((target, reduction))
targets, reductions = zip(*data_reduction)
ax.plot(targets, reductions, 'o-', linewidth=2, markersize=8, color='#9966cc')
ax.set_xlabel('Target Performance', fontsize=11)
ax.set_ylabel('Data Efficiency - (Fold Reduction)', fontsize=11)
ax.set_title('Transfer Learning Advantage', fontsize=12, fontweight='bold')
ax.grid(True, alpha=0.3)
# Annotate typical operating point
ax.annotate(f'{reductions[5]:.1f}x fewer - samples needed',
xy=(targets[5], reductions[5]),
xytext=(targets[5] - 0.05, reductions[5] + 1),
arrowprops=dict(arrowstyle='->', color='black', lw=1.5),
fontsize=10, fontweight='bold',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
plt.tight_layout()
plt.show()
print(f" - To reach 85% performance:")
print(f" Training from scratch: {scratch_needed} samples")
print(f" Transfer learning: {transfer_needed} samples")
print(f" Efficiency gain: {scratch_needed/transfer_needed:.1f}x")24.14.4 Code Block 4: Active Learning for Receptive Field Mapping
# Simulate active learning for receptive field mapping
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
# True receptive field (unknown to experimenter)
true_rf_center = (30, 40)
true_rf_sigma = 15
def neuron_response(x, y):
"""Simulate neuron with Gaussian receptive field."""
dist = np.sqrt((x - true_rf_center[0])**2 + (y - true_rf_center[1])**2)
response = np.exp(-(dist**2) / (2 * true_rf_sigma**2))
# Add noise
response += np.random.randn() * 0.1
return np.clip(response, 0, 1)
# Active learning: choose next stimulus location based on uncertainty
def random_sampling(n_samples, grid_size=80):
"""Baseline: random stimulus locations."""
samples = []
for _ in range(n_samples):
x = np.random.randint(0, grid_size)
y = np.random.randint(0, grid_size)
response = neuron_response(x, y)
samples.append((x, y, response))
return samples
def active_sampling(n_samples, grid_size=80):
"""Active learning: sample where uncertainty is highest."""
samples = []
# Start with a few random samples
for _ in range(5):
x = np.random.randint(0, grid_size)
y = np.random.randint(0, grid_size)
response = neuron_response(x, y)
samples.append((x, y, response))
# Then sample near high-response regions (exploitation)
# and unexplored regions (exploration)
for _ in range(n_samples - 5):
if np.random.rand() < 0.7: # Exploitation
# Sample near previously strong responses
strong_samples = [s for s in samples if s[2] > 0.5]
if strong_samples:
base_x, base_y, _ = strong_samples[np.random.randint(len(strong_samples))]
x = int(np.clip(base_x + np.random.randn() * 10, 0, grid_size-1))
y = int(np.clip(base_y + np.random.randn() * 10, 0, grid_size-1))
else:
x = np.random.randint(0, grid_size)
y = np.random.randint(0, grid_size)
else: # Exploration
x = np.random.randint(0, grid_size)
y = np.random.randint(0, grid_size)
response = neuron_response(x, y)
samples.append((x, y, response))
return samples
# Compare strategies
n_samples_list = [10, 20, 50, 100]
grid_size = 80
fig, axes = plt.subplots(2, 4, figsize=(18, 9))
for idx, n_samples in enumerate(n_samples_list):
# Random sampling
random_samples = random_sampling(n_samples, grid_size)
x_rand, y_rand, r_rand = zip(*random_samples)
# Active sampling
active_samples = active_sampling(n_samples, grid_size)
x_active, y_active, r_active = zip(*active_samples)
# Plot random sampling
ax = axes[0, idx]
scatter = ax.scatter(x_rand, y_rand, c=r_rand, s=50, cmap='hot',
vmin=0, vmax=1, edgecolors='black', linewidth=0.5)
# Show true RF center
circle = plt.Circle(true_rf_center, true_rf_sigma, color='cyan',
fill=False, linewidth=2, linestyle='--', label='True RF')
ax.add_patch(circle)
ax.set_xlim([0, grid_size])
ax.set_ylim([0, grid_size])
ax.set_aspect('equal')
ax.set_title(f'Random Sampling (N={n_samples})', fontsize=11, fontweight='bold')
if idx == 0:
ax.set_ylabel('Y Position', fontsize=10)
# Plot active sampling
ax = axes[1, idx]
scatter = ax.scatter(x_active, y_active, c=r_active, s=50, cmap='hot',
vmin=0, vmax=1, edgecolors='black', linewidth=0.5)
circle = plt.Circle(true_rf_center, true_rf_sigma, color='cyan',
fill=False, linewidth=2, linestyle='--', label='True RF')
ax.add_patch(circle)
ax.set_xlim([0, grid_size])
ax.set_ylim([0, grid_size])
ax.set_aspect('equal')
ax.set_title(f'Active Learning (N={n_samples})', fontsize=11, fontweight='bold')
ax.set_xlabel('X Position', fontsize=10)
if idx == 0:
ax.set_ylabel('Y Position', fontsize=10)
# Add colorbar
cbar = plt.colorbar(scatter, ax=axes, orientation='vertical', fraction=0.02, pad=0.02)
cbar.set_label('Neural Response', fontsize=11)
plt.tight_layout()
plt.show()
print("Active learning concentrates samples in informative regions")
print("Result: More accurate receptive field map with fewer samples")24.14.5 Code Block 5: Bayesian Optimization for Parameter Tuning
# Simulate Bayesian optimization for parameter tuning
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
np.random.seed(42)
# True (unknown) objective function
def true_objective(x):
"""Simulate response to a parameter (e.g., stimulation intensity)."""
return np.sin(x * 3) * np.exp(-x/10) + 0.5
# Bayesian optimization
class SimpleBayesianOptimization:
def __init__(self, bounds, n_initial=3):
self.bounds = bounds
self.X_sampled = []
self.y_sampled = []
# Initial random samples
for _ in range(n_initial):
x = np.random.uniform(bounds[0], bounds[1])
y = true_objective(x) + np.random.randn() * 0.05
self.X_sampled.append(x)
self.y_sampled.append(y)
def acquisition_function(self, X):
"""Expected Improvement acquisition function (simplified)."""
if len(self.y_sampled) == 0:
return np.ones_like(X)
# Simple heuristic: balance exploration (distance from samples) and exploitation (predicted value)
exploration = np.ones_like(X)
for x_samp in self.X_sampled:
exploration *= np.abs(X - x_samp)
# Exploitation: interpolate between known points
exploitation = np.zeros_like(X)
for i, x in enumerate(X):
weights = np.exp(-np.array([(x - xs)**2 / 2 for xs in self.X_sampled]))
weights /= weights.sum() if weights.sum() > 0 else 1
exploitation[i] = np.dot(weights, self.y_sampled)
# Combine
acquisition = 0.5 * exploration / exploration.max() + 0.5 * exploitation
return acquisition
def next_sample(self):
"""Choose next point to sample."""
X_candidate = np.linspace(self.bounds[0], self.bounds[1], 1000)
acquisition_values = self.acquisition_function(X_candidate)
next_x = X_candidate[np.argmax(acquisition_values)]
# Sample the objective
next_y = true_objective(next_x) + np.random.randn() * 0.05
self.X_sampled.append(next_x)
self.y_sampled.append(next_y)
return next_x, next_y
# Compare Bayesian optimization vs random search
bounds = (0, 10)
n_iterations = 15
# Bayesian optimization
bayes_opt = SimpleBayesianOptimization(bounds, n_initial=3)
bayes_history = [(x, y) for x, y in zip(bayes_opt.X_sampled, bayes_opt.y_sampled)]
for _ in range(n_iterations - 3):
x, y = bayes_opt.next_sample()
bayes_history.append((x, y))
# Random search
random_history = []
for _ in range(n_iterations):
x = np.random.uniform(bounds[0], bounds[1])
y = true_objective(x) + np.random.randn() * 0.05
random_history.append((x, y))
# Visualization
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Panel 1: Bayesian optimization progress
ax = axes[0, 0]
X_true = np.linspace(bounds[0], bounds[1], 200)
y_true = true_objective(X_true)
ax.plot(X_true, y_true, 'k-', linewidth=2, label='True Function', alpha=0.7)
x_bayes, y_bayes = zip(*bayes_history)
ax.scatter(x_bayes, y_bayes, c=range(len(x_bayes)), cmap='cool',
s=100, edgecolors='black', linewidth=1.5, label='Sampled Points', zorder=5)
# Show best found
best_idx = np.argmax(y_bayes)
ax.scatter([x_bayes[best_idx]], [y_bayes[best_idx]], c='red', s=300,
marker='*', edgecolors='black', linewidth=2, label='Best Found', zorder=10)
ax.set_xlabel('Parameter Value', fontsize=11)
ax.set_ylabel('Objective (Response)', fontsize=11)
ax.set_title('Bayesian Optimization Trajectory', fontsize=12, fontweight='bold')
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
# Panel 2: Random search progress
ax = axes[0, 1]
ax.plot(X_true, y_true, 'k-', linewidth=2, label='True Function', alpha=0.7)
x_random, y_random = zip(*random_history)
ax.scatter(x_random, y_random, c=range(len(x_random)), cmap='cool',
s=100, edgecolors='black', linewidth=1.5, label='Sampled Points', zorder=5)
best_idx_random = np.argmax(y_random)
ax.scatter([x_random[best_idx_random]], [y_random[best_idx_random]], c='red', s=300,
marker='*', edgecolors='black', linewidth=2, label='Best Found', zorder=10)
ax.set_xlabel('Parameter Value', fontsize=11)
ax.set_ylabel('Objective (Response)', fontsize=11)
ax.set_title('Random Search Trajectory', fontsize=12, fontweight='bold')
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
# Panel 3: Convergence comparison
ax = axes[1, 0]
bayes_best = [max(y_bayes[:i+1]) for i in range(len(y_bayes))]
random_best = [max(y_random[:i+1]) for i in range(len(y_random))]
true_optimum = np.max(y_true)
ax.plot(bayes_best, 'o-', linewidth=2, markersize=8, color='#0066cc',
label='Bayesian Optimization')
ax.plot(random_best, 's-', linewidth=2, markersize=8, color='#cc0000',
label='Random Search')
ax.axhline(y=true_optimum, color='green', linestyle='--', linewidth=2,
label='True Optimum', alpha=0.7)
ax.set_xlabel('Iteration', fontsize=11)
ax.set_ylabel('Best Value Found', fontsize=11)
ax.set_title('Convergence Comparison', fontsize=12, fontweight='bold')
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Panel 4: Efficiency gain
ax = axes[1, 1]
# Calculate how many iterations needed to reach 90% of optimum
target = 0.9 * true_optimum
bayes_to_target = next((i for i, val in enumerate(bayes_best) if val >= target), len(bayes_best))
random_to_target = next((i for i, val in enumerate(random_best) if val >= target), len(random_best))
categories = ['Bayesian - Optimization', 'Random - Search']
iterations = [bayes_to_target + 1, random_to_target + 1]
colors = ['#0066cc', '#cc0000']
bars = ax.bar(categories, iterations, color=colors, alpha=0.7,
edgecolor='black', linewidth=2)
for bar, iters in zip(bars, iterations):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height,
f'{iters}',
ha='center', va='bottom', fontsize=12, fontweight='bold')
ax.set_ylabel('Iterations to 90% Optimum', fontsize=11)
ax.set_title('Efficiency: Iterations Required', fontsize=12, fontweight='bold')
ax.set_ylim([0, max(iterations) * 1.2])
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.show()
print(f"Bayesian Optimization reached target in {bayes_to_target + 1} iterations")
print(f"Random Search reached target in {random_to_target + 1} iterations")
print(f"Efficiency gain: {random_to_target / bayes_to_target:.1f}x")