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