88normal, which helps when training data pools heterogeneous cohorts.
99"""
1010
11+ from __future__ import annotations
12+
1113import pickle
1214import functools
1315import numpy
@@ -34,7 +36,7 @@ class TrainedArch:
3436 variational : bool = False
3537
3638
37- def triu_to_mat (triu ) :
39+ def triu_to_mat (triu : Any ) -> Any :
3840 "Fold flat upper-triangular vectors into symmetric square matrices."
3941 squeeze = triu .ndim == 1
4042 if squeeze :
@@ -47,7 +49,7 @@ def triu_to_mat(triu):
4749 return mat [0 ] if squeeze else mat
4850
4951
50- def triu_to_mat_np (triu ) :
52+ def triu_to_mat_np (triu : Any ) -> Any :
5153 "NumPy equivalent of :func:`triu_to_mat` for post-training paths."
5254 squeeze = triu .ndim == 1
5355 if squeeze :
@@ -61,25 +63,25 @@ def triu_to_mat_np(triu):
6163 return mat [0 ] if squeeze else mat
6264
6365
64- def _xavier (key , shape , scale = 1.0 ):
66+ def _xavier (key : Any , shape : tuple [ int , ...], scale : float = 1.0 ) -> Any :
6567 return jax .random .normal (key , shape ) * np .sqrt (scale / shape [0 ])
6668
6769
6870class MvNorm :
6971 "Multivariate normal with persistent PRNG key for sampling."
7072
71- def __init__ (self , us , mean , cov , key = None ):
73+ def __init__ (self , us : Any , mean : Any , cov : Any , key : Any | None = None ) -> None :
7274 self .us = us
7375 self .mean = mean
7476 self .cov = cov
7577 self .key = key if key is not None else jax .random .PRNGKey (SEED )
7678
77- def sample (self , n ) :
79+ def sample (self , n : int ) -> Any :
7880 self .key , k = jax .random .split (self .key )
7981 return jax .random .multivariate_normal (k , self .mean , self .cov , shape = (n ,))
8082
8183
82- def _denorm (flat , ntype , mean , std , scale , nonneg ) :
84+ def _denorm (flat : Any , ntype : str , mean : Any , std : float , scale : float , nonneg : bool ) -> Any :
8385 "Invert the per-view normalization applied by :meth:`CrossCoder.add_view`."
8486 if ntype == 'logit' :
8587 probs = jax .nn .sigmoid (flat * std + mean )
@@ -106,7 +108,7 @@ class CrossCoder:
106108 and returns to Python only for logging. Disable for step-wise debug.
107109 """
108110
109- def __init__ (self , variational = True , chunked_training = True ):
111+ def __init__ (self , variational : bool = True , chunked_training : bool = True ) -> None :
110112 self .variational = variational
111113 self .chunked_training = chunked_training
112114 self .conns = []
@@ -125,7 +127,7 @@ def __init__(self, variational=True, chunked_training=True):
125127 'scales' , 'parcs' , 'tts' , 'wbs' , 'history' , 'norm_types' , 'nonneg' ,
126128 'archs' )
127129
128- def to_pkl (self , fname ) :
130+ def to_pkl (self , fname : str ) -> None :
129131 data = {k : getattr (self , k ) for k in self ._pkl_keys }
130132 # Convert TrainedArch dataclasses to dicts for pickling
131133 if 'archs' in data :
@@ -135,7 +137,7 @@ def to_pkl(self, fname):
135137 pickle .dump (data , fd )
136138
137139 @classmethod
138- def from_pkl (cls , fname ) :
140+ def from_pkl (cls , fname : str ) -> CrossCoder :
139141 with open (fname , 'rb' ) as fd :
140142 data = pickle .load (fd )
141143 self = cls (variational = data .get ('variational' , False ),
@@ -172,8 +174,8 @@ def from_pkl(cls, fname):
172174 return self
173175
174176 @classmethod
175- def from_numpy_array (cls , weights , tts = None , parc = 'Schaefer-17Networks' ,
176- variational = False , chunked_training = True , normalize = 'center' ):
177+ def from_numpy_array (cls , weights : numpy . ndarray , tts : int | None = None , parc : str = 'Schaefer-17Networks' ,
178+ variational : bool = False , chunked_training : bool = True , normalize : str = 'center' ) -> CrossCoder :
177179 "Build a single-view CrossCoder from an ``(ns, nn, nn)`` connectome stack."
178180 weights = numpy .maximum (weights , 0.0 )
179181 ns , nn , _ = weights .shape
@@ -183,7 +185,7 @@ def from_numpy_array(cls, weights, tts=None, parc='Schaefer-17Networks',
183185 self .add_view (weights [:, i , j ], f'{ nn } -{ parc } ' , normalize = normalize , nonneg = True )
184186 return self
185187
186- def add_view (self , data , parc_name , normalize = 'zscore' , nonneg = False ):
188+ def add_view (self , data : Any , parc_name : str , normalize : str = 'zscore' , nonneg : bool = False ) -> None :
187189 "Register a view, normalizing its flat upper-tri connectomes."
188190 data = np .asarray (data )
189191 scale = 1.0
@@ -217,15 +219,15 @@ def add_view(self, data, parc_name, normalize='zscore', nonneg=False):
217219 self .norm_types .append (normalize )
218220 self .nonneg .append (nonneg )
219221
220- def shuffle (self , seed = None ):
222+ def shuffle (self , seed : int | None = None ) -> numpy . ndarray :
221223 "Shuffle all views with a common permutation and return it."
222224 n = self .conns [0 ].shape [0 ]
223225 key = jax .random .PRNGKey (SEED if seed is None else seed )
224226 perm = jax .random .permutation (key , np .arange (n ))
225227 self .conns = [c [perm ] for c in self .conns ]
226228 return numpy .asarray (perm )
227229
228- def make_wbs (self , nlat , key = None ):
230+ def make_wbs (self , nlat : int , key : Any | None = None ) -> list :
229231 "Initialize weights/biases for all views at given latent size."
230232 if key is None :
231233 key = jax .random .PRNGKey (SEED )
@@ -247,7 +249,7 @@ def make_wbs(self, nlat, key=None):
247249 return wbs
248250
249251
250- def make_loss (self ):
252+ def make_loss (self ) -> tuple :
251253 "Build the cross-prediction loss and its gradient."
252254 if self .variational :
253255 @jax .jit
@@ -281,8 +283,8 @@ def loss(wbs, conns):
281283 return loss , grad
282284
283285
284- def train (self , nlat , lr = 3e-4 , niter = 2000 , tts = None , mb = 64 ,
285- beta_start = 0.0 , beta_end = 1e-3 , anneal_steps = 1500 , key = None ):
286+ def train (self , nlat : int , lr : float = 3e-4 , niter : int = 2000 , tts : int | None = None , mb : int = 64 ,
287+ beta_start : float = 0.0 , beta_end : float = 1e-3 , anneal_steps : int = 1500 , key : Any | None = None ) -> tuple :
286288 """
287289 Fit a single architecture. Appends the learned weights and trace
288290 into ``self.wbs`` / ``self.history`` and returns
@@ -320,9 +322,9 @@ def train(self, nlat, lr=3e-4, niter=2000, tts=None, mb=64,
320322 cr = self .calc_confusion_rate (nlat , tts = tts )
321323 return trace , wbs , cr
322324
323- def _train_var (self , loss_fn , grad_fn , opt_update , get_params , opt_state ,
324- mbkey , train_c , test_c , tts , mb , niter ,
325- beta_start , beta_end , anneal_steps ) :
325+ def _train_var (self , loss_fn : Any , grad_fn : Any , opt_update : Any , get_params : Any , opt_state : Any ,
326+ mbkey : Any , train_c : Any , test_c : Any , tts : int , mb : int , niter : int ,
327+ beta_start : float , beta_end : float , anneal_steps : int ) -> tuple :
326328 def beta_at (it ):
327329 return np .minimum (
328330 beta_start + (beta_end - beta_start ) * (it / anneal_steps ), beta_end )
@@ -385,8 +387,8 @@ def body(carry, i):
385387
386388 return trace , get_params (opt_state ), log_freq
387389
388- def _train_det (self , loss_fn , grad_fn , opt_update , get_params , opt_state ,
389- mbkey , train_c , test_c , tts , mb , niter ) :
390+ def _train_det (self , loss_fn : Any , grad_fn : Any , opt_update : Any , get_params : Any , opt_state : Any ,
391+ mbkey : Any , train_c : Any , test_c : Any , tts : int , mb : int , niter : int ) -> tuple :
390392 def step (i , opt_state , mbkey ):
391393 mbkey , kb = jax .random .split (mbkey )
392394 idx = jax .random .randint (kb , (mb ,), 0 , tts )
@@ -444,7 +446,7 @@ def body(carry, i):
444446 return trace , get_params (opt_state ), log_freq
445447
446448
447- def _get_arch (self , nlat ) :
449+ def _get_arch (self , nlat : int ) -> TrainedArch :
448450 """Get TrainedArch by nlat value."""
449451 for a in self .archs :
450452 if a .nlat == nlat :
@@ -463,7 +465,7 @@ def arch(self):
463465 return [wb [0 ][0 ][0 ][1 ].size if self .variational else wb [0 ][0 ][1 ].size
464466 for wb in self .wbs ]
465467
466- def _conf_rates_det (self , wbs , conns ) :
468+ def _conf_rates_det (self , wbs : Any , conns : Any ) -> numpy . ndarray :
467469 @jax .jit
468470 def dist (a , b ):
469471 return np .sum ((a [:, None ] - b ) ** 2 , axis = - 1 )
@@ -476,7 +478,7 @@ def dist(a, b):
476478 crs [i , j ] = 1 - ok .mean ()
477479 return crs
478480
479- def _conf_rates_var (self , wbs , conns , n_samples = 0 ) :
481+ def _conf_rates_var (self , wbs : Any , conns : Any , n_samples : int = 0 ) -> numpy . ndarray :
480482 """Compute (n_views, n_views) confusion rate matrix for variational mode."""
481483 @jax .jit
482484 def dist (a , b ):
@@ -508,7 +510,7 @@ def dist(a, b):
508510
509511 return crs
510512
511- def calc_confusion_rate (self , arch , tts = None , self_recon_only = True , n_samples = 0 ) :
513+ def calc_confusion_rate (self , arch : int , tts : int | None = None , self_recon_only : bool = True , n_samples : int = 0 ) -> float :
512514 "Fraction of test subjects not fingerprinted correctly."
513515 tts = tts or self .tts
514516 wbs = self ._get_arch (arch ).wbs
@@ -522,7 +524,7 @@ def calc_confusion_rate(self, arch, tts=None, self_recon_only=True, n_samples=0)
522524 return float (numpy .diag (crs ).mean ())
523525 return float (crs .mean ())
524526
525- def confusion_matrix (self , arch , tts = None , n_samples = 0 ) :
527+ def confusion_matrix (self , arch : int , tts : int | None = None , n_samples : int = 0 ) -> numpy . ndarray :
526528 """Return (n_views, n_views) confusion rate matrix.
527529
528530 Entry (i, j) is the fraction of test subjects from view i whose
@@ -549,7 +551,7 @@ def confusion_matrix(self, arch, tts=None, n_samples=0):
549551 return self ._conf_rates_var (wbs , test_c , n_samples = n_samples )
550552
551553
552- def encode (self , arch , parc , tts = None , sample = False , key = None ):
554+ def encode (self , arch : int , parc : str , tts : int | None = None , sample : bool = False , key : Any | None = None ) -> Any :
553555 "Encode the normalized connectomes of a view into latent space."
554556 ta = self ._get_arch (arch )
555557 iparc = self .parcs .index (parc )
@@ -565,7 +567,7 @@ def encode(self, arch, parc, tts=None, sample=False, key=None):
565567 (ew , eb ), _ = ta .wbs [iparc ]
566568 return c @ ew + eb
567569
568- def encode_all (self , arch , tts = None , sample = False , key = None ):
570+ def encode_all (self , arch : int , tts : int | None = None , sample : bool = False , key : Any | None = None ) -> dict [ str , Any ] :
569571 """Encode all views into latent space.
570572
571573 Returns
@@ -576,7 +578,7 @@ def encode_all(self, arch, tts=None, sample=False, key=None):
576578 return {parc : self .encode (arch , parc , tts = tts , sample = sample , key = key )
577579 for parc in self .parcs }
578580
579- def decode (self , arch , parc , z , raw = False ):
581+ def decode (self , arch : int , parc : str , z : Any , raw : bool = False ) -> Any :
580582 "Decode latent vectors into flat upper-tri connectomes."
581583 iparc = self .parcs .index (parc )
582584 _ , (w_dec , b_dec ) = self ._get_arch (arch ).wbs [iparc ]
@@ -586,19 +588,19 @@ def decode(self, arch, parc, z, raw=False):
586588 return _denorm (rec , self .norm_types [iparc ], self .means [iparc ],
587589 self .stds [iparc ], self .scales [iparc ], self .nonneg [iparc ])
588590
589- def decode_conn (self , arch , parc , z , clip_positive = None ):
591+ def decode_conn (self , arch : int , parc : str , z : Any , clip_positive : bool | None = None ) -> Any :
590592 "Decode latents into full symmetric connectomes ``(ns, nn, nn)``."
591593 flat = self .decode (arch , parc , z , raw = False )
592594 if clip_positive is True :
593595 flat = np .maximum (flat , 0.0 )
594596 return triu_to_mat (flat )
595597
596- def get_triu (self , parc , tts = None ):
598+ def get_triu (self , parc : str , tts : int | None = None ) -> Any :
597599 "Return normalized flat upper-tri connectomes for a view."
598600 return self .conns [self .parcs .index (parc )][
599601 self .tts if tts is None else tts :]
600602
601- def get_conn (self , parc , tts = None ):
603+ def get_conn (self , parc : str , tts : int | None = None ) -> Any :
602604 "Return empirical connectomes ``(ns, nn, nn)`` for a view."
603605 iparc = self .parcs .index (parc )
604606 flat = _denorm (self .get_triu (parc , tts ), self .norm_types [iparc ],
@@ -607,7 +609,7 @@ def get_conn(self, parc, tts=None):
607609 return triu_to_mat (flat )
608610
609611
610- def calc_mvn (self , arch , tts = None ):
612+ def calc_mvn (self , arch : int , tts : int | None = None ) -> MvNorm :
611613 "Total-variance multivariate normal over the cohort latents."
612614 ta = self ._get_arch (arch )
613615 tts = tts or self .tts
@@ -628,7 +630,7 @@ def calc_mvn(self, arch, tts=None):
628630 cov = cov + np .diag (np .mean (np .concatenate (var , axis = 0 ), axis = 0 ))
629631 return MvNorm (us , mean , cov )
630632
631- def decompose_latent (self , arch , tts = None ):
633+ def decompose_latent (self , arch : int , tts : int | None = None ) -> dict :
632634 "SVD of the centered cohort latents for a given architecture."
633635 mvn = self .calc_mvn (arch , tts )
634636 X = mvn .us - mvn .mean
@@ -640,7 +642,7 @@ def decompose_latent(self, arch, tts=None):
640642
641643
642644 @classmethod
643- def combine (cls , cc1 , cc2 , shuffle = True ):
645+ def combine (cls , cc1 : CrossCoder , cc2 : CrossCoder , shuffle : bool = True ) -> CrossCoder :
644646 "Concatenate two CrossCoders with identical views and normalizations."
645647 assert cc1 .variational == cc2 .variational
646648 assert cc1 .parcs == cc2 .parcs
@@ -660,12 +662,12 @@ def combine(cls, cc1, cc2, shuffle=True):
660662 return cc
661663
662664
663- def sweep_crosscoder (model , dims , n_trials = 20 , seed = 42 , keep_best = False ,
664- lr_range = (1e-5 , 1e-2 ), niter_range = (500 , 5000 ),
665- mb_choices = (32 , 64 , 128 ),
666- beta_end_range = (1e-6 , 1e-3 ),
667- anneal_range = (500 , 3000 ),
668- score_fn = None ):
665+ def sweep_crosscoder (model : CrossCoder , dims : list [ int ] , n_trials : int = 20 , seed : int = 42 , keep_best : bool = False ,
666+ lr_range : tuple [ float , float ] = (1e-5 , 1e-2 ), niter_range : tuple [ int , int ] = (500 , 5000 ),
667+ mb_choices : tuple [ int , ...] = (32 , 64 , 128 ),
668+ beta_end_range : tuple [ float , float ] = (1e-6 , 1e-3 ),
669+ anneal_range : tuple [ int , int ] = (500 , 3000 ),
670+ score_fn : Any | None = None ) -> tuple [ list [ dict ], dict | None ] :
669671 """
670672 Random hyperparameter sweep over ``CrossCoder.train``.
671673
0 commit comments