Elena, Annika
Unser grundsätzliches Ziel ist es, EKG-Daten auszuwerten und diese, mithilfe eines Algorithmus, nach krank und gesund zu unterscheiden. Da das Analysieren eines solchen EKGs eine für den Menschen sehr komplexe Aufgabe ist, wollten wir herausfinden, ob es einfacher ist, diese Problemstellung von einem Computer lösen zu lassen.
Ein Elektrokardiogramm ist ein weit verbreitetes Diagnoseverfahren, welches Medizinern Aufschlüsse über die Gesundheit des Herzens eines Patienten liefert. Es ist eine nicht-invasive Methode, die für Patienten kaum Risiken birgt, weshalb sie in der Praxis standardmäßig eingesetzt wird.
Ein Elektrokardiogramm misst die elektrische Erregung im Herzen, indem 10 Elektroden den Strom an der Hautoberfläche detektieren. Diese werden im Anschluss so miteinander verschaltet, dass 12 Spannungskurven entstehen, aus denen dann Rückschlüsse auf die Pumpaktivität des Herzens und somit die kardiologische Gesundheit eines Menschen gezogen werden können.
Hier seht ihr einen gesunden Sinusrhythmus (links schematisch und rechts ein Plot aus dem Datensatz), wie genau er aufgebaut ist könnt ihr hier erfahren.
Die Daten, die wir analysiert haben, stammen aus einem riesigen öffentlichen Datensatz im Internet, der Daten von 18.885 Patienten umfasst. Das Besondere an diesem Datensatz ist, dass auch sehr viele kardiologisch gesunde Patienten inbegriffen sind. Oft wird das vernachlässigt, ist für uns aber besonders entscheidend, da wir ja einen Algorithmus kreieren wollen, der auch gesunde EKGs erkennt. Der Datensatz ist relativ neu und stammt von 2020. Er enthält Angaben zu den Patienten (sogenannte Metadaten), ein diagnostisches Label, wobei der Datensatz fünf Ober- und 24 Unterklassen unterscheidet, und die EKG-Messdaten. An den Datensatz anknüpfend wurde auch ein Python-Code zur Verfügung gestellt, mithilfe dessen, auf die Daten zugegriffen werden kann und, der die Daten schon in Trainings- und Testdaten unterteilt.
Mithilfe von mathematischer Funktionen lassen sich allgemeine Informationen aus Daten ableiten. Bei dieser Art von Daten ist der Zusammenhang recht offensichtlich. Doch EKGs sind weitaus komplexere Datenstrukturen, dementsprechend komplex gestaltet sich auch unsere mathematische Analyse der Daten. Um Zusammenhänge zwischen solch komplexen Daten zu ermitteln, bietet sich Machine Learning an.
Maschinelles Lernen ist das näherungsweise Optimieren einer Funktion. Diese Funktion ordnet, mittels eines neuronalen Netzes, einem komplexen Input einen vorgegebenen Output zu. Dies bedeutet im Grunde, dass neuronale Netze einen mathematischen Weg nutzen, um allgemeine Merkmale aus Daten zu abstrahieren, die sich zum Beispiel mittels objektiver Labels verifizieren lassen. Da das Finden dieser mathematischen Funktion näherungsweise und Schritt für Schritt passiert, und an unserem menschlichen Lernen orientiert ist, spricht man von maschinellem Lernen.
Neuronale Netze sind von den neuronalen Verknüpfungen unserer Gehirne inspirierte Strukturen. So bestehen sie aus mehreren Schichten aus Neuronen bzw. Knotenpunkten, die zunächst keinen eigenen Wert besitzen. Zwischen den Neuronen der Schichten gibt es Verbindungen und zwar von jedem Neuron der vorhergehenden Schicht zu jedem Neuron der darauffolgenden Schicht. Diese Verbindungen sind essentieller Bestandteil des Netzes. Denn jede Verbindung hat eine Gewichtung, die bestimmt, wie relevant die Verknüpfung zwischen den beiden Neuronen ist.
Für unser Netzwerk haben wir mit Pytorch gearbeitet. Das ist eine Python-Bibliothek, die viele Bausteine für die Implementierung neuronaler Netze enthält. Es vereinfacht vor allem den mathematisch-rechnerischen Teil und erleichtert es den Benutzern, eine Netzwerk-Architektur mit Schichten und Hyperparametern zu erstellen.
Des Weiteren haben wir Google CoLab verwendet, eine Online-Programmierumgebung, die auf Jupyter-Notebooks basiert. Google stellt dort Server, also Rechenkapazität, zur Verfügung, was für neuronale Netze von großer Bedeutung ist, da diese sehr viel Speicher und Rechenleistung benötigen. Dabei haben wir mit CUDA gearbeitet, einer Technologie, mithilfe derer Teile des Codes auf einer Grafikkarte (GPU) ausgeführt werden können.
Zuerst haben wir eine Netzwerk-Klasse, bestehend aus _init_-Konstruktor und forward-Funktion, mithilfe der Pytorch-Bibliothek implementiert. Dabei enthält der _init_-Konstruktor die Definition für die Schichten im Netz. In der forward-Funktion werden dann die Verknüpfungen zwischen diesen Schichten hergestellt, in unserem Fall mittels der ReLu-Aktivierungsfunktion. Zunächst haben wir nur linearen Schichten gebaut, allerdings konnten wir damit keinen Lernerfolg erzielen, da jeglicher räumlicher Zusammenhang der Datenpunkte verloren geht.
class Netzwerk(nn.Module): def __init__(self): self.lin1 = nn.Linear(12*61, 800) self.lin2 = nn.Linear(800, 40) self.lin3 = nn.Linear(40, 10) self.lin4 = nn.Linear(10, 2) def forward(self, x): x = F.relu(self.lin1(x)) x = F.relu(self.lin2(x)) x = F.relu(self.lin3(x)) x = self.lin4(x) return x
Deshalb sind wir zu einer Architektur mit gefalteten Schichten (Convolutional Layern) übergegangen. Dabei läuft ein Filter (Kernel) über das Bild, der einen bestimmten Ausschnitt der Daten zusammenfasst, bevor diese den linearen Schichten übergeben werden. Dies war für unser Netz essentiell, da so nicht alle Datenpunkte individuell betrachtet werden, sondern die einzelnen Datenpunkte in ihrem räumlichen Zusammenhang erkannt werden. Mithilfe der Convolutional Layers konnten wir unseren ersten Lernerfolg erzielen.
Im Anschluss haben wir eine ResNet-Architektur eingeführt, um einen besseren Lernerfolg durch ein tieferes Lernen zu erreichen. Bei der ResNet-Architektur werden noch zusätzliche Layer eingeführt, sogenannte Skip Connections, die immer ein paar Schichten überspringen und so einer späteren Schicht einen output von einer vorherigen Schicht geben. Dies ermöglicht mehr Schichten, ohne dass Informationen verloren gehen. Wir haben für die Implementierung die Netzwerk-Klasse minimal verändert (zur aktualisierten Klasse kommt ihr hier) und eine zusätzliche Klasse Resnet.
Im folgenden erzeugen wir ein Objekt der Netzwerk-Klasse: unser Netz, dass wir mit den EKG-Daten trainieren wollen. Außerdem definieren wir den optimizer, also die Art und Weise, wie die Gewichte im Netz optimiert werden sollen, sodass das Netz EKGs erkennt. Dafür benutzen wir den SGD (=stochastic Gradient Descent) optimizer aus der Pytorch-Bibliothek. Diesem übergeben wir eine learning_rate, die die Schrittweite entlang des Gradienten bestimmt. Auch die batch_size, also die Anzahl an Trainings-Daten, über die gemittelt der Gradient berechnet wird, legen wir im folgenden Code fest.
net = Netzwerk() learning_rate = 0.3 batch_size = 50 optimizer = optim.SGD(net.parameters(), lr=learning_rate)
In der Trainingsfunktion werden zunächst die Input- und Ziel-Werte entsprechend der Batch-Size eingelesen, sodass sie dem Netzwerk übergeben werden können. Anschließend werden die Input-Werte vorwärts durch das Netz geschickt (forward propagation) und die Differenz der Ergebnisse mit den Zielwerten verglichen (Fehler berechnen). Daraufhin wird dieser rückwarts durch das Netz zurück geschickt (backward propagation), wobei die Neujustierung der Gewichte im Netzwerk berechnet wird. All dies passiert innerhalb einer for-Schleife, welche dafür sorgt, dass die Anpassung der Parameter, entsprechend der Epochen-Anzahl, wiederholt wird.
def train(epoch = 3000, b_s = batch_size): net.train() for i in range(epoch): random_batch = [np.random.randint(len(X_train)) for i in range(b_s)] # input-Werte my_in = np.swapaxes(X_train[random_batch], 1, 2) my_in = Variable(torch.Tensor(my_in)) my_in.unsqueeze(2) # target-Werte ziel = y_train[y_train.index[random_batch]].tolist() zielw = Variable(torch.Tensor(ziel)) zielw = zielw.type(torch.LongTensor) # forward propagation criterion = nn.CrossEntropyLoss() optimizer.zero_grad() out = net(my_in) # Fehler berechnen loss = criterion(out, zielw) #backward propagation loss.backward() optimizer.step() net.history_loss.append(loss)
Die Evaluationsfunktion dient der Überprüfung des Lernprozesses und -erfolges. Wir übergen ihr unabhängige Testdaten, die das Netzwerk während des Trainings noch nicht gesehen hat und schicken diese durch unser Netzwerk. Anschließend ermitteln wir, wie viele der Testdaten von dem trainierten Netz richtig erkannt wurden.
def evaluate(test_x = X_evaluation, test_y = y_evaluation): net.eval() z = 0 for i in range(len(test_x)): x = np.swapaxes(test_x[i], 0, 1) x = Variable(torch.Tensor(x)) x = x.unsqueeze(0) if torch.argmax(net(x)) == test_y[test_y.index[i]]: z += 1 return z/len(test_x)
Der Graph zeigt die Klassifizierungsgenauigkeit (blau) und den Fehler (orange) über 9.000 Epochen. Unser Netz erkennt also ungefähr 83% der unabhängigen Test-Daten richtig. Das ist insofern ein Erfolg, dass ohne Training nur in etwa die Hälfte der EKGs richtig zugeordnet werden, da circa eine Hälfte der Daten jeweils gesunde beziehungsweise ungesunde EKGs enthalten. Somit hat unser Netz erfolgreich gelernt!
Allerdings ist dies noch ausbaufähig, da in etwa jedes fünfte EKG falsch zugeordnet wird. Für eine Anwendung in der klinischen Praxis ist die Fehlerquote demnach viel zu hoch und der Algorithmus nicht zuverlässig. Dennoch ist es für uns, mit sehr wenig Programmier-Erfahrung, für ein Erst-Semester Projekt ein großer Erfolg.
Wir haben beide Python-Kenntnisse und medizinisches Wissen erworben und Erfahrungen im Bereich Machine Learning sammeln können. Fehlerbehebung mussten wir vor allem vornehmen, wenn es darum ging, dass bestimmte Tensor-Größen nicht zusammenpassten oder in- und output sich in ihrer Dimension unterschieden. Man könnte das Netz mit Sicherheit noch weiter verbessern, indem man die Hyperparameter optimal einstellt und die Netzwerkarchitektur noch ein wenig anpasst. Außerdem könnte man als Erweiterung, das Netz spezifische Krankheitsbilder erkennen lassen. Dies hatten wir ganz zu Beginn auch überlegt, allerdings hat die „Verbesserungsphase“ unseres Netzes sehr viel länger gedauert, als wir dachten und so sind wir aus zeitlichen Gründen leider nicht mehr dazu gekommen.
Michael Nielsen: neural networks and deep learning
pytorch basics: how to train a neural net into cnn - Towards Data Science
a beginner friendly guide to PyTorch and how it works from scratch
Deep Learning with PyTorch: A 60 Minute Blitz
How to build your own Neural Network from scratch in Python - Towards Data Science
How to Use Convolutional Neural Networks for Time Series Classification
A Comprehensive Guide to Convolutional Neural Networks — the ELI5 way
How Do Convolutional Layers Work in Deep Learning Neural Networks?
PyTorch ResNet implementation from scratch video
Beispielcode mit ResNet und PyTorch
ResNet implementation - Towards Data Science
an overview of ResNet and its variants - Towards Data Science