Informationen zum Implementieren einer Deep-Learning-Bibliothek in Python

Deep-Learning-Technologien haben in kurzer Zeit einen langen Weg zurückgelegt - von einfachen neuronalen Netzen bis hin zu ziemlich komplexen Architekturen. Um die rasche Verbreitung dieser Technologien zu unterstützen, wurden verschiedene Bibliotheken und Deep-Learning-Plattformen entwickelt. Eines der Hauptziele solcher Bibliotheken ist es, Entwicklern einfache Schnittstellen zum Erstellen und Trainieren neuronaler Netzwerkmodelle bereitzustellen. Mit solchen Bibliotheken können ihre Benutzer den zu lösenden Aufgaben mehr Aufmerksamkeit schenken und nicht den Feinheiten der Modellimplementierung. Dazu müssen Sie möglicherweise die Implementierung grundlegender Mechanismen hinter mehreren Abstraktionsebenen verbergen. Dies erschwert wiederum das Verständnis der Grundprinzipien, auf denen Deep-Learning-Bibliotheken basieren.



Der Artikel, dessen Übersetzung wir veröffentlichen, zielt darauf ab, die Merkmale des Geräts von Low-Level-Bausteinen von Deep-Learning-Bibliotheken zu analysieren. Zunächst sprechen wir kurz über die Essenz des tiefen Lernens. Dadurch können wir die funktionalen Anforderungen für die jeweilige Software verstehen. Anschließend betrachten wir die Entwicklung einer einfachen, aber funktionierenden Deep-Learning-Bibliothek in Python mit NumPy. Diese Bibliothek bietet End-to-End-Schulungen für einfache neuronale Netzwerkmodelle. Auf dem Weg werden wir über die verschiedenen Komponenten von Deep-Learning-Frameworks sprechen. Die Bibliothek, die wir in Betracht ziehen werden, ist ziemlich klein, weniger als 100 Codezeilen. Und das bedeutet, dass es ganz einfach sein wird, es herauszufinden. Den vollständigen Projektcode, mit dem wir uns befassen werden, finden Sie hier .

Allgemeine Information


In der Regel bestehen Deep-Learning-Bibliotheken (wie TensorFlow und PyTorch) aus den in der folgenden Abbildung gezeigten Komponenten.


Komponenten des Deep-Learning-Frameworks

Lassen Sie uns diese Komponenten analysieren.

▍ Bediener


Die Konzepte „Operator“ und „Schicht“ (Schicht) werden normalerweise synonym verwendet. Dies sind die Grundbausteine ​​eines jeden neuronalen Netzwerks. Operatoren sind Vektorfunktionen, die Daten transformieren. Unter den häufig verwendeten Operatoren kann man beispielsweise lineare und Faltungsschichten, Unterabtastschichten (Pooling), halblineare (ReLU) und Sigmoid- (Sigmoid) Aktivierungsfunktionen unterscheiden.

▍Optimizer (Optimierer)


Optimierer sind die Grundlage für Deep-Learning-Bibliotheken. Sie beschreiben Methoden zur Anpassung von Modellparametern anhand bestimmter Kriterien und unter Berücksichtigung des Ziels der Optimierung. Unter den bekannten Optimierern sind SGD, RMSProp und Adam zu nennen.

▍ Verlustfunktionen


Verlustfunktionen sind analytische und differenzierbare mathematische Ausdrücke, die als Ersatz für das Ziel der Optimierung bei der Lösung eines Problems verwendet werden. Beispielsweise werden die Kreuzentropiefunktion und die stückweise lineare Funktion normalerweise bei Klassifizierungsproblemen verwendet.

▍ Initialisierer


Initialisierer liefern Anfangswerte für Modellparameter. Es sind diese Werte, die die Parameter zu Beginn des Trainings haben. Initialisierer spielen eine wichtige Rolle beim Training neuronaler Netze, da erfolglose Anfangsparameter bedeuten können, dass das Netzwerk langsam oder gar nicht lernt. Es gibt viele Möglichkeiten, die Gewichte eines neuronalen Netzwerks zu initialisieren. Zum Beispiel können Sie ihnen kleine Zufallswerte aus der Normalverteilung zuweisen. Auf dieser Seite erfahren Sie mehr über die verschiedenen Arten von Initialisierern.

