пятница, 18 января 2019 г.

PyTorch: загрузка и обработка данных изображений

Много усилий в решении любой проблемы машинного обучения идет на подготовку данных. PyTorch предоставляет множество инструментов для упрощения загрузки данных. В этом посте мы рассмотрим, как загружать и обрабатывать/дополнять данные из нетривиального набора данных.

Чтобы выполнять примеры из этого поста, убедитесь, что следующие пакеты установлены:

  • scikit-image - для ввода-вывода изображений и преобразований
  • pandas - для более простого разбора csv

# -*- coding: utf-8 -*-
from __future__ import print_function, division
import os
import torch
import pandas as pd
from skimage import io, transform
import numpy as np
import matplotlib.pyplot as plt
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms, utils

# игнорируем предупреждения
import warnings
warnings.filterwarnings("ignore")

plt.ion()   # интерактивный режим

Набор данных, с которым мы собираемся иметь дело, это набор лицевых поз. Это означает, что лицо помечается так:

В целом, 68 различных ориентиров отмечены для каждого лица.

Примечание. Загрузите набор данных отсюда и сделайте так, чтобы они располагались в каталоге названном data/faces/.

Набор данных поставляется с CSV-файлом с аннотациями, который выглядит следующим образом:

image_name,part_0_x,part_0_y,part_1_x,part_1_y,part_2_x, ... ,
part_67_x,part_67_y
0805personali01.jpg,27,83,27,98, ... 84,134
1084239450_e76e00b7e7.jpg,70,236,71,257, ... ,128,312

Давайте быстро прочитаем CSV и получим аннотации в массиве (N, 2), где N - количество ориентиров.

landmarks_frame = pd.read_csv('data/faces/face_landmarks.csv')

n = 65
img_name = landmarks_frame.iloc[n, 0]
landmarks = landmarks_frame.iloc[n, 1:].as_matrix()
landmarks = landmarks.astype('float').reshape(-1, 2)

print('Image name: {}'.format(img_name))
print('Landmarks shape: {}'.format(landmarks.shape))
print('First 4 Landmarks: {}'.format(landmarks[:4]))

Вывод:

Image name: person-7.jpg
Landmarks shape: (68, 2)
First 4 Landmarks: [[32. 65.]
 [33. 76.]
 [34. 86.]
 [34. 97.]]

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

# -*- coding: utf-8 -*-
def show_landmarks(image, landmarks):
    """Показывает изображение с ориентирами"""
    plt.imshow(image)
    plt.scatter(landmarks[:, 0], landmarks[:, 1], s=10, marker='.', 
                c='r')
    plt.pause(0.001)  # pause a bit so that plots are updated

plt.figure()
show_landmarks(io.imread(os.path.join('data/faces/', img_name)),
               landmarks)
plt.show()


Класс Dataset

torch.utils.data.Dataset - абстрактный класс, представляющий набор данных. Ваш пользовательский набор данных должен наследовать Dataset и переопределять следующие методы:

  • __len__, чтобы len(dataset) возвращал размер набора данных.
  • __getitem__ для поддержки индексации, так что dataset[i] может использоваться для получения i-го экземпляра

Давайте создадим dataset класс для нашего набора данных лицевых ориентиров. Мы будем читать csv в __init__, но оставим чтение изображений для __getitem__. Это эффективно для памяти, потому что все изображения не сохраняются в памяти сразу, но читаются по мере необходимости.

Образец нашего набора данных будет словарем (dict) {'image': image, 'landmarks': landmarks}. Наш набор данных примет необязательный аргумент transform, так что любая необходимая обработка может быть наносима на образец. Мы увидим пользу transform в следующем разделе.

# -*- coding: utf-8 -*-

class FaceLandmarksDataset(Dataset):
    """Face Landmarks dataset."""

    def __init__(self, csv_file, root_dir, transform=None):
        """
        Args:
            csv_file (string): Путь к csv файлу с аннотациями.
            root_dir (string): Каталог со всеми изображениями.
            transform (callable, optional): Необязательный transform 
                который будет применен к экземпляру.
        """
        self.landmarks_frame = pd.read_csv(csv_file)
        self.root_dir = root_dir
        self.transform = transform

    def __len__(self):
        return len(self.landmarks_frame)

    def __getitem__(self, idx):
        img_name = os.path.join(self.root_dir,
                                self.landmarks_frame.iloc[idx, 0])
        image = io.imread(img_name)
        landmarks = self.landmarks_frame.iloc[idx, 1:].as_matrix()
        landmarks = landmarks.astype('float').reshape(-1, 2)
        sample = {'image': image, 'landmarks': landmarks}

        if self.transform:
            sample = self.transform(sample)

        return sample

Давайте создадим экземпляр этого класса и пройдемся по образцам данных. Мы распечатаем размеры первых 4 образцов и покажем их ориентиры.

face_dataset = FaceLandmarksDataset(
                   csv_file='data/faces/face_landmarks.csv',
                   root_dir='data/faces/')

fig = plt.figure()

for i in range(len(face_dataset)):
    sample = face_dataset[i]

    print(i, sample['image'].shape, sample['landmarks'].shape)

    ax = plt.subplot(1, 4, i + 1)
    plt.tight_layout()
    ax.set_title('Sample #{}'.format(i))
    ax.axis('off')
    show_landmarks(**sample)

    if i == 3:
        plt.show()
        break

Вывод:

0 (324, 215, 3) (68, 2)
1 (500, 333, 3) (68, 2)
2 (250, 258, 3) (68, 2)
3 (434, 290, 3) (68, 2)


Преобразования

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

  • Rescale: масштабировать изображение
  • RandomCrop: обрезать изображение случайным образом. Это увеличение данных.
  • ToTensor: преобразовать отдельные изображения в torch изображения (нам нужно поменять оси).

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

tsfm = Transform(params)
transformed_sample = tsfm(sample)

Обратите внимание, как эти преобразования должны применяться как к изображению, так и к ориентирам.

# -*- coding: utf-8 -*-

class Rescale(object):
    """Масштабирует изображение экземпляра в заданный размер.

    Args:
        output_size (tuple or int): Требуемый размер вывода. 
              Если tuple, вывод соответствует output_size. 
              Если int, меньшие края изображения соответствуют 
              output_size сохраняя прежнее соотношение сторон.
    """

    def __init__(self, output_size):
        assert isinstance(output_size, (int, tuple))
        self.output_size = output_size

    def __call__(self, sample):
        image, landmarks = sample['image'], sample['landmarks']

        h, w = image.shape[:2]
        if isinstance(self.output_size, int):
            if h > w:
                new_h, new_w = \
                       self.output_size * h / w, self.output_size
            else:
                new_h, new_w = \
                       self.output_size, self.output_size * w / h
        else:
            new_h, new_w = self.output_size

        new_h, new_w = int(new_h), int(new_w)

        img = transform.resize(image, (new_h, new_w))

        # h и w поменялись местами для ориентиров, 
        # потому что для изображений,
        # x и y оси - это оси 1 и 0 соответственно
        landmarks = landmarks * [new_w / w, new_h / h]

        return {'image': img, 'landmarks': landmarks}


