L'article précédent était ici Dans l'article précédent, j'ai écrit que "cela prend plusieurs heures pour apprendre avec l'ensemble de données MNIST de Keras." Je ne peux pas faire cela avec un petit jeu de données comme le jeu de données MNIST, donc je vais l'accélérer. Parlant d'accélération du deep learning, c'est l'utilisation du GPU et du TPU ~ Donc, dans cet article, nous allons faire de la programmation GPU pour utiliser les GPU NVIDIA. Le package utilisé est «CuPy». La raison sera plus tard ...
CuPy
](Programmation GPU avec #cupy)CuPy
](Installation et confirmation de #cupy)CuPy
](programmation #cupy)Remarque: vous pouvez l'ignorer car il parle paresseusement.
Le développement de l'architecture informatique au cours des dernières années progresse à un niveau sans précédent. Par exemple, quand j'étais enfant, Game Boy Advance était populaire, mais la capacité de données des jeux en cours d'exécution était de ** 32 Mo ** au maximum. Cependant, de nos jours, en ce qui concerne les jeux fonctionnant sur du matériel de jeu extrêmement spécifique tel que PS4, PS5 et Switch, il semble qu'il existe une capacité de données de ** 10 Go ** bien sûr. Étant donné que Go représente 1024 fois celui de Mo, cela signifie que ** il est devenu possible de traiter près de 1000 fois la quantité de données en seulement 10 ans **. À partir de là, vous pouvez voir la progression des supports de stockage tels que le disque dur et le SSD.
Bien sûr, à mesure que la quantité de données traitées augmente, le nombre d'instructions traitées par l'ordinateur augmente également. Je ne sais pas si la puissance de traitement nécessaire au CPU est épuisée, mais pour y répondre, le développement du CPU suit une règle empirique appelée "** Loi de Moore **" ** Doublé en 18 mois (récemment 24 mois) Il est devenu ** [^ 1]. Cela signifie que ** 15 ans augmenteront les performances de traitement de votre ordinateur de 1024 fois **. C'est incroyable ~
Cependant, comme mentionné précédemment, la puissance de traitement requise d'un processeur est requise pour le plafond bleu chaque fois que les performances du processeur s'améliorent et que davantage de choses peuvent être faites. Pour cette raison, le manque de performance a toujours été déploré.
Il ne fait aucun doute que le contexte de l'apprentissage profond sous les feux de la rampe est l'amélioration des performances du processeur, et il existe de nombreuses situations dans lesquelles le manque de performances du processeur est déploré. L'un d'eux est la reconnaissance d'image et le ** réseau de neurones convolutifs (CNN) **. Étant donné que les données d'image sont bidimensionnelles, si vous essayez d'utiliser un ensemble de données d'image légèrement volumineux pour l'apprentissage, elles deviendront rapidement un tenseur avec plus de 10000 unités d'éléments, et le processeur actuel est extrêmement insuffisant en performances. Article précédent ne le mentionne pas spécifiquement, mais quand j'ai expérimenté, quand j'ai essayé d'apprendre en utilisant le jeu de données MNIST de Keras, c'était sur google colaboratory. Cela prend également ** 1 époque 30 minutes **. Étant donné que google colaboratory a une limite de 12 heures, vous ne pouvez apprendre que 24 époques (sauf si vous reprenez l'apprentissage de l'enregistrement et du rechargement temporaires). Eh bien, vous pouvez toujours apprendre avec une précision suffisante avec l'ensemble de données MNIST.
Quoi qu'il en soit, ce n'est pas facile d'expérimenter cela. Le GPU était au centre de l'attention.
GPU Le CPU est l'unité centrale de traitement, tandis que le GPU est appelé l'unité de traitement graphique. Comme son nom l'indique, il s'agit d'un ** processeur semi-conducteur spécialisé dans les calculs pour le dessin d'écran **. ** Le processeur est excellent pour le calcul à usage général **, tandis que ** le GPU est spécialisé pour le calcul pour le traitement d'image **, donc sa vitesse est écrasante. Étant donné que le calcul massivement parallèle est effectué avec des milliers de cœurs ou plus, le dessin d'écran est essentiellement effectué sans décalage. Et voici le miso, mais ce calcul super parallèle et ce calcul matriciel ont une affinité. Veuillez noter que ce chiffre n'est pas qu'une métaphore, il ne le fait pas réellement sur le GPU. Ce que je voudrais que vous lisiez sur cette figure, c'est que ** le calcul matriciel peut être exécuté en parallèle **. À propos, la figure ci-dessus peut être réalisée en parallélisant les processeurs, mais l'échelle des GPU est d'un ordre de grandeur.
Les GPU qui se sont concentrés sur l'apprentissage profond comme celui-ci apporteront une grande contribution au développement de GPGPU: General-Purpose Computing on Graphics Processing Units: avec l'avènement de la technologie qui utilise des dispositifs de traitement graphique pour le calcul général.
TPU Aujourd'hui, avec l'avènement des GPU et des GPGPU, l'apprentissage profond a fait des progrès rapides, mais c'est la nature humaine qui ne suffit pas. C'est pourquoi TPU: Tensor Processing Unit: Tensol Processing Device a été introduit.
Le GPU a été conçu uniquement pour les graphiques, mais à partir de là, le TPU ** a été conçu pour réaliser un calcul de tenseur à grande vitesse pour l'apprentissage en profondeur. Au détriment de la polyvalence et d'un peu de précision de calcul, nous avons atteint des vitesses qui submergent le GPU, et même atteignent nos pieds.
Comme il est spécialisé dans le calcul tenseur, il est moins polyvalent que le GPU, et il est accéléré en réduisant le calcul de 32 bits ou 64 bits à 8 bits ou 16 bits. De plus, afin de réduire même l'écriture dans la mémoire cache, des données sont échangées dans le circuit arithmétique, et de toute façon, le calcul du tenseur est conçu à grande vitesse.
Alpha Go Zero est un exemple typique de sa puissance écrasante. Le montant du calcul qui prendrait ** 30 000 ans ** une fois converti en CPU par un simple calcul a été effectué en ** 3 jours ** en utilisant plusieurs TPU. Je ne comprends pas le sens. Lol
En tant que tel, les avantages du calcul massivement parallèle sur l'apprentissage profond sont énormes.
CuPy
Maintenant, entrons dans le sujet principal. Dans cet article, nous utiliserons CuPy
pour la programmation GPU.
Il semble que CuPy
était à l'origine un package développé pour l'implémentation de programme GPU (programmation CUDA) dans Chainer.
Le plus grand avantage est qu'il suit numpy
, donc la plupart du code réécrit simplement np
(ʻimport numpy as np) en
cp (ʻimport cupy as cp
). C'est pourquoi j'ai décidé d'utiliser CuPy
dans cet article. Facile c'est génial! Lol
J'y pense rarement. Ou plutôt, c'est sale parce que je l'ai implémenté sous forme de prototype ... Je vais le régler bientôt. Je me demande si je devrais utiliser un décorateur ... Faites-moi savoir si vous avez une bonne idée. Le code est ici.
** À propos, pour utiliser GPU avec google colaboratory, vous devez sélectionner GPU comme type d'exécution. ** **
CuPy
Pour installer CuPy
, exécutez la cellule dans laquelle vous avez entré le code suivant.
!curl https://colab.chainer.org/install | sh -
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1580 100 1580 0 0 6666 0 --:--:-- --:--:-- --:--:-- 6666
+ apt -y -q install cuda-libraries-dev-10-0
Reading package lists...
Building dependency tree...
Reading state information...
cuda-libraries-dev-10-0 is already the newest version (10.0.130-1).
0 upgraded, 0 newly installed, 0 to remove and 11 not upgraded.
+ pip install -q cupy-cuda100 chainer
|████████████████████████████████| 348.0MB 51kB/s
+ set +ex
Installation succeeded!
Cela installera automatiquement la version requise de CuPy
. Aussi «Chainer». Je ne l'utilise pas, mais ça va.
Vous pouvez vérifier s'il est correctement installé avec le code suivant.
!python -c 'import chainer; chainer.print_runtime_info()'
Platform: Linux-4.19.112+-x86_64-with-Ubuntu-18.04-bionic
Chainer: 7.4.0
ChainerX: Not Available
NumPy: 1.18.5
CuPy: Not Available
iDeep: 2.0.0.post3
Ce n'est pas grave si vous pouvez confirmer la sortie de cette manière.
CuPy
Prenons (une partie de) la fonction d'activation comme exemple.
activator.py
import numpy as np
import cupy as cp
class Activator():
def __init__(self, *args, mode="cpu", **kwds):
self.mode = mode
if self.mode == "cpu":
self.forward = self.cpu_forward
self.backward = self.cpu_backward
self.update = self.cpu_update
elif self.mode == "gpu":
self.forward = self.gpu_forward
self.backward = self.gpu_backward
self.update = self.gpu_update
def cpu_forward(self, *args, **kwds):
raise NotImplemented
def gpu_forward(self, *args, **kwds):
raise NotImplemented
def cpu_backward(self, *args, **kwds):
raise NotImplemented
def gpu_backward(self, *args, **kwds):
raise NotImplemented
def cpu_update(self, *args, **kwds):
raise NotImplemented
def gpu_update(self, *args, **kwds):
raise NotImplemented
class step(Activator):
def cpu_forward(self, x, *args, **kwds):
return np.where(x > 0, 1, 0)
def gpu_forward(self, x, *args, **kwds):
return cp.where(x > 0, 1, 0)
def cpu_backward(self, x, *args, **kwds):
return np.zeros_like(x)
def gpu_backward(self, x, *args, **kwds):
return cp.zeros_like(x)
J'écris avec la mort cérébrale. D'une manière ou d'une autre, il doit y avoir un moyen plus intelligent ...
Ce que nous faisons, c'est créer des branches en tirant parti de la capacité de Python à assigner des fonctions comme une sorte d'objet.
La partie implémentation de la fonction est également différente de np
et cp
! C'est la bonne chose à propos de «CuPy». Vous pouvez facilement et commodément effectuer la programmation GPU ~
Expérimentons avec l'ensemble de données MNIST de Keras. L'exécution exécute toutes les cellules jusqu'au code expérimental, exécute la cellule qui lit les données Keras et enfin exécute le corps du code expérimental CNN.
cnn_main.py
%matplotlib inline
#Créer une couche de convolution et une couche de sortie
M, F_h, F_w = 10, 3, 3
lm = LayerManager((x_train, x_test), (t_train, t_test), mode="gpu")
#lm.append(name="c", I_shape=(C, I_h, I_w), F_shape=(M, F_h, F_w), pad=1,
# wb_width=0.1, opt="AdaDelta", opt_dic={"eta": 1e-2})
lm.append(name="c", I_shape=(C, I_h, I_w), F_shape=(M, F_h, F_w), pad=1)
lm.append(name="p", I_shape=lm[-1].O_shape, pool=2)
#lm.append(name="m", n=100, wb_width=0.1,
# opt="AdaDelta", opt_dic={"eta": 1e-2})
lm.append(name="m", n=100)
#lm.append(name="o", n=n_class, act="softmax", err_func="Cross", wb_width=0.1,
# opt="AdaDelta", opt_dic={"eta": 1e-2})
lm.append(name="o", n=n_class, act="softmax", err_func="Cross")
#Apprendre
epoch = 5
threshold = 1e-8
n_batch = 8
lm.training(epoch, threshold=threshold, n_batch=n_batch, show_train_error=True)
#Prédire
print("training dataset")
_ = lm.predict(x=lm.x_train, y=lm.y_train)
print("test dataset")
if lm.mode == "cpu":
y_pred = lm.predict()
elif lm.mode == "gpu":
y_pred = lm.predict().get()
progress:[XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]483s/514s
training dataset
correct: [5 0 4 1 9 2 1 3 1 4 3 5 3 6 1 7]
predict: [5 0 4 1 9 2 1 3 1 4 3 5 3 6 1 7]
accuracy rate: 98.58 % (59148/60000)
test dataset
correct: [7 2 1 0 4 1 4 9 5 9 0 6 9 0 1 5]
predict: [7 2 1 0 4 1 4 9 5 9 0 6 9 0 1 5]
accuracy rate: 97.58 % (9758/10000)
Cela ne veut rien dire, mais c'est par défaut la fonction d'activation, la plage de poids wb_width
, l'optimiseur, etc. En d'autres termes, la fonction d'activation est ReLU, wb_width
vaut 0,05 et l'optimiseur est Adam. L'époque d'apprentissage est définie sur 5.
Le résultat de l'exécution est d'environ 100 secondes par époque! Nous avons réussi à accélérer 18 fois. C'est encore lent, mais cela devrait être pratique. Sauf pour MNIST ... (yeux lointains)
À propos, au bas du code de test se trouve le code d'entraînement de l'ensemble de données MNIST dans Keras. Je l'ai copié depuis ici.
mnist_cnn.py
'''Trains a simple convnet on the MNIST dataset.
Gets to 99.25% test accuracy after 12 epochs
(there is still a lot of margin for parameter tuning).
16 seconds per epoch on a GRID K520 GPU.
'''
from __future__ import print_function
import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K
batch_size = 128
num_classes = 10
epochs = 12
# input image dimensions
img_rows, img_cols = 28, 28
# the data, split between train and test sets
(x_train, y_train), (x_test, y_test) = mnist.load_data()
if K.image_data_format() == 'channels_first':
x_train = x_train.reshape(x_train.shape[0], 1, img_rows, img_cols)
x_test = x_test.reshape(x_test.shape[0], 1, img_rows, img_cols)
input_shape = (1, img_rows, img_cols)
else:
x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)
x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
input_shape = (img_rows, img_cols, 1)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
print('x_train shape:', x_train.shape)
print(x_train.shape[0], 'train samples')
print(x_test.shape[0], 'test samples')
# convert class vectors to binary class matrices
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),
activation='relu',
input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
model.compile(loss=keras.losses.categorical_crossentropy,
optimizer=keras.optimizers.Adadelta(),
metrics=['accuracy'])
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(x_test, y_test))
score = model.evaluate(x_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11493376/11490434 [==============================] - 0s 0us/step
x_train shape: (60000, 28, 28, 1)
60000 train samples
10000 test samples
Epoch 1/12
469/469 [==============================] - 4s 9ms/step - loss: 2.2889 - accuracy: 0.1426 - val_loss: 2.2611 - val_accuracy: 0.2889
Epoch 2/12
469/469 [==============================] - 4s 9ms/step - loss: 2.2432 - accuracy: 0.2350 - val_loss: 2.2046 - val_accuracy: 0.4885
Epoch 3/12
469/469 [==============================] - 4s 9ms/step - loss: 2.1837 - accuracy: 0.3312 - val_loss: 2.1279 - val_accuracy: 0.5908
Epoch 4/12
469/469 [==============================] - 4s 9ms/step - loss: 2.1039 - accuracy: 0.4035 - val_loss: 2.0235 - val_accuracy: 0.6492
Epoch 5/12
469/469 [==============================] - 4s 9ms/step - loss: 1.9959 - accuracy: 0.4669 - val_loss: 1.8864 - val_accuracy: 0.6989
Epoch 6/12
469/469 [==============================] - 4s 9ms/step - loss: 1.8604 - accuracy: 0.5193 - val_loss: 1.7149 - val_accuracy: 0.7420
Epoch 7/12
469/469 [==============================] - 4s 9ms/step - loss: 1.6990 - accuracy: 0.5681 - val_loss: 1.5179 - val_accuracy: 0.7688
Epoch 8/12
469/469 [==============================] - 4s 9ms/step - loss: 1.5315 - accuracy: 0.6014 - val_loss: 1.3180 - val_accuracy: 0.7912
Epoch 9/12
469/469 [==============================] - 4s 9ms/step - loss: 1.3717 - accuracy: 0.6327 - val_loss: 1.1394 - val_accuracy: 0.8029
Epoch 10/12
469/469 [==============================] - 4s 9ms/step - loss: 1.2431 - accuracy: 0.6562 - val_loss: 0.9945 - val_accuracy: 0.8171
Epoch 11/12
469/469 [==============================] - 4s 9ms/step - loss: 1.1369 - accuracy: 0.6757 - val_loss: 0.8818 - val_accuracy: 0.8263
Epoch 12/12
469/469 [==============================] - 4s 9ms/step - loss: 1.0520 - accuracy: 0.6957 - val_loss: 0.7949 - val_accuracy: 0.8356
Test loss: 0.7948545217514038
Test accuracy: 0.8356000185012817
C'est rapide ... et 20 fois plus rapide ... cela signifie qu'il y a encore de la place pour la vitesse!
Voyons maintenant où se trouve le goulot d'étranglement de la vitesse de calcul dans mon code actuel. Le temps de traitement est mesuré à l'aide de la magie «timeit». Si vous l'utilisez, il mesurera bien le temps de traitement.
Premièrement, mesurons le temps nécessaire au calcul des erreurs.
search_bottleneck.py
#Calcul de l'erreur d'entraînement
%%timeit
lm.forward(lm.x_train)
error = lm[-1].get_error(lm.y_train)
#----------output----------
# 1 loop, best of 3: 957 ms per loop
#--------------------------
#Calcul de l'erreur de test
%%timeit
lm.forward(lm.x_test)
error = lm[-1].get_error(lm.y_test)
#----------output----------
# 10 loops, best of 3: 160 ms per loop
#--------------------------
La quantité de données pour le calcul d'erreur des données d'entraînement est de 60 000, donc ce sera comme ça. Je pense que c'est normal d'en avoir moins ... Je pense que nous pouvons nous améliorer ici. Comme pour les données de test, si vous les réduisez à 10 000, elles seront raccourcies d'environ 0,8 seconde par époque. Eh bien, c'est une sorte d'erreur. Compte tenu de l'ensemble (100 secondes par époque), le taux d'erreur de calcul est d'environ 1% dans son ensemble, ce n'est donc pas un goulot d'étranglement. La partie apprentissage semble donc être un problème.
Nous mesurerons le temps de traitement de la partie d'apprentissage. Quelque part dans l'apprentissage, 99% du temps de traitement par époque devrait être ...
search_bottleneck.py
#Les données d'un mini-lot constituent la cible de mesure.
rand_index = np.arange(lm.x_train.get().shape[0])
np.random.shuffle(rand_index)
rand = rand_index[0 : n_batch]
#Calcul de la propagation vers l'avant
%%timeit
lm.forward(lm.x_train[rand])
#----------output----------
# 1000 loops, best of 3: 1.32 ms per loop
#--------------------------
#Calcul de rétropropagation
%%timeit
lm.backward(lm.y_train[rand])
#----------output----------
# 100 loops, best of 3: 10.3 ms per loop
#--------------------------
#Calcul de mise à jour du poids
%%timeit
lm.update()
#----------output----------
# 1000 loops, best of 3: 1.64 ms per loop
#--------------------------
De toute évidence, seule la rétropropagation prend un temps inhabituel. La propagation vers l'avant et les mises à jour de poids prennent 10 fois plus de temps.
Étant donné que les données d'entraînement sont de 60000 cette fois et que la taille du mini-lot est de 8, ce processus de calcul sera répété 7500 fois, donc au total ~~ $ (1,32 + 10,3 + 1,64) \ fois 7500 \ fois 10 ^ {- Cela coûtera 3} = 23,92s $. C'est moins que ce à quoi je m'attendais ... Cela a pris assez de temps, mais ce n'est toujours pas assez ... Eh bien, il y a beaucoup de fluctuations, alors ne vous inquiétez pas pour le moment. ~~
C'était juste une erreur de calcul ... Je dois bien comprendre les spécifications de la calculatrice.
Donc, nous allons diviser le processus de propagation arrière et le mesurer.
search_bottleneck.py
#Préparation préalable
err3 = lm[3].backward(lm.y_train[rand])
err2 = lm[2].backward(err3)
err2 = err2.reshape(n_batch, *lm[1].O_shape)
err1 = lm[1].backward(err2)
err0 = lm[0].backward(err1)
#Rétropropagation de la couche de sortie
%%timeit
err3 = lm[3].backward(lm.y_train[rand])
#----------output----------
# 10000 loops, best of 3: 152 µs per loop
#--------------------------
#Rétropropagation de la couche intermédiaire
%%timeit
err2 = lm[2].backward(err3)
err2 = err2.reshape(n_batch, *lm[1].O_shape)
#----------output----------
# 1000 loops, best of 3: 224 µs per loop
#--------------------------
#Rétropropagation de la couche de mise en commun
%%timeit
err1 = lm[1].backward(err2)
#----------output----------
# 1000 loops, best of 3: 9.72 ms per loop
#--------------------------
#Rétropropagation de la couche convolutive
%%timeit
err0 = lm[0].backward(err1)
#----------output----------
# 1000 loops, best of 3: 442 µs per loop
#--------------------------
Il s'avère que la couche de mise en commun est des ordres de grandeur plus lente. Le temps de traitement de la couche de pooling représente environ 93,6% du temps de calcul de la rétro-propagation. Au fait, si vous ajoutez ceci, ce sera environ 10 ms, donc c'est presque la même chose.
Examinons donc de plus près la rétropropagation de la couche de pooling en question.
search_bottleneck.py
#Préparation préalable
B, C, O_h, O_w = n_batch, *lm[1].O_shape
grad = err2.transpose(0, 2, 3, 1).reshape(-1, 1)
grad_x = cp.zeros((grad.size, lm[1].pool*lm[1].pool))
grad_x1 = grad_x.copy()
grad_x1[:, lm[1].max_index] = grad
grad_x2 = grad_x1.reshape(B*O_h*O_w, C*lm[1].pool*lm[1].pool).T
#Echange dimensionnel et transformation des erreurs
%%timeit
grad = err2.transpose(0, 2, 3, 1).reshape(-1, 1)
#----------output----------
# 100000 loops, best of 3: 17.1 µs per loop
#--------------------------
#Génération de matrice vide
%%timeit
grad_x = cp.zeros((grad.size, lm[1].pool*lm[1].pool))
#----------output----------
# 100000 loops, best of 3: 7.89 µs per loop
#--------------------------
#Remplissage de valeur
%%timeit
grad_x1[:, lm[1].max_index] = grad
#----------output----------
# 1000 loops, best of 3: 9.5 ms per loop
#--------------------------
#Déformation et translocation
%%timeit
grad_x2 = grad_x1.reshape(B*O_h*O_w, C*lm[1].pool*lm[1].pool).T
#----------output----------
# 1000000 loops, best of 3: 1.86 µs per loop
#--------------------------
# col2im
%%timeit
grad_x3 = lm[1].col2im(grad_x2, (n_batch, *lm[1].I_shape), lm[1].O_shape,
stride=lm[1].pool, pad=lm[1].pad_state)
#----------output----------
# 10000 loops, best of 3: 112 µs per loop
#--------------------------
Le remplissage des prix est extrêmement lent que les autres ... C'est le goulot d'étranglement ici. Le rapport entre le remplissage de valeur et la rétropropagation de la couche de regroupement est en fait d'environ 98,6%.
Le GPU est puissant pour les calculs simples, mais quand il s'agit d'un traitement aussi peu compliqué, il ralentit immédiatement et vous ne pouvez pas utiliser pleinement les performances. Pensons donc à un plan d'amélioration.
Je me suis demandé s'il y avait un bon moyen de renseigner le prix lors de l'accélération. La première chose à laquelle j'ai pensé était que ce type de traitement compliqué est mieux adapté au processeur, il devrait donc être traité par le processeur au lieu du GPU. Cependant, des expériences ont montré que le goulot d'étranglement lorsque l'ensemble du processus est traité par le processeur est également dans la même partie, donc cette idée a été rejetée.
L'idée suivante était de réécrire cette partie du processus sous une autre forme. En d'autres termes, j'ai pensé: "Remplaçons ce processus d'affectation par un processus de calcul dans lequel le GPU est bon." Cela signifie qu'au lieu de conserver l'index, vous devez conserver une matrice éparse qui a la même forme que l'entrée (renvoyée à la fonction ʻim2col`). 1 uniquement lorsque la valeur maximale correspond, 0 dans le cas contraire. La quantité de mémoire requise est le double du $ pool ^ 2 $ normal, mais $ pool $ est généralement petit, ce qui est très bien.
pool.py
import numpy as np
import cupy as cp
class PoolingLayer(BaseLayer):
def __init__(self, *, mode="cpu",
I_shape=None, pool=1, pad=0,
name="", **kwds):
self.mode = mode
self.name = name
if I_shape is None:
raise KeyError("Input shape is None.")
if len(I_shape) == 2:
C, I_h, I_w = 1, *I_shape
else:
C, I_h, I_w = I_shape
self.I_shape = (C, I_h, I_w)
#Contient la fonction im2col et la fonction col2im
if self.mode == "cpu":
self.im2col = cpu_im2col
self.col2im = cpu_col2im
elif self.mode == "gpu":
self.im2col = gpu_im2col
self.col2im = gpu_col2im
if self.mode == "cpu":
_, O_shape, self.pad_state = self.im2col(
np.zeros((1, *self.I_shape)),
(pool, pool),
stride=pool, pad=pad)
elif self.mode == "gpu":
_, O_shape, self.pad_state = self.im2col(
cp.zeros((1, *self.I_shape)),
(pool, pool),
stride=pool, pad=pad)
self.O_shape = (C, *O_shape)
self.n = np.prod(self.O_shape)
self.pool = pool
self.F_shape = (pool, pool)
def forward(self, x):
B = x.shape[0]
C, O_h, O_w = self.O_shape
self.x, _, self.pad_state = self.im2col(x, self.F_shape,
stride=self.pool,
pad=self.pad_state)
self.x = self.x.T.reshape(B*O_h*O_w*C, -1)
if self.mode == "cpu":
#self.max_index = np.argmax(self.x, axis=1)
self.y = np.max(self.x, axis=1, keepdims=True)
self.max_index = np.where(self.y == self.x, 1, 0)
self.y = self.y.reshape(B, O_h, O_w, C).transpose(0, 3, 1, 2)
elif self.mode == "gpu":
#self.max_index = cp.argmax(self.x, axis=1)
self.y = cp.max(self.x, axis=1, keepdims=True)
self.max_index = cp.where(self.y == self.x, 1, 0)
self.y = self.y.reshape(B, O_h, O_w, C).transpose(0, 3, 1, 2)
return self.y
def backward(self, grad):
B = grad.shape[0]
I_shape = B, *self.I_shape
C, O_h, O_w = self.O_shape
grad = grad.transpose(0, 2, 3, 1).reshape(-1, 1)
if self.mode == "cpu":
self.grad_x = np.zeros((grad.size, self.pool*self.pool))
elif self.mode == "gpu":
self.grad_x = cp.zeros((grad.size, self.pool*self.pool))
#self.grad_x[:, self.max_index] = grad
self.grad_x = self.max_index*grad
self.grad_x = self.grad_x.reshape(B*O_h*O_w, C*self.pool*self.pool).T
self.grad_x = self.col2im(self.grad_x, I_shape, self.O_shape,
stride=self.pool, pad=self.pad_state)
return self.grad_x
def update(self, **kwds):
pass
Expérimentons.
search_bottleneck.py
#Rétropropagation de la couche de mise en commun
%%timeit
err1 = lm[1].backward(err2)
#----------output----------
# 1000 loops, best of 3: 280 µs per loop
#--------------------------
#Remplissage de valeur
%%timeit
grad_x1 = lm[1].max_index*grad
#----------output----------
# 100000 loops, best of 3: 16.3 µs per loop
#--------------------------
D'ailleurs, on pense que les résultats ci-dessus sont attribués à un GPU différent des résultats expérimentaux précédents, il est donc délicat de comparer les résultats en général, mais pour le moment, nous avons réussi à accélérer. Il n'y aucun doute à propos de ça. Dans ce cas, la fonction col2im
, etc. deviendra un problème cette fois, il y a donc encore de la place pour accélérer autour de cela.
Aussi, dans son ensemble
progress:[XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]287s/285s
training dataset
correct: [5 0 4 1 9 2 1 3 1 4 3 5 3 6 1 7]
predict: [5 0 4 1 9 2 1 3 1 4 3 5 3 6 1 7]
accuracy rate: 99.21333333333334 % (59528/60000)
test dataset
correct: [7 2 1 0 4 1 4 9 5 9 0 6 9 0 1 5]
predict: [7 2 1 0 4 1 4 9 5 9 0 6 9 0 1 5]
accuracy rate: 98.03 % (9803/10000)
De cette façon, nous avons pu le réduire à environ 50 s par époque! De plus, comme le temps d'apprentissage par mini-lot est d'environ 6 ms, le temps d'apprentissage par époque est de 6 $ \ times 7500 \ times 10 ^ {-3} = 45s $. ~~ Et la précédente discordance a été résolue ... Qu'est-ce que c'était après tout? J'aurais dû expérimenter en attribuant au même GPU ... eh bien. ~~
Avec ce genre de sentiment, nous allons rechercher la partie goulot d'étranglement, l'améliorer et l'accélérer. Nous continuerons de l'améliorer de temps en temps.
Lorsque la taille du mini-lot P.S. était définie sur 128, le temps d'exécution était presque le même que celui de l'expérience avec Keras. C'était bon.
[^ 1]: Strictement parlant, "le taux d'intégration des semi-conducteurs double en 18 mois (24 mois)".
Recommended Posts