
In deze PyTorch-tutorial behandelen we de kernfuncties achter neurale netwerken en bouwen we er zelf een vanaf nul. Het hoofddoel van dit artikel is om de basis van PyTorch, een geoptimaliseerde deep-learning-tensorbibliotheek, te demonstreren en je tegelijk een gedetailleerde achtergrond te geven over hoe neurale netwerken werken.
Let op: Bekijk deze DataCamp-workspace om de code uit dit artikel te volgen.
Wat zijn neurale netwerken?
Neurale netwerken worden ook wel artificiële neurale netwerken (ANNs) genoemd. De architectuur vormt de basis van deep learning, een subset van machine learning die zich bezighoudt met algoritmen die geïnspireerd zijn op de structuur en werking van het menselijk brein. Simpel gezegd vormen neurale netwerken de basis van architecturen die nabootsen hoe biologische neuronen signalen aan elkaar doorgeven.
Daarom kom je vaak bronnen tegen die de eerste vijf minuten besteden aan het in kaart brengen van de neurale structuur van het menselijk brein om je visueel te helpen begrijpen hoe een neuraal netwerk werkt. Maar als je geen vijf minuten over hebt, is het makkelijker om een neuraal netwerk te definiëren als een functie die inputs naar gewenste outputs afbeeldt.
De generieke architectuur van een neuraal netwerk bestaat uit het volgende:
- Invoallaag: Data wordt via de invoerlaag in het netwerk gevoerd. Het aantal neuronen in de invoerlaag is gelijk aan het aantal features in de data. Technisch gezien wordt de invoerlaag niet beschouwd als een van de lagen in het netwerk, omdat er op dit punt geen berekening plaatsvindt.
- Verborgen laag: De lagen tussen de invoer- en uitvoerlaag heten verborgen lagen. Een netwerk kan een willekeurig aantal verborgen lagen hebben: hoe meer verborgen lagen, hoe complexer het netwerk.
- Uitvoerlaag: De uitvoerlaag wordt gebruikt om een voorspelling te doen.
- Neuronen: Elke laag heeft een verzameling neuronen die met neuronen in andere lagen interageren.
- Activatiefunctie: Voert niet-lineaire transformaties uit om het model te helpen complexe patronen uit de data te leren.

