DIGITAL
In dieser Lehreinheiten
Wir benötigen für den folgenden Aufgaben eine funktionierende Lösung der letzten Lehreinheit.
Bestenfalls kann an dieser Stelle alles mit PyTorch implementiert werden, Sie können selbstverständlich aber auch eine eigene Implementierung etwa mit Numpy, oder mit einer Auswahl der funktionalität von PyTorch verwenden.
Stellen Sie also sicher, dass Ihr Code einen der 2D-Spielzeugdatensätze (siehe LE10) Klassifizieren kann. (Es ist hierbei nicht so wichtig möglichst hohe Accuracies zu erhalten, besser als die Lösung zu erraten soll das Netz jedoch schon sein).
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
class Net(nn.Module):
def __init__(self, input_dim, output_dim, layers, filters_per_layer, weight_std):
super().__init__()
self.input_dim = input_dim
self.output_dim = output_dim
self.linear = nn.ModuleList([])
self.saved_act = [None for i in range(num_layers-1)]
last_filters = input_dim
if layers == 1:
self.linear.append(self.make_layer(input_dim, output_dim, weight_std))
else:
self.linear.append(self.make_layer(input_dim, filters_per_layer, weight_std))
for i in range(layers-2):
self.linear.append(self.make_layer(filters_per_layer, filters_per_layer, weight_std))
self.linear.append(self.make_layer(filters_per_layer, output_dim, weight_std))
def make_layer(self, input_dim, output_dim, weight_std):
# create layer with weights & bias
layer = nn.Linear(input_dim, output_dim)
# reinit
torch.nn.init.normal_(layer.weight, mean=0.0, std=weight_std)
# torch.nn.init.kaiming_normal_(layer.weight, nonlinearity="relu")
return layer
def forward(self, x):
for i, linear_layer in enumerate(self.linear):
x = linear_layer(x)
if linear_layer != self.linear[-1]:
x = F.relu(x)
self.saved_act[i] = x.detach()
return x
# ======== #
# SETTINGS #
# ======== #
lr = 5e-3
weight_std = 1.0
num_layers = 5
num_filters= 25
numpts = 100
get_batch = lambda numpts: (np.random.random((numpts,2))*2 - 1)
# ======== #
# DATASETS #
# ======== #
# num_classes, get_label = 2, lambda x, numcheck=3, offset=0: np.asarray( (np.floor((x[:,0]*numcheck) % num_classes) + offset * np.floor(x[:,1]*numcheck % num_classes)) % num_classes, int )
num_classes, get_label = 2, lambda x, radius=0.5, center=0: 1.0*(torch.sum((x-center)*(x-center), axis=-1) < radius)
# num_classes, get_label = 3, lambda x, radius=0.3, radius2=0.6, center=0: 1.0*(torch.sum((x-center)*(x-center), axis=-1) < radius) + (torch.sum((x-center)*(x-center), axis=-1) < radius2)
# num_classes, get_label = 2, lambda x: 1.0*(torch.sin(x[:,0]*10) < x[:,1]*10)
# get_label = lambda x, radius=0.5, max_radius=0.75, center=0: 1.0*(torch.sum((x-center)*(x-center), axis=-1) > radius)*(torch.sum((x-center)*(x-center), axis=-1) < max_radius)
net = Net(2, num_classes, num_layers, num_filters, weight_std)
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
def train_step():
data = torch.tensor(get_batch(numpts), dtype=torch.float)
label = torch.tensor(get_label(data), dtype=torch.int64)
pred = net(data)
l = loss(pred, label)
acc = (torch.argmax(pred, axis=-1) == label).sum() / len(label)
print(acc, l)
optimizer.zero_grad()
l.backward()
optimizer.step()
width, height = 100, 100
ε = 1e-7
x_min, x_max, y_min, y_max = -1, 1-ε, -1, 1-ε
extent = [x_min, x_max, y_min, y_max]
anim_data = torch.tensor(np.meshgrid(np.linspace(x_min,x_max,width), np.linspace(y_min,y_max,height)), dtype=torch.float).reshape((2, -1)).T
anim_label = get_label(anim_data)
# a dumb hash function works here well enough
num_colors = 20
def act_to_hash(a):
return a.sum(1) % num_colors # 20 different colors
def bin2numbers(input):
numbers = []
for i in range((input.size(1)-1)//64 + 1):
block = input[:,i*64:(i+1)*64]
base = 2**torch.arange(block.size(1), device=input.device)
# ensure that base is at correct dimension
base = base.unsqueeze(0)
for i in range(len(input.shape)-2):
base = base.unsqueeze(i+2)
# pytorch does not support uint64_t, but int64_t
# thus, we interpret last bit as sign
if block.size(1) == 64:
base[:,-1] = -1
num = (block*base).sum(1)
numbers.append(num)
return torch.stack(numbers, dim=1)
def act_to_hash(act):
FIRSTH = 37 # prime
A = 54059 # a prime
B = 76963 # another prime
device = act.device
act = act.int()
numbers = bin2numbers(act)
numbers = numbers * B
result = FIRSTH;
d = numbers.shape[1];
for i in range(d):
number = numbers[:,i]
result = number ^ (result * A)
return result
class AnimatedTraining(object):
"""An animated scatter plot using matplotlib.animations.FuncAnimation."""
def __init__(self, numpoints=50):
self.numpoints = numpoints
# Setup the figure and axes...
self.fig, self.axs = plt.subplots(int(np.sqrt(num_layers+1)),int(np.ceil((num_layers+1)/int(np.sqrt(num_layers+1)))))
self.axs = self.axs.flat
self.imgs = []
# Then setup FuncAnimation.
self.ani = animation.FuncAnimation(self.fig, self.update, interval=10, init_func=self.setup_plot, blit=True)
def setup_plot(self):
"""Initial drawing of the scatter plot."""
img_data = self.axs[0].imshow(anim_label.reshape((width, height)), vmin=0, vmax=num_classes, cmap="jet", extent=extent)
self.axs[0].set_title("input data")
self.imgs.append(img_data)
# set extent for all axes
for ax in self.axs:
ax.axis(extent)
# get current activations
out = net(anim_data)
# now calculate colors for all layers
for i, (ax, act) in enumerate(zip(self.axs[1:-1], net.saved_act)):
color = act_to_hash(act.sign()) % num_colors
img_data = ax.imshow(color.reshape((width, height)), vmin=0, vmax=num_colors, cmap="Set3", extent=extent)
ax.set_title("Layer %i" % i)
self.imgs.append(img_data)
# lastly, calculate colors for classification
color = torch.argmax(out, axis=-1)
img_data = self.axs[-1].imshow(color.reshape((width, height)), vmin=0, vmax=num_classes, cmap="Set3", extent=extent)
self.imgs.append(img_data)
self.axs[-1].set_title("classifier decision")
# For FuncAnimation's sake, we need to return the artist we'll be using
# Note that it expects a sequence of artists, thus the trailing comma.
# return self.imgs[1:-1]
return self.imgs
def update(self, i):
# do one training step
train_step()
updated = []
# get updated activations
out = net(anim_data)
# now calculate colors for all layers
for ax, act, imshow in zip(self.axs[1:-1], net.saved_act, self.imgs[1:-1]):
color = act_to_hash(act.sign()) % num_colors
imshow.set_array(color.reshape((width, height)))
updated.append(imshow)
# lastly, calculate colors for classification
color = torch.argmax(out, axis=-1)
self.imgs[-1].set_array(color.reshape((width, height)))
updated.append(self.imgs[-1])
# We need to return the updated artist for FuncAnimation to draw..
# Note that it expects a sequence of artists, thus the trailing comma.
# return self.scat,
return updated
if __name__ == '__main__':
a = AnimatedTraining()
plt.show()
Wir haben in der letzten Woche die Datenverteilung so normiert, dass sie in etwa Standardnormalverteilt ist. Dieses Wissen haben wir genutzt, um die Gewichte passend darauf auszurichten; wir haben die Initialisierung der Gewichte ebenfalls Standardnormalverteilt gewählt.
Warum tun wir dies?
Die Idee dabei ist, dass die Varianz der Zwischenergebnisse ungefähr gleich groß bleiben soll.
Im Falle einer steigenden Varianz (über die Layer hinweg) würden wir irgendwann ein numerical Overflow in den Zwischenrechnungen erhalten, da dieser Effekt sich multiplikativ auswirken würde. Entsprechend würde eine sinkende Varianz numerical Underflow hervorrufen. Von den Gradienten haben wir hier noch gar nicht gesprochen.
Konkret schauen wir uns einen beliebigen Layer der Form \(y = a(x^T W + b)\) an, wobei \(x\) die Ausgabe des Layers davor oder die Eingabe des Neuronalen Netzes selbst ist und \(a\) eine Aktivierungsfunktion darstellt. Unser Ziel ist es die resultierenden Varianzen zweier beliebig aufeinanderfolgenden linearer Layer (inklusive Nicht-Linearität) konstant zu halten, also die Gleichung \[ \operatorname{Var}(y) = \operatorname{Var}(x) \] zu erfüllen.
Wir nehmen der Einfachheit halber an, dass alle Dimensionen von \(x\) stochastisch unabhängig sind. Streng genommen stimmt dies natürlich nicht, aber bessere Annäherungen sind zum einen nur schwierig zu bestimmen, zum anderen interessieren wir uns hier für die initialisierung der Gewichte von \(W\). Zu begin des Trainings sehen die jeweiligen Eingabedaten-Dimensionen unkorrelliert aus. Denken Sie hier beispielsweise an Aufgabe 2 aus Lehreinheit 7 — für einige Dimensionen kann man diese Annahme näherungsweise durchaus treffen.
Aufgaben:
In der letzten Lehreinheit haben wir die komplexität eines Netzwerks nachvollzogen, indem wir die „Entscheidungszellen“ der jeweiligen Layer visualisiert haben.
In dieser Aufgabe möchten wir verstehen was die Aktivierungen tatsächlich mit der Eingabe tun (und so die in der Vorlesung vorgestellte Visualisierung nachimplementieren).
Wir möchten uns anschauen, was die ReLU nicht-linearität eigentlich mit dem Eingaberaum anstellt. (Auch hier nutzen wir die Darstellbarkeit des Eingaberaumes aus.) Da eine Abbildung wie \(y = x \cdot W + b\) lediglich einen affinen Basiswechsel darstellt, analysieren wir was es bedeutet diesen Basiswechsel nach der Nicht-linearität rückgängig zu machen: \[ \begin{aligned} y & = \text{relu} \left( x\cdot W + b \right)\\ \hat x & = (y-b)W^+, \quad \text{ mit } W = WW^+W \end{aligned} \] Die Matrix \(W^+\) ist dabei die pseudoinverse dar (im speziellen die links-pseudoinverse)
Jede Ausgabedimension \(y_i\) kann nun einen der beiden Fällen folgen:
Wir interessieren uns für die Daten abseits einer konkreten Basis. Aus dem Grund definieren wir noch was es heißt, dass zwei Funktionen in der Äquivalenzklasse aller affinen Basen gleich sind.
Definition:
Gleichheit unter einer passenden affinen Basis Wir sagen, zwei Funktionen \(f,g:\mathbb{R}^n\rightarrow \mathbb{R}^m\) sind gleich unter einer passenden afinen Basis, falls für jevde affine Transformation \((V,c)\) auf \(f(x)\) es eine entsprechende affine Transformation \((V',c')\) auf \(g(x)\) gibt, so dass \[
f(x)V + c = g(x)V' + c'
\] gilt.
Theorem:
Angenommen es gelte \(n ≥ m\), wobei \(\mathbb{R}^n\) der Eingaberaum and \(\mathbb{R}^m\) the Ausgaberaum der Funktion \(y = x\cdot W + b\) ist. Dann ist die Aktivierung eines linearen Layers, \(y=\text{relu} \left( x\cdot W + b \right)\) gleich unter einer passenden affinien Basis zur Rückprojektion \(\hat x=(y-b)W^+\).
Aufgaben: