Freitag, 27. Dezember 2024

Die Determinante als gerichtetes Volumen

Die folgenden Bilder und Formeln sind im Wesentlichen den entsprechenden Wikipedia Artikeln entnommen.

Aus dem Physikunterricht ist die rechte Hand Regel bekannt, um die Richtung der Kraft auf einen stromdurchflossenen Leiters in einem Magnetfeld zu bestimmen.

 Für diese Kraft, die auch Lorentzkraft \(\vec{F}_L \) genannt wird, gilt:

$$\vec{F}_L = q (\vec{v} \times \vec{B})$$

Dabei beschreibt \(\vec{v}\) die Geschwindigkeit einer bewegten Ladung und \(\vec{B}\) den Betrag und die Richtung des Magnetfelds. Die Richtung der Kraft ist also davon abhängig ob der Strom eine Leiterschleife mit oder gegen den Uhrzeigersinn durchläuft. In diesem Sinne lässt sich das Vektorprodukt aus zwei Vektoren \(\vec{a}\) und \(\vec{b}\) als gerichtete Fläche interpretieren. Der Betrag von \(\vec{a} \times \vec{b}\) gibt den Flächeninhalt des aufgespannten Parallelogramms an.

Mit dem  Levi-Civita-Symbol schreibt sich das Kreuzprodukt als

$$\vec{a} \times \vec{b} = \sum_{i,j,k=1}^{3} \epsilon_{ijk} a_i b_j \vec{e}_k$$

und mit Hilfe der Regel von Sarrus lässt es sich als Determinante ausdrücken.

$$\vec{a} \times \vec{b} = \det \begin{pmatrix}\vec{e}_1 & a_1 & b_1 \\ \vec{e}_2 & a_2 & b_2 \\ \vec{e}_3 & a_3 & b_3 \end{pmatrix}$$

Gehen wir aus 2 Dimensionen auf 3 Dimensionen über, so wird das von 3 Vektoren aufgespannte Volumen durch das sogenannte Spatprodukt definiert.


$$(\vec{a} \times \vec{b}) \cdot \vec{c} = (\vec{b} \times \vec{c}) \cdot \vec{a} = (\vec{c} \times \vec{a})$$

Das Spatprodukt \((\vec{a},\vec{b},\vec{c})\) ist wie das Vektorprodukt nicht kommutativ. Auch das Spatprodukt kann man mit Hilfe der Determinante ausdrücken. 

 $$(\vec{a},\vec{b},\vec{c}) = \det\begin{pmatrix} a_1 & b_1 & c_1 \\ a_2 & b_2 & c_ 2 \\ a_3 & b_3 & c_3\end{pmatrix} = - \det\begin{pmatrix} b_1 & a_1 & c_1 \\ b_2 & a_2 & c_ 2 \\ b_3 & a_3 & c_3 \end{pmatrix}$$

Bei der Vertauschung zweier Faktoren tritt ein Vorzeichenwechsel auf. Der Wert ändert sich jedoch nicht, wenn man die Faktoren zyklisch vertauscht.

In n-dimensionalen Räumen liefert die Leibniz-Reihe eine Formel zur Berechnung der Determinante, und damit zur Berechnung des Volumens eines höherdimensionalen 'Parallelepipeds', so wie dessen Orientierung. Die Reihe wurde bereits im Jahr 1682 von Gottfried Wilhelm Leibniz entwickelt, und in der Zeitschrift Acta Eruditorum veröffentlicht. Für eine \(n \times n\) Matrix \(A\) gilt:

$$\det A = \sum_{\sigma \in S_n} \left(\operatorname{sgn}(\sigma) \prod_{i=1}^n a_{i, \sigma(i)}\right)$$

Die Summe wird über alle Permutationen \(\sigma\) der symmetrischen Gruppe \(S_n\) vom Grad n berechnet. \(\operatorname{sgn}(\sigma) \) bezeichnet das Signum der Permuation und liefert +1 falls \(\sigma\) eine gerade Permutation ist, und -1 falls sie ungerade ist. 

Den bei Permutation notwendigen Vorzeichenwechsel kann man sich auch anhand von sogenannten Zopfdiagramme aus der Knotentherorie veranschaulichen. Die folgenden Bilder sind einem  YouTube Video von broke math student entnommen.

Das Vorzeichen einer Permutation ist gleich der Anzahl der Vertauschungen, modulo 2, die benötigt werden, um die Indices wieder in die Reihenfolge 1, 2, 3, usw. zu bringen. Die Zahl der Permutationen kann durch Abzählen der Knoten im Zopfdiagramm bestimmt werden. Dabei gilt, dass sich Verbindungslinien nicht auf gleicher Höhe schneiden dürfen, Verbindungslinien sich nicht einfach nur berühren, und sich nicht mehr als 2 Verbindungslinien an einem Punkt kreuzen dürfen. 



Allgemein liefert der Laplacescher Entwicklungssatz eine häufige genutzte alternative Methode die Determinante einer \(n \times  n\) Matrix zu berechen. Es gilt:
$$\det A = \sum_{i=1}^n (-1)^{i+j} \cdot a_{ij} \cdot \det A_{ij}$$
wobei \( A_{ij}\) die \((n-1) \times (n-1)\) Untermatrix von \(A\) ist, die durch Streichen der\(i\) -ten Zeile und \(j\)-ten Spalte entsteht.

Viel Spaß beim Selber Rechnen...

Sonntag, 8. Dezember 2024

Hopfield Netze und Boltzmann Maschinen

Der Physik Nobelpreis 2024 ging an John J. Hopfield und Geoffrey Hinton „für bahnbrechende Entdeckungen und Erfindungen, die maschinelles Lernen mit künstlichen neuronalen Netzen ermöglichen“.

Die Bilder und mathematischen Erläuterungen dieses Artikels sind dem Video A Brain-Inspired Algorithm For Memory entnommen, oder orientieren sich an der Presseerklärung des Nobelpreiskomitees.

John Hopfield erfand 1982 ein Netzwerk, das eine Methode zum Speichern und Wiederherstellen von Mustern liefert. Wir können uns die Knoten des Netzwerks z.B. als Pixel eines Bildes vorstellen, wobei +1 einen schwarzen Pixel und -1 einen weißen Pixel repräsentiert.

