---
# System prepended metadata

title: fmri2text

---

# fmri2text

- mind retreat
    - https://gitlab.inria.fr/parietal/non_continuous_decoding
        - design matrix of LM features, BOLD encoding
        - generative model, missing predicted / actual bold comparison

## 24 Octobre 2022

- init github repo
	- [x] refactor utils file @athual 
	- [x] fix fir util @ccaucheteux
	- [x] run decoding on multiple TRs @ccaucheteux
- setup narratives on dragostore
	- [x] get mean subject left and right
	- [ ] script datalad configuration
	    - certificate error preventing download
- project narrative subjects on FUGW barycenter
	- alignment data
		- can it be the same as the encoding data?
		- idea
		    - PCA on LM features, trained on text that is independant from our transcripts
		    - align subjects using beta coefficients of encoding model fitted on PCs
		    - this would allow alining subjects who listened to different stories?

- modules
    - barycenter
        - inputs
            - list of subjects
                - v0 who were scanned listening to the same story
                - v1 who have been scanned listening to different stories
            - story name
        - outputs
            - OT plan from each subject to barycentric subject
            - subject's features transported to barycentric subject
            - PCA transformer fitted on LM features
    - alignment
        - inputs
            - subject who was scanned listening to the story used to compute barycenter
            - barycentring subject (BOLD)
            - PCA transformer fitted on LM features
        - outputs
            - OT plan from subject to barycentric subject
            - subject's BOLD transported to barycentric subject
    - encoding
        - inputs
            - list of aligned BOLD matrices (n_tr, n_voxels)
            - transcript for each matrix
        - outputs
            - encoding transformer

- Alexis:
    - avg (exp 1): {story: bold}. bold of shape `[tr, voxels]`
    - single (exp 3-5): Fugw().transform() : `[tr, voxels] -> [tr, voxels]`

- Charlotte: 
    - debug features in decode/encode (notebook)
    - go all to scripts
        - `utils.py` (`def build_design_matrix()`)
        - `encode.py`
        - `decode.py` (decode, decode_one_step)
        - `0_run_exp_avg_subject.py`
            - encoder trained on voxel-wise average of multiple subjects, eval on new story
        - `1_run_exp_fugw_avg_subject.py`
            - encoder trained on voxel-wise average of multiple sujects after they have been aligned with FUGW, eval on new story
        - `2_run_exp_one_subject.py`
            - encoder trained on one subject, eval on same or different subject
        - `3_run_exp_one_subject_fugw.py`
            - encoder trained on one subject, eval on different subject aligned with fugw
        - `4_run_exp_concat_subject.py`
            - encoder trained on concatenated subjects, eval on left-out subject
        - `5_run_exp_concat_fugw_subject.py`
            - encoder trained on concatenated subjects after they have been aligned with FUGW, eval on left-out subject
    - plusieurs histoires
    - out of story generalisation
    - autre generative LM
    - Mardi 25 Octobre / Mercredi: 
        - parle `paths.py`
        - `eval.py` 
```python
# eval.py
def eval(true_texts, decoded_texts):
    """
    true_texts: list of str of shape [n]
    decoded_texts: list of str of shape [n]
    """
    return bleu, meteor, wer, bert
```


## 25/26 Octobre 2022 (Charlotte)
    
**Questions pour le 26:**
- definir la prbmatique/contrib + exp + outputs (e.g. plots)
- on aimerait l'eval sur quoi exactement (combien de TRs)
- Questions sur le papier de Huth:
    - are we sure that its model is not multi-subjects?
    - combien de TR pour l'eval?
    - combien de candidates K? 
    - quel model generatif de langage?
        
        
        
TODO: 
- [x] start FIR at TR=1 or 2
- [x] add WER in eval
- [x] avg, out of story. Test on a ~1,000 words story.
- [x] Fix eval (predictions should be a long string, or at least several TRs.)
- [x] single subject
- [x] concat subject
- [ ] code Beam decoder
- [ ] add encoder eval

    
    
## Huth paper

**Important points to discuss:** 
- code/data availability soon
- cross-subjects story perception. They *do* align subjects (supp table 4). 
- eval:
    - they show short segments. But say they eval by generating 1,800 words in a row?? 
    - empty starting point + no context at all? Strange no?
    - written: *"The language 628 model is provided with the words that occur in last 8 seconds of the candidate"*