class RandomCrop(object):
    """Обрезает случайным образом изображение экземпляра.

    Args:
        output_size (tuple or int): Требуемый размер вывода. 
            Если int, выполняется обрезание квадратом.
    """

    def __init__(self, output_size):
        assert isinstance(output_size, (int, tuple))
        if isinstance(output_size, int):
            self.output_size = (output_size, output_size)
        else:
            assert len(output_size) == 2
            self.output_size = output_size

    def __call__(self, sample):
        image, landmarks = sample['image'], sample['landmarks']

        h, w = image.shape[:2]
        new_h, new_w = self.output_size

        top = np.random.randint(0, h - new_h)
        left = np.random.randint(0, w - new_w)

        image = image[top: top + new_h,
                      left: left + new_w]

        landmarks = landmarks - [left, top]

        return {'image': image, 'landmarks': landmarks}


class ToTensor(object):
    """Преобразовывает ndarrays экземпляра в Tensors."""

    def __call__(self, sample):
        image, landmarks = sample['image'], sample['landmarks']

        # меняем местами цветовые оси поскольку
        # numpy изображение: H x W x C
        # torch изображение: C X H X W
        image = image.transpose((2, 0, 1))
        return {'image': torch.from_numpy(image),
                'landmarks': torch.from_numpy(landmarks)}


Совмещение преобразований

Теперь мы применим преобразования к образцу.

Допустим, мы хотим изменить масштаб более короткой стороны изображения до 256 и затем случайным образом обрезать квадрат размером 224 от него. Мы хотим совместить Rescale и RandomCrop преобразования. torchvision.transforms.Compose - это простой вызываемый класс, который позволяет нам сделать это.

scale = Rescale(256)
crop = RandomCrop(128)
composed = transforms.Compose([Rescale(256),
                               RandomCrop(224)])

# Применяем каждую из трансформаций выше к экземпляру.
fig = plt.figure()
sample = face_dataset[65]
for i, tsfrm in enumerate([scale, crop, composed]):
    transformed_sample = tsfrm(sample)

    ax = plt.subplot(1, 3, i + 1)
    plt.tight_layout()
    ax.set_title(type(tsfrm).__name__)
    show_landmarks(**transformed_sample)

plt.show()


Итерация по набору данных

Давайте соединим все это, чтобы создать набор данных с трансформациями.
Подводя итог, каждый раз, когда выбирается экземпляр этого набора данных:

  • Изображение читается из файла на лету
  • Преобразования применяются к считанному изображению
  • Поскольку одно из преобразований является случайным, данные увеличиваются при выборке

Мы можем выполнить итерацию по созданному набору данных с помощью
for i in range цикла, как и раньше.

transformed_dataset = FaceLandmarksDataset(
                              csv_file='data/faces/face_landmarks.csv',
                              root_dir='data/faces/',
                              transform=transforms.Compose([
                                  Rescale(256),
                                  RandomCrop(224),
                                  ToTensor()
                              ]))

for i in range(len(transformed_dataset)):
    sample = transformed_dataset[i]

    print(i, sample['image'].size(), sample['landmarks'].size())

    if i == 3:
        break

Вывод:

0 torch.Size([3, 224, 224]) torch.Size([68, 2])
1 torch.Size([3, 224, 224]) torch.Size([68, 2])
2 torch.Size([3, 224, 224]) torch.Size([68, 2])
3 torch.Size([3, 224, 224]) torch.Size([68, 2])

Однако мы теряем множество функций, используя простой цикл for для перебора данных. В частности, нам не хватает:

  • Пакетирования данных
  • Перемешивания данных
  • Загрузки данных параллельно, используя multiprocessing воркеры (многопроцессорные обработчики).

torch.utils.data.DataLoader - это итератор, который предоставляет все эти функции. Параметры, используемые ниже, должны быть ясны. Один параметр, представляющий интерес - это collate_fn. Вы можете указать, как именно образцы должны быть упакованным с использованием collate_fn. Тем не менее, сортировка по умолчанию должна работать хорошо для большинства случаев использования.

# -*- coding: utf-8 -*-

dataloader = DataLoader(transformed_dataset, batch_size=4,
                        shuffle=True, num_workers=4)


# Helper function to show a batch
def show_landmarks_batch(sample_batched):
    """Показывает изображение с ориентирами для пакета экземпляров."""
    images_batch, landmarks_batch = \
            sample_batched['image'], sample_batched['landmarks']
    batch_size = len(images_batch)
    im_size = images_batch.size(2)

    grid = utils.make_grid(images_batch)
    plt.imshow(grid.numpy().transpose((1, 2, 0)))

    for i in range(batch_size):
        plt.scatter(landmarks_batch[i, :, 0].numpy() + i * im_size,
                    landmarks_batch[i, :, 1].numpy(),
                    s=10, marker='.', c='r')

        plt.title('Batch from dataloader')

for i_batch, sample_batched in enumerate(dataloader):
    print(i_batch, sample_batched['image'].size(),
          sample_batched['landmarks'].size())

    #  просматриваем 4й пакет и останавливаемся
    if i_batch == 3:
        plt.figure()
        show_landmarks_batch(sample_batched)
        plt.axis('off')
        plt.ioff()
        plt.show()
        break

Вывод:

0 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])
1 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])
2 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])
3 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])


Резюме: torchvision

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

root/ants/xxx.png
root/ants/xxy.jpeg
root/ants/xxz.png
.
.
.
root/bees/123.jpg
root/bees/nsdf3.png
root/bees/asd932_.png

где ‘ants’, ‘bees’ и т.д. это метки классов. Доступны также схожие общеиспользуемые преобразования, которые работают на PIL.Image, такие как RandomHorizontalFlip, Scale. Вы можете использовать их для написания загрузчика следующим образом:

import torch
from torchvision import transforms, datasets

data_transform = transforms.Compose([
        transforms.RandomSizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406],
                             std=[0.229, 0.224, 0.225])
    ])
hymenoptera_dataset = datasets.ImageFolder(
                                    root='hymenoptera_data/train',
                                    transform=data_transform)
dataset_loader = torch.utils.data.DataLoader(hymenoptera_dataset,
                                    batch_size=4, shuffle=True,
                                    num_workers=4)


Читайте также:


воскресенье, 30 декабря 2018 г.

PyTorch: использование GPU

В этом посте мы рассмотрим, как в PyTorch использовать несколько GPU, с помощью DataParallel.

С PyTorch очень легко использовать GPU. Вы можете поместить модель на GPU:

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

Затем вы можете скопировать все свои тензоры в GPU:

mytensor = my_tensor.to(device)

Обратите внимание, что простой вызов my_tensor.to(device) возвращает новую копию my_tensor на GPU вместо переписывания my_tensor. Вы должны назначить его на новый тензор и использовать этот тензор на GPU.