Eine bestimmte Konfiguration in diesem Netzwerk wird durch eine Energie beschrieben, die der eines Systems winziger Magneten entspricht. In der Physik spricht man von Ising Spin Modellen. Das Hopfield Netz wird so trainiert, dass Werte für die Verbindungen zwischen den Knoten gefunden werden, die zu minimaler Energie führen. Es gilt:

$$ E= -\sum_{ij} w_{ij} x_i x_j $$

Für Verbindungen zwischen Knoten mit gleichem Vorzeichen muss \(\omega_{ij}\) also > 0 sein und für Verbindungen mit ungleichem Vorzeichen < 0 um die Energie zu minimieren.

Wenn das trainierte Hopfield-Netzwerk mit einem verzerrten oder unvollständigen Bild gefüttert wird, arbeitet es sich methodisch durch alle Knoten und aktualisiert die Werte, sodass die Energie des Netzwerks sinkt. Dazu berechnen wir zunächst für den zu verändernden Knoten \(i\) die gewichtete Summe h über alle Nachbarknoten. Ist \(h_i > 0\) , wird der Knoten auf +1 gesetzt und für \(h_i\ < 0\) auf -1.  

$$ h_i = w_{i1}x_1 + \cdots + w_{iN}x_N = \sum_{j \neq i} w_{ij} x_j $$

Das Netzwerk arbeitet schrittweise, um das gespeicherte Bild zu finden, das dem unvollkommenen Bild, am ähnlichsten ist.

Doch wie sollen die Gewichtungen \(\omega\) gesetzt werden? Soll nur eine einzige Konfiguration \(\zeta\) gespeichert bzw. angelernt werden, gilt ein einfachster Ansatz \(\omega_{ij} = \zeta_i\zeta_j\) . Sollen mehrere sehr unterschiedliche Konfigurationen \(\zeta\) trainiert werden gilt \(\omega_{ij} = \zeta^1_{ij}+\zeta^2_{ij}+...\)


Veranschaulicht man sich dies im Konfigurationsraum wird deutlich, dass die Konfigurationen für diesen Ansatz 'weit' voneinander 'entfernt' sein müssen. Der von Hopfield gewählte Ansatz ist daher noch wenig von praktischem Nutzen, da die maximale Zahl der speicherbaren Muster nur ca. 0.14 mal der Zahl der Knoten oder Neuronen entspricht.

Geoffrey Hinton nutzte das Hopfield-Netzwerk als Grundlage für ein neues Netzwerk, das eine andere Methode verwendet: die Boltzmann-Maschine. Die Boltzmann Maschine kann charakteristische Elemente in den Daten erkennen. Der neue Ansatz besteht darin dem Netzwerk neben den 'sichtbaren' Neuronen sogenannte 'unsichtbare' Neuronen hinzuzufügen. Diese sind im Bild unten violett gefärbt.


Alle Neuronen sind über eine Matrix \(\omega_{ij}\) miteinander gekoppelt. Die Maschine wird trainiert, indem man die sichtbareren Neutronen mit Beispielen füttert, die mit hoher Wahrscheinlichkeit auftreten. Sowohl sichtbare als auch unsichtbare Neuronen werden jedoch neu gesetzt wenn die Maschine läuft. In der statistischen Physik beschreibt die Boltzmann-Verteilung die Wahrscheinlichkeit, dass ein System in einem bestimmten Zustand \(S_E\) mit der Gesamtenergie \(E\) bei einer Temperatur \(T\) ist. Die Formel lautet:

$$p(S_E) = \frac{1}{Z} e^{-E/T} \, \text{;} \quad Z = \sum_{S_j} e^{-E_j/T}$$

Hierbei bezeichnet \(Z\) die Zustandssumme, die alle möglichen Zustände des Systems berücksichtigt.
Für die Wahrscheinlichkeit eines aktivierten Zustand \(S_{on}\) mit \(E_{on}=E-E_{off}\) gilt daher:

$$p_{on}= \frac{1}{Z} e^{-E_{on}/T} = \frac{e^{-E_{on}}}{e^{-E_{on}}+e^{-E_{off}}}= \frac{1}{1+e^{\Delta E}}$$

Dies entspricht der häufig in der Statistik und im maschinellen Lernen verwendeten  Sigmoid Aktivierungsfunktion.
Definieren wir die Energie des Systems ähnlich wie in Hopfield Netzwerken durch

$$ E= -\sum_{ij} w_{ij} x_i x_j $$

so stellt sich erneut die Frage, wie \(\omega_{ij}\) während des Trainingsvorgangs gesetzt bzw. aktualisiert werden soll. Dazu betrachten wir zunächst den Logarithmus der Wahrscheinlichkeit \(P(S)\) eines Datensatzes \(S\) mit Einzeldatensätzen \(x^{(n)}\). Es gilt:

\begin{eqnarray}\log P(\text{S}) & = & \log \left[ \prod_{n=1}^{N} P(x^{(n)}) \right]= \sum_{n=1}^{N} \log P(x^{(n)}) \\ & = & \sum_{n=1}^{N} \log \left( \frac{1}{Z} e^{-E(x^{(n)})/T} \right) \\ & = & \sum_{n=1}^{N} \left( \frac{-E(x^{(n)})}{T} - \log Z \right) \\ & = & -\frac{1}{T} \sum_{n=1}^{N} E(x^{(n)}) - N \log Z\end{eqnarray}

Die Schritte \(\Delta\omega_{ij}\) mit denen sich \(\omega_{ij}\) während des Trainings ändern soll, seien wie folgt definiert:

\[\Delta\omega_{ij} = \frac{\partial \log P(\text{S})}{\partial w_{ij}} = \frac{1}{T} \sum_{n=1}^{N} \frac{\partial E(x^{(n)})}{\partial w_{ij}} - N \frac{\partial \log Z}{\partial w_{ij}}\]


Dadurch ist gewährleistet dass sich \(\omega_{ij}\) bei Erreichen von hoher Wahrscheinlichkeit bzw niedriger Gesamtenergie nicht mehr ändert.

Der erste Term lässt sich zu

\[\frac{\partial}{\partial w_{ij}} E(x^{(n)}) = \frac{\partial}{\partial w_{ij}} \left[ - \sum_{ij} w_{ij} x_i x_j \right] = - x_i x_j\]

vereinfachen, und für den zweiten Term gilt:

