Skip to content

Replicated softmax#137

Open
Fede-Rausa wants to merge 17 commits into
masterfrom
replicated_softmax
Open

Replicated softmax#137
Fede-Rausa wants to merge 17 commits into
masterfrom
replicated_softmax

Conversation

@Fede-Rausa

Copy link
Copy Markdown
Collaborator

Adding Replicated Softmax (a restricted boltzmann machine for topic modeling) and Over Replicated Softmax (a deep version of the RSM) models to the octis topic models list.
Both can be currently evaluated only on classification, diversity and coherence metrics, but support locally a fast computation for the perplexity upper bound (that is supported also for the LDA in the gensim implementation, used by octis).

@silviatti

Copy link
Copy Markdown
Collaborator

Hi Federico, regarding the Python 3.8 error, it seems that some libraries have dropped support for python 3.8. I think it's time to deprecate it. Could you please remove Python 3.8 from python-publish.yml and any other related references?

Also, please add tests to ensure everything works as expected. I don't currently have time to verify the implementation manually, but adding tests will give us more confidence and serve as guardrails. You can follow the same patterns as in: https://github.com/MIND-Lab/OCTIS/blob/master/tests/test_octis.py and https://github.com/MIND-Lab/OCTIS/blob/master/tests/test_optimization.py.

Lastly, could you run a formatter (e.g., ruff) on the files you added?

Thanks,

Silvia

@silviatti silviatti self-requested a review November 24, 2025 00:20

@Fede-Rausa Fede-Rausa left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following Your advice, the old tests are passed.
Now the build process works for 3.9 and 3.10. I will supply soon the test functions for the RSM and oRSM, plus a better formatting. Thank You for the support.

Best regards,

Federico

@silviatti silviatti left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review the comments in the code. I recommend running a linter to check for unused functions, duplicated code blocks, and other potential issues. I also asked ChatGPT for a quick review and it identified several points worth examining.

I am not an expert in RSM, so I can only validate those findings to a certain extent. I suggest doing a similar review yourself to further assess and verify potential issues in the implementation.

Comment thread octis/models/oRSM.py Outdated
Comment on lines +361 to +377
def _get_topic_word_matrix(self):
"""
Return the topic representation of the words
"""
w_vh, w_v, w_h = self.W
topic_word_matrix = w_vh.T
normalized = []
for words_w in topic_word_matrix:
minimum = min(words_w)
words = words_w - minimum
normalized.append([float(i) / sum(words) for i in words])
topic_word_matrix = np.array(normalized)
return topic_word_matrix

def _get_topic_word_matrix0(self):
"""
Return the topic representation of the words

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these functions are duplicated

Comment thread octis/models/oRSM.py Outdated
visible_sample = self.sample_softmax(visible_probs, D)
return visible_sample

def sample_h2(self, h1):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this function used? if not, remove

Comment thread octis/models/oRSM.py Outdated
mu1 = self.v_and_h2_to_h1(v, h2)
mu2 = self.h1_to_softmax(mu1)

if (old_mu2 - mu2).sum() < self.epsilon:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure about this? I asked Chatgpt and they say it's incorrect because:

  • Differences may cancel out (positive/negative)
  • It does not use absolute values
    It should be:
if np.linalg.norm(old_mu2 - mu2) < self.epsilon:

or

if np.sum(np.abs(old_mu2 - mu2)) < self.epsilon:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, that's a big point. That error is also present in the original version proposed by dongwookim. Maybe I will have to tune the default value of epsilon to make it larger and let a faster convergence.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have done the tests and the convergence is reached with a reasonably small epsilon, so I have applied the changes.

Comment thread octis/models/oRSM.py Outdated
else:
self.dtm = dtm
if doval:
self.val_dtm = np.log(1 + val_dtm)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it correct that you log transform the validation?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it is really wrong, I will correct it. The log transform should be applied only when the user requires it, for both training and validation (it should be considered a preprocessing step).

Comment thread octis/models/oRSM.py
Comment thread octis/models/RSM.py Outdated
else:
self.dtm = dtm
if doval:
self.val_dtm = np.log(1 + val_dtm)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see similar comment in oRSM

Comment thread octis/models/RSM.py Outdated
"""
mfh = self.visible2hidden(dtm)
vprob = self.hidden2visible(mfh)
lpub = np.exp(-np.nansum(np.log(vprob) * dtm) / np.sum(dtm))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this log perplexity or simply perplexity?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply perplexity, I have to correct it or remove the np.exp