Естественно выполнять прямое и обратное распространение на нескольких GPU. Однако по умолчанию Pytorch будет использовать только один GPU. Вы можете легко запустить свои операции на нескольких GPU, заставляя вашу модель работать параллельно, используя DataParallel:

model = nn.DataParallel(model)

Рассмотрим это подробней.

Импорт и параметры

Импортируйте модули PyTorch и определите параметры.

# -*- coding: utf-8 -*-
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader

# Параметры и DataLoaders
input_size = 5
output_size = 2

batch_size = 30
data_size = 100

Устройство

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")


Фиктивный набор данных

Создаем фиктивный (случайный) набор данных. Вам только необходимо реализовать getitem

class RandomDataset(Dataset):

    def init(self, size, length):
        self.len = length
        self.data = torch.randn(length, size)

    def getitem(self, index):
        return self.data[index]

    def len(self):
        return self.len

rand_loader = DataLoader(dataset=RandomDataset(input_size, data_size),
                         batch_size=batch_size, shuffle=True)


Выполняем модель

Теперь мы можем увидеть размеры вводных и выводных тензоров.

for data in rand_loader:
    input = data.to(device)
    output = model(input)
    print("Outside: input size", input.size(),
          "output_size", output.size())

Вывод:

In Model: input size torch.Size([15, 5]) output size torch.Size([15, 2])
        In Model: input size torch.Size([15, 5]) 
                  output size torch.Size([15, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])
        In Model: input size torch.Size([15, 5]) 
                  output size torch.Size([15, 2])
        In Model: input size torch.Size([15, 5]) 
                  output size torch.Size([15, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])
        In Model: input size torch.Size([15, 5]) 
                  output size torch.Size([15, 2])
        In Model: input size torch.Size([15, 5]) 
                  output size torch.Size([15, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])
        In Model: input size torch.Size([5, 5]) 
                  output size torch.Size([5, 2])
        In Model: input size torch.Size([5, 5]) 
                  output size torch.Size([5, 2])
Outside: input size torch.Size([10, 5]) output_size torch.Size([10, 2])

Результаты

Если у вас нет GPU или один GPU, то в этом случае, когда мы пакетизируем 30 вводов и 30 выводов, модель получает 30 и выводит 30 как ожидается. Но если у нас есть несколько GPU, тогда мы можем получить результаты как следующие.

2 GPU

Если у вас 2 GPU, тогда вы увидите:

# на 2 GPU
Let's use 2 GPUs!
    In Model: input size torch.Size([15, 5]) 
              output size torch.Size([15, 2])
    In Model: input size torch.Size([15, 5]) 
              output size torch.Size([15, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])
    In Model: input size torch.Size([15, 5]) 
              output size torch.Size([15, 2])
    In Model: input size torch.Size([15, 5]) 
              output size torch.Size([15, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])
    In Model: input size torch.Size([15, 5]) 
              output size torch.Size([15, 2])
    In Model: input size torch.Size([15, 5]) 
              output size torch.Size([15, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])
    In Model: input size torch.Size([5, 5]) 
              output size torch.Size([5, 2])
    In Model: input size torch.Size([5, 5]) 
              output size torch.Size([5, 2])
Outside: input size torch.Size([10, 5]) output_size torch.Size([10, 2])

3 GPU

Если у вас 3 GPU, тогда вы увидите:

Let's use 3 GPUs!
    In Model: input size torch.Size([10, 5]) 
              output size torch.Size([10, 2])
    In Model: input size torch.Size([10, 5]) 
              output size torch.Size([10, 2])
    In Model: input size torch.Size([10, 5]) 
              output size torch.Size([10, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])
    In Model: input size torch.Size([10, 5]) 
              output size torch.Size([10, 2])
    In Model: input size torch.Size([10, 5]) 
              output size torch.Size([10, 2])
    In Model: input size torch.Size([10, 5]) 
              output size torch.Size([10, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])
    In Model: input size torch.Size([10, 5]) 
              output size torch.Size([10, 2])
    In Model: input size torch.Size([10, 5]) 
              output size torch.Size([10, 2])
    In Model: input size torch.Size([10, 5]) 
              output size torch.Size([10, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([2, 5]) 
              output size torch.Size([2, 2])
Outside: input size torch.Size([10, 5]) output_size torch.Size([10, 2])

8 GPU

Если у вас 8 GPU, тогда вы увидите:

Let's use 8 GPUs!
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([2, 5]) 
              output size torch.Size([2, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([2, 5]) 
              output size torch.Size([2, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([4, 5]) 
              output size torch.Size([4, 2])
    In Model: input size torch.Size([2, 5]) 
              output size torch.Size([2, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])
    In Model: input size torch.Size([2, 5]) 
              output size torch.Size([2, 2])
    In Model: input size torch.Size([2, 5]) 
              output size torch.Size([2, 2])
    In Model: input size torch.Size([2, 5]) 
              output size torch.Size([2, 2])
    In Model: input size torch.Size([2, 5]) 
              output size torch.Size([2, 2])
    In Model: input size torch.Size([2, 5]) 
              output size torch.Size([2, 2])
Outside: input size torch.Size([10, 5]) output_size torch.Size([10, 2])


Заключение

DataParallel разделяет ваши данные автоматически и отсылает рабочие задания на несколько моделей на нескольких GPU. После того как все модели завершат своя работу, DataParallel собирает и объединяет результаты перед тем как их вернуть.


Читайте также другие статьи в блоге:

PyTorch: тензоры (tensors)

PyTorch: нейронные сети

PyTorch: создание классификатора изображений


суббота, 29 декабря 2018 г.

PyTorch: создание классификатора изображений

В предыдущих постах мы рассмотрели, как определить нейронные сети, вычислить потери и сделать обновления весов сети.

Теперь можно задуматься,

А как насчет данных?

Как правило, когда вам приходится иметь дело с изображениями, текстом, аудио или видео данными, вы можете использовать стандартные Python пакеты, которые загружают данные в NumPy массив. Затем вы можете преобразовать этот массив в torch.*Tensor.

  • Для изображений полезны такие пакеты, как Pillow, OpenCV
  • Для аудио, такие пакеты, как scipy и librosa
  • Для текста - либо загрузка на основе Python, либо на основе Cython, либо NLTK и SpaCy будут полезны

Специально для изображений существует пакет под названием torchvision, в котором есть загрузчики данных для общераспространенных наборов данных, таких как Imagenet, CIFAR10, MNIST и т.д. И преобразователи данных для изображений, а именно, torchvision.datasets и torch.utils.data.DataLoader.

Эти пакеты дают огромное преимущество и позволяют избежать написания стандартного кода.

В этом посте мы будем использовать набор данных CIFAR10. У него есть классы: ‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’. Изображения в CIFAR-10 имеют размер 3x32x32, то есть 3-канальные цветные изображения размером 32x32 пикселей.


Создание классификатора изображений

Мы выполним следующие шаги по порядку:

  1. Загрузим и нормализуем обучающие и тестовые наборы данных CIFAR10, используя torchvision
  2. Определим сверточную нейронную сеть
  3. Определим функцию потерь
  4. Обучим сеть на тренировочных данных
  5. Протестируем сеть на тестовых данных

1. Загрузка и нормализация CIFAR10

Используя torchvision, загрузить CIFAR10 крайне просто.

import torch
import torchvision
import torchvision.transforms as transforms

Выходные данные torchvision наборов данных представляют собой изображения PILImage диапазона [0, 1]. Мы преобразуем их в тензоры нормализованного диапазона [-1, 1].

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='./data', 
                                        train=True,
                                        download=True, 
                                        transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, 
                                          batch_size=4,
                                          shuffle=True, 
                                          num_workers=2)

testset = torchvision.datasets.CIFAR10(root='./data', 
                                       train=False,
                                       download=True, 
                                       transform=transform)
testloader = torch.utils.data.DataLoader(testset, 
                                         batch_size=4,
                                         shuffle=False, 
                                         num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

Вывод:

Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz 
to ./data/cifar-10-python.tar.gz
Files already downloaded and verified

Выведем несколько тренировочных изображений:

# -*- coding: utf-8 -*-
import matplotlib.pyplot as plt
import numpy as np

# функция для показа изображения

def imshow(img):
    img = img / 2 + 0.5     # денормализуем
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()


# получаем несколько случайных тренировочных изображений
dataiter = iter(trainloader)
images, labels = dataiter.next()

# показываем изображения
imshow(torchvision.utils.make_grid(images))
# печатаем метки
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))

Вывод:

cat plane ship frog


2. Определяем сверточную нейронную сеть

Скопируем нейронную сеть из предыдущего поста и модифицируем ее для приема 3-канальных изображений (вместо 1-канальных изображений как было определено).

import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):
    def init(self):
        super(Net, self).init()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x


net = Net()


3. Определяем функцию потери и оптимизатор

Будем использовать классификацию кросс-энтропийных потерь и SGD с импульсом.

import torch.optim as optim

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)


4. Тренировка сети

Для тренировки сети нам просто нужно пройти цикл нашего итератора данных и передать вводные данные в сеть и оптимизировать.

# -*- coding: utf-8 -*-

# проходим в цикле по набору данных несколько раз
for epoch in range(2):  

    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # получаем вводные данные
        inputs, labels = data

        # обнуляем параметр gradients
        optimizer.zero_grad()

        # forward + backward + optimize
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # печатаем статистику
        running_loss += loss.item()
        if i % 2000 == 1999:    # печатаем каждые 2000 мини-пакетов
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 2000))
            running_loss = 0.0