\begin{eqnarray}\frac{\partial}{\partial w_{ij}} \log Z & = &\frac{1}{Z} \frac{\partial}{\partial w_{ij}} \left[ \sum_{s} e^{-E(s)} \right]= \frac{1}{Z} \sum_{s} \left[ e^{-E(s)} \frac{\partial}{\partial w_{ij}} [-E(s)] \right] \\ & = & \frac{1}{Z} \sum_{s} e^{-E(s)} x_i^s x_j^s = \sum_{s} \frac{e^{-E(s)}}{Z} x_i^s x_j^s =  \sum_{s} p(s) x_i^s x_j^s\end{eqnarray}

Ingesamt gilt also:

\[\Delta\omega_{ij}=\frac{\partial \log P(\text{data})}{\partial w_{ij}} = \frac{1}{T} \left( \sum_{n=1}^{N} (x_i x_j)_{\text{data}} - N \sum_{s} p(s) x_i^s x_j^s \right)\]

oder prägnanter:

\[\Delta\omega_{ij} \propto \, \langle x_i x_j \rangle_N - \langle p(s)x_i x_j \rangle_S\]

Dabei bezeichnet der erste Term, der auch Hebbian Term genannt wird, den Mittelwert eines Neuronenpaares \(x_i x_j\) über alle \(N\) Neuronen des Trainingsdatensatzes, und der zweite Term, der Anti Hebbian Term genannt wird, den Mittelwert über alle Zustände die mit der Wahrscheinlichkeit \(p(s)\) eingenommen werden. Während des Trainings der Boltzmann Maschine wird die Kopplung zwischen den Neuronen also so lange verändert, bis der Gleichgewichtszustand, der sich idealerweise ergibt wenn die Boltzmann Maschine läuft und die Neuronen ändert, nicht mehr vom Trainingsdatensatz unterscheidet.


Auf Grund der Tatsache, dass beim Training und Betrieb einer klassischen Boltzmann Maschine alle Neuronen einzeln durchlaufen werden müssen, sind klassische Boltzmann Maschinen recht langsam. Die sogenannte 'Restricted Boltzmann Machine' (RBM) schafft hier Abhilfe.


Bei der RBM bestehen keine Verbindungen zwischen den einzelnen sichtbaren und den einzelnen unsichtbaren Neuronen mehr. Das Netzwerk zerfällt in 2 Schichten. Dadurch können die sichtbaren und unsichtbaren Neuronen jeweils als Vektor betrachtet werden und in einem Rechenschritt aktualisiert werden.

Machines' die Energie eines solchen Systems durch

$$E(\mathbf{v}, \mathbf{h}) = - \sum_{i \in \text{visible}} a_i v_i - \sum_{j \in \text{hidden}} b_j h_j - \sum_{i,j} v_i h_j w_{ij}$$

beschrieben.
Der Ausdruck deckt sich im Wesentlichen mit dem für klassische Boltzmann Maschinen, wird aber durch  Variablen \(a\) für sichtbare und \(b\) für unsichtbare Neuronen in entsprechenden Zusatztermen ergänzt.
Nach Asja Fischer und Christian Igel sind die Wahrscheinlichkeit p(h) ein verstecktes Neuron zu setzen gegeben durch

$$\begin{align*} p(H_i = 1 \mid \mathbf{v}) &= \text{sig} \left( \sum_{j=1}^{m} w_{ij} v_j + a_i \right) \\ p(V_j = 1 \mid \mathbf{h}) &= \text{sig} \left( \sum_{i=1}^{n} w_{ij} h_i + b_j \right) \end{align*}$$

Auch eine RBM wird durch die sogenannte Contrastive Divergence (CD) trainiert. Wie bereits für klassische Boltzmann Maschinen beschrieben, versucht man den Gradienten der Energiefunktion oder der Wahrscheinlichkeit eines finalen Zustandes zu schätzen. Konkret macht man das bei einer RBM in dem man aus der Datenverteilung und ihrer Umkehrung sampelt. Dies umfasst die folgenden Schritte:
  1. Positive Phase: Beginne mit einer Datenprobe und berechne die versteckten Aktivierungen.
  2. Negative Phase: Rekonstruiere die sichtbaren Einheiten aus den versteckten Aktivierungen und berechne dann erneut die versteckten Aktivierungen aus diesen Rekonstruktionen.
  3. Gewichtsaktualisierung: Passe die Gewichte basierend auf dem Unterschied zwischen der Datenverteilung und der rekonstruierten Verteilung an.

Im Oktober 2006 hat Netflix einen offenen Wettbewerb für den besten kollaborativen Filteralgorithmus zur Vorhersage von Nutzerbewertungen für Filme, basierend auf früheren Bewertungen ausgeschrieben.
Gewonnen hat den ausgeschriebenen Preis letztlich im September 2009 die BellKor Solution unter Verwendung einer Restricted Boltzmann Machine.

Ich habe versucht Ähnliches zu programmieren, und unter Verwendung von gewöhnlichen Python Bibliotheken eine RBM erstellt. Der vollständige Python Code kann auf Kaggle als Notebook  ausprobiert werden. Die verwendeten Datensätze wurden über die MovieLens-Website während des siebenmonatigen Zeitraums vom 19. September 1997 bis zum 22. April 1998 gesammelt, und vom GroupLens Research Project an der University of Minnesota zusammengestellt. Insgesamt besteht der Datensatz aus 100.000 Bewertungen (1-5) von 943 Nutzern für 1682 Filme. Jeder Nutzer hat mindestens 20 Filme bewertet. Darüber hinaus sind einfache demografische Informationen der Nutzer (Alter, Geschlecht, Beruf, Postleitzahl) enthalten.

Im ersten Schritt werden die nötigen Bibliotheken, sowie die Trainings- und die Testdatensätze geladen. Dann werden die Datensätze in die gewünschte Form gebracht, und letztlich in einem Torch Tensor abgelegt. Die ursprünglichen Bewertungen der Filme reichen von 0 - 3 Sternen. Wir verwenden der Einfachheit halber nur '-1' für 'not rated' , '0' für 'not liked' und '1' für 'liked'.
# ### Importing the libraries

import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.optim as optim
import torch.utils.data
from torch.autograd import Variable

# ### Importing the dataset and preparing the training set and the test set