Let op: het neurale netwerk in de afbeelding hierboven wordt beschouwd als een drielaags neuraal netwerk en niet als een vierlaags - dit komt doordat we de invoerlaag niet als laag meetellen. Het aantal lagen in een netwerk is dus het aantal verborgen lagen plus de uitvoerlaag.
Hoe werken neurale netwerken?
Laten we het algoritme opdelen in kleinere componenten om beter te begrijpen hoe neurale netwerken werken.
Initieel wegen toekennen
Het initialiseren van gewichten is de eerste component in de architectuur van een neuraal netwerk. De beginwaarden die we instellen bepalen het startpunt voor het optimalisatieproces van het model.
Hoe we onze gewichten instellen is belangrijk, vooral bij het bouwen van diepe netwerken. Diepe netwerken zijn namelijk gevoeliger voor het probleem van exploderende of verdwijnende gradiënten. Deze problemen vallen buiten de scope van dit artikel, maar beide beschrijven een situatie waarin het algoritme niet leert.
Hoewel gewichtsinitialisatie het probleem van verdwijnende of exploderende gradiënten niet volledig oplost, draagt het wel bij aan de preventie ervan.
Hier zijn een paar veelvoorkomende manieren om gewichten te initialiseren:
Nul-initialisatie
Nul-initialisatie betekent dat gewichten als nul worden ingesteld. Dit is geen goede oplossing, omdat ons neurale netwerk de symmetrie niet doorbreekt - het zal niet leren.
Wanneer een constante waarde wordt gebruikt om de gewichten van een neuraal netwerk te initialiseren, kun je verwachten dat het slecht presteert, omdat alle lagen hetzelfde zullen leren. Als alle outputs van de verborgen eenheden dezelfde invloed op de kosten hebben, dan zijn de gradiënten identiek.
Willekeurige initialisatie
Willekeurige initialisatie doorbreekt de symmetrie, wat betekent dat het beter is dan nul-initialisatie, maar sommige factoren kunnen de algehele kwaliteit van het model bepalen.
Als de gewichten bijvoorbeeld willekeurig met grote waarden worden geïnitialiseerd, kun je verwachten dat elke matrixvermenigvuldiging een aanzienlijk grotere waarde oplevert. Wanneer in zulke scenario’s een sigmoid-activatiefunctie wordt toegepast, is het resultaat een waarde dicht bij één, wat de leersnelheid vertraagt.
Een ander scenario waarin willekeurige initialisatie problemen kan veroorzaken is wanneer de gewichten willekeurig met kleine waarden worden geïnitialiseerd. In dat geval zal elke matrixvermenigvuldiging aanzienlijk kleinere waarden opleveren, en het toepassen van een sigmoidfunctie zal een waarde dichter bij nul opleveren, wat de leersnelheid ook vertraagt.
Xavier/Glorot-initialisatie
Een Xavier- of Glorot-initialisatie - het gaat onder beide namen - is een heuristische aanpak om gewichten te initialiseren. Je ziet deze aanpak vaak wanneer een tanh- of sigmoid-activatiefunctie wordt toegepast op het gewogen gemiddelde. De aanpak werd voor het eerst voorgesteld in 2010 in het onderzoek Understanding the difficulty of training deep feedforward neural networks door Xavier Glorot en Yoshua Bengio. Deze techniek is erop gericht de variantie door het netwerk gelijk te houden om te voorkomen dat gradiënten exploderen of verdwijnen.
He/Kaiming-initialisatie
De He- of Kaiming-initialisatie is een andere heuristische aanpak. Het verschil met de He- en Xavier-heuristiek is dat He-initialisatie een andere schaalfactor voor de gewichten gebruikt die rekening houdt met de niet-lineariteit van activatiefuncties.
Dus wanneer de ReLU-activatiefunctie in de lagen wordt gebruikt, is He-initialisatie de aanbevolen aanpak. Je kunt meer over deze aanpak leren in Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification van He et al.
Forwardpropagatie
Neurale netwerken werken door een gewogen gemiddelde plus een bias-term te nemen en daar een activatiefunctie op toe te passen om een niet-lineaire transformatie toe te voegen. In de formulering van het gewogen gemiddelde bepaalt elk gewicht het belang van elke feature (dus hoeveel die bijdraagt aan het voorspellen van de output).

De bovenstaande formule is het gewogen gemiddelde plus een bias-term, waarbij
- z de gewogen som van de input van een neuron is
- Wn de gewichten aanduidt
- Xn de onafhankelijke variabelen aanduidt, en
- b de bias-term is.
Als de formule bekend klinkt, komt dat omdat het lineaire regressie is. Zonder niet-lineariteit in de neuronen zouden we lineaire regressie hebben, een simpel model. De niet-lineaire transformatie stelt ons neuraal netwerk in staat complexe patronen te leren.
Activatiefuncties
We hebben in de sectie over gewichtsinitialisatie al naar enkele activatiefuncties verwezen, maar nu weet je hoe belangrijk ze zijn in een architectuur voor neurale netwerken.
Laten we dieper ingaan op een paar veelvoorkomende activatiefuncties die je waarschijnlijk tegenkomt in onderzoekspapers en de code van anderen.
Sigmoid
De sigmoidfunctie wordt gekenmerkt door een S-vormige curve die begrensd is tussen de waarden nul en één. Het is een differentieerbare functie, wat betekent dat de helling van de curve op elk willekeurig punt kan worden bepaald, en monotoon, wat betekent dat hij niet volledig stijgend of dalend is. Je gebruikt de sigmoid-activatiefunctie doorgaans voor binaire classificatieproblemen.
Zo kun je je eigen sigmoidfunctie visualiseren met Python:
# Sigmoid function in Python
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-5, 5, 50)
z = 1/(1 + np.exp(-x))
plt.subplots(figsize=(8, 5))
plt.plot(x, z)
plt.grid()
plt.show()

Tanh
De hyperbolische tangens (tanh) heeft dezelfde S-vormige curve als de sigmoidfunctie, behalve dat de waarden begrensd zijn tussen -1 en 1. Kleine inputs worden dus dichter bij -1 afgebeeld en grotere inputs dichter bij 1.

Hier is een voorbeeld van een tanh-functie gevisualiseerd met Python:
# tanh function in Python
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-5, 5, 50)
z = np.tanh(x)
plt.subplots(figsize=(8, 5))
plt.plot(x, z)
plt.grid()
plt.show()