print('Тренировка завершена')

Вывод:

[1,  2000] loss: 2.187
[1,  4000] loss: 1.852
[1,  6000] loss: 1.672
[1,  8000] loss: 1.566
[1, 10000] loss: 1.490
[1, 12000] loss: 1.461
[2,  2000] loss: 1.389
[2,  4000] loss: 1.364
[2,  6000] loss: 1.343
[2,  8000] loss: 1.318
[2, 10000] loss: 1.282
[2, 12000] loss: 1.286
Тренировка завершена


5. Тестируем сеть с помощью тестовых данных

Мы обучили сеть за 2 прохода по набору тренировочных данных. Но нам нужно проверить, научилась ли сеть вообще чему-либо.

Мы проверим это, спрогнозировав метку класса, которую выдает нейронная сеть, и проверим ее на основании истины. Если прогноз правильный, мы добавляем пример в список правильных прогнозов.

Хорошо, первый шаг. Давайте покажем изображение из тестового набора для ознакомления.

dataiter = iter(testloader)
images, labels = dataiter.next()

# печатаем изображения
imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', 
      ' '.join('%5s' % classes[labels[j]] for j in range(4)))

Вывод:

GroundTruth: cat ship ship plane

Теперь посмотрим, что нейронная сеть думает об этих примерах:

outputs = net(images)

Выводы - это рассчитанные вероятности для 10 классов. Чем больше вероятность для класса, тем вероятнее нейронная сеть предполагает, что изображение принадлежит этому классу. Таким образом, возьмем индекс наибольшей вероятности:

_, predicted = torch.max(outputs, 1)

print('Predicted: ', ' '.join('%5s' % classes[predicted[j]]
                              for j in range(4)))

Вывод:

Predicted: cat ship car ship

Результаты кажутся весьма хорошими.

Посмотрим как сеть отработает на целом наборе данных.

# -*- coding: utf-8 -*-

correct = 0
total = 0
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print('Аккуратность сети на 10000 тестовых изображений: %d %%' % (
    100 * correct / total))

Вывод:

Аккуратность сети на 10000 тестовых изображений: 54 %

Это выглядит лучше, чем простой случай, где аккуратность 10% (выбор класса из 10 классов случайным образом). Кажется сеть обучилась чему-то.

Посмотрим на каких классах прогнозы выполнились хорошо, а на каких не очень хорошо:

# -*- coding: utf-8 -*-

class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        _, predicted = torch.max(outputs, 1)
        c = (predicted == labels).squeeze()
        for i in range(4):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1


for i in range(10):
    print('Аккуратность %5s : %2d %%' % (
        classes[i], 100 * class_correct[i] / class_total[i]))

Вывод:

Аккуратность plane : 57 %
Аккуратность   car : 73 %
Аккуратность  bird : 49 %
Аккуратность   cat : 54 %
Аккуратность  deer : 18 %
Аккуратность   dog : 20 %
Аккуратность  frog : 58 %
Аккуратность horse : 74 %
Аккуратность  ship : 70 %
Аккуратность truck : 66 %


Тренировка на GPU

Также как тензор перемещается на GPU, также и сеть можно просто переместить на GPU.

Сначала зададим устройство как первое видимое CUDA устройство, если CUDA доступно:

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# Предполгаем, что у нас CUDA машина, 
# поэтому должно напечататься CUDA устройство:

print(device)

Вывод:

cuda:0

В следующих примерах предполагается, что устройство является CUDA устройством.

Тогда этот метод будет рекурсивно проходить по всем модулям и преобразовывать их параметры и буфферы в CUDA тензоры:

net.to(device)

Необходимо помнить, что требуется отправлять вводные данные и цели на каждом шагу также в GPU:

inputs, labels = inputs.to(device), labels.to(device)

На самом деле здесь не будет значительного увеличения скорости по сравнению с выполнением в CPU, потому что наша сеть очень мала.


Читайте также другие статьи в блоге:

PyTorch: тензоры (tensors)

PyTorch: Autograd, автоматическая дифференциация

PyTorch: нейронные сети

четверг, 27 декабря 2018 г.

PyTorch: нейронные сети

Нейронные сети могут быть построены с использованием пакета torch.nn.