training_set = pd.read_csv('/kaggle/input/movielens-100k-dataset/ml-100k/u1.base', delimiter = '\t')
training_set = np.array(training_set, dtype = 'int') # format of training_set is user id | movie id | rating | timestamp

test_set = pd.read_csv('/kaggle/input/movielens-100k-dataset/ml-100k/u1.test', delimiter = '\t')
test_set = np.array(test_set, dtype = 'int')

# ### Getting the number of users and movies

nb_users = int(max(max(training_set[:, 0], ), max(test_set[:, 0]))) # Calculate number of users

nb_movies = int(max(max(training_set[:, 1], ), max(test_set[:, 1]))) # Calculate number of movies

# ### Converting the data into an array with users in lines and movies in columns

def convert(data):
    new_data = []
    for id_users in range(1, nb_users + 1):
        id_movies = data[:, 1] [data[:, 0] == id_users] # Create boolean mask for all lines in 1st column matching user_id and transfer data of 2nd column to id_movies vector  
        id_ratings = data[:, 2] [data[:, 0] == id_users]
        ratings = np.zeros(nb_movies)
        ratings[id_movies - 1] = id_ratings
        new_data.append(list(ratings))
    return new_data # The reason why we make a list within a list is that torch wants the data in this structure

training_set = convert(training_set)
test_set = convert(test_set)

# ### Converting the data into Torch tensors

training_set = torch.FloatTensor(training_set)
test_set = torch.FloatTensor(test_set) # Tensors are simply an multidimensional array containing elements of a single data type. They can be processed in gpu or tpu (Tensor Processing Unit). Tensorflow also has a tensor structure, but Pytorch is easier to implement.

# ### Converting the ratings into binary ratings 1 (Liked) or 0 (Not Liked)

training_set[training_set == 0] = -1 # not rated
training_set[training_set == 1] = 0 # not liked
training_set[training_set == 2] = 0 # not liked
training_set[training_set >= 3] = 1 # liked
test_set[test_set == 0] = -1
test_set[test_set == 1] = 0
test_set[test_set == 2] = 0
test_set[test_set >= 3] = 1

Dann definieren wir die Struktur des Netzwerks. Wir führen 100 zusätzliche unsichtbare Neuronen ein,

# ### Creating the architecture of the Neural Network

class RBM():
    def __init__(self, nv, nh):
        self.W = torch.randn(nh, nv)
        # Initializing the weight between nodes. Initial weight is according to the normal distribution in dimensions nh and nv. 
        self.a = torch.randn(1, nh) 
        # Initial bias value a for the visable node. 1 row and nh columns. Each element is a random number drawn from a standard normal distribution 
        self.b = torch.randn(1, nv)
        # Initial bias value b for the hidden node
    def sample_h(self, x):
        # Sample_h will activate hidden nodes for certain probabilty using Boltzmann distribution
        wx = torch.mm(x, self.W.t())
        # Multiplying the two tensors x and W transposed.
        activation = wx + self.a.expand_as(wx) # Adding bias a
        p_h_given_v = torch.sigmoid(activation)
        # Using sigmoid to calculate probability whether to activate the hidden node or not.
        return p_h_given_v, torch.bernoulli(p_h_given_v)
        # Values of the hidden nodes are retuned in 2nd vector. Bernoulli function is used to set the values according to probability p_h (given v).
    def sample_v(self, y):
        wy = torch.mm(y, self.W)
        # Multiplying the two tensors y and W.
        activation = wy + self.b.expand_as(wy) # Adding bias b
        p_v_given_h = torch.sigmoid(activation) # Activation
        return p_v_given_h, torch.bernoulli(p_v_given_h)
    def train(self, v0, vk, ph0, phk):
        # Next, contrastive divergence will be performed.
        # v0 is the input vector for visbile nodes, vk is the result after k samplings, ph0 is the probability vector of the hidden, phk is the probability vector after k samplings
        self.W += (torch.mm(v0.t(), ph0) - torch.mm(vk.t(), phk)).t()
        # Updating the weight
        self.b += torch.sum((v0 - vk), 0)
        # Updating b, 0 is only added to make the size 2
        self.a += torch.sum((ph0 - phk), 0)
        # Updating a
        
nv = len(training_set[0])# Number of visible nodes
nh = 100 # Define number of hidden nodes
batch_size = 100 # Define batch size for training
rbm = RBM(nv, nh)

und trainieren das Netzwerk in 10 Durchläufen.
# ### Training the RBM

nb_epoch = 10 # Define number of complete passes through entire training data set
for epoch in range(1, nb_epoch + 1):
    train_loss = 0
    s = 0.
    for id_user in range(0, nb_users - batch_size, batch_size):
    # Training in batches. This training can also be done individually
        vk = training_set[id_user : id_user + batch_size]
        v0 = training_set[id_user : id_user + batch_size]
        ph0,_ = rbm.sample_h(v0)
        # Calculate initial probabilty to set hidden nodes
        for k in range(10):
        # Gibss sampling (markov chain monte carlo) for contrastive divergence.
            _,hk = rbm.sample_h(vk)
            # set the hidden node according to Bernoulli distribution
            _,vk = rbm.sample_v(hk)
            # set the visible node 
            vk[v0<0] = v0[v0<0] # transfer -1 (unrated) from v0 to vk
        phk,_ = rbm.sample_h(vk)
        # Probability to set hidden node after k training steps.
        rbm.train(v0, vk, ph0, phk)
        # Update W, a and b 
        train_loss += torch.mean(torch.abs(v0[v0 >= 0] - vk[v0 >= 0]))
        # calculate 'training loss' by taking the difference between the initial value and the subsequent values for ratings == 1.
        s += 1.
    print('epoch: '+str(epoch)+' loss: '+str(train_loss/s)) # Print normalised loss value

Wenn wir das Netzwerk mit einem anderen Testdatensatz überprüfen,
# ### Testing the RBM with a different dataset (test data set)

test_loss = 0
s = 0.
for id_user in range(nb_users):
    v = training_set[id_user:id_user+1]
    vt = test_set[id_user:id_user+1]
    # Save training_set and test_set of user 'id_user' as 2D array in v and vt
    if len(vt[vt>=0]) > 0:
        _,h = rbm.sample_h(v) # flip hidden neurons according to trained probability based on W and a
        _,v = rbm.sample_v(h) # flip visible neurons based on W and b
        test_loss += torch.mean(torch.abs(vt[vt>=0] - v[vt>=0]))# calculate the mean value of changes for visible neurons rated with 3 in test dataset
        s += 1.
