Skip to content

gaussian grbm initialization - #71

Open
jquetzalcoatl wants to merge 25 commits into
dwavesystems:mainfrom
jquetzalcoatl:feature/gaussian-rbm-init
Open

gaussian grbm initialization#71
jquetzalcoatl wants to merge 25 commits into
dwavesystems:mainfrom
jquetzalcoatl:feature/gaussian-rbm-init

Conversation

@jquetzalcoatl

@jquetzalcoatl jquetzalcoatl commented Mar 16, 2026

Copy link
Copy Markdown

grbm weights and biases initialization set to Gaussian N(0,1/number of nodes)

Hinton guide suggests 0.01 as standard deviation. See https://www.cs.toronto.edu/~hinton/absps/guideTR.pdf

Moreover, having it set to Gaussian with this dependence on the number of nodes makes the energy extensive and initializes the gRBM in a paramagnetic phase similar to that describen in the Random Energy model paper

https://journals.aps.org/prb/abstract/10.1103/PhysRevB.24.2613

See #48

@kevinchern

Copy link
Copy Markdown
Collaborator

@jquetzalcoatl IIRC, Hinton's recommendation pertains to zero-one-valued RBMs (bipartite with hidden units). Would it make sense to translate the $0.01$ to the spin-valued equivalent?

@jquetzalcoatl

Copy link
Copy Markdown
Author

@kevinchern The REM reference is for spin models i.e., {-1,1}. Ultimately, the initialization pertains to whether the model is ergodic. In this sense, the support only set an offset energy.

I believe the main motivation for initializing with 0.01 in Hinton's guide is to start in a paramagnetic phase, which ties nicely with the REM/SK spin glass model

@kevinchern kevinchern 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.

Can you add a release note to go with this?

Comment thread dwave/plugins/torch/models/boltzmann_machine.py Outdated
@jquetzalcoatl

Copy link
Copy Markdown
Author

added release note

@kevinchern kevinchern 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.

Following motivation from references, should h be initialized to 0?

Comment thread releasenotes/notes/gaussian-rbm-init-28fd4d295ef86d77.yaml Outdated
Comment thread dwave/plugins/torch/models/boltzmann_machine.py Outdated
Comment thread dwave/plugins/torch/models/boltzmann_machine.py Outdated
Comment thread dwave/plugins/torch/models/boltzmann_machine.py Outdated

@kevinchern kevinchern 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.

Tests are failing but otherwise LGTM. Thanks for the much-needed PR @jquetzalcoatl !!

@VolodyaCO offered to take a look at the tests

Comment thread dwave/plugins/torch/models/boltzmann_machine.py Outdated
Comment thread releasenotes/notes/gaussian-rbm-init-28fd4d295ef86d77.yaml Outdated
Comment thread releasenotes/notes/gaussian-rbm-init-28fd4d295ef86d77.yaml Outdated
@kevinchern
kevinchern requested a review from VolodyaCO March 17, 2026 18:01
@kevinchern

Copy link
Copy Markdown
Collaborator

Any updates on this?

@VolodyaCO

Copy link
Copy Markdown
Collaborator

The reason for this test failing is very strange. Essentially, it is making sure that both the DVAE forward (which does encode -> latent to discrete -> decode) matches encode -> latent_to_discrete -> decode, i.e., this is a pretty simple unit test:

expected_latents = self.encoders[n_latent_dims](self.data)
expected_discretes = self.dvaes[n_latent_dims].latent_to_discrete(
    expected_latents, n_samples
)
expected_reconstructed_x = self.decoders[n_latent_dims](expected_discretes)

latents, discretes, reconstructed_x = self.dvaes[n_latent_dims].forward(
    x=self.data, n_samples=n_samples
)

assert torch.equal(reconstructed_x, expected_reconstructed_x)
assert torch.equal(discretes, expected_discretes)
assert torch.equal(latents, expected_latents)

Moreover, self.dvaes is built as

self.encoders = {i: Encoder(i) for i in latent_dims_list}
self.decoders = {i: Decoder(latent_features, input_features) for i in latent_dims_list}
self.dvaes = {i: DVAE(self.encoders[i], self.decoders[i]) for i in latent_dims_list}

So even if the encoders/decoders are updated in other tests (because of training), there should be a permanent tracking of the encoders/decoders in the dvaes.

@VolodyaCO

Copy link
Copy Markdown
Collaborator

Found the issue and fixed it in a PR to @jquetzalcoatl 's repo: jquetzalcoatl#1

Please approve javi, this would update the current PR and solve the issue.

Took me a while to get the error!

Fix failing forward method unit tests

@VolodyaCO VolodyaCO 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.

I have definitely had to manually change the initialisation of GRBM weights whenever I use the GRBM. Thanks for this PR. I think it looks good to merge.

@kevinchern
kevinchern self-requested a review April 13, 2026 17:08

@kevinchern kevinchern 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.

@jquetzalcoatl I added a couple typo fixes, can you accept them?
The remaining questions/comments are for @VolodyaCO and should be good to merge after.

Comment thread dwave/plugins/torch/models/boltzmann_machine.py Outdated
Comment thread releasenotes/notes/gaussian-rbm-init-28fd4d295ef86d77.yaml Outdated
Comment thread tests/test_dvae_winci2020.py
Comment thread tests/test_dvae_winci2020.py
Comment thread releasenotes/notes/gaussian-rbm-init-28fd4d295ef86d77.yaml Outdated
- |
Initialize ``GraphRestrictedBoltzmannMachine`` weights using Gaussian
random variables with standard deviation equal to :math:`1/\sqrt(N)`, where N
denotes the number of nodes in the GRBM. The weight-initialization strategy is grounded in `Hinton's practical guide for RBM training <https://www.cs.toronto.edu/~hinton/absps/guideTR.pdf>`_, which recommends sampling weights from a Gaussian distribution with mean 0 and standard deviation 0.01 (for zero-one-valued RBMs). The scaling factor of :math:`1/\sqrt(N)` ensures that the energy functional remains extensive and initializes the GRBM in a paramagnetic regime, consistent with the `Sherrington-Kirkpatrick model<https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.35.1792>`_.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Better add some line breaks here, splitting the full paragraph on several lines.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hasn't been addressed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Please check again. Thnx!

Comment thread dwave/plugins/torch/models/boltzmann_machine.py Outdated

@jackraymond jackraymond left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nu_i = beta* (h_i + sum_j Jij s_j) controls the typical field (bias) of a variable at initialization, I think we want this to be O(1), i.i.d and high entropy. I think that is inline with the motivation for the pull request, but leads to some additional considerations. We also want h to be small compared to J, because we want to initialize outside of the weakly coupled regime ideally. h should be just large enough to break the macroscopic sign-symmetry (IMO).

I think we want a strongly coupled models, so h should be just large enough to break the symmetry and no more. I.e. the contribution from h should be O(1):
beta * sum_i h_i s_i ~ 1 which for random s implies beta h_i ~ O(1/sqrt(N)).

For extensive energy I think we require J to scale as 1/root(mean-degree). 1/sqrt(N) scaling is appropriate for dense models only.

We might want to think about putting in a beta value, that reflects the QPU. E.g. if single qubit freezeout temperature ~ 1/5 we would want to scale down by a factor 5 (I think current default scales the wrong way).

We might want to think about the fact that the J and h-distributions are bounded, so Gaussian is not the maximum-entropy choice. This is probably a technicality because the bounds turn out to be far from the initialization values, but we should certainly discuss the impacts of clipping.

@kevinchern

Copy link
Copy Markdown
Collaborator

Any updates on this? @jquetzalcoatl

@kevinchern kevinchern 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.

LGTM, thanks @jquetzalcoatl!!

Comment on lines +262 to +264
torch.testing.assert_close(latents, expected_latents)
torch.testing.assert_close(discretes, expected_discretes)
torch.testing.assert_close(reconstructed_x, expected_reconstructed_x)

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.

Suggested change
torch.testing.assert_close(latents, expected_latents)
torch.testing.assert_close(discretes, expected_discretes)
torch.testing.assert_close(reconstructed_x, expected_reconstructed_x)
with self.subTest("Test latent variables match"):
torch.testing.assert_close(latents, expected_latents)
with self.subTest("Test discrete variables match"):
torch.testing.assert_close(discretes, expected_discretes)
with self.subTest("Test reconstructed outputs match"):
torch.testing.assert_close(reconstructed_x, expected_reconstructed_x)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@VolodyaCO can you review this ^?

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.