torch.nn зависит от autograd в определении моделей и их дифференцировании. nn.Module содержит слои и метод forward(input), который возвращает output.

Например, посмотрите на эту сеть, которая классифицирует цифровые изображения:

Это простая сеть прямой связи. Она принимает вход, передает его через несколько слоев один за другим, а затем, наконец, возвращает вывод.

Типичная процедура обучения для нейронной сети следующая:

  1. Определение нейронной сети, которая имеет некоторые изучаемые параметры (или веса)
  2. Итерация по набору входных данных
  3. Обработка ввода через сеть
  4. Рассчет потери (насколько далеки результаты от правильных)
  5. Распространение градиентов обратно на параметры сети
  6. Обновление весов сети, как правило, используя простое правило обновления:
    weight = weight - learning_rate * gradient

Определение сети

Давайте определим сеть:

# -*- coding: utf-8 -*-
import torch
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):

    def init(self):
        super(Net, self).init()
        # 1 канал ввода изображения, 
        # 6 каналов вывода, 
        # 5x5 квадратное сверточное ядро
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        # аффинная операция: y = Wx + b
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        # Максимальное объединение через (2, 2) окно
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        # Если размер - это квадрат,
        # тогда вы можете задать только одно число
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, self.num_flat_features(x))
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

    def num_flat_features(self, x):
        # все измерения, исключая измерение пакета
        size = x.size()[1:]  
        num_features = 1
        for s in size:
            num_features *= s
        return num_features


net = Net()
print(net)

Вывод:

Net(
  (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
  (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (fc1): Linear(in_features=400, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)

Вам просто нужно определить функцию forward, а функция backward (где вычисляются градиенты) автоматически определена для вас, используя autograd. Вы можете использовать любую из операций Tensor в функции forward.

Изучаемые параметры модели возвращаются net.parameters()

params = list(net.parameters())
print(len(params))
print(params[0].size())  # conv1's .weight

Вывод:

10
torch.Size([6, 1, 5, 5])

Попробуем случайный 32x32 ввод (Ожидаемый размер ввода для этой net(LeNet) - 32x32.) Для использования этой сети на MNIST наборе следует изменить размер изображений в наборе на 32x32.

input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)

Вывод:

tensor([[-0.0233,  0.0159, -0.0249,  0.1413,  
          0.0663,  0.0297, -0.0940, -0.0135,
          0.1003, -0.0559]], grad_fn=<AddmmBackward>)

Обнулим градиентные буферы всех параметров и backprops со случайными градиентами:

net.zero_grad()
out.backward(torch.randn(1, 10))

Примечание

torch.nn поддерживает только мини-пакеты. Весь torch.nn пакет поддерживает только вводы, которые являются мини-пакетами примеров, а не отдельным примером.

Например, nn.Conv2d будет принимать 4D тензор
nSamples x nChannels x Height x Width.

Если у вас есть отдельный пример, просто используйте input.unsqueeze(0) для того чтобы добавить поддельное измерение пакета.

Рассмотрим все классы, которые встрчались ранее.

  • torch.Tensor - многомерный массив с поддержкой autograd операций как backward(). Также содержит градиенты тензора.
  • nn.Module - модуль нейронной сети. Удобный способ инкапсуляции параметров с вспомогательными интрументами для их перемещения в GPU, экспорта, загрузки и т. д.
  • nn.Parameter - это разновидность Tensor, который автоматически регистрируется как параметр при назначении в качестве атрибута Module.
  • autograd.Function - реализует прямое и обратное определения (forward and backward) autograd операции. Каждая Tensor операция создает как минимум один Function узел, который подключается к функциям, которые создали Tensor и кодирует его историю.

На данный момент мы рассмотрели:

  • Определение нейронной сети
  • Обработка входных данных и обратный вызов

Еще осталось:

  • Вычисление потери
  • Обновление весов сети

Функция потери

Функция потери принимает (выходную, целевую) пару входных данных и вычисляет значение, определяющее, насколько далеко вывод от цели.

Существует несколько разных функций потери в пакете torch.nn. Простая потеря: nn.MSELoss, которая вычисляет среднеквадратичную ошибку между вводом и целью.

Например:

output = net(input)
target = torch.randn(10)  # случайный target, например
target = target.view(1, -1)  # сделаем его такой же формы как вывод
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)

Вывод:

tensor(1.3389, grad_fn=<MseLossBackward>)

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

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
      -> view -> linear -> relu -> linear -> relu -> linear
      -> MSELoss
      -> loss

Таким образом, когда мы вызываем loss.backward(), весь граф дифференцирован в соотвествии с потерей, и все тензоры в графе, которые имеют requires_grad=True будут иметь их .grad тензор накопленный с градиентом.

Для иллюстрации проследуем по нескольким шагам в обратном направлении:

print(loss.grad_fn)  # MSELoss
print(loss.grad_fn.next_functions[0][0])  # Linear
print(loss.grad_fn.next_functions[0][0].next_functions[0][0])  # ReLU

Вывод:

<MseLossBackward object at 0x7fab77615278>
<AddmmBackward object at 0x7fab77615940>
<AccumulateGrad object at 0x7fab77615940>


Backprop (Обратное распространение)

Для обратного распространения ошибки все, что нам нужно сделать, это loss.backward(). Вы должны очистить существующие градиенты, иначе градиенты будут накапливается к существующим градиентам.

Теперь мы вызовем loss.backward() и посмотрим на смещение conv1 градиентов до и после backward.

# обнуляем градиентные буферы всех параметров
net.zero_grad()  

print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)

loss.backward()

print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)

Вывод:

conv1.bias.grad before backward
tensor([0., 0., 0., 0., 0., 0.])
conv1.bias.grad after backward
tensor([-0.0054,  0.0011,  0.0012,  0.0148, -0.0186,  0.0087])


Обновление весов

Самым простым правилом обновления, используемым на практике, является стохастический градиентный спуск (Stochastic Gradient Descent (SGD)):

weight = weight - learning_rate * gradient

Мы можем реализовать это с помощью простого Python кода:

learning_rate = 0.01
for f in net.parameters():
    f.data.sub_(f.grad.data * learning_rate)

Однако, когда вы используете нейронные сети, вы хотите использовать различные правила обновления, такие как SGD, Nesterov-SGD, Adam, RMSProp и др. Чтобы включить их, существует небольшой пакет: torch.optim, который реализует все эти методы. Использовать его очень просто:

import torch.optim as optim

# создайте ваш оптимизатор
optimizer = optim.SGD(net.parameters(), lr=0.01)

# в вашем тренировочном цикле:
optimizer.zero_grad()   # обнуляем градиентные буфферы
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step()    # Выполняем обновление



Читайте также другие статьи в блоге:

PyTorch: тензоры (tensors)

PyTorch: Autograd, автоматическая дифференциация

Анатомия нейронных сетей

среда, 26 декабря 2018 г.