**Eval Setups (always out of story)**: 
- story perception
- story imagination
- movie watching
- cross-subjects story perception: Sup Table 4
    
    
**Methodological points to include**:
- [x] starting at t=2s (start_tr=1 or 2)
- [x] add WER in eval
- [x] add Beam in decoder

## 28 Octobre 2022 (Charlotte)

Handling of infinite/nan values in decoding: 
- how to proceed if nan / infinite / negative values in scores: how to score ? how to normalize? 
- z_score / softmax 


## 29 Octobre (Charlotte)
- TODO:
    - [x] check NLL in generate / check infinite values in NLL / decide how to normalize (softmax for NLL, min_max for brainscore)
    - [x] Multi-story 
    - [ ] Check partial_fit sklearn
    - [ ] Organise repo. 
        - `models/`
            - `feature_extractor.py` : from array of text to design matrix. .fit() .transform()
            - `bayse_decoder.py`: from (context, bold) to text
            - `end_to_end_decoder.py`: from (context, bold) to text 
        - `align/`
            - `fugws.py`
            - `baseline_aligners.py`: from X,y,subjects to X,y. .fit(), .transform()
        - `data/`
            - `narratives.py`: Dataset with `__getitem__(session)` and .sessions, .metadata, .subjects, .stories / .tasks
            - `loader.py`
            - `concat_datasets.py` From per-session dataset, to multisessions, subjects datasets ()
        - `experiments/`:

### 31 Octobre (Charlotte)

TODO: 
- [x] Regarder resultats avg_sub
- [x] Lancer single_sub
- [ ] Lancer multi-sub. Check a quel point on est far away en RAM pour du multisujet dans un seul model
- [ ] Clean pipeline? (avec un models/)
- [ ] Dans models, introduire partial_fit_estimator
- [x] Article La Recherche
- [ ] Poster Neurips

### 3 Novembre (Charlotte)
- [x] launch with start_tr=2 to have a fair baseline model
- [x] Add error bars in eval to select the best predicted sentences
- [ ] Launch on all subjects with SGDRegressor (RAM out of memory => need to pass in partial_fit and loader)
- models: add subject embedding layer 
    - skearn based with custom SGDRegressor? OK for SGDRegressor
    - torch based 
        - [ ] TorchDataset and concat_collator 
        - [ ] Factorize Models
        - [ ] Add linear torch model
        - [ ] HYDRA

### 7 Novembre: 
**scale exp**
- [ ] torch dataset and batch loader
- [ ] partial_fit sklearn estimator
- [ ] nn.Linear model with torch lightning
- [ ] subject embedding layer

**check current**
see what is best as a metric to score language generations
- what voxel_mask / weights? (var mask, R mask, R2 mask)
- MSE or other? what could be used?

## 31 Octobre (Common)

**Summary**
Next meeting: jeudi 10 
Next steps => 10:
- Alexis + Alex: alignement (avg subject then beta values)
- Alex:
    - [ ] intégrer IBC LPP dans data
    - [ ] vérifier que les données sont clean
- Charlotte: 
    - [x] *pipeline* sans alignement avg_sub + single_sub (un seul sujet)
    - [ ] *results* sans alignement avg_sub + single_sub 
    - [x] regarder comment adapter pour gros dataset *multisub* (torch/partial_fit de sklearn) (mercredi)
    - [x] add *Huth data* (jeudi)
    - [ ] *clean* pipe avec `models/` `data/` dans une nouvelle branche (vendredi)
    - [ ] jeudi: regarder les resultats + debug Huth dataset (with Alexis)
    - [ ] vendredi: clean pipe avec `models/`


**First results on average subjects**

**Methods**
- For each story, average bolds across subjects
- Fit on n-1 story, test on last story
- Evaluate encoding and decoding on the left out story
    - Encoding eval: R score for each voxel
    - Decoding procedure:
        - start with empty context. 
        - given context, generate n_gen=30 possible continuation using gpt2 (for now, we use the *true* number of words to generate),
        - compute the brainscore of each possible generation (MSE this time because we want one measure per sample)
        - keep the n_beam=10 best in terms of brainscores ("lm_and_brain", blue), or perplexity ("lm_only", red). 
        - update context with the segments kept
    - Decoding eval: bleu / rouge / bertscore / meteor

**Encoding results** 
<img src="https://i.imgur.com/4sDM4YE.png" alt="drawing" width="200"/>