print('test loss: '+str(test_loss/s)) # print out normlized average change when using test data set instead of training set 

erzielen wir nach 10 kompletten Durchläufen ca. 25% Übereinstimmung mit dem Trainingsdatensatz. 


Die eigentliche Stärke der RBM liegt aber darin generativ zu sein. Wir können uns für einen bestimmten Nutzer Filmvorschläge basierend auf seinen bisherigen Bewertungen machen lassen. In die Vorschläge gehen durch das Training des Netzwerkes natürlich auch die Bewertungen anderer Nutzer ein, die ähnliche Filme angeschaut haben.
# # Proposing new movies for a specific user based on his/her presvious ratings

user_ID=67 # select user_ID

# load dataset containing list of: movie id | movie title | release date | video release date | IMDb URL | unknown | Action | Adventure | Animation | Children's | Comedy | Crime | Documentary | Drama | Fantasy |Film-Noir | Horror | Musical | Mystery | Romance | Sci-Fi |Thriller | War | Western
movie_set = pd.read_csv('/kaggle/input/mymod-dataset/u3.item', delimiter = '|')
movie_set = np.array(movie_set)

v = training_set[user_ID:user_ID+1] # select vector of user in training set 
v0=v # store original vector in v0
if len(v[v>=0]) > 0:
    _,h = rbm.sample_h(v) # flip h
    _,v = rbm.sample_v(h) # flip v
    v[v0<0] = v0[v0<0] # keep non rated or non liked moves as non raeted or non liked
#print rated movies
pos = (v0[0]== 1).nonzero(as_tuple=True) #select positions in training set rated w/ 3 stars
movies = [movie_set[i][1] for i in pos[0]] #look up in position in movie_set
print ("rated movies:")
for movie in movies:
    print(movie)
dv=v-v0 # calculate changes in rating 
#print proposals for new movies
pos = (dv[0]== 1).nonzero(as_tuple=True) #select positions in training set rated w/ 3 stars
movies = [movie_set[i][1] for i in pos[0]] #look up in position in movie_set
print ("\n"+"new proposed movies:")
for movie in movies:
    print(movie)

Die folgenden Vorschläge werden z.B. Nutzer 67 auf Grund seiner bisher 'gelikten' Filme gemacht.



Viel Spaß beim selber Programmieren...

Mittwoch, 20. November 2024

Eine selbstgebaute Baseball Wurfmaschine

Unser Sohn ist ein großer Baseball Fan. In den letzten Monaten sind daher ein Batting Cage fürs Schlagtraining im Carport entstanden,  sowie ein 'Screen' um die Bälle sicher zuwerfen zu können, und nun auch eine kleine Pitching Maschine.

Für die Wurfmaschine habe ich einen kleinen 230 V Elektromotor auf ein Holzbrettchen geklemmt. Der Motor treibt eine Kunststoffrolle mit einem Durchmesser von 10 cm, und läuft mit 6000 rpm, so dass sich theoretisch Ballgeschwindigkeiten von 113 km/h oder ca. 70 mph erzielen lassen. Dies entspricht zwar nicht den Geschwindigkeiten mit denen Profis der MLB werfen, ist aber ziemlich genau die Wurfgeschwindigkeit eines trainierten 14-Jährigen. Um den Anpressdruck der Rolle auf den Ball einzustellen, habe ich Kunststoffplatten unter den Motor geklemmt, bis die Bälle gut ausgeworfen werden.

Ein Abflussrohr mit 75 mm Durchmesser sorgt dafür das die Bälle in die gewünschte Richtung geschossen werden. Wir verwenden weiße Kunststoff Wiffle Bälle von Franklin. 

Diese passen genau in das Rohr und sind leicht deformierbar. Damit die Bälle am Aufsetzpunkt der Rolle nicht verkanten, habe ich das Abflussrohr seitlich geschlitzt. Die Maschine ist auf ein kleines Tischchen hinter den 'Screen' geklemmt.

Und so sieht die Maschine dann in Aktion aus.

Viel Spaß beim selber Basteln...


Dienstag, 8. Oktober 2024

Erzeugung und Verteilung von Quantenschlüsseln

Am 16.August 2024 hat eine Falcon 9 Rakete der Firma Space X den Kleinsatelliten QUBE in den Orbit gebracht.

Mit QUBE können sogenannte Quantenschlüssel zwischen Satelliten und Bodenstationen ausgetauscht werden, um damit abhörsichere Kommunikation zu realisieren. 


Durch den Einsatz mehrerer solcher Kleinsatelliten könnte eine weltweite, sichere Kommunikation möglich werden, ohne auf große Glasfasernetze zurückgreifen zu müssen.  

Der QUBE-Satellit verwendet das BB84-Protokoll für die Quantenschlüsselverteilung oder Quantum Key Distribution (QKD). Dieses Quantenschlüsselverteilungsschema wurde bereits 1984 von Charles Bennett und Gilles Brassard entwickelt, und basiert auf der Polarisation von Photonen und der Tatsache, dass jede Messung eines Quantenzustands diesen verändert.

Der erste Schritt in BB84 ist eine Quantenübertragung. Der Sender Alice erstellt ein zufälliges Bit (0 oder 1), und wählt dann zufällig eine ihrer beiden Basen (in diesem Fall geradlinig oder diagonal) aus, um es zu übertragen. 

Anschließend bereitet sie einen Photonenpolarisationszustand vor, der sowohl vom Bitwert als auch von der Basis abhängt, wie in der obenstehenden Tabelle gezeigt. So ist z.B. eine 0 in der geradlinigen Basis (+) als vertikaler 0°-Polarisationszustand kodiert, und eine 1 ist in der diagonalen Basis (x) als 135°-Zustand. 
Alice sendet dann ein einzelnes Photon, wobei Alice den Zustand, die Basis und die Zeit jedes gesendeten Photons aufzeichnet.
Da der Empfänger Bob die Basis nicht kennt, in der die Photonen kodiert wurden, kann er nur zufällig eine Basis auswählen, in der er messen möchte, entweder geradlinig oder diagonal. Er tut dies für jedes Photon, das er empfängt, und zeichnet ebenfalls die Zeit, die verwendete Messgrundlage und das Messergebnis auf. 
Spielerisch lassen sich die Vorgänge gut mit Quantum Flytrap, einer No-Code-IDE für Quantencomputing, überprüfen.