PyTorch: Autograd, автоматическая дифференциация

Центральным для всех нейронных сетей в PyTorch является пакет autograd.

Пакет autograd обеспечивает автоматическое разграничение всех операций на тензорах. Это определяемый-по-исполнению (define-by-run) фреймворк, что означает, что ваш backprop (обратное распространение в нейронной сети) определяется тем, как выполняется ваш код, и что каждая отдельная итерация может отличаться.

Рассмотрим это в более простых терминах на нескольких примерах.

Тензор

torch.Tensor является центральным классом пакета. Если вы установите его атрибут .require_grad равным True, тогда он начнет отслеживать все операции над ним. Когда вы закончите вычисления, вы можете вызвать .backward() и получить все градиенты вычисленные автоматически. Градиент для этого тензора будет накапливается в атрибуте .grad.

Чтобы остановить отслеживание тензора в истории вычислений, вы можете вызвать .detach().

Чтобы предотвратить отслеживание истории (и использование памяти), вы также можете обернуть блок кода в with torch.no_grad():. Это может быть особенно полезно при оценке модели, потому что модель может иметь обучаемые параметры с require_grad = True, но для которых нам не нужны градиенты.

Есть еще один класс, который очень важен для autograd реализации - Function.

Tensor и Function связаны между собой и создают ациклический граф, который кодирует полную историю вычислений. Каждый тензор имеет .grad_fn атрибут, который ссылается на Function, которая создала Tensor (за исключением тензоров, созданных пользователем - их grad_fn = None).

Если вы хотите вычислить производные, вы можете вызвать .backward() на Tensor. Если Tensor является скаляром (то есть он содержит один элемент данных), вам не нужно указывать какие-либо аргументы для backward(), однако, если в нем больше элементов, вам нужно указать gradient аргумент, который является тензором подходящей формы.

import torch

Создаем тензор и задаем requires_grad=True, чтобы отслеживать вычисления с ним:

x = torch.ones(2, 2, requires_grad=True)
print(x)

Вывод:

tensor([[1., 1.],
        [1., 1.]], requires_grad=True)

Выполняем операцию на тензоре:

y = x + 2
print(y)

Вывод:

tensor([[3., 3.],
        [3., 3.]], grad_fn=<AddBackward0>)

y был создан как результат операции, поэтому он имеет grad_fn.

print(y.grad_fn)

Вывод:

<AddBackward0 object at 0x7fef6f982438>

Выполняем еще несколько операций на y:

z = y * y * 3
out = z.mean()

print(z, out)

Вывод:

tensor([[27., 27.],
        [27., 27.]], grad_fn=<MulBackward0>) 
tensor(27., grad_fn=<MeanBackward0>)

.requires_grad_( ... ) изменяет существующий requires_grad флаг тензора на месте (in-place). input флаг равен False, если не задан:

a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)

Вывод:

False
True
<SumBackward0 object at 0x7fe1db427dd8>


Градиенты

Вызовем backprop теперь. Поскольку вывод содержит единственный скаляр, out.backward() эквивалентен ut.backward(torch.tensor(1.)).

out.backward()

Напечатаем градиенты d(out)/dx

print(x.grad)

Вывод:

tensor([[4.5000, 4.5000],
        [4.5000, 4.5000]])

В общем говоря, torch.autograd - это механизм для вычисления Jacobian-vector произведения.

Рассмотрим пример Jacobian-vector произведения:

x = torch.randn(3, requires_grad=True)

y = x * 2
while y.data.norm() < 1000:
    y = y * 2

print(y)

Вывод:

tensor([ 840.8677,  613.5138, -778.9942], grad_fn=<MulBackward0>)

Теперь в этом случае y больше не скаляр. torch.autograd не может вычислить полный Jacobian напрямую, но если мы просто хотим Jacobian-vector произведение, просто передаем вектор в backward как аргумент:

v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)

print(x.grad)

Вывод:

tensor([1.0240e+02, 1.0240e+03, 1.0240e-01])

Вы можете также остановить autograd от отслежвания истории на тензорах с .requires_grad=True с помощью обертывания блока кода в with torch.no_grad():

print(x.requires_grad)
print((x  2).requires_grad)

with torch.no_grad():
    print((x  2).requires_grad)

Вывод:

True
True
False



Читайте также другие статьи в блоге:

PyTorch: тензоры (tensors)

TensorFlow: классификация текста с отзывами к фильмам

Анатомия нейронных сетей

понедельник, 24 декабря 2018 г.

PyTorch: тензоры (tensors)

Что такое PyTorch?

PyTorch - это Python пакет для научных вычислений, предназначенный для двух целей:

  1. В качестве замены NumPy для использования преимуществ GPU
  2. В качестве платформы для разработки программ глубого обучения, которая предоставляет максимальную гибкость и скорость.

Тензоры (Tensors) в PyTorch

Тензоры схожи с ndarrays в NumPy, с добавлением того, что тензоры могут быть использованы на GPU для ускорения вычислений. Импортируем torch и рассмотрим несколько примеров:

from future import print_function
import torch

Контруируем 5x3 матрицу, не инициализированную:

x = torch.empty(5, 3)
print(x)

Вывод:

tensor([[1.3440e-19, 4.5673e-41, 8.3430e-18],
        [4.5673e-41, 2.5431e+30, 5.5073e+11],
        [5.2563e+05, 5.5123e+11, 1.6669e+35],
        [2.1541e+09, 3.7906e+22, 4.1644e+34],
        [7.3002e-12, 3.9694e+28, 9.4759e+21]])
print(x)

Конструируем матрицу, инициализированную случайными значениями:

x = torch.rand(5, 3)
print(x)

Вывод:

tensor([[0.4163, 0.1625, 0.9454],
        [0.8632, 0.3480, 0.1602],
        [0.3037, 0.3912, 0.3449],
        [0.3911, 0.5578, 0.7367],
        [0.6628, 0.3991, 0.3471]])
print(x)

Создадим матрицу, наполненную нулями и dtype long:

x = torch.zeros(5, 3, dtype=torch.long)
print(x)

Вывод:

tensor([[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]])

Создадим тензор напрямую из данных:

x = torch.tensor([5.5, 3])
print(x)

Вывод:

tensor([5.5000, 3.0000])

Или создадим тензор, основанный на существующем тензоре. Этот метод будет повторно использовать свойства вводного тензора, например dtype, если новые значения не переданы пользователем

# new_* методы принимают размеры
x = x.new_ones(5, 3, dtype=torch.double)      
print(x)

# переопределяем dtype!
x = torch.randn_like(x, dtype=torch.float)
# результат имеет тот же размер
print(x) 

Вывод:

tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]], dtype=torch.float64)
tensor([[-0.6714,  0.7803, -1.0026],
        [-1.1583,  1.7177,  2.7201],
        [-0.1254, -0.4324, -0.6761],
        [-2.1195, -0.7945,  0.6865],
        [ 0.1464, -0.3747, -0.8441]])