▍ Regularisierer


Regularizer sind Tools, die eine Umschulung des Netzwerks vermeiden und dem Netzwerk helfen, sich zu verallgemeinern. Sie können das Netzwerk explizit oder implizit neu trainieren. Explizite Methoden beinhalten strukturelle Einschränkungen der Gewichte. Zum Beispiel durch Minimierung ihrer L1-Norm und L2-Norm, wodurch die Gewichtswerte dementsprechend besser verteilt und gleichmäßiger verteilt werden. Implizite Methoden werden von spezialisierten Operatoren dargestellt, die die Transformation von Zwischendarstellungen durchführen. Dies erfolgt entweder durch explizite Normalisierung, beispielsweise mithilfe der Paketnormalisierungstechnik (BatchNorm), oder durch Ändern der Netzwerkkonnektivität mithilfe der DropOut- und DropConnect-Algorithmen.

Die oben genannten Komponenten gehören normalerweise zum Schnittstellenteil der Bibliothek. Mit "Schnittstellenteil" meine ich hier die Entitäten, mit denen der Benutzer interagieren kann. Sie geben ihm praktische Werkzeuge zum effizienten Entwerfen einer neuronalen Netzwerkarchitektur. Wenn wir über die internen Mechanismen von Bibliotheken sprechen, können sie die automatische Berechnung von Gradienten der Verlustfunktion unter Berücksichtigung verschiedener Parameter des Modells unterstützen. Diese Technik wird allgemein als automatische Differenzierung (AD) bezeichnet.

Automatische Differenzierung


Jede Deep-Learning-Bibliothek bietet dem Benutzer einige automatische Differenzierungsfunktionen. Dies gibt ihm die Möglichkeit, sich auf die Beschreibung der Struktur des Modells (Berechnungsdiagramm) zu konzentrieren und die Aufgabe der Berechnung der Gradienten auf das AD-Modul zu übertragen. Nehmen wir ein Beispiel, das uns wissen lässt, wie alles funktioniert. Angenommen, wir möchten die partiellen Ableitungen der folgenden Funktion in Bezug auf ihre Eingangsvariablen X₁ und X₂ berechnen:

Y = sin (x₁) + X₁ * X₂

Die folgende Abbildung, die ich hier entlehnt habe , zeigt das Diagramm der Berechnungen und die Berechnung der Ableitungen unter Verwendung einer Kettenregel.


Berechnungsgraph und Berechnung von Ableitungen durch eine Kettenregel

Was Sie hier sehen, ist so etwas wie ein „umgekehrter Modus“ der automatischen Differenzierung. Der bekannte Fehlerrückausbreitungsalgorithmus ist ein Sonderfall des obigen Algorithmus für den Fall, dass die oben befindliche Funktion eine Verlustfunktion ist. AD nutzt die Tatsache aus, dass jede komplexe Funktion aus elementaren arithmetischen Operationen und elementaren Funktionen besteht. Infolgedessen können Derivate berechnet werden, indem eine Kettenregel auf diese Operationen angewendet wird.

Implementierung


Im vorherigen Abschnitt haben wir die Komponenten untersucht, die für die Erstellung einer Deep-Learning-Bibliothek erforderlich sind, die für die Erstellung und das End-to-End-Training neuronaler Netze konzipiert ist. Um das Beispiel nicht zu komplizieren, ahme ich hier das Entwurfsmuster der Caffe- Bibliothek nach . Hier deklarieren wir zwei abstrakte Klassen - Functionund Optimizer. Darüber hinaus gibt es eine Klasse Tensor, bei der es sich um eine einfache Struktur handelt, die zwei mehrdimensionale NumPy-Arrays enthält. Einer von ihnen dient zum Speichern von Parameterwerten, der andere zum Speichern ihrer Farbverläufe. Alle Parameter in verschiedenen Ebenen (Operatoren) sind vom Typ Tensor. Bevor wir weiter gehen, werfen Sie einen Blick auf die allgemeinen Umrisse der Bibliothek.


