Der vorherige Artikel war hier Im vorherigen Artikel schrieb ich: "Das Lernen mit dem MNIST-Datensatz von Keras dauert mehrere Stunden." Ich kann das mit einem kleinen Datensatz wie dem MNIST-Datensatz nicht machen, also werde ich es beschleunigen. Apropos schnelles Lernen zu beschleunigen, es ist die Verwendung von GPU und TPU ~ In diesem Artikel werden wir die GPU-Programmierung durchführen, um NVIDIA-GPUs zu verwenden. Das verwendete Paket ist "CuPy". Der Grund wird später sein ...
CuPy
](GPU-Programmierung mit #cupy)CuPy
](Installation und Bestätigung von #cupy)CuPy
Programmierung](#cupy Programmierung)Hinweis: Sie können es überspringen, da es langsam spricht.
Die Entwicklung der Computerarchitektur in den letzten Jahren schreitet auf einem beispiellosen Niveau voran. Als ich ein Kind war, war Game Boy Advance zum Beispiel beliebt, aber die Datenkapazität der dort laufenden Spiele betrug maximal ** 32 MB **. Heutzutage haben PS4, PS5, Switch und andere Spiele, die auf extrem spezieller Spielehardware ausgeführt werden, selbstverständlich eine Datenkapazität von ** 10 GB **. Da GB das 1024-fache von MB beträgt, bedeutet dies, dass ** es in nur 10 Jahren möglich ist, fast das 1000-fache der Datenmenge zu verarbeiten **. Daraus können Sie den Fortschritt von Speichermedien wie HDD und SSD sehen.
Mit zunehmender Datenmenge steigt natürlich auch die Anzahl der Anweisungen, die der Computer verarbeitet. Ich weiß nicht, dass die für die CPU erforderliche Rechenleistung erschöpft ist, aber um darauf zu reagieren, folgt die Entwicklung der CPU einer empirischen Regel namens "** Mooresches Gesetz **" ** Verdoppelt in 18 Monaten (vor kurzem 24 Monaten) Es wurde ** [^ 1]. Dies bedeutet, dass ** 15 Jahre die Verarbeitungsleistung Ihres Computers um das 1024-fache erhöhen **. Das ist unglaublich ~
Wie bereits erwähnt, wird die für eine blaue CPU erforderliche Verarbeitungsleistung für die blaue Decke jedes Mal benötigt, wenn sich die Leistung der CPU verbessert und mehr getan werden kann. Aus diesem Grund wurde mangelnde Leistung immer beklagt.
Es besteht kein Zweifel, dass der Hintergrund des tiefen Lernens im Rampenlicht die Verbesserung der CPU-Leistung ist, und es gibt viele Fälle, in denen die mangelnde CPU-Leistung beklagt wird. Eine davon ist die Bilderkennung und das ** Convolutional Neural Network (CNN) **. Da die Bilddaten zweidimensional sind und Sie versuchen, einen leicht großen Bilddatensatz zum Lernen zu verwenden, werden sie schnell zu einem Tensor mit mehr als 10.000 Elementeinheiten, und die Leistung der aktuellen CPU ist überwiegend unzureichend. Vorheriger Artikel gibt nicht speziell an, aber im Experiment, wenn Sie versuchen, die Verwendung des Kerist-MNIST-Datasets in Google Colaboratory zu erlernen Es dauert auch ** 1 Epoche 30 Minuten **. Da Google Colaboratory ein 12-Stunden-Limit hat, können Sie nur 24 Epochen lernen (es sei denn, Sie lernen weiter durch temporäres Speichern und erneutes Laden). Nun, mit dem MNIST-Datensatz können Sie immer noch mit ausreichender Genauigkeit lernen.
Wie auch immer, es ist nicht einfach, damit zu experimentieren. Die GPU stand im Mittelpunkt der Aufmerksamkeit.
GPU
Die CPU ist die Zentraleinheit, während die GPU als Grafikverarbeitungseinheit bezeichnet wird. Wie der Name schon sagt, handelt es sich um einen ** Halbleiterprozessor, der sich auf Berechnungen für Siebzeichnungen ** spezialisiert hat.
** Die CPU eignet sich hervorragend für allgemeine Berechnungen **, während die ** GPU auf die Berechnung für die Bildverarbeitung spezialisiert ist **, sodass ihre Geschwindigkeit überwältigend ist. Da die massiv parallele Berechnung mit Tausenden oder mehr Kernen durchgeführt wird, wird die Bildschirmzeichnung grundsätzlich ohne Verzögerung durchgeführt.
Und hier ist das Miso, aber diese superparallele Berechnung und Matrixberechnung haben eine Affinität.
Bitte beachten Sie, dass diese Figur nicht nur eine Metapher ist, sondern dies auch nicht auf der GPU tut. Ich möchte, dass Sie aus dieser Abbildung lesen, dass die ** Matrixberechnung parallel ausgeführt werden kann **.
Übrigens kann die obige Abbildung durch Parallelisieren von CPUs realisiert werden, aber der Maßstab der GPUs ist eine Größenordnung.
Daher werden GPUs, die sich auf tiefes Lernen konzentriert haben, durch die Einführung von GPGPU einen großen Beitrag zu ihrer Entwicklung leisten: Allzweck-Computing auf Grafikverarbeitungseinheiten: Technologie, die Grafikverarbeitungsgeräte für allgemeine Berechnungen verwendet.
TPU Mit dem Aufkommen von GPUs und GPGPUs hat Deep Learning schnelle Fortschritte gemacht, aber es ist die menschliche Natur, die nicht ausreicht. Aus diesem Grund wurde TPU: Tensor Processing Unit: Tensol Processing Device eingeführt.
Die GPU wurde nur für Grafiken entwickelt, von dort aus wurde die TPU ** entwickelt, um eine Hochgeschwindigkeitstensorberechnung für tiefes Lernen zu realisieren. Auf Kosten der Vielseitigkeit und ein wenig Rechengenauigkeit haben wir Geschwindigkeiten erreicht, die die GPU überfordern und sogar unsere Füße erreichen.
Da es auf die Tensorberechnung spezialisiert ist, ist es weniger vielseitig als die GPU und wird beschleunigt, indem die Berechnung von 32 Bit oder 64 Bit auf 8 Bit oder 16 Bit reduziert wird. Um ein gleichmäßiges Schreiben in den Cache-Speicher zu reduzieren, werden außerdem Daten in der arithmetischen Schaltung ausgetauscht, und die Tensorberechnung wird ohnehin mit hoher Geschwindigkeit durchgeführt.
Alpha Go Zero ist ein typisches Beispiel für seine überwältigende Kraft. Der Berechnungsaufwand, der bei der Konvertierung in eine CPU durch einfache Berechnung ** 30.000 Jahre ** dauern würde, wurde in ** 3 Tagen ** mit mehreren TPUs abgeschlossen. Ich verstehe die Bedeutung nicht. Lol
Daher sind die Vorteile einer massiv parallelen Berechnung beim Deep Learning enorm.
Kommen wir nun zum Hauptthema. In diesem Artikel verwenden wir "CuPy" für die GPU-Programmierung. Es scheint, dass "CuPy" ursprünglich ein Paket war, das für die Implementierung eines GPU-Programms (CUDA-Programmierung) in Chainer entwickelt wurde. Der größte Vorteil ist, dass es auf "numpy" folgt, sodass der meiste Code nur "np" ("numpy als np importieren") in "cp" ("cupy als cp importieren") umschreibt. Aus diesem Grund habe ich mich in diesem Artikel für die Verwendung von "CuPy" entschieden. Einfach ist toll! Lol
Ich denke selten darüber nach. Oder besser gesagt, es ist schmutzig, weil ich es als Prototyp implementiert habe ... Ich werde es bald klären. Ich frage mich, ob ich einen Dekorateur verwenden sollte ... Bitte lassen Sie mich wissen, wenn Sie eine gute Idee haben. Der Code lautet hier.
** Um GPU mit Google Colaboratory verwenden zu können, müssen Sie übrigens GPU als Laufzeittyp auswählen. ** ** **
Um "CuPy" zu installieren, führen Sie die Zelle aus, in der Sie den folgenden Code eingegeben haben.
!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!
Dadurch wird automatisch die erforderliche Version von "CuPy" installiert. Auch Chainer
. Ich benutze es nicht, aber es ist okay.
Mit dem folgenden Code können Sie überprüfen, ob es ordnungsgemäß installiert ist.
!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
Es ist in Ordnung, wenn Sie die Ausgabe wie folgt bestätigen können.
Nehmen Sie als Teil (einen Teil) der Aktivierungsfunktion.
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)
Ich schreibe mit Hirntod. Irgendwie muss es einen klügeren Weg geben ...
Wir verzweigen, indem wir die Fähigkeit von Python nutzen, Funktionen als eine Art Objekt zuzuweisen.
Der Teil der Funktionsimplementierung unterscheidet sich auch von np
und cp
! Das ist das Schöne an "CuPy". Sie können die GPU-Programmierung einfach und bequem durchführen ~
Experimentieren wir mit dem MNIST-Datensatz von Keras. Die Ausführung führt alle Zellen bis zum experimentellen Code aus, führt die Zelle aus, die die Keras-Daten liest, und führt schließlich den experimentellen CNN-Code-Body aus.
cnn_main.py
%matplotlib inline
#Erstellen Sie eine Faltungsschicht und eine Ausgabeebene
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")
#Lernen
epoch = 5
threshold = 1e-8
n_batch = 8
lm.training(epoch, threshold=threshold, n_batch=n_batch, show_train_error=True)
#Vorhersagen
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)
Es hat keine Bedeutung, aber standardmäßig Aktivierungsfunktion, Gewichtsbereich "wb_width", Optimierer usw. Mit anderen Worten, die Aktivierungsfunktion ist ReLU, "wb_width" ist 0,05 und der Optimierer ist Adam. Die Lernphase ist auf 5 eingestellt.
Das Ausführungsergebnis beträgt ca. 100 Sekunden pro Epoche! Es ist uns gelungen, 18 Mal zu beschleunigen. Es ist immer noch langsam, aber es sollte praktisch sein. Außer MNIST ... (entfernte Augen)
Am Ende des Testcodes befindet sich übrigens der MNIST-Dataset-Trainingscode in Keras. Ich habe es von [hier] kopiert (https://github.com/keras-team/keras/blob/master/examples/mnist_cnn.py).
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
Es ist schnell ... und 20 mal schneller ... das heißt, es gibt immer noch Raum für Geschwindigkeit!
Lassen Sie uns nun herausfinden, wo der Engpass der Berechnungsgeschwindigkeit in meinem aktuellen Code liegt. Die Verarbeitungszeit wird mit "Timeit" -Magie gemessen. Wenn Sie dies verwenden, wird die Verarbeitungszeit gut gemessen.
Lassen Sie uns zunächst die für die Fehlerberechnung erforderliche Zeit messen.
search_bottleneck.py
#Berechnung des Trainingsfehlers
%%timeit
lm.forward(lm.x_train)
error = lm[-1].get_error(lm.y_train)
#----------output----------
# 1 loop, best of 3: 957 ms per loop
#--------------------------
#Berechnung des Testfehlers
%%timeit
lm.forward(lm.x_test)
error = lm[-1].get_error(lm.y_test)
#----------output----------
# 10 loops, best of 3: 160 ms per loop
#--------------------------
Die Datenmenge für die Fehlerberechnung von Trainingsdaten beträgt 60.000, also wird es so sein. Ich denke, es ist okay, weniger zu haben ... Ich denke, wir können uns hier verbessern. Wenn Sie die Testdaten auf 10.000 reduzieren, werden sie pro Epoche um etwa 0,8 Sekunden verkürzt. Nun, das ist eine Art Fehler. In Anbetracht des Ganzen (100 Sekunden pro Epoche) beträgt die Fehlerberechnungsquote insgesamt etwa 1%, sodass dies kein Engpass ist. Der Lernteil scheint also ein Problem zu sein.
Wir werden die Bearbeitungszeit des Lernteils messen. Irgendwo beim Lernen sollten 99% der Verarbeitungszeit pro Epoche ...
search_bottleneck.py
#Die Daten für eine Mini-Charge sind das Messziel.
rand_index = np.arange(lm.x_train.get().shape[0])
np.random.shuffle(rand_index)
rand = rand_index[0 : n_batch]
#Vorwärtsausbreitungsberechnung
%%timeit
lm.forward(lm.x_train[rand])
#----------output----------
# 1000 loops, best of 3: 1.32 ms per loop
#--------------------------
#Backpropagation-Berechnung
%%timeit
lm.backward(lm.y_train[rand])
#----------output----------
# 100 loops, best of 3: 10.3 ms per loop
#--------------------------
#Berechnung der Gewichtsaktualisierung
%%timeit
lm.update()
#----------output----------
# 1000 loops, best of 3: 1.64 ms per loop
#--------------------------
Offensichtlich nimmt nur die Rückausbreitung ungewöhnlich viel Zeit in Anspruch. Die Vorwärtsausbreitung und Gewichtsaktualisierungen dauern zehnmal länger.
Da die Trainingsdaten diesmal 60.000 und die Mini-Batch-Größe 8 betragen, wird dieser Berechnungsprozess 7500 Mal wiederholt, also insgesamt ~~ $ (1,32 + 10,3 + 1,64) \ mal 7500 \ mal 10 ^ {- Es kostet 3} = 23,92s $. Das ist weniger als ich erwartet hatte ...? Es hat genug Zeit in Anspruch genommen, aber es ist immer noch nicht genug ... Nun, es gibt viele Schwankungen, also mach dir vorerst keine Sorgen. ~~
Es war nur ein Berechnungsfehler ... Ich muss die Spezifikationen des Rechners richtig verstehen.
Also werden wir den Rückausbreitungsprozess aufteilen und messen.
search_bottleneck.py
#Vorbereitungen
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)
#Backpropagation der Ausgabeschicht
%%timeit
err3 = lm[3].backward(lm.y_train[rand])
#----------output----------
# 10000 loops, best of 3: 152 µs per loop
#--------------------------
#Rückausbreitung der Mittelschicht
%%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
#--------------------------
#Backpropagation der Pooling-Schicht
%%timeit
err1 = lm[1].backward(err2)
#----------output----------
# 1000 loops, best of 3: 9.72 ms per loop
#--------------------------
#Rückausbreitung der Faltungsschicht
%%timeit
err0 = lm[0].backward(err1)
#----------output----------
# 1000 loops, best of 3: 442 µs per loop
#--------------------------
Es stellt sich heraus, dass die Pooling-Schicht um Größenordnungen langsamer ist. Die Verarbeitungszeit der Pooling-Schicht macht etwa 93,6% der Berechnungszeit der Rückausbreitung aus. Übrigens, wenn Sie dies hinzufügen, werden es ungefähr 10 ms sein, also ist es fast das gleiche.
Schauen wir uns also die Backpropagation der betreffenden Pooling-Schicht genauer an.
search_bottleneck.py
#Vorbereitungen
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
#Dimensionsaustausch und Transformation von Fehlern
%%timeit
grad = err2.transpose(0, 2, 3, 1).reshape(-1, 1)
#----------output----------
# 100000 loops, best of 3: 17.1 µs per loop
#--------------------------
#Leere Matrixerzeugung
%%timeit
grad_x = cp.zeros((grad.size, lm[1].pool*lm[1].pool))
#----------output----------
# 100000 loops, best of 3: 7.89 µs per loop
#--------------------------
#Wertfüllung
%%timeit
grad_x1[:, lm[1].max_index] = grad
#----------output----------
# 1000 loops, best of 3: 9.5 ms per loop
#--------------------------
#Verformung und Translokation
%%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
#--------------------------
Die Preisfüllung ist überwältigend langsamer als bei anderen ... Das ist der Engpass hier. Das Verhältnis der Wertfüllung zur Rückausbreitung der Poolschicht beträgt tatsächlich etwa 98,6%.
Die GPU ist stark für einfache Berechnungen, aber wenn es um eine so etwas komplizierte Verarbeitung geht, verlangsamt sie sich sofort und Sie können die Leistung nicht voll ausnutzen. Denken wir also über einen Verbesserungsplan nach.
Ich fragte mich, ob es eine gute Möglichkeit gab, den Preis beim Beschleunigen einzugeben. Das erste, woran ich dachte, war, dass diese Art der komplizierten Verarbeitung besser für die CPU geeignet ist, daher sollte sie von der CPU anstelle der GPU verarbeitet werden. Im Experiment wurde jedoch festgestellt, dass der Engpass bei der Verarbeitung des Ganzen durch die CPU ebenfalls im selben Teil lag, sodass diese Idee abgelehnt wurde.
Die nächste Idee war, diesen Teil des Prozesses in eine andere Form umzuschreiben. Mit anderen Worten, ich dachte: "Ersetzen wir diesen Zuweisungsprozess durch einen Berechnungsprozess, in dem die GPU gut ist."
Dies bedeutet, dass Sie anstelle des Index eine dünn besetzte Matrix halten sollten, die dieselbe Form wie die Eingabe hat (die in die Funktion im2col
geworfen wird). 1 nur dort, wo der Maximalwert entspricht, sonst 0.
Der erforderliche Speicherplatz ist doppelt so groß wie der normale $ pool ^ 2 $, aber $ pool $ ist normalerweise klein, was in Ordnung ist.
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)
#Enthält die Funktion im2col und 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
Lass uns experimentieren.
search_bottleneck.py
#Backpropagation der Pooling-Schicht
%%timeit
err1 = lm[1].backward(err2)
#----------output----------
# 1000 loops, best of 3: 280 µs per loop
#--------------------------
#Wertfüllung
%%timeit
grad_x1 = lm[1].max_index*grad
#----------output----------
# 100000 loops, best of 3: 16.3 µs per loop
#--------------------------
Übrigens wird angenommen, dass die obigen Ergebnisse einer GPU zugeordnet sind, die sich von den vorherigen experimentellen Ergebnissen unterscheidet. Daher ist es schwierig, die Ergebnisse im Allgemeinen zu vergleichen, aber vorerst ist es uns gelungen, sie zu beschleunigen. Es besteht kein Zweifel daran. In diesem Fall wird die col2im
-Funktion usw. diesmal zu einem Problem, sodass noch Raum für eine Beschleunigung besteht.
Auch als Ganzes
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)
Auf diese Weise konnten wir es auf etwa 50 pro Epoche verkürzen! Da die Lernzeit pro Mini-Batch etwa 6 ms beträgt, beträgt die Lernzeit pro Epoche $ 6 \ mal 7500 \ mal 10 ^ {-3} = 45s $. ~~ Und das vorherige Missverhältnis wurde behoben ... Was war es schließlich? Ich hätte experimentieren sollen, während ich derselben GPU zugewiesen habe ... na ja. ~~
Mit diesem Gefühl werden wir nach dem Engpass suchen, ihn verbessern und beschleunigen. Wir werden es von Zeit zu Zeit weiter verbessern.
Wenn die P.S.-Minibatchgröße auf 128 eingestellt wurde, war die Ausführungszeit fast dieselbe wie beim Experiment mit Keras. Es war gut.
[^ 1]: Genau genommen "verdoppelt sich die Integrationsrate von Halbleitern in 18 Monaten (24 Monaten)".
Recommended Posts