Глубокое обучение и нейронные сети с Python и Pytorch. Часть VII: запускаем обучение на GPU

Предыдущая статья — Глубокое обучение и нейронные сети с Python и Pytorch. Часть VI: модель сверточной нейронной сети.

Данная статья предполагает, что у вас есть доступ к GPU либо локально, либо через облако.

Если вы используете сервер, вам нужно будет загрузить данные, извлечь, а также установить jupyter notebook:

wget https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_3367a.zip
sudo apt-get install unzip
unzip kagglecatsanddogs_3367a.zip
pip3 install jupyterlab

Далее вы можете запустить его следующим образом:

jupyter lab --allow-root --ip=0.0.0.0

После этого вы увидите что-то вроде этого:

To access the notebook, open this file in a browser:
        file:///root/.local/share/jupyter/runtime/nbserver-1470-open.html
    Or copy and paste one of these URLs:
        http://localhost:8888/?token=f407ba1f9a362822f2a294277b2be3e9
     or http://127.0.0.1:8888/?token=f407ba1f9a362822f2a294277b2be3e9

Затем вам нужно будет перейти по указанному выше URL-адресу, заменив 127.0.0.1 на IP-адрес вашего сервера.

Наш код на настоящий момент имеет следующий вид:

import os
import cv2
import numpy as np
from tqdm import tqdm
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

REBUILD_DATA = False # set to true to one once, then back to false unless you want to change something in your training data.

class DogsVSCats():
    IMG_SIZE = 50
    CATS = "PetImages/Cat"
    DOGS = "PetImages/Dog"
    TESTING = "PetImages/Testing"
    LABELS = {CATS: 0, DOGS: 1}
    training_data = []

    catcount = 0
    dogcount = 0

    def make_training_data(self):
        for label in self.LABELS:
            print(label)
            for f in tqdm(os.listdir(label)):
                if "jpg" in f:
                    try:
                        path = os.path.join(label, f)
                        img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
                        img = cv2.resize(img, (self.IMG_SIZE, self.IMG_SIZE))
                        self.training_data.append([np.array(img), np.eye(2)[self.LABELS[label]]])  # do something like print(np.eye(2)[1]), just makes one_hot 
                        #print(np.eye(2)[self.LABELS[label]])

                        if label == self.CATS:
                            self.catcount += 1
                        elif label == self.DOGS:
                            self.dogcount += 1

                    except Exception as e:
                        pass
                        #print(label, f, str(e))

        np.random.shuffle(self.training_data)
        np.save("training_data.npy", self.training_data)
        print('Cats:',dogsvcats.catcount)
        print('Dogs:',dogsvcats.dogcount)


class Net(nn.Module):
    def __init__(self):
        super().__init__() # just run the init of parent class (nn.Module)
        self.conv1 = nn.Conv2d(1, 32, 5) # input is 1 image, 32 output channels, 5x5 kernel / window
        self.conv2 = nn.Conv2d(32, 64, 5) # input is 32, bc the first layer output 32. Then we say the output will be 64 channels, 5x5 kernel / window
        self.conv3 = nn.Conv2d(64, 128, 5)

        x = torch.randn(50,50).view(-1,1,50,50)
        self._to_linear = None
        self.convs(x)

        self.fc1 = nn.Linear(self._to_linear, 512) #flattening.
        self.fc2 = nn.Linear(512, 2) # 512 in, 2 out bc we're doing 2 classes (dog vs cat).

    def convs(self, x):
        # max pooling over 2x2
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv3(x)), (2, 2))

        if self._to_linear is None:
            self._to_linear = x[0].shape[0]*x[0].shape[1]*x[0].shape[2]
        return x

    def forward(self, x):
        x = self.convs(x)
        x = x.view(-1, self._to_linear)  # .view is reshape ... this flattens X before 
        x = F.relu(self.fc1(x))
        x = self.fc2(x) # bc this is our output layer. No activation here.
        return F.softmax(x, dim=1)


net = Net()
print(net)

if REBUILD_DATA:
    dogsvcats = DogsVSCats()
    dogsvcats.make_training_data()

training_data = np.load("training_data.npy", allow_pickle=True)
print(len(training_data))

optimizer = optim.Adam(net.parameters(), lr=0.001)
loss_function = nn.MSELoss()

X = torch.Tensor([i[0] for i in training_data]).view(-1,50,50)
X = X/255.0
y = torch.Tensor([i[1] for i in training_data])

VAL_PCT = 0.1  # lets reserve 10% of our data for validation
val_size = int(len(X)*VAL_PCT)

train_X = X[:-val_size]
train_y = y[:-val_size]

test_X = X[-val_size:]
test_y = y[-val_size:]

BATCH_SIZE = 100
EPOCHS = 1