UML-Diagramm

der Bibliothek Zum Zeitpunkt des Schreibens dieses Materials enthält diese Bibliothek eine Implementierung der linearen Schicht, der ReLU-Aktivierungsfunktion, der SoftMaxLoss-Schicht und des SGD-Optimierers. Als Ergebnis stellt sich heraus, dass die Bibliothek zum Trainieren von Klassifizierungsmodellen verwendet werden kann, die aus vollständig verbundenen Schichten bestehen und eine nichtlineare Aktivierungsfunktion verwenden. Schauen wir uns nun einige Details zu den abstrakten Klassen an, die wir haben.

Eine abstrakte KlasseFunctionbietet eine Schnittstelle für Operatoren. Hier ist sein Code:

class  Function(object):
    def forward(self): 
        raise NotImplementedError
    
    def backward(self): 
        raise NotImplementedError
    
    def getParams(self): 
        return []

Alle Operatoren werden durch die Vererbung einer abstrakten Klasse implementiert Function. Jeder Betreiber muss eine Implementierung der Methoden forward()und bereitstellen backward(). Operatoren können eine Implementierung einer optionalen Methode enthalten getParams(), die ihre Parameter zurückgibt (falls vorhanden). Die Methode forward()empfängt Eingabedaten und gibt das Ergebnis ihrer Transformation durch den Operator zurück. Darüber hinaus löst er die internen Probleme, die für die Berechnung von Gradienten erforderlich sind. Das Verfahren backward()akzeptiert die partiellen Ableitungen der Verlustfunktion in Bezug auf die Ausgaben des Operators und implementiert die Berechnung der partiellen Ableitungen der Verlustfunktion in Bezug auf die Eingabedaten des Operators und die Parameter (falls vorhanden). Beachten Sie, dass die Methodebackward()Im Wesentlichen bietet unsere Bibliothek die Möglichkeit, eine automatische Differenzierung durchzuführen.

Um dies alles anhand eines konkreten Beispiels zu behandeln, werfen wir einen Blick auf die Implementierung der Funktion Linear:

class Linear(Function):
    def __init__(self,in_nodes,out_nodes):
        self.weights = Tensor((in_nodes,out_nodes))
        self.bias    = Tensor((1,out_nodes))
        self.type = 'linear'

    def forward(self,x):
        output = np.dot(x,self.weights.data)+self.bias.data
        self.input = x 
        return output

    def backward(self,d_y):
        self.weights.grad += np.dot(self.input.T,d_y)
        self.bias.grad    += np.sum(d_y,axis=0,keepdims=True)
        grad_input         = np.dot(d_y,self.weights.data.T)
        return grad_input

    def getParams(self):
        return [self.weights,self.bias]

Die Methode forward()implementiert die Transformation der Ansicht Y = X*W+bund gibt das Ergebnis zurück. Außerdem wird der Eingabewert gespeichert X, da er zur Berechnung der partiellen Ableitung dYder Verlustfunktion in Bezug auf den Ausgabewert Yin der Methode benötigt wird backward(). Methode backward()empfängt die partiellen Ableitungen, berechnet in Bezug auf den Eingabewert Xund die Parameter Wund b. Darüber hinaus werden die in Bezug auf den Eingabewert berechneten partiellen Ableitungen zurückgegeben X, die auf die vorherige Schicht übertragen werden.

Eine abstrakte Klasse Optimizerbietet eine Schnittstelle für Optimierer:

class Optimizer(object):
    def __init__(self,parameters):
        self.parameters = parameters
    
    def step(self): 
        raise NotImplementedError

    def zeroGrad(self):
        for p in self.parameters:
            p.grad = 0.