Softmax
De softmaxfunctie wordt meestal gebruikt als activatiefunctie in de uitvoerlaag. Het is een generalisatie van de sigmoidfunctie naar meerdere dimensies. Daarom wordt deze gebruikt in neurale netwerken om klasse-lidmaatschap te voorspellen voor meer dan twee labels.
Rectified Linear Unit (ReLU)
Het gebruiken van de sigmoid- of tanh-functie voor diepe neurale netwerken is riskant, omdat ze vatbaarder zijn voor het probleem van verdwijnende gradiënten. De Rectified Linear Unit (ReLU) werd geïntroduceerd als oplossing voor dit probleem en is vaak de standaardactivatiefunctie voor diverse neurale netwerken.
Hier is een visueel voorbeeld van de ReLU-functie in Python:
# ReLU in Python
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-5, 5, 50)
z = [max(0, i) for i in x]
plt.subplots(figsize=(8, 5))
plt.plot(x, z)
plt.grid()
plt.show()

ReLU is begrensd tussen nul en oneindig: let op dat voor inputwaarden kleiner dan of gelijk aan nul de functie nul teruggeeft, en voor waarden boven nul geeft de functie de ingevoerde waarde terug (dus als je twee invoert, wordt twee teruggegeven). Per saldo gedraagt de ReLU-functie zich erg vergelijkbaar met een lineaire functie, wat optimaliseren en implementeren een stuk eenvoudiger maakt.
Het proces van invoer naar uitvoerlaag heet de forward pass of forwardpropagatie. In deze fase worden de outputs die door het model zijn gegenereerd gebruikt om een kostenfunctie te berekenen, zodat we na elke iteratie weten hoe het neuraal netwerk presteert. Deze informatie wordt vervolgens terug door het model gestuurd om de gewichten te corrigeren, zodat het model betere voorspellingen kan doen in een proces dat backpropagatie heet.
Backpropagatie
Aan het einde van de eerste forward pass maakt het netwerk voorspellingen met de geïnitialiseerde, nog niet afgestemde gewichten. Het is dus zeer waarschijnlijk dat de voorspellingen niet accuraat zijn. Met het verlies dat tijdens forwardpropagatie is berekend, sturen we informatie terug door het netwerk om de gewichten te verfijnen in een proces dat backpropagatie heet.
Uiteindelijk gebruiken we de optimalisatiefunctie om ons te helpen de gewichten te vinden die de fout kunnen verkleinen, waardoor het model betrouwbaarder wordt en beter kan generaliseren naar nieuwe voorbeelden. De wiskunde achter hoe dit werkt valt buiten de scope van dit artikel, maar de geïnteresseerde lezer kan meer over backpropagatie leren in onze cursus Introduction to Deep Learning in Python.
PyTorch-tutorial: stap voor stap een neuraal netwerk vanaf nul bouwen
In dit artikelgedeelte bouwen we een simpel artificieel neuraal netwerk met de PyTorch-bibliotheek. Bekijk deze DataCamp-workspace om de code te volgen
PyTorch is een van de populairste bibliotheken voor deep learning. Het biedt een veel directere debugervaring dan TensorFlow. Het heeft nog diverse andere voordelen zoals gedistribueerde training, een robuust ecosysteem, cloudondersteuning, en de mogelijkheid om production-ready code te schrijven, enzovoort. Je kunt meer leren over PyTorch in het skill track Introduction to Deep Learning with PyTorch.
Laten we beginnen met de tutorial.
Datadefinitie & -voorbereiding
De dataset die we in onze tutorial gebruiken is make_circles uit scikit-learn - zie de documentatie. Het is een speeldataset met een grote cirkel met een kleinere cirkel in een tweedimensionaal vlak en twee features. Voor onze demonstratie gebruikten we 10.000 samples en voegden we een standaardafwijking van 0,05 aan Gaussiaans ruis toe aan de data.
Voordat we ons neuraal netwerk bouwen, is het goed gebruik om onze data op te splitsen in trainings- en testsets, zodat we de prestaties van het model op ongeziene data kunnen evalueren.
import matplotlib.pyplot as plt
from sklearn.datasets import make_circles
from sklearn.model_selection import train_test_split
# Create a dataset with 10,000 samples.
X, y = make_circles(n_samples = 10000,
noise= 0.05,
random_state=26)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.33, random_state=26)
# Visualize the data.
fig, (train_ax, test_ax) = plt.subplots(ncols=2, sharex=True, sharey=True, figsize=(10, 5))
train_ax.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap=plt.cm.Spectral)
train_ax.set_title("Training Data")
train_ax.set_xlabel("Feature #0")
train_ax.set_ylabel("Feature #1")
test_ax.scatter(X_test[:, 0], X_test[:, 1], c=y_test)
test_ax.set_xlabel("Feature #0")
test_ax.set_title("Testing data")
plt.show()