Возьмем его размер:

print(x.size())

Вывод:

torch.Size([5, 3])

Примечание

torch.Size по факту является tuple, поэтому он поддерживает все tuple операции.

Операции с тензорами

Существует несколько вариантов синтаксиса для операций. В следующем примере мы рассмотрим операцию сложения.

Сложение: синтаксис 1

y = torch.rand(5, 3)
print(x + y)

Вывод:

tensor([[ 0.0732,  0.9384, -0.2489],
        [-0.6905,  2.1267,  3.0045],
        [ 0.6199,  0.4936, -0.0398],
        [-2.0623, -0.5140,  1.6162],
        [ 0.3189, -0.0327, -0.5353]])

Сложение: синтаксис 2

print(torch.add(x, y))

Вывод:

tensor([[ 0.0732,  0.9384, -0.2489],
        [-0.6905,  2.1267,  3.0045],
        [ 0.6199,  0.4936, -0.0398],
        [-2.0623, -0.5140,  1.6162],
        [ 0.3189, -0.0327, -0.5353]])

Сложение: предоставление тензора вывода как аргумента

result = torch.empty(5, 3)
torch.add(x, y, out=result)
print(result)

Вывод:

tensor([[ 0.0732,  0.9384, -0.2489],
        [-0.6905,  2.1267,  3.0045],
        [ 0.6199,  0.4936, -0.0398],
        [-2.0623, -0.5140,  1.6162],
        [ 0.3189, -0.0327, -0.5353]])

Сложение: на месте (in-place)

# добавляем x к y
y.add_(x)
print(y)

Вывод:

tensor([[ 0.0732,  0.9384, -0.2489],
        [-0.6905,  2.1267,  3.0045],
        [ 0.6199,  0.4936, -0.0398],
        [-2.0623, -0.5140,  1.6162],
        [ 0.3189, -0.0327, -0.5353]])

Примечание

Любая операция, которая изменяет тензор на месте имеет постфикс _. Например: x.copy_(y), x.t_(), будут изменять x.

Вы можете использовать стандартное (как в NumPy) индексирование:

print(x[:, 1])

Вывод:

tensor([ 0.7803,  1.7177, -0.4324, -0.7945, -0.3747])

Изменение размера: если вы хотите изменить размер/форму тензора, вы можете использовать torch.view:

x = torch.randn(4, 4)
y = x.view(16)
# размер -1 выведен из других измерений
z = x.view(-1, 8)  
print(x.size(), y.size(), z.size())

Вывод:

torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8])

Если у вас есть одноэлементный тензор, используйте .item() для получения значения как Python числа

x = torch.randn(1)
print(x)
print(x.item())

Вывод:

tensor([0.1550])
0.15495021641254425


NumPy мост

Преобразование Torch тензора в NumPy массив и обратно выполняется легко.

Torch тензор и NumPy массив будут иметь общие подлежащие занятые области памяти, и изменение одного из них будет изменять другой.

Преобразование Torch тензора в NumPy массив

a = torch.ones(5)
print(a)

Вывод:

tensor([1., 1., 1., 1., 1.])

b = a.numpy()
print(b)

Вывод:

[1. 1. 1. 1. 1.]

Посмотрим как изменится значение в numpy массиве.

a.add_(1)
print(a)
print(b)

Вывод:

tensor([2., 2., 2., 2., 2.])
[2. 2. 2. 2. 2.]

Преобразование NumPy массива в Torch тензор

Рассмотрим как изменяется NumPy массив, преобразованный в Torch тензор автоматически

import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
np.add(a, 1, out=a)
print(a)
print(b)

Вывод:

[2. 2. 2. 2. 2.]
tensor([2., 2., 2., 2., 2.], dtype=torch.float64)

Все тензоры на CPU, исключая CharTensor, поддерживают преобразование в NumPy и обратно.

CUDA тензоры

Тензоры могут быть перемещены на любое устройство, используя .to метод.

# выполняем этот скрипт, только если CUDA доступно
# Мы будем использовать ``torch.device`` объекты
# чтобы перемещать тензор на GPU и обратно
if torch.cuda.is_available():
    # объект CUDA устройства
    device = torch.device("cuda")
    # напрямую создаем тензор на GPU          
    y = torch.ones_like(x, device=device) 
    # или просто используем строки ``.to("cuda")``
    x = x.to(device)                     
    z = x + y
    print(z)
    # ``.to`` может также одновременно изменять dtype!
    print(z.to("cpu", torch.double))       

Вывод:

tensor([1.1550], device='cuda:0')
tensor([1.1550], dtype=torch.float64)



Читайте также другие статьи в блоге:

TensorFlow: базовая классификация

TensorFlow: классификация текста с отзывами к фильмам

Анатомия нейронных сетей

пятница, 21 декабря 2018 г.

TensorFlow: embeddings (встраивания)

В этом посте представлена концепция встраиваний (embeddings), приведен простой пример как обучить встраивание (embedding) в TensorFlow, и объясняется, как просматривать встраивания с помощью TensorBoard Embedding Projector.

Встривание (embedding) - это отображение (mapping) дискретных объектов, таких как слова, на векторы действительных чисел. Например, 300-мерное встраивание для английских слов может включать в себя:

blue:(0.01359, 0.00075997, 0.24608, ..., -0.2524, 1.0048, 0.06259)
blues:(0.01396, 0.11887, -0.48963, ..., 0.033483, -0.10007, 0.1158)
orange:(-0.24776, -0.12359, 0.20986, ..., 0.079717, 0.23865, -0.014213)
oranges:(-0.35609, 0.21854, 0.080944, ..., -0.35413, 0.38511, -0.070976)

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

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

Встраивания также полезны в качестве результатов в машинном обучении. Потому что применяя встраивания, сопоставляющие объекты с векторами, приложения могут использовать сходство в векторном пространстве (например, евклидово расстояние или угол между векторами) как надежную и гибкую меру сходства объектов. Одним из распространенных применений является поиск ближайшего соседа. Используя те же встраивания слов, что и выше, например, покажем три ближайших соседа для каждого слова и соответствующие углы:

blue:  (red, 47.6°), (yellow, 51.9°), (purple, 52.4°)
blues:  (jazz, 53.3°), (folk, 59.1°), (bluegrass, 60.6°)
orange:  (yellow, 53.5°), (colored, 58.0°), (bright, 59.9°)
oranges:  (apples, 45.3°), (lemons, 48.3°), (mangoes, 50.4°)

По этим результатам можно сделать вывод, что яблоки и апельсины более схожи (45.3° различия), чем лимоны и апельсины (48.3° различия).

Встраивания (enbeddings) в TensorFlow

