import matplotlib.pyplot as plt
import numpy as np
from typing import Callable, List, Tuple
import tensorflow as tf
from matplotlib.backends.backend_agg import FigureCanvasAgg
from gyoza.modelling import flow_layers as gmfl
from gyoza.tutorials import data_synthesis as gtd
def __make_color_palette__() -> np.ndarray:
"""Generates interpolations between the colors red, green and blue.
:return: color_palette (:class:`np.ndarray`) - An array of shape [55, 3], listing colors in RGB format."""
# from https://github.com/JiahuiYu/generative_inpainting/blob/master/inpaint_ops.py
RY, YG, GC, CB, BM, MR = (15, 6, 4, 11, 13, 6)
ncols = RY + YG + GC + CB + BM + MR
color_palette = np.zeros([ncols, 3])
col = 0
# RY
color_palette[0:RY, 0] = 255
color_palette[0:RY, 1] = np.transpose(np.floor(255*np.arange(0, RY) / RY))
col += RY
# YG
color_palette[col:col+YG, 0] = 255 - np.transpose(np.floor(255*np.arange(0, YG) / YG))
color_palette[col:col+YG, 1] = 255
col += YG
# GC
color_palette[col:col+GC, 1] = 255
color_palette[col:col+GC, 2] = np.transpose(np.floor(255*np.arange(0, GC) / GC))
col += GC
# CB
color_palette[col:col+CB, 1] = 255 - np.transpose(np.floor(255*np.arange(0, CB) / CB))
color_palette[col:col+CB, 2] = 255
col += CB
# BM
color_palette[col:col+BM, 2] = 255
color_palette[col:col+BM, 0] = np.transpose(np.floor(255*np.arange(0, BM) / BM))
col += + BM
# MR
color_palette[col:col+MR, 2] = 255 - np.transpose(np.floor(255 * np.arange(0, MR) / MR))
color_palette[col:col+MR, 0] = 255
# Output
return color_palette
color_palette = __make_color_palette__()
"""A convenience variable, storing the color palette that is computed by :py:meth:`__make_color_palette__`."""
[docs]
def make_2_dimensional_gaussian(mu: np.ndarray, sigma: np.ndarray, shape: List[int]) -> np.ndarray:
"""Generates a 2 dimensional Gaussian distribution.
:param mu: The two means for the Gaussian variables. Assumed to be of shape [2].
:type mu: np.ndarray
:param sigma: The covariance matrix. Assumed to be of shape [2,2].
:type sigma: np.ndarray
:param shape: The desired shape of the output.
:type shape: _type_, optional
:return:
- X (:class:`numpy.ndarray`) - Coordiates of the grid of the two variables. Shape == [ ``shape`` [0]* ``shape`` [1],2].
- p (:class:`numpy.ndarray`) - The probabilities associated with the coordinates ``X``. Shape == [ ``shape`` [0]* ``shape`` [1]].
- D (:class:`numpy.ndarray`) - A matrix that arranges ``p`` with desired ``shape``.
"""
# Generate x, y coordinates
x = np.linspace(-3, 3, shape[1])
y = np.linspace(-3, 3, shape[0])
xv, yv = np.meshgrid(x, y)
X = np.concatenate([np.reshape(xv,[-1,1]), np.reshape(yv, [-1,1])], axis=1) # Shape == [shape[0]*shape[1],2]
# Compute probability
numerator = np.exp(-0.5*np.sum((X-mu).dot(np.linalg.inv(sigma)) * (X-mu), axis=1))
denominator = np.sqrt((2*np.pi)**2 * np.linalg.det(sigma))
p = numerator / denominator
D = np.reshape(p, [shape[1], shape[0]])
# Outputs
return X, p, D
[docs]
def swirl(x:np.ndarray, y: np.ndarray, x0:float = 0, y0: float = 0, radius: float = 5, rotation: float = 0, strength: float = 5) -> Tuple[np.ndarray, np.ndarray]:
"""Performs a swirl operation on given x and y coordinates.
Inputs:
- x, y: Coordinates of points that shall be swirled.
- x0, y0: The origin of the swirl.
- radius: The extent of the swirl. Small values indicate local swirl, large values indicate global swirl.
- rotation: Adds a rotation angle to the swirl.
- strength: Indicates the strength of swirl.
Outputs:
- x_new, y_new: The transformed coordinates.
"""
# Polar coordinates of each point
theta = np.arctan2((y-y0), (x-x0))
rho = np.sqrt((x-x0)**2 + (y-y0)**2)
# Swirl
r = np.log(2)*radius/5
new_theta = rotation + strength * np.exp(-rho/r) + theta
# Cartesian coordinates
x_new = rho * np.cos(new_theta)
y_new = rho * np.sin(new_theta)
# Outputs
return x_new, y_new
[docs]
def make_radial_line(radius: float, rotation: float, point_count: int) -> np.ndarray:
"""Generates a straight line with ``point_count`` many points that has one endpoint at the origin and the other endpoint on the
circle defined by defined by ``radius`` and ``rotation``.
:param radius: The radius of the circle from which lines are generated.
:type radius: float
:param rotation: The angle of rotation of the line in radians. Movement is clockwise.
:type rotation: float
:param point_count: The number of points on the line.
:type point_count: int
:return: x, y (:class:`np.ndarray`) - The coordinates of line with shape [``point_count``, 2].
"""
# Generate horizontal line
x = np.arange(start=0, stop=radius+radius/point_count, step=radius/point_count, dtype=np.float32)
y = np.zeros(x.shape, dtype=np.float32)
line = np.concatenate([x[:,np.newaxis], y[:,np.newaxis]], axis=1); del x, y
# Rotate
rotaton_matrix = np.array([[np.cos(rotation), -np.sin(rotation)], [np.sin(rotation), np.cos(rotation)]])
line = np.dot(line, rotaton_matrix)
# Unpack
x = line[:,0]; y = line[:,1]
# Outputs
return x, y
[docs]
def make_color_wheel(pixels_per_inch: int, pixel_count: int = 128, swirl_strength: float = 0, gaussian_variance: float = 1) -> np.ndarray:
"""Generates an image of a color wheel with swirl
:param dpi: The density of pixels per inch on the user machine.
:type dpi: int
:param pixel_count: The desired width and height of ``image`` in pixels, defaults to 128
:type pixel_count: int, optional
:param swirl_strength: The strength of swirl applied to the color wheel. Sensible values are in the range [0,10]. The sign is
ignored. Defaults to 0
:type swirl_strength: float, optional
:param saturation: The saturation of the colors. valid values are in range [0,1], where 0 corresponds to a white image and 1 to
a fully satured image. Defaults to 1.
:type saturation: float, optional
:return: image (:class:`np.ndarray`) - The image of shape [pixel_count, pixel_count, 4] where 4 are the channels.
"""
# Make radial lines
x_s = [None] * len(color_palette); y_s = [None] * len(color_palette)
for c in range(len(color_palette)):
# Make straight line
x, y = make_radial_line(radius=1, rotation=c*2*np.pi/(len(color_palette)), point_count=2+(int)(2*swirl_strength))
# Add swirl
if swirl_strength != 0: x,y = swirl(x=x,y=y,radius=5, rotation=0, strength=swirl_strength)
# Save to array
x_s[c] = x; y_s[c] = y
# Draw wedges
figure = plt.figure(figsize=(pixel_count/(2*pixels_per_inch), pixel_count/(2*pixels_per_inch)), dpi=pixels_per_inch)
plt.axis('off')
for c, color in enumerate(color_palette):
# A wedges has two main lines
x_c = x_s[c]; y_c = y_s[c]
x_d = x_s[(c+1) % len(color_palette)]; y_d = y_s[(c+1) % len(color_palette)]
# Draw on figure
plt.fill(np.concatenate([x_c,np.flip(x_d)]), np.concatenate([y_c,np.flip(y_d)]), color=tuple(color/255), linewidth=1/pixels_per_inch)
plt.xlim([-1,1]); plt.ylim([-1,1])
plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0,
hspace = 0, wspace = 0)
# Export as image
canvas = FigureCanvasAgg(figure)
canvas.draw()
s, (width, height) = canvas.print_to_buffer()
image = np.fromstring(s, np.uint8).reshape((height, width, 4))
plt.close()
# Apply gaussian saturation
_, _, gaussian = make_2_dimensional_gaussian(mu=np.zeros([2]), sigma=gaussian_variance*np.eye(2), shape=[height, width])
gaussian = (gaussian-np.min(gaussian))/np.max(gaussian) # Now ranges between 0 and 1
gaussian = np.array(255 *gaussian, dtype=np.uint8)
image[:,:,3] = gaussian # The alpha channel
# Outputs
return image
[docs]
def plot_instance_pairs(S: np.ndarray, Z_a: np.ndarray, Z_b: np.ndarray, Y_ab: np.ndarray, manifold_function: Callable, manifold_name: str, pair_count: int=3):
"""Plots pairs of instances along with their similarities and the manifold (without noise).
:param S: The position along the manifold. Shape == [:math:`M`, 1], where :math:`M` is the number of instances.
:type S: np.array
:param Z_a: The coordinates of the a-instances to be plotted. Shape is assumed to be [:math:`M`, :math:`N`], where :math:`M` is the number
of instances and :math:`N = 2` is the dimensionality of an instance.
:type Z_a: :class:`numpy.ndarray`
:param Z_b: The same as Z_a, but for b-instances.
:type Z_b: :class:`numpy.ndarray`
:param Y_ab: The similarities of the ``Z_ab`` instances. Shape is assumed to be [:math:`M`, :math:`F`], where :math:`M` is the number of
instances and :math:`F=2` at axis 1 is the factor count.
:type Y_ab: :class:`numpy.ndarray`
:param manifold_function: A function that takes as input the position ``S`` along the manifold and provides as output the two coordinates
that are associated with that position along the manifold. Hence, [:math:`M`,1] -> [:math:`M`, :math:`N`], where M is the number of
instances and :math:`N=2` their dimensionality
:type manifold_function: Callable
:param manifold_name: A name assigned to the manifold that is used as a label in the plot.
:type manifold_name: str
:param pair_count: The number of pairs to be illustrated
:type pair_count: int, optional, defaults to 3
"""
# Construct figure
plt.figure(figsize=(3.5,3.5)); plt.title(rf"Noisy Instances, Pairs and Similarities (s) for ${manifold_name}$")
# Plot instance pairs
individual_Z = np.concatenate([Z_a, Z_b], axis=1)
plt.scatter(individual_Z[:,0], individual_Z[:,1], c='lightgray')
plt.scatter(Z_a[:pair_count,0], Z_a[:pair_count,1]) # Instances a
plt.scatter(Z_b[:pair_count,0], Z_b[:pair_count,1]) # Instances b
# Plot manifold
plt.scatter(*manifold_function(S), color='gray', marker='.', s=1)
# Label current plots
plt.legend(['z','z_a','z_b',rf"${manifold_name}$"])
# Plot lines that connect the two instances of a pair
s_max = np.max(S)
for i in range(pair_count): # Iterate instances
plt.plot([Z_a[i,0], Z_b[i,0]], [Z_a[i,1], Z_b[i,1]], '--', color='black')
plt.text(Z_b[i,0]+0.1, Z_b[i,1], 's = ' + str(np.round(Y_ab[i,1], 3))) # Label for their similarity
plt.gca().set_aspect('equal')
#plt.ylim(-s_max/0.8,s_max/0.8); plt.xlim(-s_max/0.8,s_max/0.8)
plt.xlabel('First Dimension'); plt.ylabel('Second Dimension')
plt.show()
[docs]
def plot_instance_pairs_2(Z_a: np.ndarray, Z_b: np.ndarray, title_suffix: str = rf"$Z$"):
"""Plots the instance pairs of Z_ab (or Z_tilde_ab) in two scatter plots. The first scatter plot shows the first dimension (index 0) of instance a and b while the second scatter plot shows the second dimension (index 1) of instances a and b.
In the margins of each scatter plot, the marginal histograms are shown.
:param Z_a: The coordinates of the a-instances to be plotted. Shape is assumed to be [:math:`M`, :math:`N`], where :math:`M` is the number
of instances and :math:`N = 2` is the dimensionality of an instance.
:type Z_a: :class:`numpy.ndarray`
:param Z_b: The same as Z_a, but for b-instances.
:type Z_b: :class:`numpy.ndarray`
:param title_suffix: The suffix to be added to the title, usually a string 'Z' to indicate that instances come from the Z-space or rf'$\tilde{Z}$' to indicate that they come from the Z_tilde-space.
:type title_suffix: str, optional, defaults to rf'$Z$'
"""
fig, axs = plt.subplots(2,4,figsize=(9,4.5), gridspec_kw={'height_ratios': [4, 0.5], 'width_ratios':[0.5,4,0.5,4]})
plt.suptitle("Instance Pairs " + title_suffix)
# Iterate dimensions
for d in range(2):
plt.subplot(2,4,d*2+2)
plt.title(("First" if d==0 else "Second") + " Dimension ")
plt.scatter(Z_a[:,d], Z_b[:,d], s=0.5, c='k')
plt.xlabel(f"Instance a"); plt.ylabel(f"Instance b")
Z_a_0_lim, Z_b_0_lim = plt.xlim(), plt.ylim()
plt.legend([f"r = {np.round(np.corrcoef(Z_a[:,d], Z_b[:,d])[0,1], 3)}"])
# Histograms
plt.subplot(2,4,d*2+6)
plt.hist(Z_a[:,d], histtype='step', color='k'); plt.xlim(Z_a_0_lim); plt.gca().invert_yaxis(); plt.axis('off')
plt.subplot(2,4,d*2+1)
plt.hist(Z_b[:,d], orientation='horizontal', histtype='step', color='k'); plt.ylim(Z_b_0_lim); plt.gca().invert_xaxis(); plt.axis('off')
# Disable corner subplot
plt.subplot(2,4,d*2+5); plt.axis('off')
plt.tight_layout()
plt.show()
[docs]
def plot_loss_trajectory(epoch_loss_means: List[float], epoch_loss_standard_deviations: List[float], manifold_name: str):
"""Plots the loss trajectory after model calibration with error surface.
:param epoch_loss_means: The mean across batches for each epoch. Length = [epoch count]
:type epoch_loss_means: List[float]
:param epoch_loss_standard_deviations: The standard deviation across batches for each epoch. Length = [epoch count]
:type epoch_loss_standard_deviations: List[float]
:param manifold_name: The name of the manifold on which the model was calibrated. Used for the title.
:type manifold_name: str
"""
# Preprocess
M = len(epoch_loss_means)
means = np.array(epoch_loss_means)
errors = 2.0 * np.array(epoch_loss_standard_deviations) / np.sqrt(M)
# Create figure
plt.figure(figsize=[12,3]); plt.title(rf'Loss Trajectory on Manifold ${manifold_name}$')
# Error surface
plt.fill_between(x=list(range(M)), y1=means-errors, y2=means+errors, color='mistyrose')
# Line
plt.plot(epoch_loss_means)
# Labels
plt.xlabel('Epoch'); plt.ylabel('Loss'); plt.legend([r'$\pm 2*$ Standard Error', 'Mean Across Batches'])
[docs]
def evaluate_and_plot_networks(Z_test: List[np.ndarray], Y_test: List[np.ndarray], networks: List[gmfl.FlowModel], manifold_name: str):
"""For each network, a scatter plot for the predicted and actual position along the manifold is plotted along with a bar for the proportion
of explained variance.
:param Z_test: A list of test sets used as input to the corresponding network in ``networks``. The list is expected to have the same length
as ``networks`` and each test set is assumed to have shape [:math:`M^*`,:math:`N`], where :math:`M^*` is the number of instances in a
test set and :math:`N=2` is the dimensinoality of an instance.
:type Z_test: List[np.ndarray]
:param Y_test: A list of test sets used to evaluate to the corresponding network in ``netwroks``. The list is expected to have the same length
as ``networks`` and each test set is assumed to have shape [:math:`M^*`,:math:`F`], where :math:`M^*` is the number of instances in a
test set and :math:`F=2` is the number of factors. It is assumed that factor at index 1 encodes the position along the manifold.
:type Y_test: List[np.ndarray]
:param networks: A list of calibrated networks that take ``Z_test`` as input and whose output (of same shape as input) encodes position along
the data manifold along index 2.
:type networks: List[mfl.SupervisedFactorNetwork]
:param manifold_name: The name of the manifold used in the figure title.
:type manifold_name: str
"""
# Prepare figure
fold_count = len(networks)
fig, axs = plt.subplots(1,3*fold_count,figsize=(12,2), gridspec_kw={'width_ratios': [6, 1, 1]*fold_count})
plt.suptitle(rf"{fold_count}-Fold Cross Validated Evaluation on ${manifold_name}$")
# Iterate networks
for n, network in enumerate(networks):
# 1. Predict
Y_hat = network(Z_test[n]).numpy()
Y = Y_test[n]
# 2. Create scatter plot for manifold position
plt.subplot(1,3*fold_count, 3*n+1)
plt.scatter(Y_hat[:,1], Y[:,1], color='black', marker='.', s=1)
ax = plt.gca();ax.spines['top'].set_visible(False); ax.spines['right'].set_visible(False)
if n == 0:
plt.ylabel("Manifold Position\n\nActual"); plt.xlabel("Predicted")
else:
plt.yticks([]); plt.xlabel(f'Fold {n+1}')
# 3. Create bar plot for proportion of explained variance for position along manifold (factor at position 1)
r2 = np.corrcoef(Y[:,1], Y_hat[:,1])[1,0]**2 # Since the output is a correlation matrix, the [1,0] selects the correlation between the two variables
plt.subplot(1,3*fold_count, 3*n+2);
plt.bar([0], [r2], color='white', edgecolor='black'); plt.ylim([0,1]); plt.xlim([-1,1]); plt.xticks([])#[n+1], [f"Fold {n+1}"])
if n==0:
ax = plt.gca();ax.spines['top'].set_visible(False); ax.spines['left'].set_visible(False); ax.spines['right'].set_visible(False)
plt.ylabel(r'$r^2$'); plt.gca().yaxis.tick_right(); plt.yticks([0,1])
else:plt.axis('off')
t=plt.text(-0,r2/2,f'{np.round(r2,2)}', horizontalalignment='center'); t.set_bbox(dict(facecolor='white', alpha=0.9, edgecolor='white'))
# Padding on right of each bar plot
plt.subplot(1,3*fold_count, 3*n+3); plt.axis('off')
plt.show()
[docs]
def plot_inverse_point(position: float, residual: float, S: np.ndarray, network: gmfl.FlowModel, manifold_function: Callable, manifold_name: str):
"""This function visualizes the ``network``'s inversion ability by plotting the inverse of the point[``residual``, ``position``]. It also
plots the ``manifold_function`` on input ``S`` for reference.
:param position: The position along the manifold that shall be entered for dimensions at index 1 for inversion via the ``network``.
:type position: float
:param residual: The residual that shall be entered for dimensions at index 0 for inversion via the ``network``.
:type residual: float
:param S: Points along which the ``manifold_function`` shall be evaluated during plotting.
:type S: :class:`numpy.ndarray`
:param network: A network calibrated to disentangle manifold position (factor at dimension 1) from deviation from manifold (factor at
dimension 0). It shall map from [:math:`M`,:math:`N`] to [:math:`M`,:math:`N`], where :math:`M` is the instance count and :math:`N=2`
is the dimensionality.
:type network: mfl.SupervisedFactorNetwork
:param manifold_function: A function that maps from position on manifold (:math:`S`, shape == [instane count M]) to coordinates in :math:`N=2`
dimensional space.
:type manifold_function: _type_
:param manifold_name: The name of the manifold used for the figure title.
:type manifold_name: str
"""
# Construct figure
plt.figure(figsize=(3.5,3.5)); plt.title(rf"Inverse Modelling on ${manifold_name}$")
# Predict position using network
Z_tilde = tf.constant([[residual,position]], dtype=tf.keras.backend.floatx())
Z = network.invert(Z_tilde)
plt.scatter(Z[:,0], Z[:,1])
# Plot manifold
plt.scatter(*manifold_function(S), color='gray', marker='.', s=1)
# Axes and labels
s_max = np.max(S)
plt.ylim(-s_max/0.8,s_max/0.8); plt.xlim(-s_max/0.8,s_max/0.8)
plt.xlabel('First Dimension'); plt.ylabel('Second Dimension')
plt.show()
[docs]
def plot_contribution_per_layer(network: gmfl.FlowModel, s_range: Tuple[float, float], manifold_function: Callable, manifold_name:str, layer_steps: List[int], step_titles: List[str]):
"""Plots for each layer (or rather step of consecutive layers) the contribution to the data transformation. The plot is strucutred into three rows.
The first row shows a stacked bar chart whose bottom segment is the contribution due to affine transformation and the top segment is the contribution
due to higher order transformation. To better understand the mechanisms behind these contributions there is a pictogram in the bottom row for the
actual affine transformation and in the middle row for the remaining higher order part. This separation is done to understand the complexity of the
transformation, whereby affine is considered simple and higher order is considered complex. The decomposition into affine and higher order is obtained
by means of a first order `Maclaurin series <https://en.wikipedia.org/wiki/Taylor_series#Taylor_series_in_several_variables>`_.
:param network: The network whose transfromation shall be visualized. It is expecetd to map 1 dimensional manifolds from the real 2-dimensional
plane to the real 2-dimensional plane.
:type network: :class:`gyoza.modelling.flow_layers.FlowModel`
:param s_range: The lower and upper bounds for the position along the manifold, respectively.
:type s_range: Tuple[float, float]
:param manifold_function: A function that maps from position along manifold to coordinates on the manifold in the real two dimensional plane.
:type manifold_function: :class:`Callable`
:param manifold_name: The name of the manifold used for the figure title.
:type manifold_name: str
:param layer_steps: A list of steps across layers of the ``network``. If, for instance, the network has 7 layers and visualization shall be done for
after the 1., 3. and 7, then ``layer_steps`` shall be set to [1,3,7]. The minimum entry shall be 1, then maximum entry shall be the number of layers
in ``network`` and all entries shall be strictly increasing.
:type layer_steps: List[int]
:param step_titles: The titles associated with each step in ``layer_steps``. Used as titles in the figure.
:type step_titles: List[str]
"""
# Prepare plot
#plt.figure(figsize=(12,3.5));
layer_steps = [0] + layer_steps
K = len(step_titles)
fig, axs = plt.subplots(3, 1+K, figsize=(0.8+K,5), gridspec_kw={'height_ratios': [2,1,1], 'width_ratios':[0.3]+[1]*K})
plt.suptitle(rf'Contribution per Layer on ${manifold_name}$')
# Sample from s range
S = np.linspace(s_range[0], s_range[1], len(color_palette), dtype=tf.keras.backend.floatx())
z_1, z_2 = manifold_function(S); Z = np.concatenate([z_1[:, np.newaxis], z_2[:, np.newaxis]], axis=1)
max_bar_height = 0
# Plot annotations on left
gray = [0.8,0.8,0.8]
#plt.subplot(3,1+K,1); plt.axis('off')
plt.subplot(3,1+K,1+K+1); plt.bar([''],[1], color=gray, edgecolor='black', hatch='oo'); plt.ylim(0,1); plt.xticks([]); plt.yticks([]); plt.ylabel('Higher Order')
plt.subplot(3,1+K,2*(1+K)+1); plt.bar([''],[1], color=gray, edgecolor='black', hatch='///'); plt.ylim(0,1); plt.xticks([]); plt.yticks([]); plt.ylabel('Affine')
# Iterate layers
for k in range(1, len(layer_steps)):
# Set up 1st order Maclaurin decomposition https://en.wikipedia.org/wiki/Taylor_series#Taylor_series_in_several_variables
# Z_tilde ~= layer(0) + J(0) * Z, where J(0) is the jacobian w.r.t input evaluated at the origin
origin = tf.Variable(tf.zeros([1] + list(Z[0].shape), dtype=tf.keras.backend.floatx())) # The extra 1 is the batch dimension
Z_tilde = Z
c = origin # Shape == [1, N]. The layer's shifting of the origin
with tf.GradientTape() as tape:
for layer in network.layers[layer_steps[k-1]:layer_steps[k]]:
c = layer(c)
Z_tilde = layer(Z_tilde) # Shape == [instance count, N]
J = tf.squeeze(tape.jacobian(c, origin)) # Shape == [N z_tilde dimensions, N z dimensions]. The layer's linear combination of input dimensions
# Compute approximation error (contribution of higher order terms in the Maclaurin series)
prediction = c + tf.linalg.matmul(Z, tf.transpose(J))
P = prediction - Z # Shape == [instance count, N]. Arrows from Z to prediction
E = Z_tilde - prediction # Shape == [instance count, N]. Arrows from prediction to Z_tilde
# 2. Plot
# 2.1 Bars
plt.subplot(3,1+K,k+1); plt.title(step_titles[k-1], fontsize=10)
E_norm = np.mean(np.sqrt(np.sum(E**2, axis=1)))
P_norm = np.mean(np.sqrt(np.sum(P**2, axis=1)))
plt.bar([''],[E_norm+P_norm], color = gray, edgecolor='black', hatch='oo')
plt.bar([''],[P_norm], color = gray, edgecolor='black', hatch='///')
max_bar_height = max(max_bar_height, E_norm+P_norm); plt.axis('off')
# 2.1 Tails
# 2.1.1 Error
plt.subplot(3,1+K,1+K+k+1)
plt.scatter(prediction[:,0], prediction[:,1], color=gray, marker='.',s=0.1)
plt.quiver(prediction[:,0], prediction[:,1], E[:,0], E[:,1], angles='xy', scale_units='xy', scale=1., color=gray, zorder=3)
plt.scatter(Z_tilde[:,0], Z_tilde[:,1], c=color_palette/255.0, marker='.',s=1.5)
plt.axis('equal'); plt.xticks([]); plt.yticks([]); plt.xlim(1.3*np.array(plt.xlim())); plt.ylim(1.3*np.array(plt.ylim()))
# 2.1.2 Prediction
plt.subplot(3,1+K,2*(1+K)+k+1)
plt.scatter(Z[:,0], Z[:,1], color=gray, marker='.',s=0.1)
plt.quiver(Z[:,0], Z[:,1], P[:,0], P[:,1], angles='xy', scale_units='xy', scale=1., color=gray, zorder=3)
plt.scatter(prediction[:,0], prediction[:,1], c=color_palette/255.0, marker='.',s=1.5)
plt.axis('equal'); plt.xticks([]); plt.yticks([]); plt.xlim(1.3*np.array(plt.xlim())); plt.ylim(1.3*np.array(plt.ylim()))
# Prepare next iteration
Z=Z_tilde
# Adjust bar heights
for k in range(1, len(layer_steps)):
plt.subplot(3,1+K,k+1); plt.ylim(0, max_bar_height)
plt.subplot(3,1+K,1); plt.ylabel('Mean Change'); plt.ylim(0, max_bar_height); ax = plt.gca();ax.spines['top'].set_visible(False); ax.spines['left'].set_visible(False); ax.spines['bottom'].set_visible(False); plt.xticks([])
ax.yaxis.tick_right(); ax.tick_params(axis="y",direction="in", pad=-12)
plt.tight_layout()
plt.show()