Dans cet article, j'écrirai l'implémentation de base en utilisant PyTorch (également sous forme de mémorandum). Prenons la classification de CIFAR10 (ensemble de classification d'images couleur) comme exemple.
Copyright 2020 shun310
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
La vue d'ensemble du programme est la suivante.
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
ave = 0.5 #Moyenne normalisée
std = 0.5 #Écart type normalisé
batch_size_train = 256 #Apprentissage de la taille du lot
batch_size_test = 16 #Taille du lot de test
val_ratio = 0.2 #Rapport entre les données de validation et les données totales
epoch_num = 30 #Nombre d'époques d'apprentissage
class Net(nn.Module):
#Définition de la structure du réseau
def __init__(self):
super(Net, self).__init__()
self.init_conv = nn.Conv2d(3,16,3,padding=1)
self.conv1 = nn.ModuleList([nn.Conv2d(16,16,3,padding=1) for _ in range(3)])
self.bn1 = nn.ModuleList([nn.BatchNorm2d(16) for _ in range(3)])
self.pool = nn.MaxPool2d(2, stride=2)
self.fc1 = nn.ModuleList([nn.Linear(16*16*16, 128), nn.Linear(128, 32)])
self.output_fc = nn.Linear(32, 10)
#Calcul à terme
def forward(self, x):
x = F.relu(self.init_conv(x))
for l,bn in zip(self.conv1, self.bn1):
x = F.relu(bn(l(x)))
x = self.pool(x)
x = x.view(-1,16*16*16) # flatten
for l in self.fc1:
x = F.relu(l(x))
x = self.output_fc(x)
return x
def set_GPU():
#Paramètres GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
return device
def load_data():
#Chargement des données
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((ave,),(std,))])
train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
test_set = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
#Répartition des données de validation
n_samples = len(train_set)
val_size = int(n_samples * val_ratio)
train_set, val_set = torch.utils.data.random_split(train_set, [(n_samples-val_size), val_size])
#Définition de DataLoader
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size_train, shuffle=True, num_workers=2)
val_loader = torch.utils.data.DataLoader(val_set, batch_size=batch_size_train, shuffle=False, num_workers=2)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size_test, shuffle=False, num_workers=2)
return train_loader, test_loader, val_loader
def train():
device = set_GPU()
train_loader, test_loader, val_loader = load_data()
model = Net()
model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, verbose=True)
min_loss = 999999999
print("training start")
for epoch in range(epoch_num):
train_loss = 0.0
val_loss = 0.0
train_batches = 0
val_batches = 0
model.train() #Mode entraînement
for i, data in enumerate(train_loader): #Lire par lot
inputs, labels = data[0].to(device), data[1].to(device) #les données sont[inputs, labels]Liste de
#Réinitialisation du dégradé
optimizer.zero_grad()
outputs = model(inputs) #Calcul à terme
loss = criterion(outputs, labels) #Calcul des pertes
loss.backward() #Calcul inversé(Calcul du gradient)
optimizer.step() #Mise à jour des paramètres
#Historique cumulatif
train_loss += loss.item()
train_batches += 1
# validation_Calcul de la perte
model.eval() #Mode d'inférence
with torch.no_grad():
for i, data in enumerate(val_loader): #Lire par lot
inputs, labels = data[0].to(device), data[1].to(device) #les données sont[inputs, labels]Liste de
outputs = model(inputs) #Calcul à terme
loss = criterion(outputs, labels) #Calcul des pertes
#Historique cumulatif
val_loss += loss.item()
val_batches += 1
#Sortie historique
print('epoch %d train_loss: %.10f' %
(epoch + 1, train_loss/train_batches))
print('epoch %d val_loss: %.10f' %
(epoch + 1, val_loss/val_batches))
with open("history.csv",'a') as f:
print(str(epoch+1) + ',' + str(train_loss/train_batches) + ',' + str(val_loss/val_batches),file=f)
#Enregistrez le meilleur modèle
if min_loss > val_loss/val_batches:
min_loss = val_loss/val_batches
PATH = "best.pth"
torch.save(model.state_dict(), PATH)
#Changement dynamique du taux d'apprentissage
scheduler.step(val_loss/val_batches)
#Enregistrer le modèle de l'époque finale
print("training finished")
PATH = "lastepoch.pth"
torch.save(model.state_dict(), PATH)
if __name__ == "__main__":
train()
Les bibliothèques utilisées dans cet article sont les suivantes.
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
Définissez les constantes à utiliser ultérieurement.
ave = 0.5 #Moyenne normalisée
std = 0.5 #Écart type normalisé
batch_size_train = 256 #Apprentissage de la taille du lot
batch_size_test = 16 #Taille du lot de test
val_ratio = 0.2 #Rapport entre les données de validation et les données totales
epoch_num = 30 #Nombre d'époques d'apprentissage
Si vous utilisez GPU, vous devez spécifier le périphérique avant divers paramètres.
def set_GPU():
#Paramètres GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
return device
Le GPU est utilisé via cet objet appelé périphérique. Par exemple
data.to(device)
model.to(device)
Ce faisant, des données et des modèles de réseaux neuronaux peuvent être chargés sur le GPU.
PyTorch possède déjà plusieurs jeux de données. Par exemple, avec CIFAR10, vous pouvez vous préparer comme suit.
def load_data():
#Chargement des données
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((ave,),(std,))])
train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
test_set = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
#Répartition des données de validation
n_samples = len(train_set)
val_size = int(n_samples * val_ratio)
train_set, val_set = torch.utils.data.random_split(train_set, [(n_samples-val_size), val_size])
#Définition de DataLoader
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size_train, shuffle=True, num_workers=2)
val_loader = torch.utils.data.DataLoader(val_set, batch_size=batch_size_train, shuffle=False, num_workers=2)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size_test, shuffle=False, num_workers=2)
return train_loader, test_loader, val_loader
Je vais expliquer dans l'ordre. Premièrement, la transformation représente une série de processus qui ont pour fonction de transformer des données.
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((ave,),(std,))])
Dans l'exemple ci-dessus, transforms.ToTensor () est utilisé pour changer les données en Tensor (type de données PyTorch), puis transforms.Normalize ((ave,), (std,)) est utilisé pour normaliser la moyenne moyenne et l'écart type std. En train d'aller. Compose joue le rôle d'organiser une série de processus. Il existe plusieurs autres types de conversion de données. Consultez la documentation.
Ensuite, lisez les données de CIFAR10.
train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
test_set = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
Spécifiez la destination de téléchargement pour root. Il semble que l'ensemble de données existant de PyTorch soit divisé en train et test. Spécifiez avec une option appelée train. Ici, définissez pour effectuer la conversion de données en passant la transformation définie précédemment en tant qu'argument.
Je souhaite séparer les données de vérification des données lues. Pour cela, nous utilisons torch.utils.data.random_split.
train_set, val_set = torch.utils.data.random_split(train_set, [(n_samples-val_size), val_size])
Il divise aléatoirement les données dans le nombre spécifié par le deuxième argument.
Lors de l'apprentissage avec PyTorch, l'utilisation de DataLoader est très pratique lors de l'apprentissage. Faites-le comme suit.
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size_train, shuffle=True, num_workers=2)
val_loader = torch.utils.data.DataLoader(val_set, batch_size=batch_size_train, shuffle=False, num_workers=2)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size_test, shuffle=False, num_workers=2)
Transmettez l'ensemble de données comme premier argument. batch_size spécifie la taille du lot, shuffle spécifie la présence ou l'absence de mélange de données et num_workers spécifie le nombre de sous-processus (nombre de parallèles) au moment de la lecture. À propos, l'identité de DataLoader est un itérateur. Par conséquent, au moment de l'apprentissage, l'instruction for est utilisée pour récupérer les données de chaque lot.
Il existe de nombreuses situations dans lesquelles vous souhaitez utiliser vos propres données. Dans ce cas, vous devez définir votre propre classe de jeu de données comme suit. (Référence: https://qiita.com/mathlive/items/2a512831878b8018db02)
class MyDataset(torch.utils.data.Dataset):
def __init__(self, data, label, transform=None):
self.transform = transform
self.data = data
self.data_num = len(data)
self.label = label
def __len__(self):
return self.data_num
def __getitem__(self, idx):
out_data = self.data[idx]
out_label = self.label[idx]
if self.transform:
out_data = self.transform(out_data)
return out_data, out_label
Au moins, définissez len (une fonction qui renvoie la taille des données) et getitem (une fonction qui obtient des données) dans la classe. Avec ça,
dataset = MyDataset(Des données d'entrée,Étiquette de l'enseignant, transform=Conversion de données requise)
Vous pouvez créer un ensemble de données comme celui-ci. En passant, si vous regardez le contenu de MyDataset, vous pouvez en quelque sorte comprendre ce que PyTorch fait à l'intérieur. En d'autres termes, lorsque idx est spécifié, les données correspondant à cet index sont renvoyées. De plus, si une transformation est spécifiée, elle est appliquée aux données. Diverses opérations peuvent être effectuées en jouant avec getitem (par exemple, en utilisant deux transformations), mais elles sont omises.
PyTorch facilite la définition d'un réseau de neurones à l'aide de classes. Pour le classificateur CIFAR10, par exemple:
class Net(nn.Module):
#Définition de la structure du réseau
def __init__(self):
super(Net, self).__init__()
self.init_conv = nn.Conv2d(3,16,3,padding=1)
self.conv1 = nn.ModuleList([nn.Conv2d(16,16,3,padding=1) for _ in range(3)])
self.bn1 = nn.ModuleList([nn.BatchNorm2d(16) for _ in range(3)])
self.pool = nn.MaxPool2d(2, stride=2)
self.fc1 = nn.ModuleList([nn.Linear(16*16*16, 128), nn.Linear(128, 32)])
self.output_fc = nn.Linear(32, 10)
#Calcul à terme
def forward(self, x):
x = F.relu(self.init_conv(x))
for l,bn in zip(self.conv1, self.bn1):
x = F.relu(bn(l(x)))
x = self.pool(x)
x = x.view(-1,16*16*16) # flatten
for l in self.fc1:
x = F.relu(l(x))
x = self.output_fc(x)
return x
J'expliquerai diverses choses.
Le réseau est créé en héritant de nn.Module. Définissez chaque couche comme son propre membre dans init.
def __init__(self):
super(Net, self).__init__()
self.init_conv = nn.Conv2d(3,16,3,padding=1)
self.conv1 = nn.ModuleList([nn.Conv2d(16,16,3,padding=1) for _ in range(3)])
self.bn1 = nn.ModuleList([nn.BatchNorm2d(16) for _ in range(3)])
self.pool = nn.MaxPool2d(2, stride=2)
self.fc1 = nn.ModuleList([nn.Linear(16*16*16, 128), nn.Linear(128, 32)])
self.output_fc = nn.Linear(32, 10)
Par exemple, nn.Conv2d transmet les arguments suivants.
nn.Conv2d(Nombre de canaux d'entrée, nombre de canaux de sortie, taille du noyau, remplissage=Taille du rembourrage, foulée=Quantité de mouvement)
Voir le document officiel pour plus de détails. (https://pytorch.org/docs/stable/nn.html)
Les couches réseau peuvent également être définies comme des tableaux à l'aide de nn.ModuleList.
self.conv1 = nn.ModuleList([nn.Conv2d(16,16,3,padding=1) for _ in range(3)])
Particulièrement utile lors de la définition d'un réseau à grande échelle avec une structure répétitive. Il semble que les paramètres ne peuvent pas être mis à jour si le tableau est changé en un tableau normal sans utiliser nn.ModuleList. (Référence: https://qiita.com/perrying/items/857df46bb6cdc3047bd8) Assurez-vous d'utiliser correctement nn.ModuleList.
Le calcul avant est défini comme avant.
def forward(self, x):
x = F.relu(self.init_conv(x))
for l,bn in zip(self.conv1, self.bn1):
x = F.relu(bn(l(x)))
x = self.pool(x)
x = x.view(-1,16*16*16) # flatten
for l in self.fc1:
x = F.relu(l(x))
x = self.output_fc(x)
return x
Les couches organisées par nn.ModuleList sont extraites par l'instruction for. Ce genre d'endroit est également pratique. Aussi, sur le chemin
x = x.view(-1,16*16*16) # flatten
une. Ici, les données de type image sont converties en un vecteur unidimensionnel (passez le nombre de canaux x la largeur verticale de l'image x la largeur horizontale de l'image comme deuxième argument). La raison pour laquelle le premier argument est -1 est que la conversion est automatiquement effectuée en fonction de la taille du lot.
La fonction de perte est définie dans torch.nn et la méthode de mise à jour est définie dans torch.optim, qui sont appelées et utilisées. Cette fois, nous utilisons CrossEntropyLoss comme fonction de perte pour la classification. Adam est utilisé comme méthode de mise à jour.
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
Afin d'effectuer un apprentissage de manière efficace, nous pouvons souhaiter définir (ou réduire) dynamiquement le taux d'apprentissage. Dans ce cas, utilisez quelque chose appelé lr_scheduler. Par exemple
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, verbose=True)
Définissez le planificateur comme ceci. Avec cela, après avoir calculé la perte des données de validation,
scheduler.step(val_loss)
En écrivant, s'il n'y a pas d'amélioration pendant la période (de patience), le taux d'apprentissage sera automatiquement réduit. Cela peut empêcher la stagnation de l'apprentissage.
Comme l'ensemble de données, vous souhaiterez peut-être créer vous-même la fonction de perte. Il semble que cela puisse être créé en héritant de la classe PyTorch ou défini comme une fonction simple. (Référence: https://kento1109.hatenablog.com/entry/2018/08/13/092939) Pour les tâches simples de régression / classification, MSELoss / CrossEntropyLoss fonctionne souvent bien, mais pour les documents d'apprentissage automatique, la fonction de perte est conçue pour améliorer les performances, il s'agit donc d'une implémentation assez importante.
Après avoir préparé ce point, nous allons enfin commencer à apprendre. Tout d'abord, appliquez chaque paramètre défini jusqu'à présent.
device = set_GPU()
train_loader, test_loader, val_loader = load_data()
model = Net()
model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, verbose=True)
L'apprentissage est le suivant. Fondamentalement, comme commenté.
min_loss = 999999999
print("training start")
for epoch in range(epoch_num):
train_loss = 0.0
val_loss = 0.0
train_batches = 0
val_batches = 0
model.train() #Mode entraînement
for i, data in enumerate(train_loader): #Lire par lot
inputs, labels = data[0].to(device), data[1].to(device) #les données sont[inputs, labels]Liste de
#Réinitialisation du dégradé
optimizer.zero_grad()
outputs = model(inputs) #Calcul à terme
loss = criterion(outputs, labels) #Calcul des pertes
loss.backward() #Calcul inversé(Calcul du gradient)
optimizer.step() #Mise à jour des paramètres
#Historique cumulatif
train_loss += loss.item()
train_batches += 1
# validation_Calcul de la perte
model.eval() #Mode d'inférence
with torch.no_grad():
for i, data in enumerate(val_loader): #Lire par lot
inputs, labels = data[0].to(device), data[1].to(device) #les données sont[inputs, labels]Liste de
outputs = model(inputs) #Calcul à terme
loss = criterion(outputs, labels) #Calcul des pertes
#Historique cumulatif
val_loss += loss.item()
val_batches += 1
#Sortie historique
print('epoch %d train_loss: %.10f' %
(epoch + 1, train_loss/train_batches))
print('epoch %d val_loss: %.10f' %
(epoch + 1, val_loss/val_batches))
with open("history.csv",'a') as f:
print(str(epoch+1) + ',' + str(train_loss/train_batches) + ',' + str(val_loss/val_batches),file=f)
#Enregistrez le meilleur modèle
if min_loss > val_loss/val_batches:
min_loss = val_loss/val_batches
PATH = "best.pth"
torch.save(model.state_dict(), PATH)
#Changement dynamique du taux d'apprentissage
scheduler.step(val_loss/val_batches)
#Enregistrer le modèle de l'époque finale
print("training finished")
PATH = "lastepoch.pth"
torch.save(model.state_dict(), PATH)
Dans PyTorch, il est nécessaire de décrire explicitement le calcul de la fonction de perte et la rétro-propagation de l'erreur (il existe aussi une bibliothèque qui résume cela).
Tutoriel officiel (https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html) pour tester le modèle appris, etc. ). Dans le cas de ce modèle, la précision de chaque classe est d'environ 40 à 80% (le flou est assez important ...).
Pour la normalisation et l'abandon par lots, il est nécessaire de basculer le comportement entre l'apprentissage et l'inférence. Par conséquent, pendant l'apprentissage et l'inférence
model.train()
model.eval()
Il semble qu'il soit nécessaire d'ajouter chaque description. En plus de cela, il existe également un mode appelé torch.no_grad (). Il s'agit d'un mode qui n'enregistre pas les informations de gradient. Etant donné que le calcul en arrière n'est pas effectué au moment de la vérification, les informations de gradient sont inutiles et l'omettre augmentera la vitesse de calcul et économisera de la mémoire.
J'ai l'impression d'être épuisé en chemin, alors je pourrais l'ajouter bientôt. Je suis heureux que vous puissiez l'utiliser comme référence.
M. fukuit: https://qiita.com/fukuit/items/215ef75113d97560e599 M. perrying: https://qiita.com/perrying/items/857df46bb6cdc3047bd8
Tutoriel officiel: https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html transform Documentation officielle: https://pytorch.org/docs/stable/torchvision/transforms.html
Documentation officielle: https://pytorch.org/docs/stable/torchvision/datasets.html
Documentation officielle: https://pytorch.org/docs/stable/data.html
mathlive: https://qiita.com/mathlive/items/2a512831878b8018db02
M. kento1109: https://kento1109.hatenablog.com/entry/2018/08/13/092939
Documentation officielle: https://pytorch.org/docs/stable/generated/torch.nn.ModuleList.html
Documentation officielle: https://pytorch.org/docs/stable/optim.html#how-to-adjust-learning-rate
Tutoriel officiel: https://pytorch.org/tutorials/beginner/saving_loading_models.html jyori112: https://qiita.com/jyori112/items/aad5703c1537c0139edb
Tutoriel officiel: https://pytorch.org/tutorials/beginner/saving_loading_models.html Forum PyTorch: https://discuss.pytorch.org/t/model-eval-vs-with-torch-no-grad/19615 M. s0sem0y: https://www.hellocybernetics.tech/entry/2018/02/20/182906