As a first step, we load the multi-modal embeddings of floor plans. Note that we do not share the project page of the multi-modal floor plan model because that work is under review at the moment. The embeddings are stored in assets/embeds/
Code
import torchids, embs = torch.load(r"../assets/embeds/mm_embeds.pt", weights_only=False)# decompose into modality-specific embeddingsemb_graphs = embs["graph"].numpy()emb_images = embs["img"].numpy()emb_walls = embs["wall_graph"].numpy()# check dataset size and dimensionality of the embedding spaceprint(f"Dataset size (N): {len(emb_images)}")print(f"Dimension of embedding (d): {emb_images[0].shape[0]}")
Dataset size (N): 7168
Dimension of embedding (d): 768
Dimensionality reduction through UMAP
A dataset size of \(7168\) means that the total number of embeddings is \(3x7168=21504\). Next, we run UMAP (\(768D \rightarrow 2D\)) on this entire set of embeddings.
C:\Users\caspervanengel\PycharmProjects\SpaceSpace\.venv\Lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
To generate a 3D landscape where height indicates the population density of projected embedding points, we use a kernel density estimation (KDE) on the points. Plotting the contour maps (done for the joint set and individual modality-specific embedding sets) yields a proxy for a meaningful landscape.
The next code cells 1) compute the densities and 2) plot them nicely.
Finally, we generate the 3D landscape by projecting the population density onto the Z-axis. Note that both the height and color indicate the population density level.
Code
import plotly.io as piopio.renderers.default ="notebook"
To inspect where the model is (un)certain about alignment, we visualize each floor plan as a triangle in which each point of the triangle is one of the corresponding projected embeddings that stem from one of the three modalities (graph, wall graph, image).
Code
from matplotlib.collections import PolyCollectionimport numpy as npcmap_joint = LinearSegmentedColormap.from_list("CombinedPastels", ["white", c_graph, c_wall, c_image])fs =30alpha_c =0.7alpha_t =0.5# stack points to define the vertices of each triangleN =len(emb_graphs_2d)np.random.seed(42)idx = np.random.choice(len(emb_graphs_2d), size=N, replace=False)triangles = np.stack([ emb_images_2d[idx], emb_graphs_2d[idx], emb_walls_2d[idx]], axis=1)# collection of trianglestriangle_collection = PolyCollection( triangles, facecolors='white', edgecolors='black', linewidths=fs/30, alpha=0.3)fig, ax = plt.subplots(figsize=(fs, fs))_ = ax.axis('off')# _ = ax.set_title(f"Modality Triplets (N={N})", fontsize=fs*2, weight="bold")_ = ax.contourf(X, Y, Z, levels=l_fine, cmap=cmap_joint, alpha=alpha_c)# triangles to plottriangle_collection.set_alpha(alpha_t)_ = ax.add_collection(triangle_collection)
Clearly, the model isn’t perfect: many triangles are large, which means that the alignment of heterogeneous representation of floor plans isn’t perfect as well. But how can we visualize where the model is more (or less) certain in alignment? This could, consequently, reveal with which type of spatial arrangement the model struggles with understanding.
To model the alignment (un)certainty, we simply compute the rank of each pair of ‘connected’ embeddings. Connected in the sense that each pair that we investigate comes from the same floor plan. The rank is computed based on the distance in the original (768D) space, which isn’t entirely reflected in the 2D representative we have plotted before. We compute the ranks next and provide the first 500 of them to investigate a bit the distribution:
As you can see, the model is actually quite good, however not perfect.
Similar to the population density, we compute a density map of the ranks (note that this is different from a density over simply points) and smooth it using a Gaussian:
Code
from scipy.stats import binned_statistic_2dfrom scipy.ndimage import gaussian_filterimport numpy as npdef get_rank_surface_binned(points, ranks, X_grid, Y_grid, n_grid):# binned statistics stats, _, _, _ = binned_statistic_2d( points[:, 0], points[:, 1], ranks, statistic='mean', bins=n_grid,range=[[X_grid.min(), X_grid.max()], [Y_grid.min(), Y_grid.max()]] ) Z_rank = stats.T# temporarily fill NaNs with the average rank just so the blur doesn't break Z_filled = np.nan_to_num(Z_rank, nan=np.nanmean(Z_rank))# smooth it Z_smooth = gaussian_filter(Z_filled, sigma=3.0)return Z_smoothZ_rank_joint = get_rank_surface_binned(umap_embeds, ranks_joint, X, Y, n_grid)Z_rank_g = get_rank_surface_binned(emb_graphs_2d, ranks_g, X_g, Y_g, n_grid)Z_rank_s = get_rank_surface_binned(emb_walls_2d, ranks_s, X_s, Y_s, n_grid)Z_rank_i = get_rank_surface_binned(emb_images_2d, ranks_i, X_i, Y_i, n_grid)