De volgende stap is het converteren van de trainings- en testdata van NumPy-arrays naar PyTorch-tensors. Hiervoor gaan we een custom dataset maken voor onze trainings- en testbestanden. We maken ook gebruik van PyTorch’ Dataloader-module zodat we onze data in batches kunnen trainen. Hier is de code:
import warnings
warnings.filterwarnings("ignore")
!pip install torch -q
import torch
import numpy as np
from torch.utils.data import Dataset, DataLoader
# Convert data to torch tensors
class Data(Dataset):
def __init__(self, X, y):
self.X = torch.from_numpy(X.astype(np.float32))
self.y = torch.from_numpy(y.astype(np.float32))
self.len = self.X.shape[0]
def __getitem__(self, index):
return self.X[index], self.y[index]
def __len__(self):
return self.len
batch_size = 64
# Instantiate training and test data
train_data = Data(X_train, y_train)
train_dataloader = DataLoader(dataset=train_data, batch_size=batch_size, shuffle=True)
test_data = Data(X_test, y_test)
test_dataloader = DataLoader(dataset=test_data, batch_size=batch_size, shuffle=True)
# Check it's working
for batch, (X, y) in enumerate(train_dataloader):
print(f"Batch: {batch+1}")
print(f"X shape: {X.shape}")
print(f"y shape: {y.shape}")
break
"""
Batch: 1
X shape: torch.Size([64, 2])
y shape: torch.Size([64])
"""
Laten we nu verder gaan met het implementeren en trainen van ons neuraal netwerk.
Implementatie van het neurale netwerk & modeltraining
We implementeren een eenvoudig tweelaags neuraal netwerk dat de ReLU-activatiefunctie (torch.nn.functional.relu) gebruikt. Hiervoor maken we een klasse NeuralNetwork die erft van nn.Module, de basisklasse voor alle neurale-netwerkmodules in PyTorch.
Hier is de code:
import torch
from torch import nn
from torch import optim
input_dim = 2
hidden_dim = 10
output_dim = 1
class NeuralNetwork(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super(NeuralNetwork, self).__init__()
self.layer_1 = nn.Linear(input_dim, hidden_dim)
nn.init.kaiming_uniform_(self.layer_1.weight, nonlinearity="relu")
self.layer_2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
x = torch.nn.functional.relu(self.layer_1(x))
x = torch.nn.functional.sigmoid(self.layer_2(x))
return x
model = NeuralNetwork(input_dim, hidden_dim, output_dim)
print(model)
"""
NeuralNetwork(
(layer_1): Linear(in_features=2, out_features=10, bias=True)
(layer_2): Linear(in_features=10, out_features=1, bias=True)
)
"""
En dat is alles.
Om het model te trainen moeten we een verliesfunctie definiëren om de gradiënten te berekenen en een optimizer om de parameters bij te werken. Voor onze demonstratie gebruiken we binaire cross-entropy en stochastic gradient descent met een leersnelheid van 0,1.
learning_rate = 0.1
loss_fn = nn.BCELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=lea
Laten we ons model trainen
num_epochs = 100
loss_values = []
for epoch in range(num_epochs):
for X, y in train_dataloader:
# zero the parameter gradients
optimizer.zero_grad()
# forward + backward + optimize
pred = model(X)
loss = loss_fn(pred, y.unsqueeze(-1))
loss_values.append(loss.item())
loss.backward()
optimizer.step()
print("Training Complete")
"""
Training Complete
"""
Omdat we de verlieswaarden hebben bijgehouden, kunnen we het verlies van het model in de tijd visualiseren.
step = range(len(loss_values))
fig, ax = plt.subplots(figsize=(8,5))
plt.plot(step, np.array(loss_values))
plt.title("Step-wise Loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.show()

De visualisatie hierboven toont het verlies van ons model over 100 epochs. In het begin start het verlies op 0,7 en neemt het geleidelijk af - dit laat zien dat ons model zijn voorspellingen in de loop van de tijd heeft verbeterd. Het model lijkt echter rond epoch 60 te stagneren, wat aan verschillende redenen kan liggen, zoals dat het model zich in de buurt van een lokaal of globaal minimum van de verliesfunctie bevindt.
Hoe dan ook, het model is getraind en klaar om voorspellingen te doen op nieuwe voorbeelden - in de volgende sectie bekijken we hoe je dat doet.
Voorspellingen & modelevaluatie
Voorspellingen maken met ons PyTorch-neuraal netwerk is vrij eenvoudig.
import itertools # Import this at the top of your script
# Initialize required variables
y_pred = []
y_test = []
correct = 0
total = 0
"""
We're not training so we don't need to calculate the gradients for our outputs
"""
with torch.no_grad():
for X, y in test_dataloader:
outputs = model(X) # Get model outputs
predicted = np.where(outputs.numpy() < 0.5, 0, 1) # Convert to NumPy and apply threshold
predicted = list(itertools.chain(*predicted)) # Flatten predictions
y_pred.append(predicted) # Append predictions
y_test.append(y.numpy()) # Append true labels as NumPy
total += y.size(0) # Increment total count
correct += (predicted == y.numpy()).sum().item() # Count correct predictions
print(f'Accuracy of the network on the 3300 test instances: {100 * correct // total}%')
"""
Accuracy of the network on the 3300 test instances: 97%
"""
Let op: Elke run van de code geeft een andere output, dus je krijgt mogelijk niet exact dezelfde resultaten.
De bovenstaande code loopt door de testbatches, die zijn opgeslagen in de variabele test_dataloader, zonder de gradiënten te berekenen. We voorspellen vervolgens de voorbeelden in de batch en slaan de resultaten op in een variabele outputs. Daarna zetten we alle waarden kleiner dan 0,5 op 0 en die gelijk aan of groter dan 0,5 op 1. Deze waarden worden vervolgens toegevoegd aan een lijst met onze voorspellingen.
Daarna tellen we de daadwerkelijke voorspellingen van de voorbeelden in de batch op bij een variabele total. Vervolgens berekenen we het aantal correcte voorspellingen door het aantal voorspellingen dat gelijk is aan de werkelijke klassen te bepalen en dit op te tellen. Het totale aantal correcte voorspellingen per batch wordt opgehoogd en opgeslagen in onze variabele correct.
Om de nauwkeurigheid van het algehele model te berekenen, vermenigvuldigen we het aantal correcte voorspellingen met 100 (om een percentage te krijgen) en delen we dit dan door het aantal voorbeelden in onze testset. Ons model haalde 97% nauwkeurigheid. We duiken dieper met de confusion matrix en scikit-learn’s classification_report om een beter beeld te krijgen van hoe ons model presteerde.
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
import seaborn as sns
y_pred = list(itertools.chain(*y_pred))
y_test = list(itertools.chain(*y_test))
print(classification_report(y_test, y_pred))
"""
precision recall f1-score support
0.0 0.98 0.97 0.98 1635
1.0 0.98 0.98 0.98 1665
accuracy 0.98 3300
macro avg 0.98 0.98 0.98 3300
weighted avg 0.98 0.98 0.98 3300
"""
cf_matrix = confusion_matrix(y_test, y_pred)
plt.subplots(figsize=(8, 5))
sns.heatmap(cf_matrix, annot=True, cbar=False, fmt="g")
plt.show()

Ons model presteert behoorlijk goed. Ik raad je aan de code te verkennen en wat wijzigingen te maken, zodat wat we in dit artikel hebben behandeld beter blijft hangen.
In deze PyTorch-tutorial hebben we de fundamentele basis van neurale netwerken behandeld en PyTorch, een Python-bibliotheek voor deep learning, gebruikt om ons netwerk te implementeren. We gebruikten de circles-dataset van scikit-learn om een tweelaags neuraal netwerk te trainen voor classificatie. Daarna maakten we voorspellingen op de data en evalueerden we onze resultaten met de nauwkeurigheidsmaat.