Comment thread octis/models/RSM.py
given a document term matrix
"""
mfh = self.visible2hidden(dtm)
vprob = self.hidden2visible(mfh)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

possibily clip vprob (e.g. vprob = np.clip(vprob, 1e-12, None)) to avoid vprob=0 and Nan values later?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, can np.sum(dtm) = 0?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the first problem, your clip solution is more elegant and efficient than my sad np.nansum operation, so I will use it.
For the second, I give an answer similar to the D=0 problem above.
It shouldn't be possible that dtm is a matrix of zeros. The dtm columns are as many as the words in the vocabulary initialized by the preprocessed corpus. To handle a case were the vocabulary is given and the corpus contains empty documents, an assert check can be added to solve it.

Maybe an error can be thrown in this way?

assert np.sum(dtm)==0, 'All the documents in the corpus seems to be empty for the given vocabulary'

Comment thread octis/models/oRSM.py Outdated
DTM[i, id] = count
return DTM

class oRSM_model(object):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend implementing a shared superclass which implements the shared functions, while you override the model-specific functions. This prevents you from having a lot of duplicated code e.g. _get_topic_word_matrix, get_topics, softmax, multinomial_sample, set_train_hyper, etc.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I will add a third file, RS_utils.py, together with RSM.py and oRSM.py in the models folder.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have called the file RS_class.py, where the shared superclass is implemented. I hope I've done it in the way You mean. Many functions (like set_train_hyper) are quite different between the two classes, despite the common names they have.

Comment thread setup.py Outdated
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
#'Programming Language :: Python :: 3.7',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just remove

@Fede-Rausa

Copy link
Copy Markdown
Collaborator Author

Hi Silvia, thanks for the review, it surfaces important problems.
I will try to solve everything and answer each question as soon as possible.

@Fede-Rausa

Copy link
Copy Markdown
Collaborator Author

Hi Silvia, I have inserted the duplicated functions in RS_class.py , and removed them from the other scripts. Let me know If I have done everything in the good way, or if I have to correct something else. Thank You for your time.

Federico

@silviatti silviatti self-requested a review May 25, 2026 17:27
Comment thread octis/models/oRSM.py Outdated
converge = False
mu2 = np.random.random(self.visible) * self.M # initialize mu2 randomly

while not converge:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if it'd be better to have a max number of iterations instead of an infinite loop and warn if convergence is not reached

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I have added a new hyperparameter: max_iter_mfa. Default is 20.
I have also added a warning to advise for each document if it's latent variables are not converging.
However, convergence seems to be reached for any document inside the 20 NewsGroups dataset in less than 5 iterations.
So luckily this problem is not so present.

Comment thread octis/models/oRSM.py Outdated
adam_m2_vh = np.zeros((dictsize, num_topics))
adam_m2_v = np.zeros((dictsize))
adam_m2_h = np.zeros((num_topics))
t = 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this start at t=0?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason is mathematical: t appears in the bias correction denominators, inside self.gradient_adam, and starting at 0 would cause a division by zero.

bias_correction1 = 1 - decay1t
bias_correction2 = 1 - decay2
t

If t = 0 on the first iteration (before incrementing) they both become 1-1=0 and then

adam_m1_vh_hat = adam_m1_vh / 0 # NaN / inf
adam_m2_vh_hat = adam_m2_vh / 0 # NaN / inf

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bias_correction1 = 1 - decay ^ t
bias_correction2 = 1 - decay ^ t

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, sorry, I have reviewed the function and You are right, the starting value should be 0 since the increment happens before the bias_correction assignments.

Comment thread octis/models/oRSM.py Outdated
rms_m2_vh = np.zeros((dictsize, num_topics))
rms_m2_v = np.zeros((dictsize))
rms_m2_h = np.zeros((num_topics))
self.rms_decay = 0.9

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this overwrite the user-defined parameters?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sorry. I added them last to the main hyperparameters.

Comment thread octis/models/oRSM.py Outdated
Comment on lines +897 to +898
self.adam_decay1 = 0.9
self.adam_decay2 = 0.999

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this overwrite the user-defined parameters?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sorry. I added them last to the main hyperparameters.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have applied the same changes to RSM.py

Comment thread octis/models/oRSM.py Outdated
self.mean_h = True # whether to use mean hidden activations or sample them

self.btsz = btsz
self.batches = int(np.floor(N / btsz))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last batch seems to be discarded. maybe

self.batches = int(np.ceil(N / btsz)) and then ids = np.arange(start_id, min(start_id + self.btsz, N))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I didn't notice that. I have changed the code lines so that now works with this logic.

Comment thread octis/models/RS_class.py Outdated
self.val_dtm = val_dtm

D = self.dtm.sum(axis=1)
assert not np.any(D == 0), "all the documents should have positive length"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be better to raise an error here

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we validate val_dtm too if it's not none?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree

Comment thread octis/models/RS_class.py Outdated
Comment on lines +103 to +120
def neg_free_energy(self, v):
"""
given an array v similar to the dtm, computes the
log pdf under the replicated softmax
"""
w_vh, w_v, w_h = self.W
T = self.hidden
D = v.sum(axis=1)
fren = np.dot(v, w_v)
for j in range(T):
w_j = w_vh[:, j]
a_j = w_h[j]
fren += np.log(1 + np.exp(D * a_j + np.dot(v, w_j)))
return fren

def neg_free_energy_single_doc(self, v):
"""
given a one dimensional Bow vector v representing a single document,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these functions look very similar. maybe consolidate a single helper?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, the unique difference between the two functions was just the shape of D (the total words in the document).
Now the function neg_free_energy works as both.