Der Screenshot oben zeigt ein Virtual Lab, in dem Alice und Bob ihre Basiszustände mit Faraday Rotatoren einstellen. In der Simulation lässt sich, unter Anderem, die Zahl der durchzuführenden Messungen einstellen, und die Ergebnisse können als *.csv Datei exportiert werden.


Nachdem Bob alle Photonen gemessen hat, kommuniziert er mit Alice über einen öffentlichen klassischen Kanal. Alice sendet die Basis, nicht aber die Bits, in der jedes Photon eingesendet wurde, und Bob die Basis, in der jedes Photon gemessen wurde. Beide verwerfen Photonenmessungen (Bits), bei denen Bob eine andere Basis als Alice verwendet hat. Diese sind im Beispiel oben rot markiert. 
Um zu überprüfen, ob ein Lauscher vorhanden ist, vergleichen Alice und Bob nun eine vorgegebene Teilmenge ihrer verbleibenden Bitzeichenfolgen. Wenn eine dritte Partei (normalerweise als Eve bezeichnet, für "Lauscherin") Informationen über die Polarisation der Photonen erhalten hat, führt dies zu Fehlern in Bobs Messungen, und die gemessenen Bits stimmen nicht mit den gesendeten überein.
War kein Lauscher vorhanden bilden die nicht verglichenen Bits den Schlüssel. 
Die eigentliche Nachricht kann anschließend z.B. mit Hilfe der One Time Pad Verschlüsselung (OTP) codiert werden. Dafür ist es notwendig, dass ein Schlüssel verwendet wird, der mindestens so lang wie die Nachricht ist.



Nachricht und Schlüssel werden miteinander durch eine Exklusiv Oder Operation verknüpft. Das OTP ist informationstheoretisch sicher und kann nachweislich nicht gebrochen werden.

Viel Spaß beim Spielen mit der Quantenkryptographie...

Samstag, 31. August 2024

Ein Untergestell für die Drechselbank

Meine Frau hat sich zum Geburtstag eine Drechselbank gewünscht. Zum Einstieg in dieses neue Hobby fiel meine Wahl auf die PDB 100 A1 Drechselbank von Parkside. Die Maschine verfügt über eine Motorleistung von 400W, und durch Umlegen eines Keilriemens lassen sich Drehzahlen zwischen 890, 1260, 1760 und 2600 U/min wählen. Das Bankbett lässt sich von 50 cm auf 1 m verlängern, so dass auch größere Werkstücke verarbeitet werden können.


Ich habe die Maschine auf eine Arbeitsplatte aus Eiche montiert, und dieses auf ein kleines Untergestell geschraubt, das ich an der Wand befestigt habe. An der rechten Seitenwand ist eine Halterung für die Drechseleisen vorgesehen. 
Im Zuge des weiteren Ausbaus habe ich noch ein horizontales Brett in das Untergestell eingefügt, um eine zusätzliche Ablage für Werkzeug zu haben.


Das Bankbett der Maschine habe ich durch 3 mm starkem Flacheisen verstärkt. Das Flacheisen ist an mehreren Stellen mit der Maschine verschraubt, und verhindert ein Durchbiegen des gefalzten Bleches beim festen Einspannen der Werkstückes mit dem Reitstock. 


Zum Schärfen der Drechseleisen habe ich eine Nassschleifmaschine TIGER 2000S von Scheppach erworben.


Die Maschine wurde mit etlichem Zubehör geliefert. Um aber insbesondere die für das Langholzdrechseln wichtige Schruppröhre schärfen zu können, habe ich zusätzlich in die Drechselmesser-Schleifführung TWSTGJ von Triton investiert. Diese erlaubt durch Kippen der Rundröhren um einen fixen Auflagepunkt den sogenannten Fingernagelschliff.

Viel Spaß beim selber Drechseln...

Montag, 19. August 2024

Eine sprachgesteuerte Balleinwurfmaschine für den Kickerkasten

Ein Tischfußball begeisterter Freund hat mich auf die Idee gebracht eine Einwurfmaschine für den Kickerkasten zu bauen. Der Ball soll auf einen Sprachbefehl hin eingeworfen werden, und die Maschine soll an verschiedenen Stellen im Spielfeld platziert werden können.

Im Bild oben ist meine Realisierung zu sehen. Eine gedruckte Scheibe, mit einer Tasche für den Ball, dreht sich in einem Kunststoff Topf. Der Ball wird während einer Umdrehung der Scheibe aufgenommen und fällt dann dann durch ein Loch im Boden des Topfes. Eine röhrenförmige Barriere verhindert, dass mehr als ein Ball durch das Loch fallen kann. In der Ansicht von unten erkennt man, dass der Ball durch ein gewinkeltes Stück Abflussrohr mit dem Durchmesser von 50 mm nach vorne ausgeworfen wird.

Als Antrieb habe ich einen 12V Schneckengetriebemotor mit 30rpm verbaut.

Laut Herstellerangaben verfügt dieser über ein maximales Drehmoment von 10 kg x cm, und dreht sich daher auch in einem mit Bällen voll gefüllten Topf gut.

Die gedruckte Scheibe mit der Tasche besteht aus zwei Teilen. Der untere Teil

enthält die Aufnahme für die Motorachse und Abstandshalter. Der obere Teil besteht aus einem planen Deckel, der sich auf die Abstandshalter schrauben lässt. 

Der Kunststofftopf mit den Bällen ist auf laminierte Sperrholzplatten der Stärke 18mm geschraubt, die sich auf die Stangen des Kickertisches stellen lassen. 

Im Bild oben ist rechts unten bereits die Steuerungseinheit der Maschine und ein Konferenz Mikrofon zu erkennen. Das gedruckte Gehäuse der Steuerung besteht aus Deckel und Boden,

und enthält neben einem Raspberry Pi 3 Modell B+ auch ein Relais, das mit 3V ansteuerbar ist, und Ströme bis zu 10A schalten kann. 