![](https://i.imgur.com/EdA4x5X.png)

**Decoding results**
![](https://i.imgur.com/wtQD9hy.png)

**Decoding grid**

default params: {'n_beam': 10, 'n_gen': 30, 'start_tr': 0, 'decode': 'beam', 'n_steps': 100}

![](https://i.imgur.com/9X11E3l.png)





### Experiment example
```python=

def run_exp():
    # Data
    X, y, groups = NarrativeDataset().dataset # X=bolds, y=texts, groups=subjects/stories
    train, test = train_test_split(X, y=y, groups=groups)

    # Model
    model = Model(**model_params)

    # -- Train --
    model.fit(X[train], y[train], groups[train], **fit_params)

    # -- Eval --
    # eval fitting metrics
    run_eval_encoder(model.encoder, X[test], y[test])

    # eval decoded texts
    predicted_texts = model.predict(X[test])
    metrics, df = run_eval_texts(predicted_texts, y[test])
    return model, 
```

### Feature extractor class
```python=
class FeatureExtractor(BaseTransformer):
    
    def __init__(self):
        self.fir = FirTransformer()
        self.nlp_tokenizer = AutoTokenizer.from_pretrained(conf.nlp_model_name)
        self.nlp_model = AutoModelForCausalLM.from_pretrained(conf.nlp_model_name)
        
    def fit(self, X, y=None):
        return self
        
    def transform(self, texts, y=None):
        features = build_design_matrix(texts, model=self.nlp_model, self.nlp_tokenizer, **params)
        return features
    
    def fit_transform(self, X, y=None):
        self.fit()
        return self.transform(X, y=y)
        
```
### Model class
```python=
class SklearnEncoder(object):
    def __init__(self, conf):
        self.y_pipe = self.build_y_pipeline(conf)
        self.pipe = self.build_pipeline(conf)
    
    def build_pipeline(self, conf):
        steps = [
            ("scaler", StandardScaler()),
            ("ridge", RidgeCV(np.logspace(-3, 6, 10)))
        ]
        return Pipeline(steps)
        
    def build_y_pipeline(self, conf):
        y_steps = [
            ("scaler", RobustScaler(quantile_range=(0.1, 99.9))),
        ]
        return Pipeline(y_steps)
            
        
    def fit(self, X, y):
        y = self.y_pipe.fit_transform(y)
        self.pipe.fit(X, y)
        return self
    
    def predict(self, X):
        y_pred = self.pipe.predict(X)
        y_pred = self.y_pipe.inverse_transform(y_pred)
        return y_pred
        
class BayseModel(object):
    
    def __init__(self, conf):
        
        # Feature Extractor (text -> design matrix)
        self.feature_extractor = FeatureExtractor(**conf.feature_params, **conf.fir_params)
        
        # Encoder
        self.encoder = SklearnEncoder(**conf.encoder_params)
        
        # Decoder
        self.decoder = BayseDecoder(**conf.decoder_params)
        
    def fit(self, bolds, texts):
        # Learn encoding matrix
        features = self.feature_extractor.fit_transform(texts)
        self.encoder.fit(features, bolds)
        
    def decode(self, bolds, prev_texts, **decoder_kwargs):
        """
        Generate new text given a series of bolds and one context
        """
        decoded = _decode(self.encoder, bold, context, **decoder_kwargs)
        return decoded
        
    def evaluate(self, bolds, texts, start_tr=1):
        """
        Eval encoder (R)
        Generate sequences given start_tr
        Eval generations
        """
        encoder_metrics = _run_eval_encoder(self.encoder, bolds, texts)
        decoded_texts = self.decode(bolds[start_tr:], prev_texts[:start_tr])
        decoder_metrics = _run_eval_texts(decoded_texts, texts[start_tr:])
        decoder_metrics["context"] = prev_texts[:start_tr]
        return decoder_metrics
    
    def save(self):
        # Only need to save the sklearn encoder params
        save_model(self.encoder)
        save_conf(nlp_conf)
        
    def load(self, file):
        rebuild
        
```

## Code base

utils.py can be split into:
- features.py (get_features, generate_continuations)
- fir.py (design_matrix, apply_fir)
- data.py (get_data)

fmri/narratives.py should be renamed into data/narratives.py. Other files in fmri/ can be deleted except from exclude_scans

fmri/narratives.py should disappear and be merged with some data utils

## Datasets
Huth: https://openneuro.org/datasets/ds003020/versions/1.0.2

## From events to design_matrix @athual 


```python
import pandas as pd
import numpy as np
def get_texts_from_events(events, extra_trs=5, text_col="word_raw", tr=2):
    """
    events a dataframe of shape [n_words]. with columns: 
        - onset
        - word_raw (or text_col)
    """
    
    # Load eventsulus
    events = events.dropna(subset=[text_col])

    # Aggregate eventsulus by fMRI scans
    events["scan"] = (events["onset"].interpolate().astype(float) // tr).astype(
        int
    )
    assert not events["scan"].isna().any()
    events = events.groupby("scan")[text_col].agg(lambda x: " ".join(x).strip())

    # Subselect non empty scans
    min_tr = int(events.index.min())
    max_tr = int(events.index.max() + extra_trs)
    events = events.loc[min_tr:max_tr]

    # Re-align scans and text
    text = np.empty(max_tr, dtype=f"<U{events.apply(len).max()+1}")
    for time_frame in events.index:
        text[time_frame] = events.loc[time_frame]
    text = text[min_tr:]

    return text

if __name__=="__main__":
    from transformers import AutoTokenizer, AutoModelForCausalLM
    from src.utils.utils import get_features, build_design_matrix
    
    events = pd.DataFrame({"onset": np.arange(0, 100, 0.5),
                           "word_raw": np.arange(200).astype(str)})

    # Aggregate text by TR
    texts = get_texts_from_events(events)
    
    # Load model 
    model_name = "gpt2"
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForCausalLM.from_pretrained(model_name)
    
    # Get features GPT-2
    features = build_design_matrix(
        texts, model=model, tokenizer=tokenizer
    )
    
    print(features.shape)
```

## Example to load beta coef for one subject @athual 

```python=
import numpy as np
import torch
from sklearn.model_selection import KFold
from transformers import AutoModelForCausalLM, AutoTokenizer

from src.encode import encode
from src.utils.data import get_bolds, prepare_data


def get_beta_coef(
    subject,
    model_name="gpt2",
    device="cpu",
    tr=1.5,
):

    # Get data
    print("Loading data")
    bolds = get_bolds(subject)
    texts, bolds, stories = prepare_data(
        list(bolds.keys()), list(bolds.values()), tr=tr
    )

    # Define splits
    cv = KFold(shuffle=False)
    train, test = list(cv.split(texts, bolds))[0]

    # Init nlp model
    print(f"Loading {model_name} model")
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForCausalLM.from_pretrained(model_name)
    if torch.cuda.is_available():
        model.to(device)

    # Encoding
    print(
        f"Fitting encoder on {len(train)} samples,"
        f" {len(np.unique(stories[train]))} stories."
    )
    encoder = encode(
        texts[train],
        bolds[train],
        nlp_model=model,
        nlp_tokenizer=tokenizer,
        bold_pca=0,
    )

    # Extract weights
    weights = encoder["encoding_model"]["ridge"].coef_

    return weights


if __name__ == "__main__":
    weights = get_beta_coef("sub-004")
    print("Weights of shape [voxels, dim_gpt x n_fir_delays]: ", weights.shape)
```


## FUWGS
- SRM to get avg subject
- beta to get single subject

## Useful links @athual 
- LM Generation
https://huggingface.co/blog/how-to-generate
- Eternal Terminal
https://eternalterminal.dev/usermanual/

## Results (02/11/2022)

### Average subject

**Encoding**
<img src="https://i.imgur.com/6WImHha.png" alt="drawing" width="300"/>

![](https://i.imgur.com/rAh4AH9.png)

**Decoding**

![](https://i.imgur.com/CHmpjZU.png)



### Single-subject
**Encoding**
<img src="https://i.imgur.com/GJFNkn8.png" alt="drawing" width="300"/>


**Decoding**


![](https://i.imgur.com/UkugMxo.png)


### Multi-subject (train), left-out subject same story (test)
**Encoding**
<img src="https://i.imgur.com/h9PP6QB.png" alt="drawing" width="300"/>
<img src="https://i.imgur.com/CMHeKVA.png" alt="drawing" width="300"/>


**Decoding**

![](https://i.imgur.com/H9aGr9i.png)

**Conclusion**
On Narratives: 
* *Encoding* good pour average et multi. Relatively bad for single. 
Rmk: scores relatively bad for PrettyMouth et Lucy
* *Decoding* issue. Seems to always yiels the same results. Error in code? Wordlen fixed? 

On Lebel: issue with alignment/raw data projected. 

# Decoding end-to-end

## Options
* **Option 1 - Replace**
    + Linearly predict `text_embeddings` given fMRI using sklearn
    + Replace activations with the predicted `text_embeddings`
    + Issues: How to predict different words? Shouldn't we predict directly sentence embeddings?
* **Option 2 - CrossAttention**
     + Add a cross-attention layer to a generative model. The model has to be generative. 
     + e.g. with GPT-2. Start with context. 
* **Option 3 - Guidance**
     + Optimize activations to best predict the condition.

## Options 2: Cross Attention

``` python
class BoldSpatialReducer(nn.Module):
    """
    Project fMRI into smaller dimensionality (voxels->dim)
    """
    def __init__(self, 
                 bold_dim_in=40962,
                 bold_dim_out=768,
                 bold_dim_hidden=64,
                ):
        super().__init__()
        self.bold_dim_in = bold_dim_in
        self.bold_dim_out = bold_dim_out
        self.spatial_reducer = nn.Sequential(
            nn.Linear(bold_dim_in, bold_dim_hidden),
            nn.Linear(bold_dim_hidden, bold_dim_out)
        )
                
    def forward(self, bold):
        """
        from [T, V] to [T, D]
        """
        out = self.spatial_reducer(bold) # [TR, V] -> [TR, D]
        return out

    
class fMRIConditionalGPTDecoder(ConditionalGPTDecoder):
    
    def __init__(self, config, 
                 cross_attention_layers=(), 
                 freeze_mlp=True,
                 add_fmri=False,
                 bold_dim=40962,
                 bold_dim_hidden=64,
                 init_fmri_std=None,
                 max_fmri_position_embeddings=40,
                 add_fmri_position_embedding=True,
                 sinusoidal_fmri_embeddings=True,):
        super().__init__(config, 
                         cross_attention_layers=cross_attention_layers, 
                         freeze_mlp=freeze_mlp)
        # Add fMRI layer
        self.add_fmri = add_fmri
        if self.add_fmri:
            self.bold_dim = bold_dim
            self.fmri_layer = BoldSpatialReducer(bold_dim_in=self.bold_dim,
                                                 bold_dim_out=self.config.n_embd,
                                                 bold_dim_hidden=bold_dim_hidden)
            init_weights(self.fmri_layer, std=self.config.initializer_range)
        
        # Add positional embedding for fMRI
        self.add_fmri_position_embedding = add_fmri_position_embedding
        if self.add_fmri_position_embedding:
            if sinusoidal_fmri_embeddings: # Set gradient to False
                self.fmri_pe = SinusoidalEmbedding(self.config.n_embd)        
            else:
                self.fmri_pe = nn.Embedding(max_fmri_position_embeddings, self.config.n_embd)
                init_weights(self.fmri_pe, std=self.config.initializer_range)
            
    def forward(self, *args, fmri=None, fmri_positions=None,
                **kwargs):
        assert not "encoder_hidden_states" in kwargs or \
        kwargs["encoder_hidden_states"] is None, \
        "Encoder hidden states should be None"
        if fmri is not None:
            assert self.add_fmri
        if self.add_fmri and (fmri is not None):
            # Project fMRI onto GPT space
            B, T, V = fmri.shape
            assert V == self.bold_dim
            fmri_embeds = self.fmri_layer(fmri.reshape(B * T, V))
            fmri_embeds = fmri_embeds.reshape(B, T, self.config.n_embd)
            if self.add_fmri_position_embedding:
                if fmri_positions is None:
                    fmri_positions = torch.arange(T).long().to(fmri_embeds.device)
                fmri_embeds += self.fmri_pe(fmri_positions)[None] # [B, T, D]
        else:
            fmri_embeds = None
        
        kwargs["encoder_hidden_states"] = fmri_embeds
        out = super().forward(*args, **kwargs)
        return out
    
    def prepare_inputs_for_generation(self, *args, 
                                      fmri=None, 
                                      fmri_positions=None,
                                      **kwargs):
        """
        This is needed for generation
        """
        output = super().prepare_inputs_for_generation(*args, **kwargs)
        output.update({"fmri": fmri, 
                       "fmri_positions": fmri_positions})
        return output
```