Alle Optimierer werden durch Erben von der Basisklasse implementiert Optimizer. Eine Klasse, die eine bestimmte Optimierung beschreibt, sollte eine Implementierung der Methode bereitstellen step(). Diese Methode aktualisiert die Modellparameter unter Verwendung ihrer partiellen Ableitungen, die in Bezug auf den optimierten Wert der Verlustfunktion berechnet wurden. In der Funktion wird eine Verknüpfung zu verschiedenen Modellparametern bereitgestellt __init__(). Bitte beachten Sie, dass die universelle Funktionalität zum Zurücksetzen von Gradientenwerten in der Basisklasse selbst implementiert ist.

Um dies alles besser zu verstehen, betrachten wir ein spezielles Beispiel - die Implementierung des SGD-Algorithmus (Stochastic Gradient Descent) mit Unterstützung für die Anpassung des Impulses und die Reduzierung von Gewichten:

class SGD(Optimizer):
    def __init__(self,parameters,lr=.001,weight_decay=0.0,momentum = .9):
        super().__init__(parameters)
        self.lr           = lr
        self.weight_decay = weight_decay
        self.momentum     = momentum
        self.velocity     = []
        for p in parameters:
            self.velocity.append(np.zeros_like(p.grad))

    def step(self):
        for p,v in zip(self.parameters,self.velocity):
            v = self.momentum*v+p.grad+self.weight_decay*p.data
            p.data=p.data-self.lr*v

Die Lösung für das eigentliche Problem


Jetzt haben wir alles Notwendige, um das (tiefe) neuronale Netzwerkmodell mithilfe unserer Bibliothek zu trainieren. Dafür benötigen wir folgende Entitäten:

  • Modell: Berechnungsdiagramm.
  • Daten und Zielwert: Daten für das Netzwerktraining.
  • Verlustfunktion: Ersatz für das Optimierungsziel.
  • Optimierer: Ein Mechanismus zum Aktualisieren von Modellparametern.

Der folgende Pseudocode beschreibt einen typischen Testzyklus:

model # 
data,target # 
loss_fn # 
optim #,         
Repeat:#   ,    ,     
   optim.zeroGrad() #    
   output = model.forward(data) #   
   loss   = loss_fn(output,target) # 
   grad   = loss.backward() #      
   model.backward(grad) #    
   optim.step() #  

Obwohl dies in der Deep-Learning-Bibliothek nicht erforderlich ist, kann es nützlich sein, die oben genannten Funktionen in eine separate Klasse aufzunehmen. Dies ermöglicht es uns, beim Erlernen neuer Modelle nicht dieselben Schritte zu wiederholen (diese Idee entspricht der Philosophie der Abstraktion von Frameworks wie Keras auf hoher Ebene ). Um dies zu erreichen, deklarieren Sie eine Klasse Model:

class Model():
    def __init__(self):
        self.computation_graph = []
        self.parameters        = []

    def add(self,layer):
        self.computation_graph.append(layer)
        self.parameters+=layer.getParams()

    def __innitializeNetwork(self):
        for f in self.computation_graph:
            if f.type=='linear':
                weights,bias = f.getParams()
                weights.data = .01*np.random.randn(weights.data.shape[0],weights.data.shape[1])
                bias.data    = 0.

    def fit(self,data,target,batch_size,num_epochs,optimizer,loss_fn):
        loss_history = []
        self.__innitializeNetwork()
        data_gen = DataGenerator(data,target,batch_size)
        itr = 0
        for epoch in range(num_epochs):
            for X,Y in data_gen:
                optimizer.zeroGrad()
                for f in self.computation_graph: X=f.forward(X)
                loss = loss_fn.forward(X,Y)
                grad = loss_fn.backward()
                for f in self.computation_graph[::-1]: grad = f.backward(grad) 
                loss_history+=[loss]
                print("Loss at epoch = {} and iteration = {}: {}".format(epoch,itr,loss_history[-1]))
                itr+=1
                optimizer.step()
        
        return loss_history
    
    def predict(self,data):
        X = data
        for f in self.computation_graph: X = f.forward(X)
        return X

Diese Klasse enthält die folgenden Funktionen:

  • : add() , . computation_graph.
  • : , , , .
  • : fit() . , .
  • : predict() , , .