Comment thread octis/models/RS_class.py Outdated
monitor_loglik=False,
logdtm=False,
):
"""function to initialize the weigths matrices

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weigths is mispelled several times

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for my english.

Comment thread octis/models/RS_class.py Outdated
function to adjust the gradient of the
topic-word interaction weights during a training iteration
of a RS model by a penalty factor.
The model shoud have the attributes:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: should

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo corrected, thanks

Comment thread octis/models/RS_class.py
@@ -0,0 +1,263 @@
import numpy as np

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems that many of these functions can overflow. Not sure if how realistic this is in practice, but it might be worth validating this

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made more numerically stable the functions sigmoid, softmax_vec and neg_free_energy.
I wouldn't add error handling inside these functions.

@silviatti

Copy link
Copy Markdown
Collaborator

PR Summary from Copilot

Status: ⚠️ BLOCKED - Changes Requested

Metric Details
Title Replicated softmax
Author @Fede-Rausa
State Open
Files Changed 9 files
Scope +2,424 lines, -5 lines
Commits 13
Reviews 2 changes-requested, multiple comments
Mergeable Yes (but blocked by review feedback)

Core Changes

This PR adds two new topic modeling algorithms to OCTIS:

  1. RSM (Replicated Softmax Model) - Based on Hinton & Salakhutdinov (2009), an undirected topic model using a restricted Boltzmann machine
  2. oRSM (Over Replicated Softmax) - Based on Srivastava et al. (2013), a deep Boltzmann machine variant with an additional replicated softmax layer

Both models support classification, diversity, and coherence evaluation metrics, plus perplexity computation via approximations and Annealed Importance Sampling (AIS).

Key Implementation Files

# Base class providing shared functionality: 
# - Activation functions (softmax, sigmoid)
# - Sampling methods (multinomial, Bernoulli rejection)
# - Likelihood computation (free energy)
# - Topic-word matrix output
# - Weight initialization
# RSM model implementation with support for:
# - Multiple CD variants (mean-field, k-step, persistent, gradual)
# - Multiple optimizers (SGD, momentum, RMSProp, Adam, Adagrad)
# - Perplexity approximation and exact AIS estimation
# oRSM model with deeper architecture:
# - Pretraining phase + main training phase
# - Mean-field approximation for hidden layers
# - Similar optimizer and CD options as RSM

Other Changes

  1. Configuration updates - Added citations and hyperparameter info for both models
  2. Python version support - Removed Python 3.7/3.8 from setup.py; now supports 3.9+
  3. Test coverage - Added unit tests for both RSM and oRSM models
  4. README - Added model entries with paper references

Merge Readiness & Risk Assessment

🔴 Critical Issues (Must Address Before Merge)

1. Code Quality & Duplication

  • Heavy duplication between RSM and oRSM in gradient methods, sampling functions, and training setup
  • Recommendation: The base class RS_class.py helps, but additional refactoring is needed. Consider consolidating repeated gradient/optimizer initialization code into the base class
  • Risk: High maintenance burden; changes to one model may require updates to the other

2. Convergence Loop Issues

  • The visible_to_hiddens_gibbs() method in oRSM has an infinite loop with no max iteration limit
  • Problem: "If convergence is not reached (e.g., epsilon too small), the loop continues forever"
  • Fix Needed: Add max_iterations parameter and warning if not converged
  • Risk: Training can hang indefinitely

3. Numerical Stability Issues

  • Missing clipping in probability calculations that could lead to log(0) = -inf or NaN values
  • Log transform applied inconsistently to validation DTM
  • Potential division-by-zero when D=0 (document length)
  • Fixes Applied: Some clipping added, but need validation

4. Batch Processing Bug

  • Last batch in training may be discarded due to np.floor() in batch calculation
  • Fix: Use np.ceil() and safe indexing: ids = np.arange(start_id, min(start_id + self.btsz, N))

⚠️ Secondary Issues (Should Address)

  • Incorrect convergence check in oRSM: (np.abs(old_mu2 - mu2)).sum() can cancel signs. Should use np.linalg.norm() or np.sum(np.abs())
  • Parameter initialization: Some gradient caches overwrite user-defined parameters (e.g., self.rms_decay = 0.9)
  • Nested conditionals: Deeply nested if-else chains should use elif for readability
  • Typos: "weigths" instead of "weights", "founded" instead of "found"
  • Logging/monitoring: Missing log on method naming - unclear if values are log-perplexity or perplexity
  • Validation: Should validate val_dtm too if not None

Review Feedback Summary

Author Type Status
silviatti Changes Requested Active (most recent 22 May 2026)
Fede-Rausa Commented Acknowledges most issues

Key feedback (31 inline comments):

  • Code organization and duplication concerns
  • Numerical stability edge cases
  • Potential infinite loops
  • Documentation/naming clarity

Possible Improvements

  1. Refactor shared code into utility functions or extend base class further
  2. Add max iteration limits to all convergence loops with warnings
  3. Standardize probability handling with consistent clipping logic
  4. Add comprehensive docstrings explaining mathematical models
  5. Consider caching for expensive computations (e.g., softmax during sampling)
  6. Add logging/debugging output option for troubleshooting convergence

Note: these should be taken as suggestions rather than strict requirements.

@Fede-Rausa

Copy link
Copy Markdown
Collaborator Author

Hi Silvia!
Sorry for the wait.
I've corrected the code following each point you suggested.

To make the build possible, I had to change the requirements.txt, replacing spacy with spacy==3.7.6 (that is the last version of spacy that was compatible with python 3.9).

Let me know If I have done everything right, or have to correct something else. Thank You for your time. I'm at disposal.

Federico

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants