StructuralModel¶
The deep_lvpm.model.StructuralModel class is the core DLVPM model. It
subclasses torch.nn.Module and coordinates one PyTorch measurement model
per data view. Each measurement model produces features for one view, and
StructuralModel appends the final DLVPM projection layer that turns those
features into deep latent variables (DLVs).
The public methods compile, fit, evaluate, and predict are
Deep LVPM convenience methods. Internally they use ordinary PyTorch concepts:
model.train(), forward passes, loss calculation, loss.backward(),
optimizer.step(), model.eval(), and torch.no_grad().
Constructor¶
StructuralModel(
Path,
model_list,
regularizer_list,
tot_num,
ndims,
orthogonalization="Moore-Penrose",
momentum=0.95,
epsilon=1e-4,
train_DLV=True,
run_from_config=False,
is_siamese=False,
diag_offset=1e-3,
sparse_l1_list=0.0,
order=False,
order_association_cutoff=None,
missing_strategy="impute",
latent_imputation_rank=2,
latent_imputation_iterations=3,
device=None,
**kwargs,
)
Parameters¶
PathBinary or weighted adjacency matrix with shape
(n_views, n_views). A non-zero entryPath[i, j]means viewiis trained to agree with viewj.model_listList of
torch.nn.Modulemeasurement models. There is normally one module per data view. The modules should return feature tensors with shape(batch, features); higher-dimensional outputs are flattened before the DLVPM projection layer.regularizer_listOne regularizer entry per view. Use
Nonefor views without projection regularization. Regularizers come fromdeep_lvpm.regularizers.tot_numTotal number of training samples. The DLVPM projection layers use this for internal covariance and normalization statistics.
ndimsNumber of DLV dimensions to learn per view.
orthogonalizationProjection method for the appended DLVPM layer.
"Moore-Penrose"usesdeep_lvpm.layers.FactorLayer.FactorLayer;"zca"usesdeep_lvpm.layers.ZCALayer.ZCALayer.momentumandepsilonNumerical settings used by the projection layers and correlation calculations.
momentumcontrols moving statistics;epsilonavoids division by zero.train_DLVIf
True, target DLVs are computed with measurement models in training mode during training. IfFalse, target DLVs are computed in evaluation mode.is_siameseIf
True, the first measurement model is wrapped once and reused for all views. This is used when two or more views should share the same encoder weights, as in the CIFAR-10 Siamese tutorial.diag_offsetDiagonal jitter used by
ZCALayerto keep covariance matrices well-conditioned.sparse_l1_listProximal L1 soft-thresholding strength for the appended projection weights. Use a scalar for all views, a list with one value per view, or
0.0for no soft-thresholding.orderandorder_association_cutoffOrdered ZCA options.
order=Trueaccumulates a structural association matrix during training and orders DLV dimensions by shared structure at the end offit.order_association_cutoffoptionally keeps the smallest number of ordered dimensions needed to reach the requested cumulative association mass.missing_strategyMissing-view strategy used during training.
"impute"is the default and completes missing DLVs, not raw input features, before the structural losses and training metrics are calculated."project"keeps the previous behavior, where missing views are zero-scattered and masked out.evaluate()andpredict()always use projection behavior.latent_imputation_rankandlatent_imputation_iterationsSettings for
missing_strategy="impute". DLVPM completes missing latent view blocks with batch-local low-rank SVD completion using the given rank and number of refinement iterations.deviceOptional PyTorch device. If omitted, DLVPM uses CUDA when available and otherwise CPU.
Minimal example¶
This example creates two tabular views, defines one encoder per view, connects the views with a symmetric path matrix, and trains a ZCA-orthogonalized DLVPM model.
import numpy as np
import torch
import torch.nn as nn
from deep_lvpm.model import StructuralModel
n_samples = 512
rng = np.random.default_rng(42)
view_a = rng.normal(size=(n_samples, 8)).astype("float32")
view_b = rng.normal(size=(n_samples, 6)).astype("float32")
class Encoder(nn.Module):
def __init__(self, input_dim):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, 32),
nn.ReLU(),
nn.Linear(32, 16),
nn.ReLU(),
)
self.n_inputs = 1
def forward(self, inputs):
return self.net(inputs)
model_a = Encoder(view_a.shape[1])
model_b = Encoder(view_b.shape[1])
Path = np.array(
[
[0, 1],
[1, 0],
],
dtype="float32",
)
struct_model = StructuralModel(
Path=Path,
model_list=[model_a, model_b],
regularizer_list=[None, None],
tot_num=n_samples,
ndims=4,
orthogonalization="zca",
order=True,
)
optimizers = [
torch.optim.Adam(view_model.parameters(), lr=1e-3)
for view_model in struct_model.model_list
]
struct_model.compile(optimizer=optimizers)
history = struct_model.fit(
[view_a, view_b],
batch_size=64,
epochs=5,
verbose=True,
)
metrics = struct_model.evaluate([view_a, view_b], verbose=False)
dlvs = struct_model.predict([view_a, view_b], verbose=False)
print(metrics)
print(dlvs.shape)
Training API¶
compile accepts either a single PyTorch optimizer or a list of optimizers.
For ordinary multi-view models, use one optimizer per wrapped view model:
optimizers = [
torch.optim.Adam(view_model.parameters(), lr=1e-4)
for view_model in struct_model.model_list
]
struct_model.compile(optimizer=optimizers)
For Siamese models with shared weights, it is valid to provide the same optimizer object for each shared view, as shown in the Siamese tutorial.
fit accepts NumPy arrays, tensors, or a PyTorch DataLoader. For array
or tensor inputs, pass one item per model input:
history = struct_model.fit(
[view_a, view_b],
batch_size=64,
epochs=20,
validation_split=0.1,
shuffle=True,
)
When using schedulers, pass a list through schedulers. Each scheduler is
stepped once per epoch after the training batches finish.
schedulers = [
torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.99)
for optimizer in optimizers
]
struct_model.fit([view_a, view_b], schedulers=schedulers)
Metrics¶
fit and evaluate report a dictionary with these keys:
total_lossMean DLVPM objective across views, including reconstruction loss and any projection-layer regularization.
cross_metricMean Pearson correlation between structurally connected views. Higher values indicate stronger cross-view agreement.
mse_lossReconstruction loss between each source view and its connected target views.
redundancyMean absolute off-diagonal within-view latent correlation. Lower values indicate less redundant DLV dimensions.
order_strengthPresent only when
order=True. This diagnostic summarizes whether earlier ordered dimensions carry stronger structural association than later dimensions.
Prediction and correlation matrices¶
predict returns a NumPy array with shape (n_samples, ndims, n_views).
If ordered ZCA dimension pruning is enabled, the second dimension may be
smaller than the original ndims after training.
Use calculate_corrmat to inspect one cross-view correlation matrix per DLV
dimension:
dlvs = struct_model.predict([view_a, view_b], verbose=False)
corr_matrices = struct_model.calculate_corrmat(dlvs)
for dim_index, corr_matrix in enumerate(corr_matrices):
print("DLV dimension:", dim_index)
print(corr_matrix)
Missing views¶
StructuralModel supports samples where an entire view is missing. Mark a
missing view by setting the whole row for that view to NaN. Rows with
partial NaN values inside a view are rejected.
view_a = np.random.normal(size=(100, 20)).astype("float32")
view_b = np.random.normal(size=(100, 15)).astype("float32")
# Samples 10 through 19 are missing view B.
view_b[10:20, :] = np.nan
struct_model.fit([view_a, view_b], batch_size=32, epochs=10)
During training, the default missing_strategy="impute" skips missing raw
views when the corresponding encoder is run, then fills the missing DLVs with
detached batch-local low-rank latent imputation. The completed DLVs are used
for normalization, structural losses, redundancy, and training metrics, while
encoder updates still come only from rows where that source view has real raw
input.
Set missing_strategy="project" to use the older projection behavior:
missing view rows are zero-scattered, and only observed views contribute to
losses and metrics. Regardless of the training strategy, evaluate() and
predict() do not impute. predict() returns zeros for missing views.
Rows where every view is missing are rejected because there is no observed
information to condition on.
Multi-input views¶
Some measurement models need more than one tensor. For example, a text encoder
may need token IDs and an attention mask. Set n_inputs on that encoder so
StructuralModel knows how many consecutive tensors belong to that view.
class TextEncoder(nn.Module):
def __init__(self):
super().__init__()
self.n_inputs = 2
def forward(self, inputs):
input_ids, attention_mask = inputs
...
model_list = [image_encoder, text_encoder]
struct_model = StructuralModel(
Path=Path,
model_list=model_list,
regularizer_list=[None, None],
tot_num=n_samples,
ndims=128,
)
# One image tensor, then the two tensors consumed by text_encoder.
struct_model.fit([images, token_ids, attention_masks])
Saving¶
Use PyTorch checkpoints. save stores both the model configuration and the
state_dict. If you only need raw weights, use save_state_dict or
torch.save(model.state_dict(), path).
struct_model.save("/path/to/DLVPM_Model.pt")
struct_model.save_state_dict("/path/to/DLVPM_Model_state_dict.pt")
restored = StructuralModel(
Path=Path,
model_list=[model_a, model_b],
regularizer_list=[None, None],
tot_num=n_samples,
ndims=4,
orthogonalization="zca",
)
restored.load_state_dict_from_path("/path/to/DLVPM_Model.pt")
Plotting the structural model¶
plot_structural_model writes the path graph to a PNG file using pydot.
struct_model.plot_structural_model("path_model.png")