Das im Bild oben erkennbare USB Dongle Mikrofon mit einer Empfindlichkeit von -58 dB laut Herstellerangabe, habe ich später durch ein Konferenz-Mikrofon mit einer Empfindlichkeit von -45 dB ersetzt, um die Spracheingabequalität zu verbessern.

Für die Sprachsteuerung habe ich das Open-Source Software Toolkit VOSK von Aphacephei genutzt. Es unterstützt mehr als 20 Sprachen und Dialekte, darunter Englisch und Deutsch. Die kleinen Sprachmodelle, die sich auf dem Github repository von Alphacephei finden, laufen offline auch auf Geräten wie dem Raspberry Pi.  

Eine Einführung in des  Toolkit findet sich auf der Alphacephei website. Nach der Installation mit

pip3 install vosk

habe ich das Sprachmodell 'vosk-model-small-de-0.15.zip' durch

git clone https://github.com/alphacep/vosk-api

cd vosk-api/python/example

wget https://alphacephei.com/vosk/models/vosk-model-small-de-0.15.zip

unzip vosk-model-small-de-0.15.zip

mv vosk-model-small-de-0.15/ model

aus dem repository geladen und entpackt.

Die Ablaufsteuerung erfolgt in Python. Der folgende Code wurde mit dem Syntax Highlighter highlight.hohli von Anton Shevchuk für den Blog aufbereitet, und mit 

<div style="background: rgb(255, 255, 255); overflow: auto; width: auto;"></div> 

ergänzt, um horizontales scrolling des Abschnitts zu gewährleisten.

import queue
import json
import sounddevice as sd
import vosk
import time
import gpiozero
import sys

q = queue.Queue()
STARTCODEs = ["einwurf", "ein wurf", "ein uhr", "ein ver", "nun eingabe", "eingabe", "ein einwurf", "ein", "ein waffen", "nun ein nur", "nun einwurf", "eingabe", "mehr", "auch ein mensch", "einwirken", "nun ein wolf", "eingriff"]

def callback(indata, frames, time, status):
    #"""This is called (from a separate thread) for each audio block."""
    if status:
        print(status, file=sys.stderr)
        pass
    q.put(bytes(indata))

if __name__ == '__main__':

    #Relais initialisieren
    red_led = gpiozero.LED(4)
    green_led = gpiozero.LED(17)

    # Speech Objekt erstellen
    model = vosk.Model('/home/pi/vosk-api/python/example/model')
    
    #Stream vom Mikrofon öffnen
    with sd.RawInputStream(samplerate=44100, blocksize=16000, device=None,dtype='int16',
                            channels=1, callback=callback):
        #Sternchen zeichnen
        #print('*' * 80)
        #Programmstart mit roter LED anzeigen
        red_led.on()
        
        # Aktivierung der vosk Spracherkennung mit Übergabe des geladenen Models. Übersetze das Gesprochene in Text.
        rec = vosk.KaldiRecognizer(model, 44100)
        while True:
            # Daten aus der Queue ziehen
            data = q.get()
            #print("Bitte sprechen!")
            if rec.AcceptWaveform(data):
                # wandle das gesprochene Wort in jason (JavaScript Object Notation) 
                res = json.loads(rec.Result())
                # und gib es aus
                print(res)
                # wenn der Aktivierungscode herausgehört wurde, wird das Relais geschaltet
                for wort in STARTCODEs:
                    if wort == res['text']:
                        green_led.on()
                        red_led.off()
                        # Relais 1 Sekunden anziehen
                        time.sleep(1)
                        green_led.off()
                        red_led.on()

            else:
                pass

Nach Laden der vosk Bibliothek wird im Hauptteil des Programms beim Start die rote Status LED gesetzt. Mit der 'sounddevice' Funktion 'RawInputStream' werden Aufnahmestückchen einer bestimmten Länge in eine 'queue' geschrieben.  Werden die Stückchen vom Modell als 'waveform' akzeptiert, werden sie transkribiert und mit den in 'STARTCODES' definierten Begriffen verglichen. Wenn der richtige Aktivierungscode herausgehört wurde, zieht das Relais für 1.3 Sekunden an, und die grüne LED wird gesetzt. Die Drehscheibe vollführt etwas mehr als eine Umdrehung, und wirft einen Ball aus. Danach erlischt die grüne LED, und die rote LED wird wieder gesetzt. Das System ist bereit für  das nächste Aktivierungswort. 

Damit das Programm nach dem Booten direkt startet, muss mit 'systemd' ein Service gestartet werden. Eine detaillierte Anleitung wie ein solcher 'Service' erzeugt werden kann findet sich z.B. hier.

Mein '/etc/systemd/system/AutoRunEinwurf_Service' file sieht so aus.

Zum bequemen Programmieren via Remote Desktop vom Laptop aus, habe ich diesesmal VNC statt X11 benutzt. Der VNC Server wird von Raspberry Pi OS standardmäßig unterstützt, muss aber aktiviert werden. Um des zu tun kann man raspi-config auf der Befehlszeile mit

sudo raspi-config

öffnen. Anschließend muss man zu 'Schnittstellenoptionen' navigieren, VNC auswählen und die bestätigen.

Als Client auf dem Laptop habe ich tigervnc heruntergeladen. Nach der Installation kann man sich wie gewohnt durch Eingabe der IP Adresse des Raspberry

auf den Desktop des kleinen Rechners einloggen.

Zu guter Letzt möchte ich die Maschine noch in Aktion zeigen.


Viel Spaß beim selber Basteln...

Dienstag, 12. März 2024

Die Maxwellgleichungen anschaulich verstehen

Die Maxwellgleichungen beschreiben die grundlegenden physikalischen Gesetze im Zusammenhang mit Elektrizität und Magnetismus. 

James Clerk Maxwell

Die mathematische Formulierung  erscheint zunächst schwierig, die Gleichungen lassen sich aber anschaulich verstehen.

\[\begin{flalign} & (1) \qquad \operatorname{div}\mathbf{E} = \frac{\rho}{\varepsilon_0} \\ & (2) \qquad \operatorname{div} \mathbf{B} = 0 \\ & (3) \qquad \operatorname{rot} \mathbf{E} = -\frac{\partial \mathbf{B}}{\partial t} \\ & (4) \qquad \operatorname{rot} \mathbf{B} = \mu_0 \left(\mathbf{J} + \varepsilon_0\frac{\partial \mathbf{E}}{\partial t} \right) \end{flalign}\]