def train(net):
    for epoch in range(EPOCHS):
        for i in tqdm(range(0, len(train_X), BATCH_SIZE)): # from 0, to the len of x, stepping BATCH_SIZE at a time. [:50] ..for now just to dev
            #print(f"{i}:{i+BATCH_SIZE}")
            batch_X = train_X[i:i+BATCH_SIZE].view(-1, 1, 50, 50)
            batch_y = train_y[i:i+BATCH_SIZE]

            net.zero_grad()

            outputs = net(batch_X)
            loss = loss_function(outputs, batch_y)
            loss.backward()
            optimizer.step()    # Does the update

        print(f"Epoch: {epoch}. Loss: {loss}")


def test(net):
    correct = 0
    total = 0
    with torch.no_grad():
        for i in tqdm(range(len(test_X))):
            real_class = torch.argmax(test_y[i])
            net_out = net(test_X[i].view(-1, 1, 50, 50))[0]  # returns a list, 
            predicted_class = torch.argmax(net_out)

            if predicted_class == real_class:
                correct += 1
            total += 1

    print("Accuracy: ", round(correct/total, 3)) 

Результат:

Net(
  (conv1): Conv2d(1, 32, kernel_size=(5, 5), stride=(1, 1))
  (conv2): Conv2d(32, 64, kernel_size=(5, 5), stride=(1, 1))
  (conv3): Conv2d(64, 128, kernel_size=(5, 5), stride=(1, 1))
  (fc1): Linear(in_features=512, out_features=512, bias=True)
  (fc2): Linear(in_features=512, out_features=2, bias=True)
)
24946

Мы пошли несколько дальше и сделали быструю функцию для обработки процесса обучения, в основном для того, чтобы не запускать заново тренировочную часть нашего кода. Вместо этого мы хотим поговорить про работу на GPU.

Для начала вам понадобится версия Pytorch для GPU. А чтобы использовать Pytorch на графическом процессоре, вам понадобится графический процессор NVIDIA с поддержкой технологии CUDA.

Если его нет, то потребуются облачные провайдеры, например Linode. Прим. переводчика — отлично подойдут Google Colaboratory или Kaggle. Они абсолютно бесплатны.

Для работы на локальном компьютере вам нужно загрузить CUDA toolkit.

После этого необходимо загрузить и распаковать CuDNN, переместив содержимое CuDNN в каталог Cuda Toolkit. Когда вы распакуете архив CuDNN, у вас будет 3 каталога внутри каталога cuda. Вам просто нужно переместить содержимое этих каталогов (bin, include и lib) в одноименные директории, находящиеся в каталоге Cuda Toolkit.

Сделав это, вам надо будет убедиться что у вас установлена версия Pytorch для GPU. Ее можно найти на стартовой странице фреймворка Pytorch. Также надо сказать, что если вы используете пакет Anaconda, то в нем уже предустановлены все возможные версии Pytorch.

Убедиться, что CUDA установлена, можно следующим образом:

torch.cuda.is_available()

Результат:

True

Теперь мы можем подумать о том, что именно хотим делать с графическим процессором. Как минимум нам нужно, чтобы на нем выполнялось обучение нашей модели.

Это означает, что все наши данные также должны обрабатываться на графическом процессоре.

Для начала давайте разместим на GPU нашу нейронную сеть. Для этого мы можем просто установить следующий флаг:

device = torch.device("cuda:0")
device

Результат:

device(type='cuda', index=0)

Однако зачастую мы хотим написать код, которым будут пользоваться разные люди, в том числе и те, у кого может отсутствовать возможность использовать графический процессор. Решить эту проблему можно следующим образом:

if torch.cuda.is_available():
    device = torch.device("cuda:0")  # здесь вы можете продолжить, например,cuda:1 cuda:2... и т. д. 
    print("Running on the GPU")
else:
    device = torch.device("cpu")
    print("Running on the CPU")

Результат:

Running on the GPU

Большинство базовых нейронных сетей не особо выигрывают от использования нескольких графических процессоров, но в дальнейшем вы можете все же захотеть использовать несколько GPU для своей задачи. Узнать, сколько вам доступно процессоров, можно следующим образом:

torch.cuda.device_count()

Результат:

1

Таким образом мы можем экстраполировать номера индексов и назначить конкретным графическим процессорам определенные слои нашей сети.

На данный момент мы пишем код, которому в действительности нужен только один графический (или обычный) процессор, поэтому мы будем использовать только одно устройство. И теперь, выяснив, какое устройство лучше всего использовать, мы можем приступить к его настройке. Сделать это очень просто, достаточно лишь выполнить следующий код:

net.to(device)

Результат:

Net(
  (conv1): Conv2d(1, 32, kernel_size=(5, 5), stride=(1, 1))
  (conv2): Conv2d(32, 64, kernel_size=(5, 5), stride=(1, 1))
  (conv3): Conv2d(64, 128, kernel_size=(5, 5), stride=(1, 1))
  (fc1): Linear(in_features=512, out_features=512, bias=True)
  (fc2): Linear(in_features=512, out_features=2, bias=True)
)

Выше мы уже определили нашу сеть, но обычно вы просто сразу ее определяете и отправляете на устройство, например следующим образом:

net = Net().to(device)

Теперь мы можем перейти к обучению, но на этот раз мы поместим наши батчи в GPU. В этом примере мы реально можем за один раз поместить все наши данные в GPU, так как у нас не очень большой объем данных. И это, конечно, сэкономит нам некоторое время, затрачиваемое на передачу данных из VRAM в GPU и обратно. Но обычно подобное невозможно, поэтому мы покажем вам, как это происходит в нормальных условиях.

Мы скопируем нашу функцию train, а затем произведем в ней одну небольшую модификацию: после задания наших батчей мы сразу будем перемещать их в GPU, выполнив batch_X, batch_y = batch_X.to(device), batch_y.to(device). Итак, теперь наша функция train имеет следующий вид:

EPOCHS = 3

def train(net):
    optimizer = optim.Adam(net.parameters(), lr=0.001)
    BATCH_SIZE = 100
    EPOCHS = 3
    for epoch in range(EPOCHS):
        for i in range(0, len(train_X), BATCH_SIZE): # from 0, to the len of x, stepping BATCH_SIZE at a time. [:50] ..for now just to dev
            #print(f"{i}:{i+BATCH_SIZE}")
            batch_X = train_X[i:i+BATCH_SIZE].view(-1, 1, 50, 50)
            batch_y = train_y[i:i+BATCH_SIZE]

            batch_X, batch_y = batch_X.to(device), batch_y.to(device)
            net.zero_grad()

            optimizer.zero_grad()   # zero the gradient buffers
            outputs = net(batch_X)
            loss = loss_function(outputs, batch_y)
            loss.backward()
            optimizer.step()    # Does the update

        print(f"Epoch: {epoch}. Loss: {loss}")

train(net) 

Результат:

Epoch: 0. Loss: 0.23834262788295746
Epoch: 1. Loss: 0.20373524725437164
Epoch: 2. Loss: 0.1704103648662567

Как можно заметить, сейчас это работает намного быстрее. Теперь мы можем проверить нашу модель на тестовой выборке (это можно сделать как на CPU, так и на GPU). Поскольку эта выборка совсем небольшая, сделаем это тоже на GPU:

test_X.to(device)
test_y.to(device)

def test(net):
    correct = 0
    total = 0
    with torch.no_grad():
        for i in tqdm(range(len(test_X))):
            real_class = torch.argmax(test_y[i]).to(device)
            net_out = net(test_X[i].view(-1, 1, 50, 50).to(device))[0]  # returns a list, 
            predicted_class = torch.argmax(net_out)

            if predicted_class == real_class:
                correct += 1
            total += 1

    print("Accuracy: ", round(correct/total, 3))

test(net) 

Результат:

100%|a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^| 2494/2494 [00:08<00:00, 299.45it/s]
Accuracy:  0.706

А вот тоже самое с использованием батчей работает быстрее:

correct = 0
total = 0
for i in tqdm(range(0, len(test_X), BATCH_SIZE)):

    batch_X = test_X[i:i+BATCH_SIZE].view(-1, 1, 50, 50).to(device)
    batch_y = test_y[i:i+BATCH_SIZE].to(device)
    batch_out = net(batch_X)

    out_maxes = [torch.argmax(i) for i in batch_out]
    target_maxes = [torch.argmax(i) for i in batch_y]
    for i,j in zip(out_maxes, target_maxes):
        if i == j:
            correct += 1
        total += 1
print("Accuracy: ", round(correct/total, 3)) 

Результат:

100%|a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^| 25/25 [00:01<00:00, 19.49it/s]
Accuracy:  0.706

Итак, по сравнению с тем, что было, мы добились значительного прогресса.

Мы научились создавать и обучать нейронные сети и уже получили приличный результат.

Но можем ли мы добиться большего, чем точность в 70%? Должны ли мы увеличивать количество эпох? И если да, то когда нам все же остановиться?

Что, если у нас есть несколько моделей, и чтобы их сравнить, нужно довести их до совершенства? Мы же не знаем, когда это произойдет!

В следующей статье мы рассмотрим некоторые базовые методы анализа и визуализации результатов. А также разберем несколько концепций, которые необходимо учитывать при анализе эффективности работы модели.

Следующая статья — Глубокое обучение и нейронные сети с Python и Pytorch. Часть VIII: основы анализа нейронных сетей.