Чтобы создать встраивание слов в TensorFlow, мы сначала разбиваем текст на слова, а затем назначаем целое число каждому слову в словаре. Давайте предположим, что это уже сделано, и что word_ids является вектором этих целых чисел. Например, предложение «I have a cat.». Можно разделить на [«I», «have», «a», «cat», «.»], и тогда соответствующий тензор word_ids будет иметь форму [5]и состоять из 5 целых чисел. Чтобы создать представление этих идентификаторов слов в векторах, нам нужно создать переменную встраивание и использовать tf.nn.embedding_lookup функцию следующим образом:

word_embeddings = tf.get_variable(“word_embeddings”,
    [vocabulary_size, embedding_size])
embedded_word_ids = tf.nn.embedding_lookup(word_embeddings, word_ids)

После этого тензор embedded_word_ids будет иметь форму [5, embedding_size] в нашем примере и содержать встраивания (embeddings) (плотные векторы)(dense vectors) для каждого из 5 слов. В конце обучения word_embeddings будет содержать встраивания для всех слов в словаре.

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

Визуализация встраиваний

TensorBoard включает в себя Embedding Projector, инструмент, который позволяет интерактивно визуализировать встраивания. Этот инструмент может читать встраивания из вашей модели и визуализировать их в двух или трех измерениях.

Embedding Projector имеет три панели:

  • Панель данных (Data panel) в левом верхнем углу, где вы можете выбрать запуск, переменную встраивания и колонки данных (data columns) для задания цвета и меток точек.
  • Панель проекций (Projections panel) в левом нижнем углу, где вы можете выбрать тип проекции.
  • Панель инспектора (Inspector panel) справа, где вы можете искать конкретные точки и увидеть список ближайших соседей.

Проекции

Embedding Projector обеспечивает три способа уменьшения размерности набора данных.

  • t-SNE: нелинейный недетерминированный алгоритм (стохастическое вложение соседей с t-распределением), который пытается сохранить локальное соседство в данных, часто за счет искажения глобальной структуры. Вы можете выбрать, следует ли вычислять двух- или трехмерные проекции.

  • PCA: линейный детерминированный алгоритм (метод главных компонент), который пытается захватить как можно большую часть изменчивости данных в как можно меньшем количестве измерений. PCA имеет тенденцию выделять крупномасштабную структуру данных, но может искажать локальное соседство. Embedding Projector вычисляет 10 главных компонентов, из которых вы можете выбрать два или три для просмотра.

  • Пользовательский (Custom): линейная проекция на горизонтальную и вертикальную оси, которую вы указываете с помощью меток в данных. Вы определяете горизонтальную ось, например, предоставляя текстовые шаблоны для "Левый" и "Правый". Embedding Projector находит все точки, метка которых соответствует "Левый" шаблону и вычисляет центр тяжести этого множества; аналогично для "Правый". Линия, проходящая через эти два центроида определяет горизонтальную ось. Вертикальная ось, аналогично, вычисляется по центроидам для точек, совпадающих с "Вверх" и "Вниз" текстовыми шаблонами.


Исследование (Exploration)

Вы можете исследовать визуально с помощью масштабирования, поворота и панорамирования с помощью нажатия-и-перетаскивания (click-and-drag). Если навести указатель мыши на какую-либо из точек, будут показаны метаданные для этой точки. Вы также можете проверить ближайшие соседние подмножества. Нажатие на точку приводит к тому, что на правой панели отображается список ближайших соседей, а также расстояния до текущей точки. Ближайший сосед точки также выделяется в проекции.

Иногда полезно ограничить вид подмножеством точек и выполнить прогнозы только по этим пунктам. Для этого вы можете выбрать точки несколькими способами:

  • После нажатия на точку также выбираются ее ближайшие соседи.
  • После поиска выбираются точки, соответствующие запросу.
  • Включение выделения, нажатие на точку и перетаскивание определяют выделяемую сферу.

Затем нажмите "Isolate nnn points" кнопку в верхней части Inspector панели на правой стороне. На следующем рисунке показаны 101 точка, выбранные и готовые, чтобы пользователь щелкнул "Isolate 101 points":

Выбор ближайших соседей “важных” в наборе данных встраиваний слов.

Фильтрация с пользовательской проекцией может быть весьма удобной. Ниже мы отфильтровали 100 ближайших соседей “politics” и спроецировали их на “worst” - “best” вектор в виде оси х. Ось у случайна. В результате справа находятся “ideas”, “science”, “perspective”, “journalism”, а слева “crisis”, “violence” и “conflict”.

Чтобы поделиться своими результатами, вы можете использовать панель закладок в правом нижнем углу и сохранить текущее состояние (включая вычисленные координаты любой проекции) в виде небольшого файла. Затем в Projector можно указать на выбор одного или более из этих файлов, создавая панель ниже. Другие пользователи могут ходить по последовательности закладок.


Метаданные

Если вы работаете со встраиванием, возможно, вы захотите прикрепить метки/изображения к точкам данных. Вы можете сделать это, сгенерировав файл метаданных, содержащий метки для каждой точки, и нажав кнопку "Load data" в панели данных Embedding Projector.

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

Word\tFrequency
Airplane\t345
Car\t241
...

Предполагается, что порядок строк в файле метаданных соответствует порядку векторов в переменной встраивания, кроме заголовка. Следовательно, (i+1)-я строка в файле метаданных соответствует i-й строке переменной встраивания. Если файл метаданных TSV содержит только один столбец, мы не ожидаем строку заголовка и предполагаем, что каждая строка является меткой встраивания. Мы включаем это исключение, потому что оно соответствует обычно используемому "vocab file" формату.

Чтобы использовать изображения в качестве метаданных, необходимо создать одно спрайт изображение, состоящее из маленьких миниатюр, по одному на каждый вектор встраивания. Спрайт должен хранить миниатюры в порядке строк: первая размещенная точка данных в левом верхнем углу и последняя точка данных в правом нижнем углу, хотя последняя строка не должна быть заполнена, как показано ниже:


Embedding мини-FAQ

"Встраивание (embedding)" - это действие или вещь? И то и другое. Люди говорят о встраивании слов в векторное пространство (действие) и о произведении встраиваний слов (вещи). Общим для обоих является понятие встраивания как отображения (mapping) дискретных объектов в векторах. Создание или применение этого отображения (mapping) - это действие, но само отображение (mapping) - это вещь.

Встраивания (embeddings) являются многомерными или низкоразмерными (high-dimensional или low-dimensional)? Это зависит от того с какой стороны расматривать. 300-мерное векторное пространство слов и фраз, например, часто называют низкоразмерным (и плотным) по сравнению с миллионами слов и фраз, которые оно может содержать. Но математически оно многомерное, поскольку показывает много свойств, что резко отличаются от того, что человек интуитивно знает о 2- и 3-мерных пространствах.

Является ли встраивание (embedding) тем же, что и слой встраивания (embedding layer)? Нет. слой встраивания (embedding layer) является частью нейронной сети, но встраивание (embedding) является более общей концепцией.


Читайте также другие статьи в блоге:

TensorFlow: базовая классификация

TensorFlow: классификация текста с отзывами к фильмам

Анатомия нейронных сетей