From ae73c4d0043ccdc187b6550139d322231e3536a4 Mon Sep 17 00:00:00 2001 From: Steven Braun Date: Tue, 22 Oct 2024 21:00:49 +0200 Subject: [PATCH] feat: Refactor module structure, improve distribution handling, and enhance sampling capabilities Too much to track. Here is a gpt-4o-mini summary of `git diff`: - Module Structure Update: - Moved `Dist` import from `simple_einet.data` to `simple_einet.dist` to improve module organization and clarity of distribution-related functionalities. - Main Script Updates (`main.py`): - Integrated `DataType`, `Dist`, and `PiecewiseLinear` into the imports to accommodate new distribution functionalities. - Enhanced the training logic within the `train()` function to manage the caching mechanism for piecewise linear distributions. When `args.dist` is set to `Dist.PIECEWISE_LINEAR`, the caching configuration allows for more efficient sampling of outputs. - Modified the output generation section to incorporate the new cache parameters: `cache_leaf` and `cache_index`, allowing for conditional caching based on the specified distribution type. - Distribution Enhancements: - Updated `args.py` to streamline the handling of distributions, ensuring that settings for piecewise linear distributions are correctly configured. - Added a `ConditioningNetwork` class to `abstract_layers.py`, which provides a neural network structure for conditioning inputs based on parameterized layers. - New Piecewise Linear Distribution: - Introduced `PiecewiseLinear` class to the `distributions` layer, allowing for a piecewise linear distribution to be specified for leaf nodes in models (affected files include `piecewise_linear.py` and respective imports). - Implemented methods for handling piecewise linear distribution parameters, including initialization and sampling. - Data Input Handling: - Enhanced `data.py` to include diverse datasets and improved the `get_data_shape()` and `get_data_num_classes()` methods to support new datasets. The function now identifies the number of classes for each dataset effectively, including changing implementations for the 'mnist-bin' dataset. - Refactored the data loading process to include new preprocessing functions that standardize and normalize data before passing to the model. - Sampling Improvements: - Updated the `SamplingContext` to include a `return_leaf_params` boolean flag which allows for returning parameters of the leaf distributions instead of actual samples. This provides more flexibility during sampling, particularly useful for monitoring and debugging. - Modified the `sample()` method across different distributions (including those in `multidistribution.py`, `normal.py`, `bernoulli.py`, etc.) to support the new `return_leaf_params` functionality. - Added logic for handling leaf parameters in both differentiable and non-differentiable contexts to accommodate complex sampling requirements. - Added detailed assertions and logging around sampling methods to ensure shape integrity and functional behavior. - Testing Enhancements: - Expanded unit tests in `test_einet.py` to validate the new configurations for model structure and layer types. Tests verify the functionality of both existing and new distribution types, including their respective sampling shapes and behaviors under different configurations. - Adjusted test cases to validate against realistic scenarios that utilize the newly introduced piecewise linear distribution behavior, ensuring robust coverage for edge cases. --- args.py | 2 +- benchmark/benchmark.md | 253 ------- exp_utils.py | 1 - main.py | 118 ++-- main_pl.py | 4 +- models_pl.py | 3 +- notebooks/iris_classification.ipynb | 193 +++--- pyproject.toml | 9 +- simple_einet/abstract_layers.py | 51 +- simple_einet/data.py | 621 ++++++++++++++---- simple_einet/einet.py | 315 +++++++-- simple_einet/einet_mixture.py | 4 +- simple_einet/layers/distributions/__init__.py | 1 - .../layers/distributions/abstract_leaf.py | 80 ++- .../layers/distributions/bernoulli.py | 3 +- simple_einet/layers/distributions/binomial.py | 42 +- .../layers/distributions/categorical.py | 7 +- .../layers/distributions/multidistribution.py | 37 +- .../distributions/multivariate_normal.py | 7 +- simple_einet/layers/distributions/normal.py | 28 +- simple_einet/layers/factorized_leaf.py | 273 +++++++- simple_einet/layers/linsum.py | 224 ++++++- simple_einet/layers/product.py | 16 + simple_einet/sampling_utils.py | 31 +- simple_einet/utils.py | 95 ++- tests/test_einet.py | 24 +- 26 files changed, 1791 insertions(+), 651 deletions(-) delete mode 100644 benchmark/benchmark.md diff --git a/args.py b/args.py index c2b3d83..c6eaf06 100644 --- a/args.py +++ b/args.py @@ -2,7 +2,7 @@ import os import pathlib -from simple_einet.data import Dist +from simple_einet.dist import Dist def parse_args(): diff --git a/benchmark/benchmark.md b/benchmark/benchmark.md deleted file mode 100644 index 54c55cc..0000000 --- a/benchmark/benchmark.md +++ /dev/null @@ -1,253 +0,0 @@ -# Inference/Backward Time Comparison - -The following lists different forward pass and backward pass results for this library (`simple-einet`) in comparison to the official EinsumNetworks implementation ([`EinsumNetworks`](https://github.com/cambridge-mlg/EinsumNetworks)). - -The benchmark code can be found in [benchmark.py](./benchmark.py). - -The default values for different hyperparameters are as follows: - -```python -batch_size = 256 -num_features = 512 -depth = 5 -num_sums = 32 -num_leaves = 32 -num_repetitions = 32 -num_channels = 1 -num_classes = 1 -``` - -## Results - -The `simple-einet` implementation is 1.5x - 3.0x faster almost everywhere but scales similar to the official `EinsumNetworks` implementation - -`OOM` indicates an `OutOfMemory` runtime exception. - -``` -[------------ batch_size-forward ------------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 2.5 | 3.4 - 2 | 2.3 | 3.3 - 4 | 2.4 | 3.1 - 8 | 2.9 | 3.1 - 16 | 4.7 | 3.8 - 32 | 8.6 | 6.7 - 64 | 14.4 | 14.5 - 128 | 27.3 | 36.8 - 256 | 54.2 | 75.3 - 512 | 106.0 | 146.1 - 1024 | 211.7 | 292.5 - 2048 | 418.7 | 575.9 - -Times are in milliseconds (ms). - -[----------- batch_size-backward ------------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 7.1 | 10.8 - 2 | 6.9 | 10.5 - 4 | 7.4 | 11.3 - 8 | 7.7 | 12.1 - 16 | 10.6 | 15.1 - 32 | 14.9 | 22.9 - 64 | 27.7 | 43.1 - 128 | 58.3 | 99.9 - 256 | 119.7 | 218.8 - 512 | 240.2 | 435.8 - 1024 | 481.2 | 873.1 - -Times are in milliseconds (ms). - -[----------- num_features-forward -----------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 4 | 1.9 | 2.8 - 8 | 3.5 | 7.6 - 16 | 8.0 | 22.8 - 32 | 20.3 | 53.1 - 64 | 22.7 | 53.7 - 128 | 26.8 | 56.6 - 256 | 34.7 | 62.7 - 512 | 53.7 | 74.2 - 1024 | 91.9 | 100.0 - 2048 | 167.3 | 146.2 - 4096 | 313.5 | 253.5 - -Times are in milliseconds (ms). - -[---------- num_features-backward -----------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 4 | 4.3 | 12.5 - 8 | 8.6 | 27.7 - 16 | 18.7 | 65.5 - 32 | 43.2 | 143.7 - 64 | 47.8 | 145.5 - 128 | 57.4 | 155.0 - 256 | 77.8 | 177.4 - 512 | 119.6 | 218.2 - 1024 | 202.4 | 302.3 - 2048 | 370.9 | 472.1 - 4096 | 628.7 | 729.1 - -Times are in milliseconds (ms). - -[-------------- depth-forward ---------------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 36.9 | 10.9 - 2 | 38.0 | 12.5 - 3 | 39.2 | 19.2 - 4 | 43.3 | 38.1 - 5 | 53.8 | 75.3 - 6 | 71.3 | 151.0 - 7 | 107.5 | 301.9 - 8 | 217.8 | OOM - 9 | 526.7 | OOM - -Times are in milliseconds (ms). - -[-------------- depth-backward --------------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 82.9 | 55.7 - 2 | 84.7 | 63.8 - 3 | 89.4 | 83.6 - 4 | 97.7 | 129.6 - 5 | 120.4 | 220.0 - 6 | 158.3 | 401.5 - 7 | 237.9 | 765.7 - -Times are in milliseconds (ms). - -[------------- num_sums-forward -------------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 49.1 | 50.9 - 2 | 50.0 | 52.5 - 4 | 49.8 | 52.9 - 8 | 50.1 | 53.0 - 16 | 50.7 | 54.9 - 32 | 53.6 | 74.4 - 64 | 65.9 | 139.9 - 128 | 156.5 | OOM - -Times are in milliseconds (ms). - -[------------ num_sums-backward -------------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 102.7 | 152.9 - 2 | 106.4 | 157.8 - 8 | 106.9 | 158.5 - 16 | 110.1 | 166.6 - 32 | 120.4 | 219.7 - 64 | 164.1 | 404.4 - -Times are in milliseconds (ms). - -[------------ num_leaves-forward ------------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 10.1 | 23.5 - 2 | 6.4 | 24.1 - 4 | 8.0 | 25.5 - 8 | 14.4 | 28.7 - 16 | 26.0 | 38.7 - 32 | 53.2 | 75.2 - 64 | 130.1 | 181.6 - 128 | 363.7 | OOM - -Times are in milliseconds (ms). - -[----------- num_leaves-backward ------------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 20.7 | 68.4 - 2 | 17.0 | 68.9 - 4 | 19.6 | 73.0 - 8 | 29.7 | 83.2 - 16 | 57.2 | 116.9 - 32 | 119.6 | 218.8 - 64 | 274.8 | 504.4 - -Times are in milliseconds (ms). - -[----------- num_channels-forward -----------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 54.4 | 74.1 - 2 | 65.8 | 78.4 - 4 | 89.9 | 85.1 - 8 | 138.1 | 97.5 - 16 | 235.1 | 125.8 - -Times are in milliseconds (ms). - -[---------- num_channels-backward -----------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 120.5 | 219.8 - 2 | 175.1 | 249.2 - 4 | 288.4 | 303.8 - 8 | 452.0 | 391.3 - -Times are in milliseconds (ms). - -[--------- num_repetitions-forward ----------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 2.2 | 2.8 - 2 | 3.2 | 3.0 - 4 | 5.4 | 5.1 - 8 | 10.7 | 11.3 - 16 | 22.8 | 30.4 - 32 | 53.7 | 75.2 - 64 | 109.4 | 192.2 - 128 | 224.1 | 520.7 - -Times are in milliseconds (ms). - -[--------- num_repetitions-backward ---------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 5.8 | 10.3 - 2 | 6.3 | 11.4 - 4 | 9.8 | 18.1 - 8 | 21.6 | 39.5 - 16 | 51.4 | 95.4 - 32 | 119.1 | 220.2 - 64 | 250.6 | 520.8 - 128 | 504.6 | 1316.4 - -Times are in milliseconds (ms). - -[----------- num_classes-forward ------------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 53.9 | 75.1 - 2 | 53.9 | 75.0 - 4 | 54.0 | 75.1 - 8 | 54.1 | 75.5 - 16 | 53.5 | 75.5 - 32 | 54.0 | 75.2 - 64 | 53.7 | 74.7 - 128 | 54.3 | 75.6 - -Times are in milliseconds (ms). - -[----------- num_classes-backward -----------] - | simple-einet | EinsumNetworks -1 threads: ----------------------------------- - 1 | 119.8 | 218.5 - 2 | 120.6 | 220.7 - 4 | 120.2 | 220.3 - 8 | 119.9 | 221.4 - 16 | 119.8 | 221.2 - 32 | 120.4 | 217.7 - 64 | 120.4 | 221.2 - 128 | 121.0 | 219.4 - -Times are in milliseconds (ms). -``` diff --git a/exp_utils.py b/exp_utils.py index 4460dc8..e4f4403 100644 --- a/exp_utils.py +++ b/exp_utils.py @@ -29,7 +29,6 @@ import torch from torch.backends import cudnn as cudnn from torch.nn.parallel.distributed import DistributedDataParallel -from torch.utils.tensorboard import SummaryWriter from torchvision.transforms import ToTensor diff --git a/main.py b/main.py index 703bfbf..9766e8f 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,9 @@ from args import parse_args from simple_einet.data import build_dataloader, get_data_shape +from simple_einet.dist import DataType, Dist, get_data_type_from_dist, Domain from simple_einet.layers.distributions.categorical import Categorical +from simple_einet.layers.distributions.piecewise_linear import PiecewiseLinear from simple_einet.utils import preprocess install() @@ -60,8 +62,15 @@ def train(args, model: Union[Einet, EinetMixture], device, train_loader, optimiz optimizer.zero_grad() + if args.dist == Dist.PIECEWISE_LINEAR: + cache_leaf = True + cache_index = batch_idx + else: + cache_leaf = False + cache_index = None + # Generate outputs - outputs = model(data) + outputs = model(data, cache_leaf=cache_leaf, cache_index=cache_index) if args.classification: model.posterior(data) @@ -163,6 +172,9 @@ def test(model, device, loader, tag): elif args.dist == "categorical": leaf_type = Categorical leaf_kwargs = {"num_bins": n_bins} + elif args.dist == "piecewise_linear": + leaf_type = PiecewiseLinear + leaf_kwargs = {} # num_classes = 18 data_shape = get_data_shape(args.dataset) @@ -199,7 +211,7 @@ def test(model, device, loader, tag): print(model) home_dir = os.getenv("HOME") - result_dir = os.path.join(home_dir, "results", "simple-einet", "mnist") + result_dir = os.path.join(home_dir, "results", "simple-einet", args.dataset) os.makedirs(result_dir, exist_ok=True) data_dir = os.path.join("~", "data") @@ -210,19 +222,82 @@ def test(model, device, loader, tag): num_workers=os.cpu_count(), normalize=False, loop=False, + seed=args.seed, ) train_loader, val_loader, test_loader = fabric.setup_dataloaders(train_loader, val_loader, test_loader) + if args.dist == Dist.PIECEWISE_LINEAR: + # Initialize the piecewise linear function + # Collect data + batches = [] + count = 0 + for data, _ in train_loader: + batches.append(data) + count += data.shape[0] + if count > 10000: + break + data_init_pwl = torch.cat(batches, dim=0) + + # Prepare data + data_init_pwl = preprocess( + data_init_pwl, + n_bits, + n_bins, + dequantize=True, + has_gauss_dist=has_gauss_dist, + ) + + data_init_pwl = data_init_pwl.view(data_init_pwl.shape[0], data_init_pwl.shape[1], num_features) + + domains = [Domain.discrete_range(min=0, max=255)] * num_features + with torch.no_grad(): + model.leaf.base_leaf.initialize(data_init_pwl, domains=domains) + + # Use mixture weights obtained in leaf initialization and set these to the first linsum layer weights + model.layers[0].logits.data[:] = model.leaf.base_leaf.mixture_weights.permute(1, 0).view(1, config.num_leaves, 1, config.num_repetitions).log() + + # Visualize a couple of pixel distributions and their piecewise linear functions + # Select 20 random pixels + pixels = list(range(64))[::3] + # pixels = [36, 766, 720, 588, 759, 403, 664, 428, 25, 686, 673, 638, 44, 147, 610, 470, 540, 179, 698, 420] + + d = model.leaf.base_leaf._get_base_distribution() + log_probs = d.log_prob(data_init_pwl) + + xs = d.xs + ys = d.ys + + for pixel in pixels: + # Get data subset + # xs_pixel = xs[pixel][0][0][0].squeeze() + # ys_pixel = ys[pixel][0][0][0].squeeze() + xs_pixel = xs[0][0][pixel][0].squeeze().cpu() + ys_pixel = ys[0][0][pixel][0].squeeze().cpu() + + # Plot pixel distribution with pixel value as x and logprob as y values + import matplotlib.pyplot as plt + + plt.figure(figsize=(12, 6)) + plt.plot(xs_pixel, ys_pixel, label="PWL") + + # Plot histogram of pixel values + plt.hist(data_init_pwl[:, :, pixel].flatten().cpu().numpy(), bins=100, density=True, alpha=0.5, label="Data") + plt.xlabel("Pixel Value") + plt.ylabel("Density") + plt.legend() + plt.savefig(os.path.join(result_dir, f"pwl-{pixel}.png"), dpi=300) + plt.close() + if args.train: for epoch in range(1, args.epochs + 1): train(args, model, device, train_loader, optimizer, epoch) # lr_scheduler.step() torch.save(model.state_dict(), os.path.join(result_dir, "model.pth")) - test(model, device, train_loader, "Train") - test(model, device, val_loader, "Val") - test(model, device, test_loader, "Test") + # test(model, device, train_loader, "Train") + # test(model, device, val_loader, "Val") + # test(model, device, test_loader, "Test") else: model.load_state_dict(torch.load(os.path.join(result_dir, "model.pth"))) @@ -335,39 +410,6 @@ def test(model, device, loader, tag): grid, os.path.join(result_dir, f"reconstructions{suffix}{suffix_mpe_at_leaves}.png") ) - ################################################ - # sample subparts multiple times conditionally # - ################################################ - # Sample once - samples = model.sample( - num_samples=100, - is_differentiable=diff, - mpe_at_leaves=mpe_at_leaves, - seed=0, - ) - - if not diff: - # Sample 10 times conditionally - for k in range(100): - marginalized_scopes = torch.randperm(num_features)[: num_features // 2] - samples = model.sample( - evidence=samples, - temperature_leaves=args.temperature_leaves, - is_differentiable=diff, - mpe_at_leaves=mpe_at_leaves, - marginalized_scopes=marginalized_scopes, - seed=0, - ) - - if not has_gauss_dist: - samples = samples / n_bins - samples = samples.squeeze() - - samples = samples.view(-1, *data_shape) - grid = torchvision.utils.make_grid(samples, **grid_kwargs) - torchvision.utils.save_image( - grid, os.path.join(result_dir, f"samples-conditionally{suffix}{suffix_mpe_at_leaves}.png") - ) ####### # MPE # ####### diff --git a/main_pl.py b/main_pl.py index 5add532..845eb7b 100644 --- a/main_pl.py +++ b/main_pl.py @@ -25,7 +25,7 @@ plot_distribution, ) from models_pl import SpnDiscriminative, SpnGenerative -from simple_einet.data import Dist +from simple_einet.dist import Dist from simple_einet.data import build_dataloader from simple_einet.sampling_utils import init_einet_stats @@ -161,7 +161,7 @@ def main(cfg: DictConfig): profiler=cfg.profiler, default_root_dir=run_dir, enable_checkpointing=False, - detect_anomaly=True, + detect_anomaly=cfg.debug, ) if not cfg.load_and_eval: diff --git a/models_pl.py b/models_pl.py index b7cfe43..806567c 100644 --- a/models_pl.py +++ b/models_pl.py @@ -10,7 +10,8 @@ from rtpt import RTPT from torch import nn -from simple_einet.data import get_data_shape, Dist, get_distribution +from simple_einet.data import get_data_shape +from simple_einet.dist import Dist, get_distribution from simple_einet.einet import EinetConfig, Einet from simple_einet.einet_mixture import EinetMixture diff --git a/notebooks/iris_classification.ipynb b/notebooks/iris_classification.ipynb index 3fc3eda..e02a99b 100644 --- a/notebooks/iris_classification.ipynb +++ b/notebooks/iris_classification.ipynb @@ -2,6 +2,13 @@ "cells": [ { "cell_type": "markdown", + "id": "c4073907b5884891", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "# Classifying the Iris Dataset with Einets\n", "\n", @@ -10,16 +17,36 @@ "## Environment Setup\n", "\n", "First, we need to import the necessary libraries. Make sure to install these using `pip` or `conda` before starting.\n" - ], - "metadata": { - "collapsed": false - }, - "id": "c4073907b5884891" + ] }, { "cell_type": "code", - "execution_count": 19, - "outputs": [], + "execution_count": 1, + "id": "bf59a1171554403", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-08T07:41:19.532367Z", + "start_time": "2023-11-08T07:41:19.525736Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'simple_einet.layers'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[1], line 5\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01msklearn\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m datasets\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01msklearn\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmodel_selection\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m train_test_split\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01msimple_einet\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01meinet\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Einet, EinetConfig\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01msimple_einet\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mlayers\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mdistributions\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mnormal\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Normal\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mnumpy\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mnp\u001b[39;00m\n", + "File \u001b[0;32m~/.conda/envs/simple-einet/lib/python3.11/site-packages/simple_einet/einet.py:10\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mtorch\u001b[39;00m\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mtorch\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m nn\n\u001b[0;32m---> 10\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01msimple_einet\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mlayers\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mdistributions\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mabstract_leaf\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m AbstractLeaf\n\u001b[1;32m 11\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01msimple_einet\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mlayers\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01meinsum\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m (\n\u001b[1;32m 12\u001b[0m EinsumLayer,\n\u001b[1;32m 13\u001b[0m )\n\u001b[1;32m 14\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01msimple_einet\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mlayers\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmixing\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m MixingLayer\n", + "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'simple_einet.layers'" + ] + } + ], "source": [ "import torch\n", "from matplotlib.colors import ListedColormap\n", @@ -30,31 +57,37 @@ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns" - ], + ] + }, + { + "cell_type": "markdown", + "id": "fc5e7c4ee7c1a6cb", "metadata": { "collapsed": false, - "ExecuteTime": { - "end_time": "2023-11-08T07:41:19.532367Z", - "start_time": "2023-11-08T07:41:19.525736Z" + "jupyter": { + "outputs_hidden": false } }, - "id": "bf59a1171554403" - }, - { - "cell_type": "markdown", "source": [ "## Data Preparation\n", "\n", "The Iris dataset can be loaded directly from scikit-learn, and we will use PyTorch for handling the data.\n" - ], - "metadata": { - "collapsed": false - }, - "id": "fc5e7c4ee7c1a6cb" + ] }, { "cell_type": "code", "execution_count": 20, + "id": "eb86dfd0276fbd9a", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-08T07:41:20.273361Z", + "start_time": "2023-11-08T07:41:20.261877Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "# Load the Iris dataset\n", @@ -70,31 +103,37 @@ "y_train = torch.tensor(y_train).long()\n", "X_test = torch.tensor(X_test).float()\n", "y_test = torch.tensor(y_test).long()\n" - ], + ] + }, + { + "cell_type": "markdown", + "id": "97534bdbb486ecf6", "metadata": { "collapsed": false, - "ExecuteTime": { - "end_time": "2023-11-08T07:41:20.273361Z", - "start_time": "2023-11-08T07:41:20.261877Z" + "jupyter": { + "outputs_hidden": false } }, - "id": "eb86dfd0276fbd9a" - }, - { - "cell_type": "markdown", "source": [ "## Model Configuration\n", "\n", "Here, we set up the Einet model configuration using the predefined structure and parameters.\n" - ], - "metadata": { - "collapsed": false - }, - "id": "97534bdbb486ecf6" + ] }, { "cell_type": "code", "execution_count": 56, + "id": "d9bdd0dcabfe1fa7", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-08T07:45:59.810097Z", + "start_time": "2023-11-08T07:45:59.794082Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [ { "name": "stdout", @@ -120,31 +159,37 @@ "\n", "# Initialize the model\n", "model = Einet(config)\n" - ], + ] + }, + { + "cell_type": "markdown", + "id": "faead0c438ba1924", "metadata": { "collapsed": false, - "ExecuteTime": { - "end_time": "2023-11-08T07:45:59.810097Z", - "start_time": "2023-11-08T07:45:59.794082Z" + "jupyter": { + "outputs_hidden": false } }, - "id": "d9bdd0dcabfe1fa7" - }, - { - "cell_type": "markdown", "source": [ "## Training the Model\n", "\n", "The training process involves defining an optimizer, loss function, and iterating over the training data for a number of epochs.\n" - ], - "metadata": { - "collapsed": false - }, - "id": "faead0c438ba1924" + ] }, { "cell_type": "code", "execution_count": 57, + "id": "2541fdc29aa72d5", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-08T07:46:00.798913Z", + "start_time": "2023-11-08T07:46:00.752397Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [ { "name": "stdout", @@ -198,36 +243,44 @@ " acc_train = accuracy(model, X_train, y_train)\n", " acc_test = accuracy(model, X_test, y_test)\n", " print(f\"Epoch: {epoch + 1}, Loss: {loss.item():.2f}, Accuracy Train: {acc_train:.2f} %, Accuracy Test: {acc_test:.2f} %\")\n" - ], + ] + }, + { + "cell_type": "markdown", + "id": "404d3a4dac9e9cf2", "metadata": { "collapsed": false, - "ExecuteTime": { - "end_time": "2023-11-08T07:46:00.798913Z", - "start_time": "2023-11-08T07:46:00.752397Z" + "jupyter": { + "outputs_hidden": false } }, - "id": "2541fdc29aa72d5" - }, - { - "cell_type": "markdown", "source": [ "## Visualizing the Decision Boundary\n", "\n", "Finally, let's visualize the decision boundary of our trained model along with the test data points.\n" - ], - "metadata": { - "collapsed": false - }, - "id": "404d3a4dac9e9cf2" + ] }, { "cell_type": "code", "execution_count": 58, + "id": "80eaae891938ae7a", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-08T07:46:01.898725Z", + "start_time": "2023-11-08T07:46:01.721169Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [ { "data": { - "text/plain": "
", - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA1IAAAImCAYAAABZ4rtkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/SrBM8AAAACXBIWXMAAA9hAAAPYQGoP6dpAADVAElEQVR4nOzdd3ib1fXA8e+rbcuyLXmP2BnOIovsvdgbmkJpy49VKNCGTRkte7SUFlIggbDaUgqU0JayGmgJK2QPkpABGThxEsdbtpa19f7+cG3i2EmkxLI8zud5/CSW7tV7dL10dO89V1FVVUUIIYQQQgghRNQ0iQ5ACCGEEEIIIbobSaSEEEIIIYQQIkaSSAkhhBBCCCFEjCSREkIIIYQQQogYSSIlhBBCCCGEEDGSREoIIYQQQgghYiSJlBBCCCGEEELESBIpIYQQQgghhIiRJFJCCCFEF6aqaqJDiEki4+1uYyWE6N4kkRJCiCO49NJLGTx4cMvHkCFDGD16NHPmzOGVV14hFAp1+DXfeustBg8ezP79++PS/ng0X+vgj5EjR3LmmWfy3HPPEQ6H4x5DLPbv38/gwYN56623Eh1KVA79Wu7cuZMf/ehHrdoMHjyY+fPnR/2YzWNwtI/Vq1cfd/ztxXuo1atXt7n28OHDmT59Orfddhu7du06pms/++yz/PGPfzymvkIIcSx0iQ5ACCG6uhNOOIH7778fgHA4jMPhYOnSpTz66KOsW7eOJ598Eo2m496XmjVrFosWLSI7Ozsu7TvCggULyMrKQlVVvF4vX375JU8//TQ+n4+bb7650+LoaQ79Wn744Yds2LDhuB4zOzubRYsWtXxeU1PD9ddfz89+9jNmzZrVcntJSclxXQdii/e+++5j2LBhAPh8Pvbt28dLL73EhRdeyMsvv8yJJ54Y07Wfeuoprr/++lhDFkKIYyaJlBBCHEVKSkqbF3UnnXQS/fv359e//jXvv/8+5513Xoddz2azYbPZ4ta+IwwdOpTCwsKWz6dMmcK+fft44403JJE6DvH4WhoMhlbfv82zXUVFRTEnKx2ppKSk1fUnTZrE6aefzpw5c7jrrrv497//jVarTVh8QghxNLK0TwghjtH//d//kZOTwxtvvNHq9r///e+cffbZDB8+nFmzZjF//vw2S94+//xzfvjDH3LiiScybdo07rvvPpxOJ9B2eZfdbue2225j6tSpjBgxgvPPP5+333675bHaW9q3fPlyfvzjHzN27FgmTpzIbbfdRkVFRas+J5xwAps2beLiiy9mxIgRzJ49+7iWRqWmpqIoSqvb9uzZw4033sjUqVM58cQTufTSS1m/fn3L/c3LvA5dVnbppZdy6aWXtnx+0kkn8fTTT/PYY48xZcoURo4cyVVXXcWePXta9fvvf//Leeedx8iRI/ne977HN9980ybOb775huuvv55JkyYxbNgwpk+fziOPPILP52tpM3jwYBYsWMCcOXMYOXIkCxYsYMSIEcybN6/VY3m9XsaOHcvChQvbXOfjjz9m8ODBbNu2reW2t99+m8GDB/P3v/+95bavv/6awYMHs2HDhlZfy/nz57NgwYKWeA5ezud2u7n77ruZMGECo0eP5sYbb6S2trZNDLHw+/387ne/Y+bMmQwfPpxzzz2XxYsXt2qzZcsWLr/8csaOHcvo0aO54oor2LhxI8AR441WamoqV199Nbt372bNmjUtt69du5arrrqK8ePHM3z4cE466STmz59PJBJpuR40zZQ2/x9gyZIl/PjHP2b06NEMHz6cM844g9deey3muIQQoj2SSAkhxDHSaDRMnjyZr776qmWv1PPPP8+9997L5MmTee6557jkkkt48cUXuffee1v6ffrpp1x77bVkZGTw5JNP8otf/IIlS5Zwyy23tHud22+/nW+//ZYHH3yQF198kRNOOIE777yTVatWtdv+7bff5ic/+Ql5eXnMmzePX/7yl2zYsIGLL76Yurq6lnaRSISbb76Zs846ixdeeIExY8bwu9/9ji+++OKozz0SiRAKhQiFQrjdbpYuXco777zDJZdc0tJm165dzJkzh/3793PPPffw+OOPoygKl19+easXydF65ZVXKC0t5dFHH+WRRx5hy5Yt3HnnnS33f/LJJ9x4440MHjyYZ555hjPPPJPbb7+91WNUV1dzySWX4PV6+e1vf8uLL77I2WefzV//+ldeeeWVVm2fe+45zj33XJ5++mlOP/10TjnlFN57771WBQ0++ugjGhsbueCCC9rEO3nyZAwGAytWrGi5rflrtm7dupbbli5dis1mY9SoUa36X3TRRVx44YUALFq0iIsuuqjVWASDQZ566iluu+02PvnkEx566KFoh7INVVWZO3cub7zxBldeeSULFy5k9OjR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OmrUKPR6PVVVVYwYMaLlQ6fTMW/ePPbv34/ZbGbo0KF8+umnrR5r6dKlXHPNNVRXV7e6vby8nJkzZ/Lhhx8CTS9kf/rTnzJlyhQOHDjQJqZ+/fqRlZXF+++/3+r2ffv2sXHjxsPOOHSEr776CqAliRw/fjyffvppq5mncDjMv//9b0aMGIHBYGhJIJpnIQAcDgfffvttTNc2Go2MHj2a//73v61mGz755JNW7davX09JSQnf//73W5KoqqoqduzY0SaZa09BQQGTJ0/mlVde4euvv2bOnDlH7TNr1ixWr17N+vXrmThxIkDLPp0vvviC2bNnH7ZvR55PdiQTJkygsbERVVVbfe/u2LGDZ555hlAoxIcffsikSZOoqalBq9UyevRoHnjgAVJTU1u+F483XrfbzZ///GcGDx7c8r3aPG6nnHJKSxK1ZcsW7HZ7q6/Zoddev349p512GhMnTmyZLVu6dCnQNnEXQohjITNSQghxFG63u6XEcyQSob6+nmXLlrFo0SLOO+88TjvtNKBphuXqq6/mqaeewu12M3HiRKqqqnjqqadQFIUhQ4YAcOONN/Kzn/2MW2+9lQsuuIDa2lrmzZvHKaecwqBBg9iyZUvLtQsKCsjNzeWRRx7B7XZTVFTEli1b+Pzzz7n22mvbxKrRaLj11lv55S9/yW233cZ5551HfX09CxYsIC0trVXxhOPx9ddft8z+RCIRvv32W+bPn09WVhbf+973ALj++utZunQpl112Gddccw16vZ5XX3215eBVaCpbnZeXxzPPPNMyW/f888+3WcYXjVtvvZXLL7+c66+/nosvvpjdu3fz3HPPtWozcuRInn32WV544QVOPPFEysrKeP755wkEAni93qiuc+GFF3LrrbcyYMCAqAoWzJw5kz/96U9AU8ICTWcmNe/dOlIi1TzL+f777zNq1Cj69OkTVYyxmjlzJuPHj+fnP/85P//5zxkwYABfffUVTz/9NNOnT8dmszFmzBgikQhz587lmmuuwWw288EHH+ByuVp+BmKJd9euXS2zeX6/n9LSUv76179SX1/f8jMDTV+zDz74gL/97W8MGDCAb775hoULF6IoSquvWWpqKl9++SVr165l3LhxjBw5kvfee49hw4aRm5vLl19+yQsvvNCmnxBCHCtJpIQQ4ii2bdvGxRdfDDRtgDebzQwaNIgHHnig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YsiQIbj77ruRlZXVpvfcu3cvRFGEQmhP5R1DFEV4PG7I5QoIQicoqAtiHwcX+zf42MfBxf4Nvu7Qx6LHBbcgwPPb51PI5KF7b1GEx+OBXC7vsv0bbuzj4Ap7/3oBQRCQl5d31qZhn9onxeHDhwE0dPATTzwBu92OF198EXPnzsWmTZuQkpIi+TUb/4BkSmWH1tpWcqjDXUKXxz4OLvZv8LGPg4v9G3xdvo+VKoQuOrX0/uF8826CfRxcYexfl8sVcICLqCA1YsQIbN++HYmJib4P+Le//Q2TJk3Chx9+iBtvvFHya+bm5nZ0mURERERE1MVFVJACmq/Op9FocM4556CioiJMFRERERERUXcT9sUmpHjvvfcwatQoWK1W3zGLxYJjx44hOzs7jJUREREREVF30qmDlMfjQVVVFex2OwBgwoQJ8Hq9uOeee3D48GH8/PPPuPXWW5GUlITZs2eHuVoiIiIiIuouOnWQKi8vx7hx4/Dpp58CADIyMrB+/XpYrVZcddVVuO666xAXF4c333wTanUXv3GViIiIiIg6jU61/DkREREREVEk6NQjUkRERERERJ0RgxQREREREZFEDFJEREREREQSMUgRERERERFJxCBFREREREQkEYMUERERERGRRAxSREREREREEjFIhUlxcTFyc3Px4YcfttqmtrYWd955J0aOHIn8/Hw8/PDDsNlsIawysgXSxx9//DFycnKaPU6ePBnCSiNHRUVFi/3VWh/zGpZOah/zGpZu48aNuOSSSzB48GDMmDEDn332WattHQ4HHn74YYwZMwa5ubm48847YTAYQlhtZJLSx3v27GnxGt65c2cIK44MO3fubLGvcnJyMHXq1BbP4TUsTVv6mNewNG63G6tXr8bkyZORm5uLq6++Gj/99FOr7TvzdwlFuAvojlwuF+666y5YrdYztrvttttgs9mwfv16mEwm3H///bBarXjqqadCVGnkCrSPCwoKkJ+fj1WrVvkdT0pKCmZ5EevQoUNQq9X44osvIAiC73hcXFyL7XkNSye1j3kNS/PRRx/h/vvvx3333Yfx48fjP//5DxYvXgydTofc3Nxm7ZcvX47du3fjhRdegEqlwrJly3Dbbbfh7bffDkP1kUFqHxcUFKB379545513/I5rtdpQlRwxcnNzsW3bNr9jP/30E2699VYsWrSoxXN4DUvT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3DEC7WNew9IMGTIEw4cPx5IlS7Bjxw4cO3YMzz33HLZv344bb7wx8r5LhHv99e6s6R5HJSUlzfY8qq6uFm+99VZx2LBh4qhRo8Rly5aJdrs9XOVGpLP18YEDB8Trr79eHD58uJiXlyfeeuutYllZWbjK7fSqqqrEpUuXimPHjhUHDx4szpkzR9y1a5coiryGO4rUPuY1LN3rr78uTpkyRRw0aJB46aWXilu2bPE9N2DAAPH555/3/VxfXy/ef//94ogRI8QRI0aIixcvFg0GQzjKjihS+vj48ePirbfeKubn54tDhw4Vb7jhBrGgoCAcZUeM+fPni7fffnuLz/Ea7hhS+pjXsDRGo1Fcvny5OGnSJDE3N1ecM2eOuHPnTlEUI++7hCCKohjuMEdERERERBRJOLWPiIiIiIhIIgYpIiIiIiIiiRikiIiIiIiIJGKQIiIiIiIikohBioiIiIiISCIGKSIiIiIiIokYpIiIiDoAdxMhIupeGKSIiMinsLAQd9xxB8aOHYvzzz8f48aNw+23345Dhw6FraaTJ08iJycHH374YattpkyZgqVLl4awqlNMJhPuuece7N6923fsmmuuwTXXXNPm13z99ddx1113dUR5raqtrcWkSZNQUlIS1PchIuqqGKSIiAgAcPjwYcyZMwdGoxEPPPAAXn/9ddxzzz0oKyvDlVdeiZ9++incJXZKBw8exEcffQSv19shr3f06FG8/PLLuPvuuzvk9VqTmJiI6667Dvfddx9H04iI2oBBioiIAABvvPEGEhMT8corr+B3v/sd8vPzcemll2L9+vVISEjA2rVrw11it/DMM89g5syZSE9PD/p7zZ07F4WFhdiyZUvQ34uIqKthkCIiIgBAdXU1RFFsNrISHR2N++67D7/73e/8jn/xxReYPXs2Bg8ejLFjx+LRRx+F1Wr1Pf/CCy9gypQp+OqrrzB9+nQMHToUV155JXbu3On3OocOHcItt9yC0aNHY9CgQRg/fjweffRR2O32Nn8Wh8OBp59+GhMnTsT555+PWbNm4dNPP/VrM2XKFDz//PN46qmncMEFF2DIkCGYN28ejh075tfu3//+Ny655BIMHjwYl156KbZv347zzjsPH374IXbu3Ilrr70WAHDttdf6TecTRRGvvPIKJk2ahCFDhmDOnDnYv3//GesuLCzE119/jZkzZ/od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+kaarrvuOmzcuBHz58/HggULoFQq8eKLL0Kn02HWrFlnvOcqEHv27AHQcL8aEREFjkGKiIgANCx//v777+O1117DSy+9BIPBAJVKhfPOOw/PPvssLrroIl/bK664AjExMXj11Vfx3nvvITo6Gnl5eVixYgV69erl97rLly/H448/DoPBgLy8PPzjH//wTa9bsGABamtr8eabb2LNmjXIyMjA73//ewiCgJdffhkmk0ny55DJZFi3bh1Wr16Nl19+GTU1NUhPT8f111+Pm2++WdJrzZo1C1arFa+99ho++OAD9O/fH/fffz/uv/9+3/1H/fv3x8yZM7FhwwZs3boVn3zyieSaG1100UVYs2YN9u/fj6FDhwIAMjIy8M477+CZZ57B0qVLoVKpMGrUKDz77LMtLh4h1bfffoshQ4agZ8+e7XodIqLuRhCbbvhBRETUQV544QX87W9/Q0FBQbhLabNPPvkE5513Hvr27es79vXXX2PBggX46KOP2jX61JqFCxciMTERTzzxRIe/9umsVivGjx+Pp556CtOmTQv6+xERdSW8R4qIiKgVH3/8Mf785z9j06ZN2L17Nz744AMsW7YM+fn5QQlRQMO0yc8//xxlZWVBef2m3n33XfTv3x9Tp04N+nsREXU1nNpHRETUiqeeegorV67EM888A4PBgJSUFEyfPh233XZb0N4zJycHCxYswIoVK7Bq1aqgvY/BYMD69evx1ltvdcjKf0RE3Q2n9hEREREREUnEqX1EREREREQSMUgRERERERFJxCBFREREREQkEYMUERERERGRRAxSREREREREEjFIERERERERScQgRUREREREJBGDFBERERERkUQMUkRERERERBL9f/lQCsccKFORAAAAAElFTkSuQmCC" + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA1IAAAImCAYAAABZ4rtkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/SrBM8AAAACXBIWXMAAA9hAAAPYQGoP6dpAADVAElEQVR4nOzdd3ib1fXA8e+rbcuyLXmP2BnOIovsvdgbmkJpy49VKNCGTRkte7SUFlIggbDaUgqU0JayGmgJK2QPkpABGThxEsdbtpa19f7+cG3i2EmkxLI8zud5/CSW7tV7dL10dO89V1FVVUUIIYQQQgghRNQ0iQ5ACCGEEEIIIbobSaSEEEIIIYQQIkaSSAkhhBBCCCFEjCSREkIIIYQQQogYSSIlhBBCCCGEEDGSREoIIYQQQgghYiSJlBBCCCGEEELESBIpIYQQQgghhIiRJFJCCCFEF6aqaqJDiEki4+1uYyWE6N4kkRJCiCO49NJLGTx4cMvHkCFDGD16NHPmzOGVV14hFAp1+DXfeustBg8ezP79++PS/ng0X+vgj5EjR3LmmWfy3HPPEQ6H4x5DLPbv38/gwYN56623Eh1KVA79Wu7cuZMf/ehHrdoMHjyY+fPnR/2YzWNwtI/Vq1cfd/ztxXuo1atXt7n28OHDmT59Orfddhu7du06pms/++yz/PGPfzymvkIIcSx0iQ5ACCG6uhNOOIH7778fgHA4jMPhYOnSpTz66KOsW7eOJ598Eo2m496XmjVrFosWLSI7Ozsu7TvCggULyMrKQlVVvF4vX375JU8//TQ+n4+bb7650+LoaQ79Wn744Yds2LDhuB4zOzubRYsWtXxeU1PD9ddfz89+9jNmzZrVcntJSclxXQdii/e+++5j2LBhAPh8Pvbt28dLL73EhRdeyMsvv8yJJ54Y07Wfeuoprr/++lhDFkKIYyaJlBBCHEVKSkqbF3UnnXQS/fv359e//jXvv/8+5513Xoddz2azYbPZ4ta+IwwdOpTCwsKWz6dMmcK+fft44403JJE6DvH4WhoMhlbfv82zXUVFRTEnKx2ppKSk1fUnTZrE6aefzpw5c7jrrrv497//jVarTVh8QghxNLK0TwghjtH//d//kZOTwxtvvNHq9r///e+cffbZDB8+nFmzZjF//vw2S94+//xzfvjDH3LiiScybdo07rvvPpxOJ9B2eZfdbue2225j6tSpjBgxgvPPP5+333675bHaW9q3fPlyfvzjHzN27FgmTpzIbbfdRkVFRas+J5xwAps2beLiiy9mxIgRzJ49+7iWRqWmpqIoSqvb9uzZw4033sjUqVM58cQTufTSS1m/fn3L/c3LvA5dVnbppZdy6aWXtnx+0kkn8fTTT/PYY48xZcoURo4cyVVXXcWePXta9fvvf//Leeedx8iRI/ne977HN9980ybOb775huuvv55JkyYxbNgwpk+fziOPPILP52tpM3jwYBYsWMCcOXMYOXIkCxYsYMSIEcybN6/VY3m9XsaOHcvChQvbXOfjjz9m8ODBbNu2reW2t99+m8GDB/P3v/+95bavv/6awYMHs2HDhlZfy/nz57NgwYKWeA5ezud2u7n77ruZMGECo0eP5sYbb6S2trZNDLHw+/387ne/Y+bMmQwfPpxzzz2XxYsXt2qzZcsWLr/8csaOHcvo0aO54oor2LhxI8AR441WamoqV199Nbt372bNmjUtt69du5arrrqK8ePHM3z4cE466STmz59PJBJpuR40zZQ2/x9gyZIl/PjHP2b06NEMHz6cM844g9deey3muIQQoj2SSAkhxDHSaDRMnjyZr776qmWv1PPPP8+9997L5MmTee6557jkkkt48cUXuffee1v6ffrpp1x77bVkZGTw5JNP8otf/IIlS5Zwyy23tHud22+/nW+//ZYHH3yQF198kRNOOIE777yTVatWtdv+7bff5ic/+Ql5eXnMmzePX/7yl2zYsIGLL76Yurq6lnaRSISbb76Zs846ixdeeIExY8bwu9/9ji+++OKozz0SiRAKhQiFQrjdbpYuXco777zDJZdc0tJm165dzJkzh/3793PPPffw+OOPoygKl19+easXydF65ZVXKC0t5dFHH+WRRx5hy5Yt3HnnnS33f/LJJ9x4440MHjyYZ555hjPPPJPbb7+91WNUV1dzySWX4PV6+e1vf8uLL77I2WefzV//+ldeeeWVVm2fe+45zj33XJ5++mlOP/10TjnlFN57771WBQ0++ugjGhsbueCCC9rEO3nyZAwGAytWrGi5rflrtm7dupbbli5dis1mY9SoUa36X3TRRVx44YUALFq0iIsuuqjVWASDQZ566iluu+02PvnkEx566KFoh7INVVWZO3cub7zxBldeeSULFy5k9OjR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OmrUKPR6PVVVVYwYMaLlQ6fTMW/ePPbv34/ZbGbo0KF8+umnrR5r6dKlXHPNNVRXV7e6vby8nJkzZ/Lhhx8CTS9kf/rTnzJlyhQOHDjQJqZ+/fqRlZXF+++/3+r2ffv2sXHjxsPOOHSEr776CqAliRw/fjyffvppq5mncDjMv//9b0aMGIHBYGhJIJpnIQAcDgfffvttTNc2Go2MHj2a//73v61mGz755JNW7davX09JSQnf//73W5KoqqoqduzY0SaZa09BQQGTJ0/mlVde4euvv2bOnDlH7TNr1ixWr17N+vXrmThxIkDLPp0vvviC2bNnH7ZvR55PdiQTJkygsbERVVVbfe/u2LGDZ555hlAoxIcffsikSZOoqalBq9UyevRoHnjgAVJTU1u+F483XrfbzZ///GcGDx7c8r3aPG6nnHJKSxK1ZcsW7HZ7q6/Zoddev349p512GhMnTmyZLVu6dCnQNnEXQohjITNSQghxFG63u6XEcyQSob6+nmXLlrFo0SLOO+88TjvtNKBphuXqq6/mqaeewu12M3HiRKqqqnjqqadQFIUhQ4YAcOONN/Kzn/2MW2+9lQsuuIDa2lrmzZvHKaecwqBBg9iyZUvLtQsKCsjNzeWRRx7B7XZTVFTEli1b+Pzzz7n22mvbxKrRaLj11lv55S9/yW233cZ5551HfX09CxYsIC0trVXxhOPx9ddft8z+RCIRvv32W+bPn09WVhbf+973ALj++utZunQpl112Gddccw16vZ5XX3215eBVaCpbnZeXxzPPPNMyW/f888+3WcYXjVtvvZXLL7+c66+/nosvvpjdu3fz3HPPtWozcuRInn32WV544QVOPPFEysrKeP755wkEAni93qiuc+GFF3LrrbcyYMCAqAoWzJw5kz/96U9AU8ICTWcmNe/dOlIi1TzL+f777zNq1Cj69OkTVYyxmjlzJuPHj+fnP/85P//5zxkwYABfffUVTz/9NNOnT8dmszFmzBgikQhz587lmmuuwWw288EHH+ByuVp+BmKJd9euXS2zeX6/n9LSUv76179SX1/f8jMDTV+zDz74gL/97W8MGDCAb775hoULF6IoSquvWWpqKl9++SVr165l3LhxjBw5kvfee49hw4aRm5vLl19+yQsvvNCmnxBCHCtJpIQQ4ii2bdvGxRdfDDRtgDebzQwaNIgHHnig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YsiQIbj77ruRlZXVpvfcu3cvRFGEQmhP5R1DFEV4PG7I5QoIQicoqAtiHwcX+zf42MfBxf4Nvu7Qx6LHBbcgwPPb51PI5KF7b1GEx+OBXC7vsv0bbuzj4Ap7/3oBQRCQl5d31qZhn9onxeHDhwE0dPATTzwBu92OF198EXPnzsWmTZuQkpIi+TUb/4BkSmWH1tpWcqjDXUKXxz4OLvZv8LGPg4v9G3xdvo+VKoQuOrX0/uF8826CfRxcYexfl8sVcICLqCA1YsQIbN++HYmJib4P+Le//Q2TJk3Chx9+iBtvvFHya+bm5nZ0mURERERE1MVFVJACmq/Op9FocM4556CioiJMFRERERERUXcT9sUmpHjvvfcwatQoWK1W3zGLxYJjx44hOzs7jJUREREREVF30qmDlMfjQVVVFex2OwBgwoQJ8Hq9uOeee3D48GH8/PPPuPXWW5GUlITZs2eHuVoiIiIiIuouOnWQKi8vx7hx4/Dpp58CADIyMrB+/XpYrVZcddVVuO666xAXF4c333wTanUXv3GViIiIiIg6jU61/DkREREREVEk6NQjUkRERERERJ0RgxQREREREZFEDFJEREREREQSMUgRERERERFJxCBFREREREQkEYMUERERERGRRAxSREREREREEjFIhUlxcTFyc3Px4YcfttqmtrYWd955J0aOHIn8/Hw8/PDDsNlsIawysgXSxx9//DFycnKaPU6ePBnCSiNHRUVFi/3VWh/zGpZOah/zGpZu48aNuOSSSzB48GDMmDEDn332WattHQ4HHn74YYwZMwa5ubm48847YTAYQlhtZJLSx3v27GnxGt65c2cIK44MO3fubLGvcnJyMHXq1BbP4TUsTVv6mNewNG63G6tXr8bkyZORm5uLq6++Gj/99FOr7TvzdwlFuAvojlwuF+666y5YrdYztrvttttgs9mwfv16mEwm3H///bBarXjqqadCVGnkCrSPCwoKkJ+fj1WrVvkdT0pKCmZ5EevQoUNQq9X44osvIAiC73hcXFyL7XkNSye1j3kNS/PRRx/h/vvvx3333Yfx48fjP//5DxYvXgydTofc3Nxm7ZcvX47du3fjhRdegEqlwrJly3Dbbbfh7bffDkP1kUFqHxcUFKB379545513/I5rtdpQlRwxcnNzsW3bNr9jP/30E2699VYsWrSoxXN4DUvT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3DEC7WNew9IMGTIEw4cPx5IlS7Bjxw4cO3YMzz33HLZv344bb7wx8r5LhHv99e6s6R5HJSUlzfY8qq6uFm+99VZx2LBh4qhRo8Rly5aJdrs9XOVGpLP18YEDB8Trr79eHD58uJiXlyfeeuutYllZWbjK7fSqqqrEpUuXimPHjhUHDx4szpkzR9y1a5coiryGO4rUPuY1LN3rr78uTpkyRRw0aJB46aWXilu2bPE9N2DAAPH555/3/VxfXy/ef//94ogRI8QRI0aIixcvFg0GQzjKjihS+vj48ePirbfeKubn54tDhw4Vb7jhBrGgoCAcZUeM+fPni7fffnuLz/Ea7hhS+pjXsDRGo1Fcvny5OGnSJDE3N1ecM2eOuHPnTlEUI++7hCCKohjuMEdERERERBRJOLWPiIiIiIhIIgYpIiIiIiIiiRikiIiIiIiIJGKQIiIiIiIikohBioiIiIiISCIGKSIiIiIiIokYpIiIiDoAdxMhIupeGKSIiMinsLAQd9xxB8aOHYvzzz8f48aNw+23345Dhw6FraaTJ08iJycHH374YattpkyZgqVLl4awqlNMJhPuuece7N6923fsmmuuwTXXXNPm13z99ddx1113dUR5raqtrcWkSZNQUlIS1PchIuqqGKSIiAgAcPjwYcyZMwdGoxEPPPAAXn/9ddxzzz0oKyvDlVdeiZ9++incJXZKBw8exEcffQSv19shr3f06FG8/PLLuPvuuzvk9VqTmJiI6667Dvfddx9H04iI2oBBioiIAABvvPEGEhMT8corr+B3v/sd8vPzcemll2L9+vVISEjA2rVrw11it/DMM89g5syZSE9PD/p7zZ07F4WFhdiyZUvQ34uIqKthkCIiIgBAdXU1RFFsNrISHR2N++67D7/73e/8jn/xxReYPXs2Bg8ejLFjx+LRRx+F1Wr1Pf/CCy9gypQp+OqrrzB9+nQMHToUV155JXbu3On3OocOHcItt9yC0aNHY9CgQRg/fjweffRR2O32Nn8Wh8OBp59+GhMnTsT555+PWbNm4dNPP/VrM2XKFDz//PN46qmncMEFF2DIkCGYN28ejh075tfu3//+Ny655BIMHjwYl156KbZv347zzjsPH374IXbu3Ilrr70WAHDttdf6TecTRRGvvPIKJk2ahCFDhmDOnDnYv3//GesuLCzE119/jZkzZ/od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+kaarrvuOmzcuBHz58/HggULoFQq8eKLL0Kn02HWrFlnvOcqEHv27AHQcL8aEREFjkGKiIgANCx//v777+O1117DSy+9BIPBAJVKhfPOOw/PPvssLrroIl/bK664AjExMXj11Vfx3nvvITo6Gnl5eVixYgV69erl97rLly/H448/DoPBgLy8PPzjH//wTa9bsGABamtr8eabb2LNmjXIyMjA73//ewiCgJdffhkmk0ny55DJZFi3bh1Wr16Nl19+GTU1NUhPT8f111+Pm2++WdJrzZo1C1arFa+99ho++OAD9O/fH/fffz/uv/9+3/1H/fv3x8yZM7FhwwZs3boVn3zyieSaG1100UVYs2YN9u/fj6FDhwIAMjIy8M477+CZZ57B0qVLoVKpMGrUKDz77LMtLh4h1bfffoshQ4agZ8+e7XodIqLuRhCbbvhBRETUQV544QX87W9/Q0FBQbhLabNPPvkE5513Hvr27es79vXXX2PBggX46KOP2jX61JqFCxciMTERTzzxRIe/9umsVivGjx+Pp556CtOmTQv6+xERdSW8R4qIiKgVH3/8Mf785z9j06ZN2L17Nz744AMsW7YM+fn5QQlRQMO0yc8//xxlZWVBef2m3n33XfTv3x9Tp04N+nsREXU1nNpHRETUiqeeegorV67EM888A4PBgJSUFEyfPh233XZb0N4zJycHCxYswIoVK7Bq1aqgvY/BYMD69evx1ltvdcjKf0RE3Q2n9hEREREREUnEqX1EREREREQSMUgRERERERFJxCBFREREREQkEYMUERERERGRRAxSREREREREEjFIERERERERScQgRUREREREJBGDFBERERERkUQMUkRERERERBL9f/lQCsccKFORAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] }, "metadata": {}, "output_type": "display_data" @@ -263,34 +316,26 @@ "plt.ylabel(iris.feature_names[1].capitalize())\n", "plt.legend(loc=\"upper left\", title=\"Class\")\n", "plt.show()\n" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2023-11-08T07:46:01.898725Z", - "start_time": "2023-11-08T07:46:01.721169Z" - } - }, - "id": "80eaae891938ae7a" + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.11.10" } }, "nbformat": 4, diff --git a/pyproject.toml b/pyproject.toml index bce145d..bc81d8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,11 @@ urls = { GitHub = "https://github.com/braun-steven/simple-einet" } dependencies = [ "numpy~=1.26.1", "torch~=2.0", - "fast_pytorch_kmeans~=0.2.0" + # "fast_pytorch_kmeans~=0.2.0", + "fast_pytorch_kmeans@git+https://github.com/DeMoriarty/fast_pytorch_kmeans#egg=1d41c5bda5647e344da3d5432f81f96f6fe21cf6", + "tqdm~=4.0", + "scipy~=1.14.0", + "imageio~=2.36.0" ] [project.optional-dependencies] @@ -39,8 +43,7 @@ app = [ "wandb~=0.15.0", "rich~=13.0", "icecream~=2.0", - "hydra-core~=1.3.0", - "tqdm~=4.0" + "hydra-core~=1.3.0" ] [tool.black] diff --git a/simple_einet/abstract_layers.py b/simple_einet/abstract_layers.py index ff6fa95..514931f 100644 --- a/simple_einet/abstract_layers.py +++ b/simple_einet/abstract_layers.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from typing import Tuple +import numpy as np import torch from torch import nn, Tensor @@ -9,6 +10,7 @@ from torch.nn import functional as F + class AbstractLayer(nn.Module, ABC): """ This is the abstract base class for all layers in the SPN. @@ -54,6 +56,48 @@ def logits_to_log_weights(logits: torch.Tensor, dim: int, temperature: float = 1 return F.log_softmax(logits / temperature, dim=dim) +class ConditioningNetwork(nn.Module): + def __init__(self, num_features_out: int, num_sums_in: int, num_hidden: int): + super().__init__() + input_size = num_features_out * num_sums_in + self.input_size = input_size + self.num_features_out = num_features_out + self.num_sums_in = num_sums_in + self.num_hidden = num_hidden + + layers = [nn.Linear(input_size, input_size // 2), nn.SiLU()] + + # Construct dims + dims = [] + + for i in range(1, num_hidden // 2 + 1): + dims.append(input_size // 2**i) + + for i in range(num_hidden // 2 + 1, 0, -1): + dims.append(input_size // 2**i) + + for i in range(len(dims) - 1): + layers.append(nn.Linear(dims[i], dims[i + 1])) + layers.append(nn.SiLU()) + + layers += [nn.Linear(input_size // 2, input_size)] + # layers += [nn.Linear(input_size // 4, input_size // 2)] + + self._net = nn.Sequential( + *layers, + ) + + def forward(self, log_prior: torch.Tensor, lls: torch.Tensor): + # x = torch.cat([log_prior, lls], dim=1).view(-1, self.input_size) + x = log_prior + lls + x = x - torch.logsumexp(x, dim=2, keepdim=True) + x = x.view(-1, self.input_size) + out = self._net(x) + out = out.view(-1, self.num_features_out, self.num_sums_in) + log_posterior = F.log_softmax(out, dim=2) + return log_posterior + + class AbstractSumLayer(AbstractLayer): """ This is the abstract base class for all kinds of sum layers in the circuit. @@ -75,7 +119,12 @@ class AbstractSumLayer(AbstractLayer): """ def __init__( - self, num_features: int, num_sums_in: int, num_sums_out: int, num_repetitions: int, dropout: float = 0.0 + self, + num_features: int, + num_sums_in: int, + num_sums_out: int, + num_repetitions: int, + dropout: float = 0.0, ): super().__init__(num_features=num_features, num_repetitions=num_repetitions) self.num_sums_in = check_valid(num_sums_in, int, 1) diff --git a/simple_einet/data.py b/simple_einet/data.py index de2ec01..5966a92 100644 --- a/simple_einet/data.py +++ b/simple_einet/data.py @@ -1,15 +1,22 @@ +import time + +from simple_einet.layers.distributions.piecewise_linear import PiecewiseLinear +import imageio.v3 as imageio import itertools -import csv -import subprocess import os +import subprocess from dataclasses import dataclass from enum import Enum from typing import Optional, Tuple +import csv import numpy as np import torch import torchvision.transforms as transforms from sklearn import datasets +from sklearn.decomposition import PCA +from sklearn.model_selection import train_test_split +from sklearn.preprocessing import OneHotEncoder, StandardScaler, LabelEncoder from torch.utils.data import DataLoader, Dataset, random_split, ConcatDataset from torch.utils.data.sampler import Sampler from torchvision.datasets import ( @@ -26,11 +33,14 @@ ) from simple_einet.layers.distributions.binomial import Binomial -from simple_einet.layers.distributions.bernoulli import Bernoulli from simple_einet.layers.distributions.categorical import Categorical from simple_einet.layers.distributions.multivariate_normal import MultivariateNormal from simple_einet.layers.distributions.normal import Normal, RatNormal +import logging + +logger = logging.getLogger(__name__) + @dataclass class Shape: @@ -68,42 +78,21 @@ def get_data_shape(dataset_name: str) -> Shape: Tuple[int, int, int]: Tuple of [channels, height, width]. """ if "synth" in dataset_name: - return Shape(1, 2, 1) - - if "debd" in dataset_name: - return Shape( - *{ - "accidents": (1, 111, 1), - "ad": (1, 1556, 1), - "baudio": (1, 100, 1), - "bbc": (1, 1058, 1), - "bnetflix": (1, 100, 1), - "book": (1, 500, 1), - "c20ng": (1, 910, 1), - "cr52": (1, 889, 1), - "cwebkb": (1, 839, 1), - "dna": (1, 180, 1), - "jester": (1, 100, 1), - "kdd": (1, 64, 1), - "kosarek": (1, 190, 1), - "moviereview": (1, 1001, 1), - "msnbc": (1, 17, 1), - "msweb": (1, 294, 1), - "nltcs": (1, 16, 1), - "plants": (1, 69, 1), - "pumsb_star": (1, 163, 1), - "tmovie": (1, 500, 1), - "tretail": (1, 135, 1), - "voting": (1, 1359, 1), - }[dataset_name.replace("debd-", "")] - ) + return Shape(2, 1, 1) + + if dataset_name in DEBD: + shape = DEBD_shapes[dataset_name]["train"] + return Shape(channels=1, height=shape[1], width=1) return Shape( *{ - "mnist": (1, 32, 32), - "mnist-28": (1, 28, 28), - "fmnist": (1, 32, 32), - "fmnist-28": (1, 28, 28), + "mnist-16": (1, 16, 16), + "mnist-32": (1, 32, 32), + "mnist-bin": (1, 28, 28), + "mnist": (1, 28, 28), + "fmnist": (1, 28, 28), + "fmnist-16": (1, 16, 16), + "fmnist-32": (1, 32, 32), "cifar": (3, 32, 32), "svhn": (3, 32, 32), "svhn-extra": (3, 32, 32), @@ -115,11 +104,58 @@ def get_data_shape(dataset_name: str) -> Shape: "flowers": (3, 32, 32), "tiny-imagenet": (3, 32, 32), "lfw": (3, 32, 32), + "20newsgroup": (1, 50, 1), + "kddcup99": (1, 118, 1), + "covtype": (1, 54, 1), + "breast_cancer": (1, 30, 1), + "wine": (1, 13, 1), "digits": (1, 8, 8), }[dataset_name] ) +def get_data_num_classes(dataset_name: str) -> int: + """Get the number of classes for a specific dataset. + + Args: + dataset_name (str): Dataset name. + + Returns: + int: Number of classes. + """ + if "synth" in dataset_name: + return 2 + + if dataset_name in DEBD: + return 0 + + return { + "mnist-16": 10, + "mnist-32": 10, + "mnist-bin": 10, + "mnist": 10, + "fmnist": 10, + "fmnist-16": 10, + "fmnist-32": 10, + "cifar": 10, + "svhn": 10, + "svhn-extra": 10, + "celeba": 0, + "celeba-small": 0, + "celeba-tiny": 0, + "lsun": 0, + "fake": 10, + "flowers": 102, + "tiny-imagenet": 200, + "lfw": 0, + "20newsgroup": 20, + "kddcup99": 23, + "covtype": 7, + "breast_cancer": 2, + "wine": 3, + }[dataset_name] + + @torch.no_grad() def generate_data(dataset_name: str, n_samples: int = 1000) -> Tuple[torch.Tensor, torch.Tensor]: tag = dataset_name.replace("synth-", "") @@ -198,25 +234,32 @@ def generate_data(dataset_name: str, n_samples: int = 1000) -> Tuple[torch.Tenso return data, labels +def to_255_int(x): + return (x * 255).int() + + def maybe_download_debd(data_dir: str): - if os.path.isdir(f"{data_dir}/debd"): + debd_dir = os.path.join(data_dir, "debd") + if os.path.isdir(debd_dir): return - subprocess.run(f"git clone https://github.com/arranger1044/DEBD {data_dir}/debd".split()) + subprocess.run(["git", "clone", "https://github.com/arranger1044/DEBD", debd_dir]) wd = os.getcwd() - os.chdir(f"{data_dir}/debd") - subprocess.run("git checkout 80a4906dcf3b3463370f904efa42c21e8295e85c".split()) - subprocess.run("rm -rf .git".split()) + os.chdir(debd_dir) + subprocess.run(["git", "checkout", "80a4906dcf3b3463370f904efa42c21e8295e85c"]) + subprocess.run(["rm", "-rf", ".git"]) os.chdir(wd) -def load_debd(name, data_dir, dtype="float"): +def load_debd(name, data_dir, dtype="int32"): """Load one of the twenty binary density esimtation benchmark datasets.""" maybe_download_debd(data_dir) - train_path = os.path.join(data_dir, "debd", "datasets", name, name + ".train.data") - test_path = os.path.join(data_dir, "debd", "datasets", name, name + ".test.data") - valid_path = os.path.join(data_dir, "debd", "datasets", name, name + ".valid.data") + debd_dir = os.path.join(data_dir, "debd") + + train_path = os.path.join(debd_dir, "datasets", name, name + ".train.data") + test_path = os.path.join(debd_dir, "datasets", name, name + ".test.data") + valid_path = os.path.join(debd_dir, "datasets", name, name + ".valid.data") reader = csv.reader(open(train_path, "r"), delimiter=",") train_x = np.array(list(reader)).astype(dtype) @@ -230,6 +273,82 @@ def load_debd(name, data_dir, dtype="float"): return train_x, test_x, valid_x +DEBD = [ + "accidents", + "ad", + "baudio", + "bbc", + "bnetflix", + "book", + "c20ng", + "cr52", + "cwebkb", + "dna", + "jester", + "kdd", + "kosarek", + "moviereview", + "msnbc", + "msweb", + "nltcs", + "plants", + "pumsb_star", + "tmovie", + "tretail", + "voting", +] + +DEBD_shapes = { + "accidents": dict(train=(12758, 111), valid=(2551, 111), test=(1700, 111)), + "ad": dict(train=(2461, 1556), valid=(491, 1556), test=(327, 1556)), + "baudio": dict(train=(15000, 100), valid=(3000, 100), test=(2000, 100)), + "bbc": dict(train=(1670, 1058), valid=(330, 1058), test=(225, 1058)), + "bnetflix": dict(train=(15000, 100), valid=(3000, 100), test=(2000, 100)), + "book": dict(train=(8700, 500), valid=(1739, 500), test=(1159, 500)), + "c20ng": dict(train=(11293, 910), valid=(3764, 910), test=(3764, 910)), + "cr52": dict(train=(6532, 889), valid=(1540, 889), test=(1028, 889)), + "cwebkb": dict(train=(2803, 839), valid=(838, 839), test=(558, 839)), + "dna": dict(train=(1600, 180), valid=(1186, 180), test=(400, 180)), + "jester": dict(train=(9000, 100), valid=(4116, 100), test=(1000, 100)), + "kdd": dict(train=(180092, 64), valid=(34955, 64), test=(19907, 64)), + "kosarek": dict(train=(33375, 190), valid=(6675, 190), test=(4450, 190)), + "moviereview": dict(train=(1600, 1001), valid=(250, 1001), test=(150, 1001)), + "msnbc": dict(train=(291326, 17), valid=(58265, 17), test=(38843, 17)), + "msweb": dict(train=(29441, 294), valid=(5000, 294), test=(3270, 294)), + "nltcs": dict(train=(16181, 16), valid=(3236, 16), test=(2157, 16)), + "plants": dict(train=(17412, 69), valid=(3482, 69), test=(2321, 69)), + "pumsb_star": dict(train=(12262, 163), valid=(2452, 163), test=(1635, 163)), + "tmovie": dict(train=(4524, 500), valid=(591, 500), test=(1002, 500)), + "tretail": dict(train=(22041, 135), valid=(4408, 135), test=(2938, 135)), + "voting": dict(train=(1214, 1359), valid=(350, 1359), test=(200, 1359)), +} + +DEBD_display_name = { + "accidents": "accidents", + "ad": "ad", + "baudio": "audio", + "bbc": "bbc", + "bnetflix": "netflix", + "book": "book", + "c20ng": "20ng", + "cr52": "reuters-52", + "cwebkb": "web-kb", + "dna": "dna", + "jester": "jester", + "kdd": "kdd-2k", + "kosarek": "kosarek", + "moviereview": "moviereview", + "msnbc": "msnbc", + "msweb": "msweb", + "nltcs": "nltcs", + "plants": "plants", + "pumsb_star": "pumsb-star", + "tmovie": "each-movie", + "tretail": "retail", + "voting": "voting", +} + + def get_datasets(dataset_name, data_dir, normalize: bool) -> Tuple[Dataset, Dataset, Dataset]: """ Get the specified dataset. @@ -255,6 +374,9 @@ def get_datasets(dataset_name, data_dir, normalize: bool) -> Tuple[Dataset, Data ] ) + # if not normalize: + # transform.transforms.append(transforms.Lambda(to_255_int)) + kwargs = dict(root=data_dir, download=True, transform=transform) # Custom split generator with fixed seed @@ -263,46 +385,18 @@ def get_datasets(dataset_name, data_dir, normalize: bool) -> Tuple[Dataset, Data # Select the datasets if "synth" in dataset_name: # Train - data, labels = generate_data(dataset_name, n_samples=3000) - dataset_train = torch.utils.data.TensorDataset(data, labels) + X, labels = generate_data(dataset_name, n_samples=3000) + dataset_train = torch.utils.data.TensorDataset(X, labels) # Val - data, labels = generate_data(dataset_name, n_samples=1000) - dataset_val = torch.utils.data.TensorDataset(data, labels) + X, labels = generate_data(dataset_name, n_samples=1000) + dataset_val = torch.utils.data.TensorDataset(X, labels) # Test - data, labels = generate_data(dataset_name, n_samples=1000) - dataset_test = torch.utils.data.TensorDataset(data, labels) - - elif "debd" in dataset_name: - # Call load_debd - train_x, test_x, valid_x = load_debd(dataset_name.replace("debd-", ""), data_dir) - dataset_train = torch.utils.data.TensorDataset(torch.from_numpy(train_x), torch.zeros(train_x.shape[0])) - dataset_val = torch.utils.data.TensorDataset(torch.from_numpy(valid_x), torch.zeros(valid_x.shape[0])) - dataset_test = torch.utils.data.TensorDataset(torch.from_numpy(test_x), torch.zeros(test_x.shape[0])) - - elif dataset_name == "digits": - if normalize: - transform.transforms.append(transforms.Normalize([0.5], [0.5])) - - data, labels = datasets.load_digits(return_X_y=True) - data, labels = torch.from_numpy(data).float(), torch.from_numpy(labels).long() - data[data == 16] = 15 - # Normalize to [0, 1] - data = data / 15 - dataset_train = torch.utils.data.TensorDataset(data, labels) - - N = data.shape[0] - N_train = round(N * 0.7) - N_val = round(N * 0.2) - N_test = N - N_train - N_val - lenghts = [N_train, N_val, N_test] - - dataset_train, dataset_val, dataset_test = random_split( - dataset_train, lengths=lenghts, generator=split_generator - ) + X, labels = generate_data(dataset_name, n_samples=1000) + dataset_test = torch.utils.data.TensorDataset(X, labels) - elif dataset_name == "mnist" or dataset_name == "mnist-28": + elif dataset_name == "mnist" or dataset_name == "mnist-32" or dataset_name == "mnist-16": if normalize: transform.transforms.append(transforms.Normalize([0.5], [0.5])) @@ -311,11 +405,13 @@ def get_datasets(dataset_name, data_dir, normalize: bool) -> Tuple[Dataset, Data dataset_test = MNIST(**kwargs, train=False) # for dataset in [dataset_train, dataset_test]: + # import warnings + # warnings.warn("Using only digits 0 and 1 for MNIST.") # digits = [0, 1] # mask = torch.zeros_like(dataset.targets).bool() # for digit in digits: # mask = mask | (dataset.targets == digit) - # + # dataset.data = dataset.data[mask] # dataset.targets = dataset.targets[mask] @@ -326,7 +422,77 @@ def get_datasets(dataset_name, data_dir, normalize: bool) -> Tuple[Dataset, Data dataset_train, dataset_val = random_split(dataset_train, lengths=lenghts, generator=split_generator) - elif dataset_name == "fmnist" or dataset_name == "fmnist-28": + elif dataset_name == "mnist-bin": + # Download binary mnist dataset + if not os.path.exists(os.path.join(data_dir, "mnist-bin")): + # URL of the image + url = "https://i.imgur.com/j0SOfRW.png" + output_filename = "mnist-bin.png" + + # Use wget to download the image + os.system(f"curl {url} --output {output_filename}") + + # Load the downloaded image using imageio + image = imageio.imread(output_filename) + else: + # Load image + image = imageio.imread(os.path.join(data_dir, "mnist-bin.png")) + + ims, labels = np.split(image[..., :3].ravel(), [-70000]) + ims = np.unpackbits(ims).reshape((-1, 1, 28, 28)) + ims, labels = [np.split(y, [50000, 60000]) for y in (ims, labels)] + + (train_x, train_labels), (test_x, test_labels), (_, _) = ( + (ims[0], labels[0]), + (ims[1], labels[1]), + (ims[2], labels[2]), + ) + + # Make dataset from numpy images and labels + dataset_train = torch.utils.data.TensorDataset(torch.tensor(train_x), torch.tensor(train_labels)) + dataset_test = torch.utils.data.TensorDataset(torch.tensor(test_x), torch.tensor(test_labels)) + + # for dataset in [dataset_train, dataset_test]: + # import warnings + # warnings.warn("Using only digits 0 and 1 for MNIST.") + # digits = [0, 1] + # mask = torch.zeros_like(dataset.targets).bool() + # for digit in digits: + # mask = mask | (dataset.targets == digit) + # + # dataset.data = dataset.data[mask] + # dataset.targets = dataset.targets[mask] + + N = len(dataset_train.tensors[0]) + N_train = round(N * 0.9) + N_val = N - N_train + lenghts = [N_train, N_val] + + dataset_train, dataset_val = random_split(dataset_train, lengths=lenghts, generator=split_generator) + elif dataset_name == "digits": + # SKlearn digits dataset + digits = datasets.load_digits() + X, y = digits.data, digits.target + + X = X / X.max() + + # Split into train, val, test + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42, stratify=y) + X_train, X_val, y_train, y_val = train_test_split( + X_train, y_train, test_size=0.1, random_state=42, stratify=y_train + ) + + + # Reshape + X_train = X_train.reshape(-1, *shape) + X_val = X_val.reshape(-1, *shape) + X_test = X_test.reshape(-1, *shape) + + dataset_train = torch.utils.data.TensorDataset(torch.tensor(X_train), torch.tensor(y_train)) + dataset_val = torch.utils.data.TensorDataset(torch.tensor(X_val), torch.tensor(y_val)) + dataset_test = torch.utils.data.TensorDataset(torch.tensor(X_test), torch.tensor(y_test)) + + elif dataset_name == "fmnist" or dataset_name == "fmnist-32": if normalize: transform.transforms.append(transforms.Normalize([0.5], [0.5])) @@ -435,28 +601,263 @@ def get_datasets(dataset_name, data_dir, normalize: bool) -> Tuple[Dataset, Data dataset_train, dataset_val = random_split(dataset_train, lengths=lenghts, generator=split_generator) + elif dataset_name in DEBD: + name = dataset_name + + # Load the DEBD dataset + train_x, test_x, valid_x = load_debd(name, data_dir) + shape = get_data_shape(dataset_name) + train_x = train_x.reshape(-1, *shape) + test_x = test_x.reshape(-1, *shape) + valid_x = valid_x.reshape(-1, *shape) + dataset_train = torch.utils.data.TensorDataset(torch.tensor(train_x), torch.zeros(len(train_x))) + dataset_val = torch.utils.data.TensorDataset(torch.tensor(valid_x), torch.zeros(len(valid_x))) + dataset_test = torch.utils.data.TensorDataset(torch.tensor(test_x), torch.zeros(len(test_x))) + + elif dataset_name == "20newsgroup": + # Load the 20 newsgroup dataset + from sklearn.datasets import fetch_20newsgroups_vectorized + + # Load the dataset + X_train, y_train = fetch_20newsgroups_vectorized(return_X_y=True, data_home=data_dir, subset="train") + X_test, y_test = fetch_20newsgroups_vectorized(return_X_y=True, data_home=data_dir, subset="test") + + # Split train into train and val + X_train, X_val, y_train, y_val = train_test_split( + X_train, y_train, test_size=0.1, random_state=42, stratify=y_train + ) + + # Do dimensionality reduction with PCA + pca = PCA( + n_components=50, + ) + logger.info("Running PCA with 50 components on 20newsgroup dataset") + t0 = time.time() + X_train = pca.fit_transform(X=X_train.toarray()) + duration = time.time() - t0 + logger.info(f"PCA done in {duration:.2f}s") + X_val = pca.transform(X_val.toarray()) + X_test = pca.transform(X_test.toarray()) + + # Scale with StandardScaler + scaler = StandardScaler() + X_train = scaler.fit_transform(X_train) + X_val = scaler.transform(X_val) + X_test = scaler.transform(X_test) + + X_train = X_train.reshape(-1, *shape) + X_val = X_val.reshape(-1, *shape) + X_test = X_test.reshape(-1, *shape) + + # Convert to float32 + X_train = X_train.astype(np.float32) + X_val = X_val.astype(np.float32) + X_test = X_test.astype(np.float32) + + # Construct datasets + dataset_train = torch.utils.data.TensorDataset(torch.tensor(X_train), torch.tensor(y_train)) + dataset_val = torch.utils.data.TensorDataset(torch.tensor(X_val), torch.tensor(y_val)) + dataset_test = torch.utils.data.TensorDataset(torch.tensor(X_test), torch.tensor(y_test)) + + elif dataset_name == "covtype": + # Load the covtype dataset + from sklearn.datasets import fetch_covtype + + # Load the dataset + X, y = fetch_covtype(data_home=data_dir, return_X_y=True) + X = X.astype(np.float32) + + # Encode Labels + y = LabelEncoder().fit_transform(y) + + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42, stratify=y) + X_train, X_val, y_train, y_val = train_test_split( + X_train, y_train, test_size=0.1, random_state=42, stratify=y_train + ) + + # Apply StandardScaler + scaler = StandardScaler() + X_train = scaler.fit_transform(X_train) + X_val = scaler.transform(X_val) + X_test = scaler.transform(X_test) + + # Reshape + X_train = X_train.reshape(-1, *shape) + X_val = X_val.reshape(-1, *shape) + X_test = X_test.reshape(-1, *shape) + + + dataset_train = torch.utils.data.TensorDataset(torch.tensor(X_train), torch.tensor(y_train)) + dataset_val = torch.utils.data.TensorDataset(torch.tensor(X_val), torch.tensor(y_val)) + dataset_test = torch.utils.data.TensorDataset(torch.tensor(X_test), torch.tensor(y_test)) + + elif dataset_name == "kddcup99": + # Load the kddcup99 dataset + from sklearn.datasets import fetch_kddcup99 + + # Load the dataset + X, y = fetch_kddcup99(data_home=data_dir, return_X_y=True) + + # Encode Labels + y = LabelEncoder().fit_transform(y) + + # Convert the byte strings to regular strings + X[:, 1:4] = X[:, 1:4].astype(str) + + # Identify the categorical columns (in this case, columns 1, 2, and 3) + categorical_columns = [1, 2, 3] + + # Separate the categorical features from the numerical features + categorical_data = X[:, categorical_columns] + numerical_data = np.delete(X, categorical_columns, axis=1) + + # Apply OneHotEncoder to the categorical data + encoder = OneHotEncoder(sparse=False) + encoded_categorical_data = encoder.fit_transform(categorical_data) + + # Combine the encoded categorical features with the numerical features + X = np.hstack((numerical_data, encoded_categorical_data)) + + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42, stratify=y) + X_train, X_val, y_train, y_val = train_test_split( + X_train, y_train, test_size=0.1, random_state=42, stratify=y_train + ) + + # Apply StandardScaler + scaler = StandardScaler() + X_train = scaler.fit_transform(X_train) + X_val = scaler.transform(X_val) + X_test = scaler.transform(X_test) + + # Reshape + X_train = X_train.reshape(-1, *shape).astype(np.float32) + X_val = X_val.reshape(-1, *shape).astype(np.float32) + X_test = X_test.reshape(-1, *shape).astype(np.float32) + + + # Construct datasets + dataset_train = torch.utils.data.TensorDataset(torch.tensor(X_train), torch.tensor(y_train)) + dataset_val = torch.utils.data.TensorDataset(torch.tensor(X_val), torch.tensor(y_val)) + dataset_test = torch.utils.data.TensorDataset(torch.tensor(X_test), torch.tensor(y_test)) + + elif dataset_name == "breast_cancer": + # Load the breast cancer dataset + from sklearn.datasets import load_breast_cancer + + # Load the dataset + X, y = load_breast_cancer(return_X_y=True) + X = X.astype(np.float32) + + # Encode Labels + y = LabelEncoder().fit_transform(y) + + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42, stratify=y) + X_train, X_val, y_train, y_val = train_test_split( + X_train, y_train, test_size=0.1, random_state=42, stratify=y_train + ) + + # Apply StandardScaler + scaler = StandardScaler() + X_train = scaler.fit_transform(X_train) + X_val = scaler.transform(X_val) + X_test = scaler.transform(X_test) + + # Reshape + X_train = X_train.reshape(-1, *shape) + X_val = X_val.reshape(-1, *shape) + X_test = X_test.reshape(-1, *shape) + + + dataset_train = torch.utils.data.TensorDataset(torch.tensor(X_train), torch.tensor(y_train)) + dataset_val = torch.utils.data.TensorDataset(torch.tensor(X_val), torch.tensor(y_val)) + dataset_test = torch.utils.data.TensorDataset(torch.tensor(X_test), torch.tensor(y_test)) + + elif dataset_name == "wine": + # Load the wine dataset + from sklearn.datasets import load_wine + + # Load the dataset + X, y = load_wine(return_X_y=True) + X = X.astype(np.float32) + + # Encode Labels + y = LabelEncoder().fit_transform(y) + + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42, stratify=y) + X_train, X_val, y_train, y_val = train_test_split( + X_train, y_train, test_size=0.1, random_state=42, stratify=y_train + ) + + # Apply StandardScaler + scaler = StandardScaler() + X_train = scaler.fit_transform(X_train) + X_val = scaler.transform(X_val) + X_test = scaler.transform(X_test) + + # Reshape + X_train = X_train.reshape(-1, *shape) + X_val = X_val.reshape(-1, *shape) + X_test = X_test.reshape(-1, *shape) + + dataset_train = torch.utils.data.TensorDataset(torch.tensor(X_train), torch.tensor(y_train)) + dataset_val = torch.utils.data.TensorDataset(torch.tensor(X_val), torch.tensor(y_val)) + dataset_test = torch.utils.data.TensorDataset(torch.tensor(X_test), torch.tensor(y_test)) + else: raise Exception(f"Unknown dataset: {dataset_name}") + + # # Ensure, that all datasets are in float + # for dataset in [dataset_train, dataset_val, dataset_test]: + # if isinstance(dataset, torch.utils.data.TensorDataset): + # dataset.tensors = (dataset.tensors[0].float(), dataset.tensors[1].float()) + # elif isinstance(dataset, torch.utils.data.dataset.Subset): + # dataset.dataset.data = dataset.dataset.data.float() + # else: + # dataset.data = dataset.data.float() + + return dataset_train, dataset_val, dataset_test +def is_1d_data(dataset_name): + """Check if the dataset is 1D data.""" + if dataset_name in DEBD: + return True + + if dataset_name in ["20newsgroup", "covtype", "kddcup99", "breast_cancer", "wine"]: + return True + + if "synth" in dataset_name: + return True + + return False + + +def is_classification_data(dataset_name): + """Check if the dataset is 1D data.""" + if dataset_name in DEBD or "celeba" in dataset_name: + return False + + return True + + def build_dataloader( - dataset_name, data_dir, batch_size, num_workers, loop: bool, normalize: bool + dataset_name, data_dir, batch_size, num_workers, loop: bool, normalize: bool, seed: int ) -> Tuple[DataLoader, DataLoader, DataLoader]: # Get dataset objects dataset_train, dataset_val, dataset_test = get_datasets(dataset_name, data_dir, normalize=normalize) # Build data loader - loader_train = _make_loader(batch_size, num_workers, dataset_train, loop=loop, shuffle=True) - loader_val = _make_loader(batch_size, num_workers, dataset_val, loop=loop, shuffle=False) - loader_test = _make_loader(batch_size, num_workers, dataset_test, loop=loop, shuffle=False) + loader_train = _make_loader(batch_size, num_workers, dataset_train, loop=loop, shuffle=True, seed=seed) + loader_val = _make_loader(batch_size, num_workers, dataset_val, loop=False, shuffle=False, seed=seed) + loader_test = _make_loader(batch_size, num_workers, dataset_test, loop=False, shuffle=False, seed=seed) return loader_train, loader_val, loader_test -def _make_loader(batch_size, num_workers, dataset: Dataset, loop: bool, shuffle: bool) -> DataLoader: +def _make_loader(batch_size, num_workers, dataset: Dataset, loop: bool, shuffle: bool, seed: int) -> DataLoader: if loop: - sampler = TrainingSampler(size=len(dataset)) + sampler = TrainingSampler(size=len(dataset), seed=seed) else: sampler = None @@ -519,47 +920,3 @@ def _infinite_indices(self): yield from torch.arange(self._size).tolist() -class Dist(str, Enum): - """Enum for the distribution of the data.""" - - NORMAL = "normal" - MULTIVARIATE_NORMAL = "multivariate_normal" - NORMAL_RAT = "normal_rat" - BINOMIAL = "binomial" - CATEGORICAL = "categorical" - BERNOULLI = "bernoulli" - - -def get_distribution(dist: Dist, cfg): - """ - Get the distribution for the leaves. - - Args: - dist: The distribution to use. - - Returns: - leaf_type: The type of the leaves. - leaf_kwargs: The kwargs for the leaves. - - """ - if dist == Dist.NORMAL: - leaf_type = Normal - leaf_kwargs = {} - elif dist == Dist.NORMAL_RAT: - leaf_type = RatNormal - leaf_kwargs = {"min_sigma": cfg.min_sigma, "max_sigma": cfg.max_sigma} - elif dist == Dist.BINOMIAL: - leaf_type = Binomial - leaf_kwargs = {"total_count": 2**cfg.n_bits - 1} - elif dist == Dist.CATEGORICAL: - leaf_type = Categorical - leaf_kwargs = {"num_bins": 2**cfg.n_bits - 1} - elif dist == Dist.MULTIVARIATE_NORMAL: - leaf_type = MultivariateNormal - leaf_kwargs = {"cardinality": cfg.multivariate_cardinality} - elif dist == Dist.BERNOULLI: - leaf_type = Bernoulli - leaf_kwargs = {} - else: - raise ValueError(f"Unknown distribution ({dist}).") - return leaf_kwargs, leaf_type diff --git a/simple_einet/einet.py b/simple_einet/einet.py index 53ab358..399f213 100644 --- a/simple_einet/einet.py +++ b/simple_einet/einet.py @@ -1,6 +1,7 @@ import logging +from simple_einet.utils import invert_permutation from dataclasses import dataclass, field -from typing import Any, Dict, List, Type, Union +from typing import Any, Dict, List, Type, Union, Optional import numpy as np import torch @@ -11,9 +12,10 @@ EinsumLayer, ) from simple_einet.layers.mixing import MixingLayer -from simple_einet.layers.factorized_leaf import FactorizedLeaf -from simple_einet.layers.linsum import LinsumLayer -from simple_einet.sampling_utils import sampling_context, SamplingContext +from simple_einet.layers.factorized_leaf import FactorizedLeaf, FactorizedLeafSimple +from simple_einet.layers.linsum import LinsumLayer, LinsumLayer2 +from simple_einet.layers.product import RootProductLayer +from simple_einet.sampling_utils import index_one_hot, sampling_context, SamplingContext from simple_einet.layers.sum import SumLayer from simple_einet.type_checks import check_valid @@ -35,6 +37,7 @@ class EinetConfig: leaf_type: Type = None # Type of the leaf base class (Normal, Bernoulli, etc) leaf_kwargs: Dict[str, Any] = field(default_factory=dict) # Parameters for the leaf base class layer_type: str = "linsum" # Indicates the intermediate layer type: linsum or einsum + structure: str = "original" # Structure of the Einet: original or bottom_up def assert_valid(self): """Check whether the configuration is valid.""" @@ -49,6 +52,15 @@ def assert_valid(self): check_valid(self.num_leaves, int, 1) check_valid(self.dropout, float, 0.0, 1.0, allow_none=True) assert self.leaf_type is not None, "EinetConfig.leaf_type parameter was not set!" + assert self.layer_type in [ + "linsum", + "linsum2", + "einsum", + ], f"Invalid layer type {self.layer_type}. Must be 'linsum' or 'einsum'." + assert self.structure in [ + "original", + "bottom_up", + ], f"Invalid structure type {self.structure}. Must be 'original' or 'bottom_up'." assert isinstance(self.leaf_type, type) and issubclass( self.leaf_type, AbstractLeaf @@ -60,6 +72,9 @@ def assert_valid(self): else: cardinality = 1 + if self.structure == "bottom_up": + assert self.layer_type == "linsum", "Bottom-up structure only supports LinsumLayer due to handling of padding (not implemented for einsumlayer yet)." + # Get minimum number of features present at the lowest layer (num_features is the actual input dimension, # cardinality in multivariate distributions reduces this dimension since it merges groups of size #cardinality) min_num_features = np.ceil(self.num_features // cardinality) @@ -79,7 +94,7 @@ class Einet(nn.Module): def __init__(self, config: EinetConfig): """ - Create a Einet based on a configuration object. + Create an Einet based on a configuration object. Args: config (EinetConfig): Einet configuration object. @@ -89,15 +104,28 @@ def __init__(self, config: EinetConfig): self.config = config # Construct the architecture - self._build() + if self.config.structure == "original": + self._build_structure_original() + elif self.config.structure == "bottom_up": + self._build_structure_bottom_up() + else: + raise ValueError(f"Invalid structure type {self.config.structure}. Must be 'original' or 'bottom_up'.") + + # Leaf cache + self._leaf_cache = {} - def forward(self, x: torch.Tensor, marginalized_scopes: torch.Tensor = None) -> torch.Tensor: + def reset_cache(self): + """Reset the leaf cache.""" + self._leaf_cache = {} + + def forward(self, x: torch.Tensor, marginalized_scopes: torch.Tensor = None, cache_index: Optional[int] = None) -> torch.Tensor: """ Inference pass for the Einet model. Args: - x (torch.Tensor): Input data of shape [N, C, D], where C is the number of input channels (useful for images) and D is the number of features/random variables (H*W for images). - marginalized_scopes: torch.Tensor: (Default value = None) + x (torch.Tensor): Input data of shape [N, C, D], where C is the number of input channels (useful for images) and D is the number of features/random variables (H*W for images). + marginalized_scopes (torch.Tensor): (Default value = None) + cache_index (Optional[int]): Index of the cache. If not None, the leaf tries to retrieve the cached log-likelihoods or computes the log-likelihoods on a cache-miss and then caches the results. (Default value = None) Returns: Log-likelihood tensor of the input: p(X) or p(X | C) if number of classes > 1. @@ -115,11 +143,41 @@ def forward(self, x: torch.Tensor, marginalized_scopes: torch.Tensor = None) -> x.shape[1] == self.config.num_channels ), f"Number of channels in input ({x.shape[1]}) does not match number of channels specified in config ({self.config.num_channels})." assert ( - x.shape[2] == self.config.num_features + x.shape[2] == self.config.num_features ), f"Number of features in input ({x.shape[0]}) does not match number of features specified in config ({self.config.num_features})." # Apply leaf distributions (replace marginalization indicators with 0.0 first) - x = self.leaf(x, marginalized_scopes) + # If cache_index is set, try to retrieve the cached leaf log-likelihoods + if cache_index is not None and cache_index in self._leaf_cache: + x = self._leaf_cache[cache_index] + else: + x = self.leaf(x, marginalized_scopes) + + if cache_index is not None: # Cache index was specified but not found in cache + self._leaf_cache[cache_index] = x + + + # Factorize input channels + if not isinstance(self.leaf, (FactorizedLeaf, FactorizedLeafSimple)): + x = x.sum(dim=1) + assert x.shape == ( + x.shape[0], + self.config.num_features, + self.config.num_leaves, + self.config.num_repetitions, + ), f"Invalid shape after leaf layer. Was {x.shape} but expected ({x.shape[0]}, {self.config.num_features}, {self.config.num_leaves}, {self.config.num_repetitions})." + else: + assert x.shape == ( + x.shape[0], + self.leaf.num_features_out, + self.config.num_leaves, + self.config.num_repetitions, + ), f"Invalid shape after leaf layer. Was {x.shape} but expected ({x.shape[0]}, {self.leaf.num_features_out}, {self.config.num_leaves}, {self.config.num_repetitions})." + + # Apply permutation + if hasattr(self, "permutation"): + for i in range(self.config.num_repetitions): + x[:, :, :, i] = x[:, self.permutation[i], :, i] # Pass through intermediate layers x = self._forward_layers(x) @@ -177,7 +235,7 @@ def posterior(self, x) -> torch.Tensor: return posterior(ll_x_g_y, self.config.num_classes) - def _build(self): + def _build_structure_original(self): """Construct the internal architecture of the Einet.""" # Build the SPN bottom up: # Definition from RAT Paper @@ -186,7 +244,7 @@ def _build(self): # Internal Region: Create S sum nodes # Partition: Cross products of all child-regions - intermediate_layers: List[Union[EinsumLayer, LinsumLayer]] = [] + intermediate_layers: List[Union[EinsumLayer, LinsumLayer, LinsumLayer2]] = [] # Construct layers from top to bottom for i in np.arange(start=1, stop=self.config.depth + 1): @@ -226,6 +284,14 @@ def _build(self): num_repetitions=self.config.num_repetitions, dropout=self.config.dropout, ) + elif self.config.layer_type == "linsum2": + layer = LinsumLayer2( + num_features=in_features, + num_sums_in=_num_sums_in, + num_sums_out=_num_sums_out, + num_repetitions=self.config.num_repetitions, + dropout=self.config.dropout, + ) else: raise ValueError(f"Unknown layer type {self.config.layer_type}") @@ -247,7 +313,140 @@ def _build(self): self.leaf = self._build_input_distribution(num_features_out=leaf_num_features_out) # List layers in a bottom-to-top fashion - self.layers: List[Union[EinsumLayer, LinsumLayer]] = nn.ModuleList(reversed(intermediate_layers)) + self.layers: List[Union[EinsumLayer, LinsumLayer, LinsumLayer2]] = nn.ModuleList(reversed(intermediate_layers)) + + # If model has multiple reptitions, add repetition mixing layer + if self.config.num_repetitions > 1: + self.mixing = MixingLayer( + num_features=1, + num_sums_in=self.config.num_repetitions, + num_sums_out=self.config.num_classes, + dropout=self.config.dropout, + ) + + # Construct sampling root with weights according to priors for sampling + if self.config.num_classes > 1: + self._class_sampling_root = SumLayer( + num_sums_in=self.config.num_classes, + num_features=1, + num_sums_out=1, + num_repetitions=1, + ) + self._class_sampling_root.weights = nn.Parameter( + torch.log( + torch.ones(size=(1, self.config.num_classes, 1, 1)) * torch.tensor(1 / self.config.num_classes) + ), + requires_grad=False, + ) + + def _build_structure_bottom_up(self): + """Construct the internal architecture of the Einet.""" + # Build the SPN bottom up: + # Definition from RAT Paper + # Leaf Region: Create I leaf nodes + # Root Region: Create C sum nodes + # Internal Region: Create S sum nodes + # Partition: Cross products of all child-regions + + intermediate_layers: List[Union[EinsumLayer, LinsumLayer, LinsumLayer2]] = [] + + # Construct layers from bottom to top + in_features = self.config.num_features + for i in np.arange(start=0, stop=self.config.depth): + # Choose number of input sum nodes + # - if this is an intermediate layer, use the number of sum nodes from the previous layer + # - if this is the first layer, use the number of leaves as the leaf layer is below the first sum layer + if i == 0: + _num_sums_in = self.config.num_leaves + else: + _num_sums_in = self.config.num_sums + + # Choose number of output sum nodes + # - if this is the last layer, use the number of classes + # - otherwise use the number of sum nodes from the next layer + + # if i == self.config.depth - 1: + # _num_sums_out = self.config.num_classes + # else: + # _num_sums_out = self.config.num_sums + _num_sums_out = self.config.num_sums + + if self.config.layer_type == "einsum": + layer = EinsumLayer( + num_features=in_features, + num_sums_in=_num_sums_in, + num_sums_out=_num_sums_out, + num_repetitions=self.config.num_repetitions, + dropout=self.config.dropout, + ) + elif self.config.layer_type == "linsum": + layer = LinsumLayer( + num_features=in_features, + num_sums_in=_num_sums_in, + num_sums_out=_num_sums_out, + num_repetitions=self.config.num_repetitions, + dropout=self.config.dropout, + ) + elif self.config.layer_type == "linsum2": + layer = LinsumLayer2( + num_features=in_features, + num_sums_in=_num_sums_in, + num_sums_out=_num_sums_out, + num_repetitions=self.config.num_repetitions, + dropout=self.config.dropout, + ) + else: + raise ValueError(f"Unknown layer type {self.config.layer_type}") + + # Update number of input features: each layer merges two partitions + in_features = layer.num_features_out + + intermediate_layers.append(layer) + + if self.config.depth == 0: + # Create a single sum layer + layer = SumLayer( + num_sums_in=self.config.num_leaves, + num_features=1, + num_sums_out=self.config.num_classes, + num_repetitions=self.config.num_repetitions, + dropout=self.config.dropout, + ) + intermediate_layers.append(layer) + + # Construct final root product layer + root_sum = SumLayer( + num_sums_in=_num_sums_out, + num_sums_out=self.config.num_classes, + num_features=intermediate_layers[-1].num_features_out, + num_repetitions=self.config.num_repetitions, + ) + root_product = RootProductLayer( + num_features=intermediate_layers[-1].num_features_out, num_repetitions=self.config.num_repetitions + ) + + intermediate_layers.append(root_sum) + intermediate_layers.append(root_product) + + # Construct leaf + leaf_num_features_out = self.config.num_features + self.leaf = self._build_input_distribution_bottom_up() + # self.leaf = self._build_input_distribution(num_features_out=leaf_num_features_out) + + # List layers in a bottom-to-top fashion + self.layers: List[Union[EinsumLayer, LinsumLayer]] = nn.ModuleList(intermediate_layers) + + # Construct num_repertitions number of random permuations + permutations = torch.empty((self.config.num_repetitions, self.config.num_features), dtype=torch.long) + permutations_inv = torch.empty_like(permutations) + for i in range(self.config.num_repetitions): + permutations[i] = torch.randperm(self.config.num_features) + permutations_inv[i] = invert_permutation(permutations[i]) + + # Construct inverse permutations + + self.register_buffer("permutation", permutations) + self.register_buffer("permutation_inv", permutations_inv) # If model has multiple reptitions, add repetition mixing layer if self.config.num_repetitions > 1: @@ -273,7 +472,18 @@ def _build(self): requires_grad=False, ) - def _build_input_distribution(self, num_features_out: int): + def _build_input_distribution_bottom_up(self) -> AbstractLeaf: + """Construct the input distribution layer. This constructs a direct leaf and not a FactorizedLeaf since the bottom_up approach does not factorize.""" + # Cardinality is the size of the region in the last partitions + return self.config.leaf_type( + num_features=self.config.num_features, + num_channels=self.config.num_channels, + num_leaves=self.config.num_leaves, + num_repetitions=self.config.num_repetitions, + **self.config.leaf_kwargs, + ) + + def _build_input_distribution(self, num_features_out: int) -> FactorizedLeafSimple: """Construct the input distribution layer.""" # Cardinality is the size of the region in the last partitions base_leaf = self.config.leaf_type( @@ -284,7 +494,13 @@ def _build_input_distribution(self, num_features_out: int): **self.config.leaf_kwargs, ) - return FactorizedLeaf( + if self.config.num_repetitions == 1: + factorized_leaf_class = FactorizedLeafSimple + else: + factorized_leaf_class = FactorizedLeaf + + # factorized_leaf_class = FactorizedLeaf + return factorized_leaf_class( num_features=base_leaf.out_features, num_features_out=num_features_out, num_repetitions=self.config.num_repetitions, @@ -316,16 +532,17 @@ def mpe( def sample( self, - num_samples: int = None, + num_samples: Optional[int] = None, class_index=None, - evidence: torch.Tensor = None, + evidence: Optional[torch.Tensor] = None, is_mpe: bool = False, mpe_at_leaves: bool = False, temperature_leaves: float = 1.0, temperature_sums: float = 1.0, - marginalized_scopes: List[int] = None, + marginalized_scopes: Optional[List[int]] = None, is_differentiable: bool = False, - seed: int = None, + return_leaf_params: bool = False, + seed: Optional[int] = None, ): """ Sample from the distribution represented by this SPN. @@ -351,34 +568,19 @@ def sample( mpe_at_leaves: Flag to perform mpe only at leaves. marginalized_scopes: List of scopes to marginalize. is_differentiable: Flag to enable differentiable sampling. + return_leaf_params: Flag to return the leaf distribution instead of the samples. seed: Seed for torch.random. Returns: torch.Tensor: Samples generated according to the distribution specified by the SPN. """ - class_is_given = class_index is not None - evidence_is_given = evidence is not None - is_multiclass = self.config.num_classes > 1 - - assert not (class_is_given and evidence_is_given), "Cannot provide both, evidence and class indices." - assert ( - num_samples is None or not evidence_is_given - ), "Cannot provide both, number of samples to generate (num_samples) and evidence." - - if num_samples is not None: - assert num_samples > 0, "Number of samples must be > 0." - - # if not is_mpe: - # assert ((class_index is not None) and (self.config.num_classes > 1)) or ( - # (class_index is None) and (self.config.num_classes == 1) - # ), "Class index must be given if the number of classes is > 1 or must be none if the number of classes is 1." - - if class_is_given: - assert ( - self.config.num_classes > 1 - ), f"Class indices are only supported when the number of classes for this model is > 1." + assert class_index is None or evidence is None, "Cannot provide both, evidence and class indices." + assert num_samples is None or evidence is None, "Cannot provide both, number of samples to generate (num_samples) and evidence." + if self.config.num_classes == 1: + assert class_index is None, "Cannot sample classes for single-class models (i.e. num_classes must be 1)." + # Check if evidence contains nans if evidence is not None: # Set n to the number of samples in the evidence num_samples = evidence.shape[0] @@ -407,6 +609,7 @@ def sample( indices_out=indices_out, indices_repetition=indices_repetition, is_differentiable=is_differentiable, + return_leaf_params=return_leaf_params, ) with sampling_context(self, evidence, marginalized_scopes, requires_grad=is_differentiable, seed=seed): if self.config.num_classes > 1: @@ -437,7 +640,7 @@ def sample( ctx.indices_out = indices else: - # Sample class + # Sample class index from root ctx = self._class_sampling_root.sample(ctx=ctx) # Save parent indices that were sampled from the sampling root @@ -456,15 +659,38 @@ def sample( for layer in reversed(self.layers): ctx = layer.sample(ctx=ctx) + # Apply inverse permutation + if hasattr(self, "permutation_inv"): + # Select relevant inverse permuation based on repetition index + if is_differentiable: + permutation_inv = self.permutation_inv.unsqueeze(0) # Make space for num_samples + permutation_inv = self.permutation_inv.expand(num_samples, -1, -1) # [N, R, D] + r_idxs = ctx.indices_repetition.unsqueeze(-1) # Make space for feature dim + permutation_inv = index_one_hot(permutation_inv, r_idxs, dim=1) # [N, D] + permutation_inv = permutation_inv.unsqueeze(-1).expand(-1, -1, self.config.num_leaves).long() # [N, D, I] + ctx.indices_out = ctx.indices_out.gather(index=permutation_inv, dim=1) + else: + permutation_inv = self.permutation_inv[ctx.indices_repetition] + ctx.indices_out = ctx.indices_out.gather(index=permutation_inv, dim=1) + # Sample leaf samples = self.leaf.sample(ctx=ctx) + if return_leaf_params: + # Samples contain the distribution parameters instead of the samples + return samples + if evidence is not None: # First make a copy such that the original object is not changed - evidence = evidence.clone() + evidence = evidence.clone().float() shape_evidence = evidence.shape evidence = evidence.view_as(samples) - evidence[:, :, marginalized_scopes] = samples[:, :, marginalized_scopes].to(evidence.dtype) + if marginalized_scopes is None: + mask = torch.isnan(evidence) + evidence[mask] = samples[mask].to(evidence.dtype) + else: + evidence[:, :, marginalized_scopes] = samples[:, :, marginalized_scopes].to(evidence.dtype) + evidence = evidence.view(shape_evidence) return evidence else: @@ -492,4 +718,3 @@ def posterior(ll_x_g_y: torch.Tensor, num_classes) -> torch.Tensor: ll_x = torch.logsumexp(ll_x_and_y, dim=1, keepdim=True) ll_y_g_x = ll_x_g_y + ll_y - ll_x return ll_y_g_x - diff --git a/simple_einet/einet_mixture.py b/simple_einet/einet_mixture.py index d6c3dd5..09356c0 100644 --- a/simple_einet/einet_mixture.py +++ b/simple_einet/einet_mixture.py @@ -1,6 +1,6 @@ from _operator import xor from collections import defaultdict -from typing import Sequence, List +from typing import List, Optional, Sequence import torch from fast_pytorch_kmeans import KMeans @@ -49,7 +49,7 @@ def initialize(self, data: torch.Tensor = None, dataloader: DataLoader = None, d self.centroids.data = self._kmeans.centroids - def _predict_cluster(self, x, marginalized_scopes: List[int] = None): + def _predict_cluster(self, x, marginalized_scopes: Optional[List[int]] = None): x = x.view(x.shape[0], -1) # input needs to be [n, d] if marginalized_scopes is not None: keep_idx = list(sorted([i for i in range(self.config.num_features) if i not in marginalized_scopes])) diff --git a/simple_einet/layers/distributions/__init__.py b/simple_einet/layers/distributions/__init__.py index 5c0b794..32aa320 100644 --- a/simple_einet/layers/distributions/__init__.py +++ b/simple_einet/layers/distributions/__init__.py @@ -2,6 +2,5 @@ Module that contains a set of distributions with learnable parameters. """ - from simple_einet.layers.distributions.abstract_leaf import AbstractLeaf from simple_einet.layers.distributions.utils import * diff --git a/simple_einet/layers/distributions/abstract_leaf.py b/simple_einet/layers/distributions/abstract_leaf.py index 0beae86..0fbcabf 100644 --- a/simple_einet/layers/distributions/abstract_leaf.py +++ b/simple_einet/layers/distributions/abstract_leaf.py @@ -32,7 +32,7 @@ def dist_forward(distribution, x: torch.Tensor): # Compute log-likelihodd try: - x = distribution.log_prob(x) # Shape: [n, d, oc, r] + x = distribution.log_prob(x) # Shape: [n, c, d, oc, r] except ValueError as e: print("min:", x.min()) print("max:", x.max()) @@ -63,6 +63,7 @@ def dist_mode(distribution: dist.Distribution, ctx: SamplingContext = None) -> t from simple_einet.layers.distributions.normal import CustomNormal from simple_einet.layers.distributions.binomial import DifferentiableBinomial + from simple_einet.layers.distributions.piecewise_linear import PiecewiseLinearDist if isinstance(distribution, CustomNormal): # Repeat the mode along the batch axis @@ -84,6 +85,8 @@ def dist_mode(distribution: dist.Distribution, ctx: SamplingContext = None) -> t probs = distribution.probs.clone() mode = torch.argmax(probs, dim=-1) return mode.repeat(ctx.num_samples, 1, 1, 1, 1) + elif isinstance(distribution, PiecewiseLinearDist): + return distribution.mpe(num_samples=ctx.num_samples) else: raise Exception(f"MPE not yet implemented for type {type(distribution)}") @@ -101,43 +104,64 @@ def dist_sample(distribution: dist.Distribution, ctx: SamplingContext = None) -> """ # Sample from the specified distribution - if ctx.is_mpe or ctx.mpe_at_leaves: + if (ctx.is_mpe or ctx.mpe_at_leaves) and not ctx.return_leaf_params: samples = dist_mode(distribution, ctx).float() samples = samples.unsqueeze(1) + + # Add empty last dim to make this the same dim as params + samples = samples.unsqueeze(-1) else: from simple_einet.layers.distributions.normal import CustomNormal - if type(distribution) == dist.Normal: - distribution = dist.Normal(loc=distribution.loc, scale=distribution.scale / ctx.temperature_leaves) - elif type(distribution) == CustomNormal: - distribution = CustomNormal(mu=distribution.mu, sigma=distribution.sigma / ctx.temperature_leaves) - elif type(distribution) == dist.Categorical: - distribution = dist.Categorical(logits=F.log_softmax(distribution.logits / ctx.temperature_leaves)) - samples = distribution.sample(sample_shape=(ctx.num_samples,)).float() + if ctx.return_leaf_params: + samples = distribution.get_params() + + # Add batch dimension + samples = samples.unsqueeze(0) + else: + if type(distribution) == dist.Normal: + distribution = dist.Normal(loc=distribution.loc, scale=distribution.scale / ctx.temperature_leaves) + elif type(distribution) == CustomNormal: + distribution = CustomNormal(mu=distribution.mu, sigma=distribution.sigma / ctx.temperature_leaves) + elif type(distribution) == dist.Categorical: + distribution = dist.Categorical(logits=F.log_softmax(distribution.probs / ctx.temperature_leaves)) + + samples = distribution.sample(sample_shape=(ctx.num_samples,)).float() + + # Add empty last dim to make this the same dim as params + samples = samples.unsqueeze(-1) assert ( samples.shape[1] == 1 ), "Something went wrong. First sample size dimension should be size 1 due to the distribution parameter dimensions. Please report this issue." - # if not context.is_differentiable: # This happens only in the non-differentiable context - samples.squeeze_(1) - num_samples, num_channels, num_features, num_leaves, num_repetitions = samples.shape + samples = samples.squeeze(1) + _, num_channels, num_features, num_leaves, num_repetitions, num_params = samples.shape if ctx.is_differentiable: - r_idxs = ctx.indices_repetition.view(num_samples, 1, 1, 1, num_repetitions) - samples = index_one_hot(samples, index=r_idxs, dim=-1) + r_idxs = ctx.indices_repetition.view(-1, 1, 1, 1, num_repetitions, 1) + samples = index_one_hot(samples, index=r_idxs, dim=-2) else: - r_idxs = ctx.indices_repetition.view(-1, 1, 1, 1, 1) - r_idxs = r_idxs.expand(-1, num_channels, num_features, num_leaves, -1) - samples = samples.gather(dim=-1, index=r_idxs) - samples = samples.squeeze(-1) + r_idxs = ctx.indices_repetition.view(-1, 1, 1, 1, 1, 1) + r_idxs = r_idxs.expand(-1, num_channels, num_features, num_leaves, -1, -1) + samples = samples.gather(dim=-2, index=r_idxs) + samples = samples.squeeze(-2) # If parent index into out_channels are given if ctx.indices_out is not None: - # Choose only specific samples for each feature/scope - samples = torch.gather(samples, dim=2, index=ctx.indices_out.unsqueeze(-1)).squeeze(-1) + if ctx.is_differentiable: + p_idxs = ctx.indices_out.unsqueeze(1).unsqueeze(-1) + samples = index_one_hot(samples, index=p_idxs, dim=3) + else: + # Choose only specific samples for each feature/scope + p_idxs = ctx.indices_out.view(-1, 1, num_features, 1, 1) + p_idxs = p_idxs.expand(-1, num_channels, -1, -1, -1) + samples = samples.gather(dim=3, index=p_idxs).squeeze(-1) - return samples + if ctx.return_leaf_params: + return samples + else: + return samples.squeeze(-1) class AbstractLeaf(AbstractLayer, ABC): @@ -232,6 +256,10 @@ def _marginalize_input(self, x: torch.Tensor, marginalized_scopes: List[int]) -> s = marginalized_scopes.div(self.cardinality, rounding_mode="floor") x[:, :, s] = self.marginalization_constant + else: + if torch.any(mask := torch.isnan(x)): + x[mask] = self.marginalization_constant + return x def forward(self, x, marginalized_scopes: List[int]): @@ -284,3 +312,13 @@ def sample(self, ctx: SamplingContext) -> torch.Tensor: def extra_repr(self): return f"num_features={self.num_features}, num_leaves={self.num_leaves}, out_shape={self.out_shape}" + + def get_params(self): + """ + Obtain the parameters of this distribution. + + If the distribution consists of multiple parameters (such as the Normal distribution), the parameters are + stacked in the last dimension. That is, get_params().shape[-1] should indicate the number of parameters this + distribution has (Binomial=1, Normal=2, ...). + """ + raise NotImplementedError("This method should be implemented by the child class.") diff --git a/simple_einet/layers/distributions/bernoulli.py b/simple_einet/layers/distributions/bernoulli.py index 27d4104..7b52e05 100644 --- a/simple_einet/layers/distributions/bernoulli.py +++ b/simple_einet/layers/distributions/bernoulli.py @@ -1,5 +1,4 @@ import torch -from simple_einet.sampling_utils import SamplingContext from torch import distributions as dist from torch import nn @@ -27,6 +26,6 @@ def __init__(self, num_features: int, num_channels: int, num_leaves: int, num_re # Create bernoulli parameters self.probs = nn.Parameter(torch.randn(1, num_channels, num_features, num_leaves, num_repetitions)) - def _get_base_distribution(self, ctx: SamplingContext = None): + def _get_base_distribution(self): # Use sigmoid to ensure, that probs are in valid range return dist.Bernoulli(probs=torch.sigmoid(self.probs)) diff --git a/simple_einet/layers/distributions/binomial.py b/simple_einet/layers/distributions/binomial.py index 97a9af3..70532ec 100644 --- a/simple_einet/layers/distributions/binomial.py +++ b/simple_einet/layers/distributions/binomial.py @@ -1,9 +1,9 @@ from typing import List, Tuple, Union +from torch.distributions.utils import probs_to_logits, logits_to_probs import numpy as np import torch from torch import distributions as dist -from torch.distributions.utils import probs_to_logits, logits_to_probs from torch import nn from simple_einet.layers.distributions.abstract_leaf import ( @@ -46,17 +46,19 @@ def __init__( self.total_count = check_valid(total_count, int, lower_bound=1) # Create binomial parameters as unnormalized log probabilities - p = 0.5 + (torch.rand(1, num_channels, num_features, num_leaves, num_repetitions) - 0.5) * 0.2 self.logits = nn.Parameter(probs_to_logits(p, is_binary=True)) def _get_base_distribution(self, ctx: SamplingContext = None): - # Cast logits to probabilities + # Use sigmoid to ensure, that probs are in valid range + probs = logits_to_probs(self.logits, is_binary=True) if ctx is not None and ctx.is_differentiable: - probs = logits_to_probs(self.logits, is_binary=True) return DifferentiableBinomial(probs=probs, total_count=self.total_count) else: - return dist.Binomial(logits=self.logits, total_count=self.total_count) + return dist.Binomial(probs=probs, total_count=self.total_count) + + def get_params(self): + return self.logits.unsqueeze(-1) class DifferentiableBinomial: @@ -122,6 +124,9 @@ def log_prob(self, x): """ return dist.Binomial(probs=self.probs, total_count=self.total_count).log_prob(x) + def get_params(self): + return self.probs.unsqueeze(-1) + class ConditionalBinomial(AbstractLeaf): """ @@ -170,11 +175,12 @@ def __init__( self.cond_fn = cond_fn self.cond_idxs = cond_idxs - p = 0.5 + (torch.rand(1, num_channels, num_features // 2, num_leaves, num_repetitions) - 0.5) * 0.2 - self.logits_conditioned_base = nn.Parameter(probs_to_logits(p, is_binary=True)) - - p = 0.5 + (torch.rand(1, num_channels, num_features // 2, num_leaves, num_repetitions) - 0.5) * 0.2 - self.logits_unconditioned = nn.Parameter(probs_to_logits(p, is_binary=True)) + self.probs_conditioned_base = nn.Parameter( + 0.5 + torch.rand(1, num_channels, num_features // 2, num_leaves, num_repetitions) * 0.1 + ) + self.probs_unconditioned = nn.Parameter( + 0.5 + torch.rand(1, num_channels, num_features // 2, num_leaves, num_repetitions) * 0.1 + ) def get_conditioned_distribution(self, x_cond: torch.Tensor): """ @@ -192,22 +198,22 @@ def get_conditioned_distribution(self, x_cond: torch.Tensor): x_cond_shape = x_cond.shape # Get conditioned parameters - logits_cond = self.cond_fn(x_cond.view(-1, x_cond.shape[1], hw, hw)) - logits_cond = logits_cond.view( + probs_cond = self.cond_fn(x_cond.view(-1, x_cond.shape[1], hw, hw)) + probs_cond = probs_cond.view( x_cond_shape[0], x_cond_shape[1], self.num_leaves, self.num_repetitions, hw * hw, ) - logits_cond = logits_cond.permute(0, 1, 4, 2, 3) + probs_cond = probs_cond.permute(0, 1, 4, 2, 3) - # Add conditioned parameters as "correction" to default parameters - logits_cond = self.logits_conditioned_base + logits_cond + # Add conditioned parameters to default parameters + probs_cond = self.probs_conditioned_base + probs_cond - logits_unc = self.logits_unconditioned.expand(x_cond.shape[0], -1, -1, -1, -1) - logits = torch.cat((logits_cond, logits_unc), dim=2) - d = dist.Binomial(self.total_count, logits=logits) + probs_unc = self.probs_unconditioned.expand(x_cond.shape[0], -1, -1, -1, -1) + probs = torch.cat((probs_cond, probs_unc), dim=2) + d = dist.Binomial(self.total_count, logits=probs) return d def forward(self, x, marginalized_scopes: List[int]): diff --git a/simple_einet/layers/distributions/categorical.py b/simple_einet/layers/distributions/categorical.py index 33e6b9d..6535ca5 100644 --- a/simple_einet/layers/distributions/categorical.py +++ b/simple_einet/layers/distributions/categorical.py @@ -1,5 +1,4 @@ import torch -from torch.distributions.utils import probs_to_logits from torch import distributions as dist from torch import nn from torch.nn import functional as F @@ -28,9 +27,11 @@ def __init__(self, num_features: int, num_channels: int, num_leaves: int, num_re super().__init__(num_features, num_channels, num_leaves, num_repetitions) # Create logits - p = 0.5 + (torch.rand(1, num_channels, num_features, num_leaves, num_repetitions, num_bins) - 0.5) * 0.2 - self.logits = nn.Parameter(probs_to_logits(p)) + self.logits = nn.Parameter(torch.randn(1, num_channels, num_features, num_leaves, num_repetitions, num_bins)) def _get_base_distribution(self, ctx: SamplingContext = None): # Use sigmoid to ensure, that probs are in valid range return dist.Categorical(logits=F.log_softmax(self.logits, dim=-1)) + + def get_params(self): + return self.logits diff --git a/simple_einet/layers/distributions/multidistribution.py b/simple_einet/layers/distributions/multidistribution.py index ff433f0..5eefab4 100644 --- a/simple_einet/layers/distributions/multidistribution.py +++ b/simple_einet/layers/distributions/multidistribution.py @@ -91,11 +91,26 @@ def forward(self, x, marginalized_scopes: List[int] = None): def sample(self, ctx: SamplingContext) -> torch.Tensor: all_samples = [] + indices_out = ctx.indices_out for scope, dist in zip(self.scopes, self.dists): + if ctx.indices_out is not None: + ctx.indices_out = indices_out[:, scope] samples = dist.sample(ctx) all_samples.append(samples) - samples = torch.cat(all_samples, dim=2) + if ctx.return_leaf_params: + # Same code as in get_params() -- TODO: Refactor to reuse code + params = all_samples + max_num_params = max([p.shape[-1] for p in params]) + for i, p in enumerate(params): + if p.shape[-1] < max_num_params: + # Pad with zeros + new_shape = list(p.shape) + new_shape[-1] = max_num_params - p.shape[-1] + params[i] = torch.cat([p, torch.zeros(new_shape, device=p.device, dtype=p.dtype)], dim=-1) + samples = torch.cat(params, dim=2) + else: + samples = torch.cat(all_samples, dim=2) # If inversion is necessary, permute features to obtain the original order if self.needs_inversion: @@ -105,3 +120,23 @@ def sample(self, ctx: SamplingContext) -> torch.Tensor: def _get_base_distribution(self) -> dist.Distribution: raise NotImplementedError("MultiDistributionLayer does not implement _get_base_distribution.") + + def get_params(self): + """ + Collect params from all distributions and concatenate them along the feature dimension. + + Note: If the number of parameters of the distributions is not equal, the distributions with fewer parameters + are padded with zeros. That is, get_params().shape[-1] should contain the different paramters of the + distribution (mu, sigma) for a Normal. In the case of a MultiDistribution of a Bernoulli (a single paramter: p), + and a Normal (two parameters: mu, sigma) this will lead to the Bernoulli parameters being padded to (p, 0) + in the last dimension. + """ + params = [d.get_params() for d in self.dists] + max_num_params = max([p.shape[-1] for p in params]) + for i, p in enumerate(params): + if p.shape[-1] < max_num_params: + # Pad with zeros + new_shape = list(p.shape) + new_shape[-1] = max_num_params - p.shape[-1] + params[i] = torch.cat([p, torch.zeros(new_shape, device=p.device, dtype=p.dtype)], dim=-1) + return torch.cat(params, dim=2) diff --git a/simple_einet/layers/distributions/multivariate_normal.py b/simple_einet/layers/distributions/multivariate_normal.py index a7dab43..6db1c63 100644 --- a/simple_einet/layers/distributions/multivariate_normal.py +++ b/simple_einet/layers/distributions/multivariate_normal.py @@ -10,6 +10,8 @@ from simple_einet.sampling_utils import SamplingContext from simple_einet.type_checks import check_valid +from icecream import ic + class MultivariateNormal(AbstractLeaf): """Multivariate Gaussian layer.""" @@ -62,12 +64,11 @@ def scale_tril(self): L_full = torch.diag_embed(L_diag) + L_offdiag # Construct full lower triangular matrix return L_full - def _get_base_distribution(self, ctx: SamplingContext = None, marginalized_scopes = None): + def _get_base_distribution(self, ctx: SamplingContext = None, marginalized_scopes=None): # View means and scale_tril means = self.means.view(self._num_dists, self.cardinality) scale_tril = self.scale_tril.view(self._num_dists, self.cardinality, self.cardinality) - mv = CustomMultivariateNormalDist( mean=means, scale_tril=scale_tril, @@ -187,5 +188,3 @@ def mpe(self, num_samples) -> torch.Tensor: num_samples, self.num_channels, self.num_features, self.num_leaves, self.num_repetitions ) return samples - - diff --git a/simple_einet/layers/distributions/normal.py b/simple_einet/layers/distributions/normal.py index 57efbd2..5a8273b 100644 --- a/simple_einet/layers/distributions/normal.py +++ b/simple_einet/layers/distributions/normal.py @@ -32,10 +32,14 @@ def __init__( # Create gaussian means and stds self.means = nn.Parameter(torch.randn(1, num_channels, num_features, num_leaves, num_repetitions)) - self.log_stds = nn.Parameter(torch.rand(1, num_channels, num_features, num_leaves, num_repetitions)) + self.logvar = nn.Parameter(torch.randn(1, num_channels, num_features, num_leaves, num_repetitions)) def _get_base_distribution(self, ctx: SamplingContext = None): - return dist.Normal(loc=self.means, scale=self.log_stds.exp()) + # Use custom normal instead of PyTorch distribution + return CustomNormal(mu=self.means, sigma=torch.exp(0.5 * self.logvar)) + + def get_params(self): + return torch.stack([self.means, self.logvar], dim=-1) class RatNormal(AbstractLeaf): @@ -88,27 +92,36 @@ def __init__( self.max_mean = check_valid(max_mean, float, min_mean, allow_none=True) def _get_base_distribution(self, ctx: SamplingContext = None) -> "CustomNormal": + means, sigma = self._project_params() + + # d = dist.Normal(means, sigma) + d = CustomNormal(means, sigma) + return d + + def _project_params(self): if self.min_sigma < self.max_sigma: sigma_ratio = torch.sigmoid(self.stds) sigma = self.min_sigma + (self.max_sigma - self.min_sigma) * sigma_ratio else: sigma = 1.0 - means = self.means if self.max_mean: assert self.min_mean is not None mean_range = self.max_mean - self.min_mean means = torch.sigmoid(self.means) * mean_range + self.min_mean + return means, sigma - # d = dist.Normal(means, sigma) - d = CustomNormal(means, sigma) - return d + def get_params(self): + means, sigma = self._project_params() + return torch.stack([means, sigma], dim=-1) class CustomNormal: """ A custom implementation of the Normal distribution. + Sampling from this distribution is differentiable. + This class allows to sample from a Normal distribution with mean `mu` and standard deviation `sigma`. The `sample` method returns a tensor of samples from the distribution, with shape `sample_shape + mu.shape`. The `log_prob` method returns the log probability density/mass function evaluated at `x`. @@ -160,3 +173,6 @@ def log_prob(self, x): torch.Tensor: The log probability density of the normal distribution at the given value(s). """ return dist.Normal(self.mu, self.sigma).log_prob(x) + + def get_params(self): + return torch.stack([self.mu, self.sigma.log() * 2], dim=-1) diff --git a/simple_einet/layers/factorized_leaf.py b/simple_einet/layers/factorized_leaf.py index 8db2a59..bfffc05 100644 --- a/simple_einet/layers/factorized_leaf.py +++ b/simple_einet/layers/factorized_leaf.py @@ -83,6 +83,9 @@ def forward(self, x: torch.Tensor, marginalized_scopes: List[int]): # Factorize input channels x = x.sum(dim=1) + if self.num_features == self.num_features_out: + return x + # Merge scopes by naive factorization x = torch.einsum("bicr,ior->bocr", x, self.scopes) @@ -111,8 +114,19 @@ def sample(self, ctx: SamplingContext) -> torch.Tensor: # are not filtered in the base_leaf sampling procedure indices_out = ctx.indices_out ctx.indices_out = None - samples = self.base_leaf.sample(ctx=ctx) + # If return_leaf_params is True, we return the parameters of the leaf distribution + # instead of the samples themselves + if ctx.return_leaf_params: + params = self.base_leaf.get_params() + params = self._index_leaf_params(ctx, indices_out, params=params) + return params + else: + samples = self.base_leaf.sample(ctx) + samples = self._index_leaf_samples(ctx, indices_out, samples) + return samples + + def _index_leaf_samples(self, ctx, indices_out, samples): # Check that shapes match as expected assert samples.shape == ( ctx.num_samples, @@ -120,7 +134,6 @@ def sample(self, ctx: SamplingContext) -> torch.Tensor: self.base_leaf.num_features, self.base_leaf.num_leaves, ) - if ctx.is_differentiable: # Select the correct repetitions scopes = self.scopes.unsqueeze(0) # make space for batch dim @@ -138,12 +151,266 @@ def sample(self, ctx: SamplingContext) -> torch.Tensor: indices_in_gather = indices_out.gather(dim=1, index=scopes) indices_in_gather = indices_in_gather.view(ctx.num_samples, 1, -1, 1) - indices_in_gather = indices_in_gather.expand(-1, samples.shape[1], -1, -1) + indices_in_gather = indices_in_gather.expand(-1, self.base_leaf.num_channels, -1, -1) indices_in_gather = indices_in_gather.repeat(1, 1, self.base_leaf.cardinality, 1) samples = samples.gather(dim=-1, index=indices_in_gather) samples.squeeze_(-1) # Remove num_leaves dimension + return samples + + def _index_leaf_params(self, ctx, indices_out, params): + """ + Same as _index_leaf_samples, but indexes the parameters of the leaf distribution instead of the samples. + """ + num_params = params.shape[-1] # Number of parameters, e.g. 2 for Normal (mu and sigma) + if ctx.is_differentiable: + r_idxs = ctx.indices_repetition.view(ctx.num_samples, 1, 1, 1, ctx.num_repetitions, 1) + params = index_one_hot(params, index=r_idxs, dim=-2) # -2 is num_repetitions dim + + # Select the correct repetitions + scopes = self.scopes.unsqueeze(0) # make space for batch dim + r_idx = ctx.indices_repetition.view(ctx.num_samples, 1, 1, -1) + scopes = index_one_hot(scopes, index=r_idx, dim=-1) + + indices_in = index_one_hot(indices_out.unsqueeze(1), index=scopes.unsqueeze(-1), dim=2) + indices_in = indices_in.view( + ctx.num_samples, 1, self.num_features, self.base_leaf.num_leaves, 1 + ) # make space for channel dim + params = index_one_hot(params, index=indices_in, dim=-2) # -2 is num_leaves dim + else: + # Filter for repetition + r_idxs = ctx.indices_repetition.view(-1, 1, 1, 1, 1, 1) + r_idxs = r_idxs.expand( + -1, self.base_leaf.num_channels, self.num_features, self.base_leaf.num_leaves, -1, num_params + ) + params = params.expand(ctx.num_samples, -1, -1, -1, -1, -1) + params = params.gather(dim=-2, index=r_idxs) # Repetition dim is -2, (-1 is param stack dim) + params = params.squeeze(-2) # Remove repetition dim + # params is now [batch_size, num_channels, num_features, num_leaves, num_params] + + # Select the correct repetitions + scopes = self.scopes[..., ctx.indices_repetition].permute(2, 0, 1) + rnge_in = torch.arange(self.num_features_out, device=params.device) + scopes = (scopes * rnge_in).sum(-1).long() + indices_in_gather = indices_out.gather(dim=1, index=scopes) + indices_in_gather = indices_in_gather.view(ctx.num_samples, 1, -1, 1, 1) + indices_in_gather = indices_in_gather.expand(-1, self.base_leaf.num_channels, -1, -1, num_params) + indices_in_gather = indices_in_gather.repeat(1, 1, self.base_leaf.cardinality, 1, 1) + # indices_in_gather: [batch_size, num_channels, num_features, 1] (last dim is index into num_leaves) + params = params.gather(dim=-2, index=indices_in_gather) # -2 is num_leaves dim + params.squeeze_(-2) # Remove num_leaves dimension + assert params.shape == (ctx.num_samples, self.base_leaf.num_channels, self.num_features, num_params) + return params + + def extra_repr(self): + return f"num_features={self.num_features}, num_features_out={self.num_features_out}" + + +class FactorizedLeafSimple(AbstractLayer): + """ + A 'meta'-leaf layer that combines multiple scopes of a base-leaf layer via naive factorization. + + Attributes: + num_features (int): Number of input features. + num_features_out (int): Number of output features. + num_repetitions (int): Number of repetitions. + base_leaf (AbstractLeaf): The base leaf layer. + scopes (torch.Tensor): The scopes of the factorized groups of RVs. + """ + + def __init__( + self, + num_features: int, + num_features_out: int, + num_repetitions, + base_leaf: AbstractLeaf, + ): + """ + Args: + num_features (int): Number of input features. + num_features_out (int): Number of output features. + num_repetitions (int): Number of repetitions. + base_leaf (AbstractLeaf): The base leaf layer. + """ + + super().__init__(num_features, num_repetitions=num_repetitions) + assert ( + num_repetitions == 1 + ), f"FactorizedLeafSimple only supports num_repetitions=1 but was given num_repetitions={num_repetitions}" + + self.base_leaf = base_leaf + self.num_features_out = num_features_out + + # Size of the factorized groups of RVs + self.cardinality = int(np.ceil(self.num_features / self.num_features_out)) + + # Compute number of dummy nodes that need to be padded + self.num_dummy_nodes = self.cardinality * self.num_features_out - self.num_features + + # Idea: pad input with "rest" number of dummy nodes + permutation = torch.randperm(n=self.num_features + self.num_dummy_nodes) + self.register_buffer("permutation", permutation) + + # Invert permutation + self.register_buffer("inverse_permutation", torch.argsort(permutation)) + + def forward(self, x: torch.Tensor, marginalized_scopes: List[int]): + """ + Forward pass through the factorized leaf layer. + + Args: + x (torch.Tensor): Input tensor of shape (batch_size, num_input_channels, num_leaves, num_repetitions). + marginalized_scopes (List[int]): List of integers representing the marginalized scopes. + + Returns: + torch.Tensor: Output tensor of shape (batch_size, num_output_channels, num_leaves, num_repetitions). + """ + # Forward through base leaf + x = self.base_leaf(x, marginalized_scopes) + + # Factorize input channels + x = x.sum(dim=1) + + # Pad with dummy nodes + if self.num_dummy_nodes > 0: + x = torch.cat( + [x, torch.zeros(x.shape[0], self.num_dummy_nodes, x.shape[-2], x.shape[-1], device=x.device)], dim=1 + ) + + # Apply permutation + x = x[:, self.permutation] + + # Fold into "num_features_out" groups + x = x.view(x.shape[0], self.num_features_out, self.cardinality, x.shape[-2], x.shape[-1]) + + # Sum over the groups + x = x.sum(dim=2) + assert x.shape == ( + x.shape[0], + self.num_features_out, + self.base_leaf.num_leaves, + self.num_repetitions, + ) + return x + + def sample(self, ctx: SamplingContext) -> torch.Tensor: + """ + Samples the factorized leaf layer by generating `context.num_samples` samples from the base leaf layer, + and then mapping them to the factorized leaf layer using the indices specified in the `context` + argument. If `context.is_differentiable` is True, the mapping is done using one-hot indexing. + + Args: + ctx (SamplingContext, optional): The sampling context to use. Defaults to None. + + Returns: + torch.Tensor: A tensor of shape `(context.num_samples, self.num_features_out, self.num_leaves)`, + representing the samples generated from the factorized leaf layer. + """ + # Save original indices_out and set context indices_out to none, such that the out_channel + # are not filtered in the base_leaf sampling procedure + indices_out = ctx.indices_out + ctx.indices_out = None + + # If return_leaf_params is True, we return the parameters of the leaf distribution + # instead of the samples themselves + if ctx.return_leaf_params: + params = self.base_leaf.get_params() + params = self._index_leaf_params(ctx, indices_out, params=params) + return params + else: + samples = self.base_leaf.sample(ctx) + samples = self._index_leaf_samples(ctx, indices_out, samples) + return samples + + def _index_leaf_samples(self, ctx, indices_out, samples): + # Check that shapes match as expected + assert samples.shape == ( + ctx.num_samples, + self.base_leaf.num_channels, + self.base_leaf.num_features, + self.base_leaf.num_leaves, + ) + if ctx.is_differentiable: + + # Unfold into "num_features" + "num_dummy_nodes" by repetition + indices_out = indices_out.unsqueeze(1) + indices_out = indices_out.expand(-1, self.cardinality, -1, -1) + indices_out = indices_out.reshape( + indices_out.shape[0], self.num_features + self.num_dummy_nodes, indices_out.shape[-1] + ) + + # Invert permutation + indices_out = indices_out[:, self.inverse_permutation] + + # Remove dummy nodes + if self.num_dummy_nodes > 0: + indices_out = indices_out[:, : -self.num_dummy_nodes] + + indices_out = indices_out.unsqueeze(1) # make space for channel dim + samples = index_one_hot(samples, index=indices_out, dim=-1) + else: + # Unfold into "num_features" + "num_dummy_nodes" by repetition + indices_out = indices_out.unsqueeze(1).unsqueeze(1) + indices_out = indices_out.expand(-1, self.base_leaf.num_channels, self.cardinality, -1) + indices_out = indices_out.reshape(indices_out.shape[0], self.base_leaf.num_channels, self.num_features + self.num_dummy_nodes) + + # Invert permutation + indices_out = indices_out[:, :, self.inverse_permutation] + + # Remove dummy nodes + if self.num_dummy_nodes > 0: + indices_out = indices_out[:, :, : -self.num_dummy_nodes] + + indices_out = indices_out.unsqueeze(-1) + samples = samples.gather(index=indices_out, dim=-1) + samples = samples.squeeze(-1) return samples + def _index_leaf_params(self, ctx, indices_out, params): + """ + Same as _index_leaf_samples, but indexes the parameters of the leaf distribution instead of the samples. + """ + num_params = params.shape[-1] # Number of parameters, e.g. 2 for Normal (mu and sigma) + if ctx.is_differentiable: + # Unfold into "num_features" + "num_dummy_nodes" by repetition + indices_out = indices_out.unsqueeze(1) + indices_out = indices_out.expand(-1, self.cardinality, -1, -1) + indices_out = indices_out.reshape( + indices_out.shape[0], self.num_features + self.num_dummy_nodes, indices_out.shape[-1] + ) + + # Invert permutation + indices_out = indices_out[:, self.inverse_permutation] + + # Remove dummy nodes + if self.num_dummy_nodes > 0: + indices_out = indices_out[:, : -self.num_dummy_nodes] + + indices_out = indices_out.unsqueeze(-1) + params = params.squeeze(-2) # remove repetition index + indices_out = indices_out.unsqueeze(-1) # make space for num_channels dim + params = index_one_hot(params, index=indices_out, dim=-2) + else: + # Unfold into "num_features" + "num_dummy_nodes" by repetition + indices_out = indices_out.unsqueeze(1).unsqueeze(1) + indices_out = indices_out.expand(-1, self.base_leaf.num_channels, self.cardinality, -1) + indices_out = indices_out.reshape(indices_out.shape[0], self.base_leaf.num_channels, self.num_features + self.num_dummy_nodes) + + # Invert permutation + indices_out = indices_out[:, :, self.inverse_permutation] + + # Remove dummy nodes + if self.num_dummy_nodes > 0: + indices_out = indices_out[:, :, : -self.num_dummy_nodes] + + indices_out = indices_out.unsqueeze(-1).unsqueeze(-1) + indices_out = indices_out.expand(-1, -1, -1, -1, num_params) + params = params.squeeze(-2) # remove repetition index + params = params.expand(ctx.num_samples, -1, -1, -1, -1) + params = params.gather(index=indices_out, dim=-2) + params = params.squeeze(-2) # Remove num_leaves dimension + assert params.shape == (ctx.num_samples, self.base_leaf.num_channels, self.num_features, num_params) + return params + def extra_repr(self): return f"num_features={self.num_features}, num_features_out={self.num_features_out}" diff --git a/simple_einet/layers/linsum.py b/simple_einet/layers/linsum.py index 6282935..ecd47ac 100644 --- a/simple_einet/layers/linsum.py +++ b/simple_einet/layers/linsum.py @@ -1,10 +1,8 @@ from typing import Tuple -import numpy as np import torch from simple_einet.abstract_layers import AbstractSumLayer, logits_to_log_weights -from simple_einet.layers.einsum import logsumexp from simple_einet.sampling_utils import ( index_one_hot, sample_categorical_differentiably, @@ -26,6 +24,7 @@ def __init__( num_sums_out: int, num_repetitions: int = 1, dropout: float = 0.0, + **kwargs, ): """ Initializes a LinsumLayer instance. @@ -37,21 +36,27 @@ def __init__( num_repetitions (int, optional): The number of times to repeat the layer. Defaults to 1. dropout (float, optional): The dropout probability. Defaults to 0.0. """ + + # Number of features to be padded (assign this before the super().__init__ call, so it can be used in the + # super initializer) + self._pad = num_features % LinsumLayer.cardinality + super().__init__( num_features=num_features, num_sums_in=num_sums_in, num_sums_out=num_sums_out, num_repetitions=num_repetitions, dropout=dropout, + **kwargs, ) - assert self.num_features % LinsumLayer.cardinality == 0, "num_features must be a multiple of cardinality" + # assert self.num_features % LinsumLayer.cardinality == 0, "num_features must be a multiple of cardinality" self.out_shape = f"(N, {self.num_features_out}, {self.num_sums_out}, {self.num_repetitions})" @property def num_features_out(self) -> int: - return self.num_features // LinsumLayer.cardinality + return (self.num_features + self._pad) // LinsumLayer.cardinality def weight_shape(self) -> Tuple[int, ...]: return self.num_features_out, self.num_sums_in, self.num_sums_out, self.num_repetitions @@ -75,6 +80,11 @@ def forward(self, x: torch.Tensor): left = x[:, 0::2] right = x[:, 1::2] + + if self._pad > 0: + # Add dummy marginalized RVs + right = torch.cat([right, torch.zeros_like(right[:, : self._pad])], dim=1) + prod_output = (left + right).unsqueeze(3) # N x D/2 x Sin x 1 x R # Apply dropout: Set random sum node children to 0 (-inf in log domain) @@ -117,6 +127,12 @@ def _sample_from_weights(self, ctx, log_weights): indices = indices.repeat_interleave(2, dim=1) indices = indices.view(ctx.num_samples, -1) + + + if self._pad > 0: + # Cut off dummy marginalized RVs + indices = indices[:, : -self._pad] + return indices def _condition_weights_on_evidence(self, ctx, log_weights): @@ -135,7 +151,7 @@ def _condition_weights_on_evidence(self, ctx, log_weights): lls_left = input_cache_left.gather(index=r_idxs, dim=-1).squeeze(-1) lls_right = input_cache_right.gather(index=r_idxs, dim=-1).squeeze(-1) lls = (lls_left + lls_right).view(ctx.num_samples, self.num_features_out, self.num_sums_in) - log_prior = log_weights + log_prior = log_weights # Shape: [batch, num_features_out, num_sums_in] log_posterior = log_prior + lls log_posterior = log_posterior - torch.logsumexp(log_posterior, dim=2, keepdim=True) log_weights = log_posterior @@ -166,6 +182,7 @@ def _select_weights(self, ctx, logits): logits = logits.expand(ctx.num_samples, -1, -1, -1, -1) p_idxs = ctx.indices_out[..., None, None, None] # make space for repetition dim p_idxs = p_idxs.expand(-1, -1, self.num_sums_in, -1, self.num_repetitions) + logits = logits.gather(dim=3, index=p_idxs) # index out_channels logits = logits.squeeze(3) # squeeze out_channels dimension (is 1 at this point) @@ -193,3 +210,200 @@ def extra_repr(self): self.weight_shape(), ) ) + + +class LinsumLayer2(AbstractSumLayer): + """ + Similar to Einsum but with a linear combination of the input channels for each output channel compared to + the cross-product combination that is applied in an EinsumLayer. + """ + + cardinality = 2 # Cardinality of the layer + + def __init__( + self, + num_features: int, + num_sums_in: int, + num_sums_out: int, + num_repetitions: int = 1, + dropout: float = 0.0, + **kwargs, + ): + """ + Initializes a LinsumLayer instance. + + Args: + num_features (int): The number of input features. + num_sums_in (int): The number of input sums. + num_sums_out (int): The number of output sums. + num_repetitions (int, optional): The number of times to repeat the layer. Defaults to 1. + dropout (float, optional): The dropout probability. Defaults to 0.0. + """ + super().__init__( + num_features=num_features, + num_sums_in=num_sums_in, + num_sums_out=num_sums_out, + num_repetitions=num_repetitions, + dropout=dropout, + **kwargs, + ) + + self._pad = self.num_features % LinsumLayer2.cardinality + self.out_shape = f"(N, {self.num_features_out}, {self.num_sums_out}, {self.num_repetitions})" + + @property + def num_features_out(self) -> int: + return (self.num_features + self._pad) // LinsumLayer2.cardinality + + def weight_shape(self) -> Tuple[int, ...]: + return self.num_features, self.num_sums_in, self.num_sums_out, self.num_repetitions + + def forward(self, x: torch.Tensor): + """ + Einsum layer forward pass. + + Args: + x: Input of shape [batch, in_features, num_sums_in, num_repetitions]. + + Returns: + torch.Tensor: Output of shape [batch, ceil(in_features/2), channel * channel]. + """ + # Save input if input cache is enabled + if self._is_input_cache_enabled: + self._input_cache["x"] = x + + # Get log weights + log_weights = logits_to_log_weights(self.logits, dim=1).unsqueeze(0) + x = x.unsqueeze(3) + sum_output = torch.logsumexp(x + log_weights, dim=2) # N x D x Sout x R + + # Get left and right partition probs + left = sum_output[:, 0::2] + right = sum_output[:, 1::2] + + if self._pad > 0: + # Add dummy marginalized RVs + right = torch.cat([right, torch.zeros_like(right[:, : self._pad])], dim=1) + + prod_output = left + right # N x D/2 x Sout x 1 x R + + # Apply dropout: Set random sum node children to 0 (-inf in log domain) + if self.dropout > 0.0 and self.training: + dropout_indices = self._bernoulli_dist.sample(prod_output.shape) + invalid_index = dropout_indices.sum(2) == dropout_indices.shape[2] + while invalid_index.any(): + # Resample only invalid indices + dropout_indices[invalid_index] = self._bernoulli_dist.sample(dropout_indices[invalid_index].shape) + invalid_index = dropout_indices.sum(2) == dropout_indices.shape[2] + dropout_indices = torch.log(1 - dropout_indices) + prod_output = prod_output + dropout_indices + + return prod_output + + def _sample_from_weights(self, ctx, log_weights): + if ctx.is_differentiable: # Differentiable sampling + indices = sample_categorical_differentiably( + dim=-1, is_mpe=ctx.is_mpe, hard=ctx.hard, tau=ctx.tau, log_weights=log_weights + ) + indices = indices.view(ctx.num_samples, -1, self.num_sums_in) + + else: # Non-differentiable sampling + if ctx.is_mpe: + indices = log_weights.argmax(dim=2) + else: + # Create categorical distribution to sample from + dist = torch.distributions.Categorical(logits=log_weights) + indices = dist.sample() + + indices = indices.view(ctx.num_samples, -1) + + + if self._pad > 0: + # Cut off dummy marginalized RVs + indices = indices[:, : -self._pad] + + return indices + + def _condition_weights_on_evidence(self, ctx, log_weights): + # Extract input cache + input_cache = self._input_cache["x"] + + # Index repetition + if ctx.is_differentiable: + r_idxs = ctx.indices_repetition.view(ctx.num_samples, 1, 1, self.num_repetitions) + lls = index_one_hot(input_cache, index=r_idxs, dim=-1) + else: + r_idxs = ctx.indices_repetition[..., None, None, None] + r_idxs = r_idxs.expand(-1, self.num_features, self.num_sums_in, -1) + lls = input_cache.gather(index=r_idxs, dim=-1).squeeze(-1) + log_prior = log_weights # Shape: [batch, num_features_out, num_sums_in] + log_posterior = log_prior + lls + log_posterior = log_posterior - torch.logsumexp(log_posterior, dim=2, keepdim=True) + log_weights = log_posterior + return log_weights + + def _select_weights(self, ctx, logits): + if ctx.is_differentiable: + # Index sums_out + logits = logits.unsqueeze(0) # make space for batch dim + p_idxs = ctx.indices_out.repeat_interleave(2, dim=1) + + if self._pad > 0: + # Cut off dummy marginalized RVs + p_idxs = p_idxs[:, : -self._pad] + + p_idxs = p_idxs.unsqueeze(2).unsqueeze(-1) + + # Index into the "num_sums_out" dimension + logits = index_one_hot(logits, index=p_idxs, dim=3) + assert logits.shape == ( + ctx.num_samples, + self.num_features, + self.num_sums_in, + self.num_repetitions, + ) + + # Index repetition + r_idxs = ctx.indices_repetition.view(ctx.num_samples, 1, 1, self.num_repetitions) + logits = index_one_hot(logits, index=r_idxs, dim=3) + + else: + # Index sums_out + logits = logits.unsqueeze(0) # make space for batch dim + logits = logits.expand(ctx.num_samples, -1, -1, -1, -1) + + p_idxs = ctx.indices_out.repeat_interleave(2, dim=1) + + if self._pad > 0: + # Cut off dummy marginalized RVs + p_idxs = p_idxs[:, : -self._pad] + + p_idxs = p_idxs[..., None, None, None] # make space for repetition dim + p_idxs = p_idxs.expand(-1, -1, self.num_sums_in, -1, self.num_repetitions) + logits = logits.gather(dim=3, index=p_idxs) # index out_channels + logits = logits.squeeze(3) # squeeze out_channels dimension (is 1 at this point) + + # Index repetitions + r_idxs = ctx.indices_repetition[..., None, None, None] + r_idxs = r_idxs.expand(-1, self.num_features, self.num_sums_in, -1) + logits = logits.gather(dim=3, index=r_idxs) + logits = logits.squeeze(3) + # Check dimensions + assert logits.shape == (ctx.num_samples, self.num_features, self.num_sums_in) + + # Project logits to log weights + log_weights = logits_to_log_weights(logits, dim=2, temperature=ctx.temperature_sums) + return log_weights + + def extra_repr(self): + return ( + "num_features={}, num_sums_in={}, num_sums_out={}, num_repetitions={}, out_shape={}, " + "weight_shape={}".format( + self.num_features, + self.num_sums_in, + self.num_sums_out, + self.num_repetitions, + self.out_shape, + self.weight_shape(), + ) + ) diff --git a/simple_einet/layers/product.py b/simple_einet/layers/product.py index 36c7c64..24d127b 100644 --- a/simple_einet/layers/product.py +++ b/simple_einet/layers/product.py @@ -13,6 +13,22 @@ logger = logging.getLogger(__name__) +class RootProductLayer(AbstractLayer): + def __init__(self, num_features: int, num_repetitions: int): + super().__init__(num_features, num_repetitions) + self.out_shape = f"(N, {self.num_features}, in_channels, {self.num_repetitions})" + + def forward(self, x: torch.Tensor): + assert x.size(1) == self.num_features + return x.sum(dim=1, keepdim=True) + + def sample(self, ctx: SamplingContext) -> SamplingContext: + shape = [1] * ctx.indices_out.dim() + shape[1] = self.num_features + ctx.indices_out = ctx.indices_out.repeat(*shape) + return ctx + + class ProductLayer(AbstractLayer): """ Product Node Layer that chooses k scopes as children for a product node. diff --git a/simple_einet/sampling_utils.py b/simple_einet/sampling_utils.py index 845302b..e628c55 100644 --- a/simple_einet/sampling_utils.py +++ b/simple_einet/sampling_utils.py @@ -6,6 +6,7 @@ import torch from torch import nn from torch.nn import functional as F +from tqdm import tqdm from simple_einet.utils import __HAS_EINSUM_BROADCASTING @@ -109,12 +110,18 @@ class SamplingContext: # Do MPE at leaves mpe_at_leaves: bool = False + # Return leaf distribution instead of samples + return_leaf_params: bool = False + def __setattr__(self, key, value): if hasattr(self, key): super().__setattr__(key, value) else: raise AttributeError(f"SamplingContext object has no attribute {key}") + def __repr__(self) -> str: + return f"SamplingContext(num_samples={self.num_samples}, indices_out={self.indices_out.shape}, indices_repetition={self.indices_repetition.shape}, is_mpe={self.is_mpe}, temperature_leaves={self.temperature_leaves}, temperature_sums={self.temperature_sums}, num_repetitions={self.num_repetitions}, evidence={self.evidence.shape if self.evidence else None}, is_differentiable={self.is_differentiable}, hard={self.hard}, tau={self.tau}, mpe_at_leaves={self.mpe_at_leaves}, return_leaf_params={self.return_leaf_params})" + def get_context(differentiable): """ @@ -203,7 +210,7 @@ def sample_categorical_differentiably( tau: float, logits: torch.Tensor = None, log_weights: torch.Tensor = None, - method=DiffSampleMethod.GUMBEL, + method=DiffSampleMethod.SIMPLE, ) -> torch.Tensor: """ Perform differentiable sampling/mpe on the given input along a specific dimension. @@ -299,23 +306,21 @@ def init_einet_stats(einet: "Einet", dataloader: torch.utils.data.DataLoader): Returns: None """ stats_mean = None - stats_std = None + stats_var = None # Compute mean and std - from tqdm import tqdm - for batch in tqdm(dataloader, desc="Leaf Parameter Initialization"): data, label = batch if stats_mean == None: stats_mean = data.mean(dim=0) - stats_std = data.std(dim=0) + stats_var = data.var(dim=0) else: stats_mean += data.mean(dim=0) - stats_std += data.std(dim=0) + stats_var += data.var(dim=0) # Normalize stats_mean /= len(dataloader) - stats_std /= len(dataloader) + stats_var /= len(dataloader) from simple_einet.layers.distributions.normal import Normal from simple_einet.einet import Einet @@ -336,10 +341,10 @@ def init_einet_stats(einet: "Einet", dataloader: torch.utils.data.DataLoader): .repeat(1, einets[0].config.num_leaves, einets[0].config.num_repetitions) .view_as(einets[0].leaf.base_leaf.means) ) - stats_std_v = ( - stats_std.view(-1, 1, 1) + stats_var_v = ( + stats_var.view(-1, 1, 1) .repeat(1, einets[0].config.num_leaves, einets[0].config.num_repetitions) - .view_as(einets[0].leaf.base_leaf.log_stds) + .view_as(einets[0].leaf.base_leaf.logvar) ) # Set leaf parameters @@ -348,8 +353,8 @@ def init_einet_stats(einet: "Einet", dataloader: torch.utils.data.DataLoader): net.leaf.base_leaf.means.data = stats_mean_v + 0.1 * torch.normal( torch.zeros_like(stats_mean_v), torch.std(stats_mean_v) ) - net.leaf.base_leaf.log_stds.data = torch.log( - stats_std_v + net.leaf.base_leaf.logvar.data = torch.log( + stats_var_v + 1e-3 - + torch.clamp(0.1 * torch.normal(torch.zeros_like(stats_std_v), torch.std(stats_std_v)), min=0.0) + + torch.clamp(0.1 * torch.normal(torch.zeros_like(stats_var_v), torch.std(stats_var_v)), min=0.0) ) diff --git a/simple_einet/utils.py b/simple_einet/utils.py index 0b87e70..3c28741 100644 --- a/simple_einet/utils.py +++ b/simple_einet/utils.py @@ -2,6 +2,7 @@ import numpy as np import torch +from scipy.stats import rankdata from torch import Tensor # Assert that torch.einsum broadcasting is available check for torch version >= 1.8.0 @@ -20,14 +21,7 @@ def invert_permutation(p: torch.Tensor): - """ - The argument p is assumed to be some permutation of 0, 1, ..., len(p)-1. - Returns an array s, where s[i] gives the index of i in p. - Taken from: https://stackoverflow.com/a/25535723, adapted to PyTorch. - """ - s = torch.empty(p.shape[0], dtype=p.dtype, device=p.device) - s[p] = torch.arange(p.shape[0], device=p.device) - return s + return torch.argsort(p) def calc_bpd(log_p: Tensor, image_shape: Tuple[int, int, int], has_gauss_dist: bool, n_bins: int) -> float: @@ -85,3 +79,88 @@ def preprocess( image = image.long() return image + + +def rdc(x, y, f=np.sin, k=20, s=1 / 6.0, n=1): + """ + + Source: https://github.com/garydoranjr/rdc/blob/master/rdc/rdc.py + + Computes the Randomized Dependence Coefficient + x,y: numpy arrays 1-D or 2-D + If 1-D, size (samples,) + If 2-D, size (samples, variables) + f: function to use for random projection + k: number of random projections to use + s: scale parameter + n: number of times to compute the RDC and + return the median (for stability) + According to the paper, the coefficient should be relatively insensitive to + the settings of the f, k, and s parameters. + """ + if n > 1: + values = [] + for i in range(n): + try: + values.append(rdc(x, y, f, k, s, 1)) + except np.linalg.linalg.LinAlgError: + pass + return np.median(values) + + if len(x.shape) == 1: + x = x.reshape((-1, 1)) + if len(y.shape) == 1: + y = y.reshape((-1, 1)) + + # Copula Transformation + cx = np.column_stack([rankdata(xc, method="ordinal") for xc in x.T]) / float(x.size) + cy = np.column_stack([rankdata(yc, method="ordinal") for yc in y.T]) / float(y.size) + + # Add a vector of ones so that w.x + b is just a dot product + O = np.ones(cx.shape[0]) + X = np.column_stack([cx, O]) + Y = np.column_stack([cy, O]) + + # Random linear projections + Rx = (s / X.shape[1]) * np.random.randn(X.shape[1], k) + Ry = (s / Y.shape[1]) * np.random.randn(Y.shape[1], k) + X = np.dot(X, Rx) + Y = np.dot(Y, Ry) + + # Apply non-linear function to random projections + fX = f(X) + fY = f(Y) + + # Compute full covariance matrix + C = np.cov(np.hstack([fX, fY]).T) + + # Due to numerical issues, if k is too large, + # then rank(fX) < k or rank(fY) < k, so we need + # to find the largest k such that the eigenvalues + # (canonical correlations) are real-valued + k0 = k + lb = 1 + ub = k + while True: + # Compute canonical correlations + Cxx = C[:k, :k] + Cyy = C[k0 : k0 + k, k0 : k0 + k] + Cxy = C[:k, k0 : k0 + k] + Cyx = C[k0 : k0 + k, :k] + + eigs = np.linalg.eigvals(np.dot(np.dot(np.linalg.pinv(Cxx), Cxy), np.dot(np.linalg.pinv(Cyy), Cyx))) + + # Binary search if k is too large + if not (np.all(np.isreal(eigs)) and 0 <= np.min(eigs) and np.max(eigs) <= 1): + ub -= 1 + k = (ub + lb) // 2 + continue + if lb == ub: + break + lb = k + if ub == lb + 1: + k = ub + else: + k = (ub + lb) // 2 + + return np.sqrt(np.max(eigs)) diff --git a/tests/test_einet.py b/tests/test_einet.py index abfacd8..5df9174 100644 --- a/tests/test_einet.py +++ b/tests/test_einet.py @@ -5,14 +5,11 @@ import torch from parameterized import parameterized -from simple_einet.abstract_layers import logits_to_log_weights from simple_einet.layers.distributions.binomial import Binomial -from simple_einet.layers.linsum import LinsumLayer -from simple_einet.sampling_utils import index_one_hot class TestEinet(TestCase): - def make_einet(self, num_classes, num_repetitions): + def make_einet(self, num_classes, num_repetitions, structure, layer_type): config = EinetConfig( num_features=self.num_features, num_channels=self.num_channels, @@ -23,23 +20,24 @@ def make_einet(self, num_classes, num_repetitions): num_classes=num_classes, leaf_type=self.leaf_type, leaf_kwargs=self.leaf_kwargs, - layer_type="linsum", + layer_type=layer_type, + structure=structure, dropout=0.0, ) return Einet(config) def setUp(self) -> None: - self.num_features = 8 + self.num_features = 30 self.num_channels = 3 self.num_sums = 5 - self.num_leaves = 2 + self.num_leaves = 7 self.depth = 3 self.leaf_type = Binomial self.leaf_kwargs = {"total_count": 255} - @parameterized.expand(product([False, True], [1, 3], [1, 4])) - def test_sampling_shapes(self, differentiable: bool, num_classes: int, num_repetitions: int): - model = self.make_einet(num_classes=num_classes, num_repetitions=num_repetitions) + @parameterized.expand(product([False, True], [1, 3], [1, 4], ["original", "bottom_up"], ["linsum"])) + def test_sampling_shapes(self, differentiable: bool, num_classes: int, num_repetitions: int, structure: str, layer_type: str): + model = self.make_einet(num_classes=num_classes, num_repetitions=num_repetitions, structure=structure, layer_type=layer_type) N = 2 # Sample without evidence @@ -51,9 +49,9 @@ def test_sampling_shapes(self, differentiable: bool, num_classes: int, num_repet samples = model.sample(evidence=evidence, is_differentiable=differentiable) self.assertEqual(samples.shape, (N, self.num_channels, self.num_features)) - @parameterized.expand(product([False, True], [1, 3], [1, 4])) - def test_mpe_shapes(self, differentiable: bool, num_classes: int, num_repetitions: int): - model = self.make_einet(num_classes=num_classes, num_repetitions=num_repetitions) + @parameterized.expand(product([False, True], [1, 3], [1, 4], ["original", "bottom_up"], ["linsum"])) + def test_mpe_shapes(self, differentiable: bool, num_classes: int, num_repetitions: int, structure: str, layer_type: str): + model = self.make_einet(num_classes=num_classes, num_repetitions=num_repetitions, structure=structure, layer_type=layer_type) N = 2 # MPE without evidence