The changes are just to separate each test within their own scope. It should be fine.

Comment thread dwave/plugins/torch/models/boltzmann_machine.py Outdated

@thisac thisac left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

- |
Initialize ``GraphRestrictedBoltzmannMachine`` weights using Gaussian
random variables with standard deviation equal to :math:`1/\sqrt(N)`, where N
denotes the number of nodes in the GRBM. The weight-initialization strategy is grounded in `Hinton's practical guide for RBM training <https://www.cs.toronto.edu/~hinton/absps/guideTR.pdf>`_, which recommends sampling weights from a Gaussian distribution with mean 0 and standard deviation 0.01 (for zero-one-valued RBMs). The scaling factor of :math:`1/\sqrt(N)` ensures that the energy functional remains extensive and initializes the GRBM in a paramagnetic regime, consistent with the `Sherrington-Kirkpatrick model<https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.35.1792>`_.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hasn't been addressed.

@VolodyaCO VolodyaCO 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.

The comments by @jackraymond have not been taken into account. As far as I understood, the average absolute value should scale as 1/(mean degree) for quadratic biases and 1/(number of nodes) for linear biases. If sampling from a normal distribution that means that the standard deviation should be of those scales (you need to account for the sqrt(2/π) factor and also for the beta \approx 5 factor).

@jquetzalcoatl

Copy link
Copy Markdown
Author

@VolodyaCO will be take over the PR

@kevinchern kevinchern 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.

Minor comments/questions

Comment on lines +106 to +109
edge_idx_i = torch.tensor([self._node_to_idx[i] for i, _ in self._edges], dtype=torch.long)
edge_idx_j = torch.tensor(
[self._node_to_idx[j] for _, j in self._edges], dtype=torch.long
)

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.

Why redefine this? Just to include dtype?

Comment on lines +4 to +16
Initialize ``GraphRestrictedBoltzmannMachine`` weights using Gaussian \
random variables with graph-connectivity-dependent standard deviations. \
For an edge :math:`(u, v)`, the default standard deviation is \
:math:`2.5 / (\deg(u)\deg(v))^{1/4}`. \
The weight-initialization strategy is grounded in `Hinton's practical \
guide for RBM training \
<https://www.cs.toronto.edu/~hinton/absps/guideTR.pdf>`_, \
which recommends sampling weights from a Gaussian distribution with mean 0 and standard \
deviation 0.01 (for zero-one-valued RBMs). The connectivity scaling keeps \
the energy functional extensive on sparse graphs, while the temperature factor initializes \
the GRBM deep in a paramagnetic regime for QPU-backed sampling, \
consistent with the `Sherrington-Kirkpatrick model \
<https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.35.1792>`_.

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.

what's going on with the backslashes \ 🧐?

bm = GRBM(nodes, edges)

torch.testing.assert_close(bm.linear, torch.zeros(len(nodes)))
torch.testing.assert_close(bm.quadratic, expected_quadratic)

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 was wondering if hard-coding expected_std + annotating the code with comments would be safer but this is fine as is.

["a", "b", "c"], [("a", "b"), ("b", "c")], quadratic={("b", "c"): 1.25}
)

self.assertAlmostEqual(1.25, bm.quadratic[1].item())

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 check ("a", "b")'s weight is preserved (untouched).
e.g.,

  1. set seed -> initialize, track weight of ("a", "b")
  2. set seed -> initialize with quadratic=...
  3. check 1 and 2's weight of ("a", "b") agree.

# self.decoders is independent of number of latent dims, but we also create a dict to separate
# them
# self.decoders is independent of number of latent dims, but we also create a dict to
# separate them

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.

Update your formatter!!!!! Follow Ocean dev guide

@@ -78,12 +78,11 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
# are the models themselves

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.

Why is this test being updated?

@kevinchern
kevinchern requested a review from thisac August 20, 2026 22:49
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.

5 participants