Bertrachten wir zunächst die erste Gleichung, das sogenannte Gaußsche Gesetz.

  \[\operatorname{div} \mathbf{E} = \frac{\rho}{\varepsilon_0}\]

Die Divergenz \(\operatorname{div}\) eines Vektorfeldes \(\mathbf{E}\) ist dabei als Skalarprodukt des Nabla Operators \( \nabla = \left( \frac{\partial }{\partial x}, \frac{\partial }{\partial y}, \frac{\partial }{\partial z}\right)\) mit dem Feld \(\mathbf{E}\), also als \(\nabla \cdot \mathbf{E}\) definiert.


Entfernen wir uns im zwei dimensionalen Raum einen kleinen Schritt von der Position \(\left(x_0,y_0\right)\), so zeigt das Vektorfeld in der Regel in eine andere Richtung. Der rote Vektor im Bild oben ist der Differenzvektor.


Betrachten wir alle Differenzvektoren in der Umgebung von \(\left(x_0,y_0\right)\), und mitteln über die Skalarprodukte aus Abstandsvektor und Differenzvektor, so ergibt sich im Grenzübergang zu sehr kleinen Abständen \(\left( \partial / \partial x, \partial / \partial y\right) \cdot \left(E_x, E_y\right) \). Da sich das Skalarprodukt aus dem Produkt der Beträge der Vektoren mal dem Cosinus des Zwischenwinkels errechnet, wird es maximal wenn Abstandsvektor und Differenzvektor in die gleiche Richtung zeigen. Der Ausdruck \(\nabla\cdot \mathbf{E}\) beschreibt also wieviel 'Feld' aus dem Punkt \(\left(x_0,y_0\right)\) gemittelt über alle Richtungen 'herausfließt'. Das Gaußsche Gesetzt sagt nun, dass die 'Menge des herausfließenden elektrischen Feldes' proportional zur Punktladungsdichte ist. Punktladungen sind also die Quellen und Senken des elektrischen Feldes.


Die zweite Gleichung 

\[\operatorname{div} \mathbf{B} = 0\]

wird häufig auch als Gaußsches Gesetz des Magnetismus bezeichnet. Die Divergenz der magnetischen Flussdichte \(\mathbf{B}\) ist Null. Es gibt keine Quellen oder Senken, d.h. keine magnetischen Monopole, die analog zu elektrostatischen Ladungen wirken würden. Das magnetische Feld verhält sich wie eine nicht komprimierbare Flüssigkeit. 


Die dritte Gleichung

   \[\operatorname{rot} \mathbf{E} = -\frac{\partial \mathbf{B}}{\partial t}\]

wird als Faradaysches Induktionsgesetzt bezeichnet.
Als Rotation \(\operatorname{rot}\) des Vektorfeldes \(\mathbf{E}\) bezeichnet man dabei das Vektorprodukt des Nabla Operators \( \nabla = \left( \frac{\partial }{\partial x}, \frac{\partial }{\partial y}, \frac{\partial }{\partial z}\right)\) mit dem Feld \(\mathbf{E}\), also \(\nabla \times \mathbf{E}\).


Im Gegensatz zum Skalarprodukt, ist das Vektorprodukt ein Maß dafür, ob zwei Vektoren zu einander senkrecht stehen. Sein Betrag errechnet sich als Produkt der Beträge, mal dem Sinus des Zwischenwinkels. Der Betrag des Vektorprodukts wird also für einen Zwischenwinkel von 90° maximal. Betrachten wir wieder die Differenzvektoren in einigem Abstand von \(\left(x_0,y_0\right)\), so entspricht die Rotation \(\operatorname{rot}\) dem Grenzwert des Mittelwertes aus Abstandsvektoren \(\times\) Differenzvektoren für kleine Abstände im Übergang zum Punkt \(\left(x_0,y_0\right)\). Das Induktionsgesetz besagt nun, dass eine zeitliche Änderung des Magnetfeldes bzw. der magnetischen Flussdichte zu einer Rotation des elektrischen Feldes führt. Anders ausgedrückt resultiert also die zeitliche Änderung des durch eine Leiterschleife eingeschlossenen magnetischen Flusses in einer Spannung an den Enden der Leiterschleife. Die induzierte Spannung wirkt der Änderung entgegen. Das Bild unten von Michael Lenz veranschaulicht dies.


Die vierte Gleichung

\[ \operatorname{rot} \mathbf{B} = \mu_0 \left(\mathbf{J} + \varepsilon_0\frac{\partial \mathbf{E}}{\partial t} \right) \]

wird als Ampèresches Gesetz mit Maxwells Ergänzung bezeichnet. Der erste Term auf der rechten Seite der Gleichung besagt, dass die Rotation des Magnetfeldes bzw. die magnetische Flußdichte \( \mathbf{B}\) proportional zum elektrischen Stromfluss bzw. der Stromdichte \( \mathbf{J}\) ist. Ist das elektrische Feld \(\mathbf{E}\) zeitlich konstant, können wir den zweiten Term vernachlässigen. Für einen von Gleichstrom durchflossenen Leiter gilt also das untenstehende Bild.


Ändert sich das elektrische Feld \(\mathbf{E}\), ist die Rotation zusätzlich proportional zur zeitlichen Änderung des elektrischen Feldes. Diese Änderung wird häufig auch Verschiebungsstrom genannt, und stellt die sogenannte Maxwell Ergänzung dar.
Aus der Maxwell Ergänzung im Wechselstromfall

\[ \operatorname{rot} \mathbf{B} = \mu_0 \left(\varepsilon_0\frac{\partial \mathbf{E}}{\partial t} \right) \]

lässt sich durch erneutes Anwenden des Rotationoperators recht einfach die Wellengleichung

$$\Delta \mathbf{E} = \mu_0 \varepsilon_0 \frac{\partial^2 \mathbf{E}}{\partial t^2} $$

herleiten, \(\Delta\) bezeichnet dabei den sogenannten Laplace Operator. Eine Lösung dieser Differentialgleichung sind ebene Wellen der Form

$$\mathbf{E} = E_0 f(k \cdot \mathbf{x} - ct)$$

Sie beschreiben die Ausbreitung einer elektromagnetischen Welle im Raum.


Viel Spaß beim selber Rechnen und Verstehen...