Da diese Klasse nicht der Grundbaustein von Deep-Learning-Systemen ist, habe ich sie in einem separaten Modul implementiert utilities.py. Beachten Sie, dass die Methode fit()eine Klasse verwendet, DataGeneratorderen Implementierung sich im selben Modul befindet. Diese Klasse ist nur ein Wrapper für Trainingsdaten und generiert Minipakete für jede Iteration des Trainings.

Modelltraining


Betrachten Sie nun den letzten Code, in dem das neuronale Netzwerkmodell unter Verwendung der oben beschriebenen Bibliothek trainiert wird. Ich werde ein mehrschichtiges Netzwerk auf spiralförmig angeordneten Daten trainieren. Ich wurde von dieser Veröffentlichung aufgefordert . Code zum Generieren und Visualisieren dieser Daten finden Sie in der Datei utilities.py.


Daten mit drei spiralförmig angeordneten Klassen. 

Die vorherige Abbildung zeigt die Visualisierung der Daten, auf denen das Modell trainiert wird. Diese Daten sind nichtlinear trennbar. Wir können hoffen, dass ein Netzwerk mit einer verborgenen Schicht nichtlineare Entscheidungsgrenzen korrekt finden kann. Wenn Sie alles zusammenstellen, worüber wir gesprochen haben, erhalten Sie das folgende Codefragment, mit dem Sie das Modell trainieren können:

import dl_numpy as DL
import utilities

batch_size        = 20
num_epochs        = 200
samples_per_class = 100
num_classes       = 3
hidden_units      = 100
data,target       = utilities.genSpiralData(samples_per_class,num_classes)
model             = utilities.Model()
model.add(DL.Linear(2,hidden_units))
model.add(DL.ReLU())
model.add(DL.Linear(hidden_units,num_classes))
optim   = DL.SGD(model.parameters,lr=1.0,weight_decay=0.001,momentum=.9)
loss_fn = DL.SoftmaxWithLoss()
model.fit(data,target,batch_size,num_epochs,optim,loss_fn)
predicted_labels = np.argmax(model.predict(data),axis=1)
accuracy         = np.sum(predicted_labels==target)/len(target)
print("Model Accuracy = {}".format(accuracy))
utilities.plot2DDataWithDecisionBoundary(data,target,model)

Das Bild unten zeigt die gleichen Daten und die entscheidenden Grenzen des trainierten Modells.


Daten- und Entscheidungsgrenzen des trainierten Modells

Zusammenfassung


Angesichts der zunehmenden Komplexität von Deep-Learning-Modellen besteht die Tendenz, die Fähigkeiten der jeweiligen Bibliotheken zu erhöhen und die Menge an Code zu erhöhen, die zur Implementierung dieser Fähigkeiten erforderlich ist. Die grundlegendste Funktionalität solcher Bibliotheken kann jedoch immer noch in relativ kompakter Form implementiert werden. Obwohl die von uns erstellte Bibliothek für das End-to-End-Training einfacher Netzwerke verwendet werden kann, ist sie in vielerlei Hinsicht begrenzt. Wir sprechen über Einschränkungen im Bereich der Fähigkeiten, die es ermöglichen, Deep-Learning-Frameworks in Bereichen wie Bildverarbeitung, Sprach- und Texterkennung zu verwenden. Damit sind die Möglichkeiten solcher Frameworks natürlich nicht begrenzt.

Ich glaube, dass jeder das Projekt teilen kann, den Code, den wir hier untersucht haben, und als Übung darin einzuführen, was sie darin sehen möchten. Hier sind einige Mechanismen, die Sie versuchen können, selbst zu implementieren:

  • Operatoren: Faltung, Unterabtastung.
  • Optimierer: Adam, RMSProp.
  • Regulierungsbehörden: BatchNorm, DropOut.

Ich hoffe, dieses Material hat es Ihnen ermöglicht, zumindest aus den Augenwinkeln zu sehen, was in den Eingeweiden von Bibliotheken für tiefes Lernen geschieht.

Liebe Leser! Welche Deep-Learning-Bibliotheken verwenden Sie?

Source: https://habr.com/ru/post/undefined/


All Articles