#![allow(dead_code)]
use crate::candle::loss::nb_log_likelihood;
use crate::candle::nn::linear::*;
use crate::candle::traits::model::*;
use candle_core::{Result, Tensor};
use candle_nn::{ops, Module, VarBuilder};
pub struct MultinomTopicDecoder {
n_features: usize,
n_topics: usize,
dictionary: SoftmaxLinear,
feature_weights: Option<Tensor>,
}
impl MultinomTopicDecoder {
pub fn new(n_features: usize, n_topics: usize, vs: VarBuilder) -> Result<Self> {
let dictionary = log_softmax_linear(n_topics, n_features, vs.pp("dictionary"))?;
Ok(Self {
n_features,
n_topics,
dictionary,
feature_weights: None,
})
}
pub fn dictionary(&self) -> &SoftmaxLinear {
&self.dictionary
}
pub fn set_feature_weights(
&mut self,
weights: &[f32],
dev: &candle_core::Device,
) -> Result<()> {
debug_assert_eq!(weights.len(), self.n_features);
let t = Tensor::from_slice(weights, (1, self.n_features), dev)?;
self.feature_weights = Some(t);
Ok(())
}
}
impl NewDecoder for MultinomTopicDecoder {
fn new(n_features: usize, n_topics: usize, vs: VarBuilder) -> Result<Self> {
MultinomTopicDecoder::new(n_features, n_topics, vs)
}
}
impl DecoderModuleT for MultinomTopicDecoder {
fn forward(&self, z_nk: &Tensor) -> Result<Tensor> {
self.dictionary.forward(z_nk)
}
fn get_dictionary(&self) -> Result<Tensor> {
self.dictionary.weight_dk()
}
fn forward_with_llik<LlikFn>(
&self,
z_nk: &Tensor,
x_nd: &Tensor,
_llik: &LlikFn,
) -> Result<(Tensor, Tensor)>
where
LlikFn: Fn(&Tensor, &Tensor) -> Result<Tensor>,
{
let log_recon_nd = self.dictionary.forward_log(z_nk)?;
let recon_nd = log_recon_nd.exp()?;
let weighted_x = match &self.feature_weights {
Some(w) => x_nd.broadcast_mul(w)?,
None => x_nd.clone(),
};
let llik = weighted_x.mul(&log_recon_nd)?.sum(x_nd.rank() - 1)?;
Ok((recon_nd, llik))
}
fn llik_is_gene_chunked(&self) -> bool {
true
}
fn llik_gene_chunked(&self, z_nk: &Tensor, x_nd: &Tensor, gene_chunk: usize) -> Result<Tensor> {
let last = x_nd.rank() - 1;
let log_w_kd = self.dictionary.log_weight_kd()?;
let mut llik: Option<Tensor> = None;
for (start, len) in super::gene_slices(self.n_features, gene_chunk) {
let log_recon = self
.dictionary
.forward_log_slice(z_nk, Some(&log_w_kd), start, len)?;
let x = x_nd.narrow(last, start, len)?;
let weighted = match &self.feature_weights {
Some(w) => x.broadcast_mul(&w.narrow(last, start, len)?)?,
None => x,
};
let part = weighted.mul(&log_recon)?.sum(last)?;
llik = Some(match llik.take() {
Some(acc) => acc.add(&part)?,
None => part,
});
}
llik.ok_or_else(|| candle_core::Error::Msg("no gene slices to score".into()))
}
fn dim_obs(&self) -> usize {
self.n_features
}
fn dim_latent(&self) -> usize {
self.n_topics
}
fn attach_feature_weights(&mut self, weights: &[f32], dev: &candle_core::Device) -> Result<()> {
MultinomTopicDecoder::set_feature_weights(self, weights, dev)
}
}
pub struct NbTopicDecoder {
n_features: usize,
n_topics: usize,
dictionary: SoftmaxLinear,
log_phi_1d: Tensor,
}
impl NbTopicDecoder {
pub fn new(n_features: usize, n_topics: usize, vs: VarBuilder) -> Result<Self> {
let dictionary = log_softmax_linear(n_topics, n_features, vs.pp("dictionary"))?;
let init_val = candle_nn::Init::Const(0.693); let log_phi_1d = vs.get_with_hints((1, n_features), "log_phi", init_val)?;
Ok(Self {
n_features,
n_topics,
dictionary,
log_phi_1d,
})
}
pub fn phi(&self) -> Result<Tensor> {
self.log_phi_1d.exp()
}
pub fn log_phi(&self) -> &Tensor {
&self.log_phi_1d
}
}
impl NewDecoder for NbTopicDecoder {
fn new(n_features: usize, n_topics: usize, vs: VarBuilder) -> Result<Self> {
NbTopicDecoder::new(n_features, n_topics, vs)
}
}
impl DecoderModuleT for NbTopicDecoder {
fn forward(&self, z_nk: &Tensor) -> Result<Tensor> {
self.dictionary.forward(z_nk)
}
fn get_dictionary(&self) -> Result<Tensor> {
self.dictionary.weight_dk()
}
fn forward_with_llik<LlikFn>(
&self,
z_nk: &Tensor,
x_nd: &Tensor,
_llik: &LlikFn,
) -> Result<(Tensor, Tensor)>
where
LlikFn: Fn(&Tensor, &Tensor) -> Result<Tensor>,
{
let log_recon_nd = self.dictionary.forward_log(z_nk)?;
let recon_nd = log_recon_nd.exp()?;
let lib_size = x_nd.sum(x_nd.rank() - 1)?.unsqueeze(1)?;
let mu_nd = recon_nd.broadcast_mul(&lib_size)?;
let llik = nb_log_likelihood(x_nd, &mu_nd, &self.log_phi_1d)?;
Ok((log_recon_nd.exp()?, llik))
}
fn llik_is_gene_chunked(&self) -> bool {
true
}
fn llik_gene_chunked(&self, z_nk: &Tensor, x_nd: &Tensor, gene_chunk: usize) -> Result<Tensor> {
let last = x_nd.rank() - 1;
let lib_size = x_nd.sum(last)?.unsqueeze(1)?; let log_w_kd = self.dictionary.log_weight_kd()?;
let mut llik: Option<Tensor> = None;
for (start, len) in super::gene_slices(self.n_features, gene_chunk) {
let recon = self
.dictionary
.forward_log_slice(z_nk, Some(&log_w_kd), start, len)?
.exp()?;
let mu = recon.broadcast_mul(&lib_size)?;
let x = x_nd.narrow(last, start, len)?;
let log_phi = self
.log_phi_1d
.narrow(1, start, len)?
.broadcast_as(x.shape())?;
let part = crate::candle::loss::nb_log_likelihood_elem(&x, &mu, &log_phi)?.sum(last)?;
llik = Some(match llik.take() {
Some(acc) => acc.add(&part)?,
None => part,
});
}
llik.ok_or_else(|| candle_core::Error::Msg("no gene slices to score".into()))
}
fn dim_obs(&self) -> usize {
self.n_features
}
fn dim_latent(&self) -> usize {
self.n_topics
}
fn build_ess_llik<'a>(
&'a self,
x_nd: &'a Tensor,
topic_smoothing: f64,
) -> Result<EssLlikFn<'a>> {
let log_dict_dk = self.get_dictionary()?.detach();
let beta_kd = log_dict_dk.t()?.exp()?.contiguous()?;
let log_phi = self.log_phi_1d.detach();
let lib_n1 = x_nd.sum(x_nd.rank() - 1)?.unsqueeze(1)?;
let k = self.dim_latent() as f64;
Ok(Box::new(move |z_nk: &Tensor| {
let mut z = ops::softmax(z_nk, 1)?;
if topic_smoothing > 0.0 {
z = ((z * (1.0 - topic_smoothing))? + topic_smoothing / k)?;
}
let mu = z.matmul(&beta_kd)?.broadcast_mul(&lib_n1)?;
nb_log_likelihood(x_nd, &mu, &log_phi)
}))
}
}
#[cfg(test)]
#[path = "topic_tests.rs"]
mod tests;