Source code for gclda.decode

# emacs: -*- mode: python-mode; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-
# ex: set sts=4 ts=4 sw=4 et:
"""
Functions for functional decoding/reverse inference using a GCLDA model.
"""
from __future__ import print_function, division

from builtins import object
import numpy as np
import pandas as pd
import nibabel as nib
from nilearn.masking import apply_mask, unmask
from sklearn.feature_extraction.text import CountVectorizer

from .utils import weight_priors
from .due import due, Doi


[docs]@due.dcite(Doi('10.1371/journal.pcbi.1005649'), description='Describes decoding methods using GC-LDA.') def decode_roi(model, roi, topic_priors=None, prior_weight=1.): """ Perform image-to-text decoding for discrete image inputs (e.g., regions of interest, significant clusters). Parameters ---------- model : :obj:`gclda.model.Model` Model object needed for decoding. roi : :obj:`nibabel.Nifti1Image` or :obj:`str` Binary image to decode into text. If string, path to a file with the binary image. topic_priors : :obj:`numpy.ndarray` of :obj:`float`, optional A 1d array of size (n_topics) with values for topic weighting. If None, no weighting is done. Default is None. prior_weight : :obj:`float`, optional The weight by which the prior will affect the decoding. Default is 1. Returns ------- decoded_df : :obj:`pandas.DataFrame` A DataFrame with the word-tokens and their associated weights. topic_weights : :obj:`numpy.ndarray` of :obj:`float` The weights of the topics used in decoding. Notes ----- ====================== ============================================================== Notation Meaning ====================== ============================================================== :math:`v` Voxel :math:`t` Topic :math:`w` Word type :math:`r` Region of interest (ROI) :math:`p(v|t)` Probability of topic given voxel (``p_topic_g_voxel``) :math:`p(t|r)` Probability of topic given ROI (``topic_weights``) :math:`p(w|t)` Probability of word type given topic (``p_word_g_topic``) ====================== ============================================================== 1. Compute :math:`p(v|t)`. - From :obj:`gclda.model.Model.get_spatial_probs()` 2. Compute topic weight vector (:math:`\\tau_{t}`) by adding across voxels within ROI. - :math:`\\tau_{t} = \sum_{i} {p(t|v_{i})}` 3. Multiply :math:`\\tau_{t}` by :math:`p(w|t)`. - :math:`p(w|r) \propto \\tau_{t} \cdot p(w|t)` 4. The resulting vector (``word_weights``) reflects arbitrarily scaled term weights for the ROI. """ if isinstance(roi, str): roi = nib.load(roi) elif not isinstance(roi, nib.Nifti1Image): raise IOError('Input roi must be either a nifti image ' '(nibabel.Nifti1Image) or a path to one.') dset_aff = model.dataset.mask_img.affine if not np.array_equal(roi.affine, dset_aff): raise ValueError('Input roi must have same affine as mask img:' '\n{0}\n{1}'.format(np.array2string(roi.affine), np.array2string(dset_aff))) # Load ROI file and get ROI voxels overlapping with brain mask mask_vec = model.dataset.mask_img.get_data().ravel().astype(bool) roi_vec = roi.get_data().astype(bool).ravel() roi_vec = roi_vec[mask_vec] roi_idx = np.where(roi_vec)[0] p_topic_g_voxel, _ = model.get_spatial_probs() p_topic_g_roi = p_topic_g_voxel[roi_idx, :] # p(T|V) for voxels in ROI only topic_weights = np.sum(p_topic_g_roi, axis=0) # Sum across words if topic_priors is not None: weighted_priors = weight_priors(topic_priors, prior_weight) topic_weights *= weighted_priors # Multiply topic_weights by topic-by-word matrix (p_word_g_topic). n_word_tokens_per_topic = np.sum(model.n_word_tokens_word_by_topic, axis=0) p_word_g_topic = model.n_word_tokens_word_by_topic / n_word_tokens_per_topic[None, :] p_word_g_topic = np.nan_to_num(p_word_g_topic, 0) word_weights = np.dot(p_word_g_topic, topic_weights) decoded_df = pd.DataFrame(index=model.dataset.word_labels, columns=['Weight'], data=word_weights) decoded_df.index.name = 'Term' return decoded_df, topic_weights
[docs]@due.dcite(Doi('10.1371/journal.pcbi.1005649'), description='Describes decoding methods using GC-LDA.') def decode_continuous(model, image, topic_priors=None, prior_weight=1.): """ Perform image-to-text decoding for continuous inputs (e.g., unthresholded statistical maps). Parameters ---------- model : :obj:`gclda.model.Model` Model object needed for decoding. image : :obj:`nibabel.Nifti1Image` or :obj:`str` Whole-brain image to decode into text. Must be in same space as model and dataset. Model's template available in `model.dataset.mask_img`. topic_priors : :obj:`numpy.ndarray` of :obj:`float`, optional A 1d array of size (n_topics) with values for topic weighting. If None, no weighting is done. Default is None. prior_weight : :obj:`float`, optional The weight by which the prior will affect the decoding. Default is 1. Returns ------- decoded_df : :obj:`pandas.DataFrame` A DataFrame with the word-tokens and their associated weights. topic_weights : :obj:`numpy.ndarray` of :obj:`float` The weights of the topics used in decoding. Notes ----- ====================== ============================================================== Notation Meaning ====================== ============================================================== :math:`v` Voxel :math:`t` Topic :math:`w` Word type :math:`i` Input image :math:`p(v|t)` Probability of topic given voxel (``p_topic_g_voxel``) :math:`p(t|i)` Topic weight vector (``topic_weights``) :math:`p(w|t)` Probability of word type given topic (``p_word_g_topic``) :math:`\omega` 1d array from input image (``input_values``) ====================== ============================================================== 1. Compute :math:`p(t|v)` (``p_topic_g_voxel``). - From :obj:`gclda.model.Model.get_spatial_probs()` 2. Squeeze input image to 1d array :math:`\omega` (``input_values``). 3. Compute topic weight vector (:math:`\\tau_{t}`) by multiplying :math:`p(t|v)` by input image. - :math:`\\tau_{t} = p(t|v) \cdot \omega` 4. Multiply :math:`\\tau_{t}` by :math:`p(w|t)`. - :math:`p(w|i) \propto \\tau_{t} \cdot p(w|t)` 5. The resulting vector (``word_weights``) reflects arbitrarily scaled term weights for the input image. """ if isinstance(image, str): image = nib.load(image) elif not isinstance(image, nib.Nifti1Image): raise IOError('Input image must be either a nifti image ' '(nibabel.Nifti1Image) or a path to one.') # Load image file and get voxel values input_values = apply_mask(image, model.dataset.mask_img) p_topic_g_voxel, _ = model.get_spatial_probs() topic_weights = np.squeeze(np.dot(p_topic_g_voxel.T, input_values[:, None])) if topic_priors is not None: weighted_priors = weight_priors(topic_priors, prior_weight) topic_weights *= weighted_priors # Multiply topic_weights by topic-by-word matrix (p_word_g_topic). n_word_tokens_per_topic = np.sum(model.n_word_tokens_word_by_topic, axis=0) p_word_g_topic = model.n_word_tokens_word_by_topic / n_word_tokens_per_topic[None, :] p_word_g_topic = np.nan_to_num(p_word_g_topic, 0) word_weights = np.dot(p_word_g_topic, topic_weights) decoded_df = pd.DataFrame(index=model.dataset.word_labels, columns=['Weight'], data=word_weights) decoded_df.index.name = 'Term' return decoded_df, topic_weights
[docs]@due.dcite(Doi('10.1371/journal.pcbi.1005649'), description='Describes decoding methods using GC-LDA.') def encode(model, text, out_file=None, topic_priors=None, prior_weight=1.): """ Perform text-to-image encoding. Parameters ---------- model : :obj:`gclda.model.Model` Model object needed for decoding. text : :obj:`str` or :obj:`list` Text to encode into an image. out_file : :obj:`str`, optional If not None, writes the encoded image to a file. topic_priors : :obj:`numpy.ndarray` of :obj:`float`, optional A 1d array of size (n_topics) with values for topic weighting. If None, no weighting is done. Default is None. prior_weight : :obj:`float`, optional The weight by which the prior will affect the encoding. Default is 1. Returns ------- img : :obj:`nibabel.Nifti1Image` The encoded image. topic_weights : :obj:`numpy.ndarray` of :obj:`float` The weights of the topics used in encoding. Notes ----- ====================== ============================================================== Notation Meaning ====================== ============================================================== :math:`v` Voxel :math:`t` Topic :math:`w` Word type :math:`h` Input text :math:`p(v|t)` Probability of topic given voxel (``p_topic_g_voxel``) :math:`\\tau_{t}` Topic weight vector (``topic_weights``) :math:`p(w|t)` Probability of word type given topic (``p_word_g_topic``) :math:`\omega` 1d array from input image (``input_values``) ====================== ============================================================== 1. Compute :math:`p(v|t)` (``p_voxel_g_topic``). - From :obj:`gclda.model.Model.get_spatial_probs()` 2. Compute :math:`p(t|w)` (``p_topic_g_word``). 3. Vectorize input text according to model vocabulary. 4. Reduce :math:`p(t|w)` to only include word types in input text. 5. Compute :math:`p(t|h)` (``p_topic_g_text``) by multiplying :math:`p(t|w)` by word counts for input text. 6. Sum topic weights (:math:`\\tau_{t}`) across words. - :math:`\\tau_{t} = \sum_{i}{p(t|h_{i})}` 7. Compute voxel weights. - :math:`p(v|h) \propto p(v|t) \cdot \\tau_{t}` 8. The resulting array (``voxel_weights``) reflects arbitrarily scaled voxel weights for the input text. 9. Unmask and reshape ``voxel_weights`` into brain image. """ if isinstance(text, list): text = ' '.join(text) # Assume that words in word_labels are underscore-separated. # Convert to space-separation for vectorization of input string. vocabulary = [term.replace('_', ' ') for term in model.dataset.word_labels] max_len = max([len(term.split(' ')) for term in vocabulary]) vectorizer = CountVectorizer(vocabulary=model.dataset.word_labels, ngram_range=(1, max_len)) word_counts = np.squeeze(vectorizer.fit_transform([text]).toarray()) keep_idx = np.where(word_counts > 0)[0] text_counts = word_counts[keep_idx] n_topics_per_word_token = np.sum(model.n_word_tokens_word_by_topic, axis=1) p_topic_g_word = model.n_word_tokens_word_by_topic / n_topics_per_word_token[:, None] p_topic_g_word = np.nan_to_num(p_topic_g_word, 0) p_topic_g_text = p_topic_g_word[keep_idx] # p(T|W) for words in text only prod = p_topic_g_text * text_counts[:, None] # Multiply p(T|W) by words in text topic_weights = np.sum(prod, axis=0) # Sum across words if topic_priors is not None: weighted_priors = weight_priors(topic_priors, prior_weight) topic_weights *= weighted_priors _, p_voxel_g_topic = model.get_spatial_probs() voxel_weights = np.dot(p_voxel_g_topic, topic_weights) img = unmask(voxel_weights, model.dataset.mask_img) if out_file is not None: img.to_filename(out_file) return img, topic_weights