Повысьте производительность с помощью неразмеченных данных.

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

Для кого это полезно? Все, у кого есть непомеченные и расширяемые данные.

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

Предварительные требования: хорошее понимание сверточных и плотных сетей.

Код: Полный код можно найти здесь.

Самоконтроль против других подходов

Как правило, когда кто-то думает о моделях, они обычно рассматривают два лагеря: контролируемые и неконтролируемые модели.

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

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

Самостоятельное обучение (SSL) направлено на создание полезных представлений функций без доступа к каким-либо аннотациям данных, помеченных человеком. - К. Гупта и др.

Коротко о самоконтроле

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

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

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

p.s. Другой распространенный способ осмыслить это — стилевая инвариантность. Другими словами, вы обучаете модель игнорировать стилистические различия в изображениях.

Проекционные головы

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

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

Почему проекционные головы так важны для самоконтроля.

Представьте, что вы играете в монополию. Есть чему поучиться; инвестиции в недвижимость могут приносить дивиденды, важно думать о будущем, прежде чем делать инвестиции, пройти мимо и собрать 200 долларов, нет принципиальной разницы между ботинком и наперстком и т. д. В игре монополии есть два типа информации: общеприменимую и специфичную для задачи информацию. Вы не должны волноваться каждый раз, когда видите слово «идти» в своей повседневной жизни, это зависит от конкретной задачи, но вы должны тщательно обдумывать свои инвестиции, это обычно полезно.

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

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

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

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

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

Самоконтроль в PyTorch

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

MNIST состоит из 60 000 помеченных обучающих изображений и 10 000 помеченных тестовых изображений. Однако в этом примере мы отбросим все обучающие ярлыки, кроме 200. Это означает, что у нас будет набор из 200 помеченных изображений для обучения и 59 800 немаркированных изображений для обучения. Эта модификация отражает типы приложений, в которых самоконтроль наиболее полезен: наборы данных с большим количеством данных, которые дорого маркировать.

Полный код можно найти здесь.

1) Загрузите данные

Загрузка набора данных

"""
Downloading and rendering sample MNIST data
"""

#torch setup
import torch
import torchvision
import torchvision.datasets as datasets
device = 'cuda' if torch.cuda.is_available() else 'cpu'

#downloading mnist
mnist_trainset = datasets.MNIST(root='./data', train=True,
                                download=True, transform=None)
mnist_testset = datasets.MNIST(root='./data', train=False,
                               download=True, transform=None)

#printing lengths
print('length of the training set: {}'.format(len(mnist_trainset)))
print('length of the test set: {}'.format(len(mnist_testset)))

#rendering a few examples
for i in range(3):
  print('the number {}:'.format(mnist_trainset[i][1]))
  mnist_trainset[i][0].show()

2) Разделить на помеченные и неразмеченные данные

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

"""
Creating un-labled data, and handling necessary data preprocessing
"""

from tqdm import tqdm
import numpy as np
from sklearn.preprocessing import OneHotEncoder

# ========== Data Extraction ==========
# unlabeling some data, and one hot encoding the labels which remain
# =====================================

partition_index = 200

def one_hot(y):
  #For converting a numpy array of 0-9 into a one hot encoding of vectors of length 10
  b = np.zeros((y.size, y.max() + 1))
  b[np.arange(y.size), y] = 1
  return b

print('processing labeld training x and y')
train_x = np.asarray([np.asarray(mnist_trainset[i][0]) for i in tqdm(range(partition_index))])
train_y = one_hot(np.asarray([np.asarray(mnist_trainset[i][1]) for i in tqdm(range(partition_index))]))

print('processing unlabled training data')
train_unlabled = np.asarray([np.asarray(mnist_trainset[i][0]) for i in tqdm(range(partition_index,len(mnist_trainset)))])

print('processing labeld test x and y')
test_x = np.asarray([np.asarray(mnist_testset[i][0]) for i in tqdm(range(len(mnist_testset)))])
test_y = one_hot(np.asarray([np.asarray(mnist_testset[i][1]) for i in tqdm(range(len(mnist_testset)))]))

# ========== Data Reformatting ==========
# adding a channel dimension and converting to pytorch
# =====================================

#adding a dimension to all X values to put them in the proper shape
#(batch size, channels, x, y)
print('reformatting shape...')
train_x = np.expand_dims(train_x, 1)
train_unlabled = np.expand_dims(train_unlabled, 1)
test_x = np.expand_dims(test_x, 1)

#converting data to pytorch type
torch_train_x = torch.tensor(train_x.astype(np.float32), requires_grad=True).to(device)
torch_train_y = torch.tensor(train_y).to(device)
torch_test_x = torch.tensor(test_x.astype(np.float32), requires_grad=True).to(device)
torch_test_y = torch.tensor(test_y).to(device)
torch_train_unlabled = torch.tensor(train_unlabled.astype(np.float32), requires_grad=True).to(device)

print('done')

3) Определение модели

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

"""
Using PyTorch to create a modified, smaller version of AlexNet
"""
import torch.nn.functional as F
import torch.nn as nn

class Backbone(nn.Module):
    def __init__(self):
        super(Backbone, self).__init__()
        self.conv1 = nn.Conv2d(1, 16, 3)
        self.conv2 = nn.Conv2d(16, 16, 3)
        self.conv3 = nn.Conv2d(16, 32, 3)

        if torch.cuda.is_available():
            self.cuda()

    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), 2)
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = F.max_pool2d(F.relu(self.conv3(x)), 2)
        x = torch.flatten(x, 1)
        return x


class Head(nn.Module):
    def __init__(self, n_class=10):
        super(Head, self).__init__()
        self.fc1 = nn.Linear(32, 32)
        self.fc2 = nn.Linear(32, 16)
        self.fc3 = nn.Linear(16, n_class)

        if torch.cuda.is_available():
            self.cuda()

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return F.softmax(x,1)


class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.backbone = Backbone()
        self.head = Head()

        if torch.cuda.is_available():
            self.cuda()

    def forward(self, x):
        x = self.backbone(x)
        x = self.head(x)
        return x

model_baseline = Model()
print(model_baseline(torch_train_x[:1]).shape)
model_baseline

4) Тренируйтесь и тестируйте, используя в качестве основы только контролируемое обучение.

Чтобы понять, насколько самоконтроль повышает производительность, мы будем обучать нашу базовую модель только на 200 помеченных образцах.

"""
Training model using only supervised learning, and rendering the results.
This supervised training function is reused in the future for fin tuning
"""

def supervised_train(model):

    #defining key hyperparamaters explicitly (instead of hyperparamater search)
    batch_size = 64
    lr = 0.001
    momentum = 0.9
    num_epochs = 20000

    #defining a stocastic gradient descent optimizer
    optimizer = torch.optim.SGD(model.parameters(), lr=lr, momentum=momentum)

    #defining loss function
    loss_fn = torch.nn.CrossEntropyLoss()

    train_hist = []
    test_hist = []
    test_accuracy = []

    for epoch in tqdm(range(num_epochs)):

        #iterating over all batches
        for i in range(int(len(train_x)/batch_size)-1):

            #Put the model in training mode, so that things like dropout work
            model.train(True)

            # Zero gradients
            optimizer.zero_grad()

            #extracting X and y values from the batch
            X = torch_train_x[i*batch_size: (i+1)*batch_size]
            y = torch_train_y[i*batch_size: (i+1)*batch_size]

            # Make predictions for this batch
            y_pred = model(X)

            #compute gradients
            loss_fn(model(X), y).backward()

            # Adjust learning weights
            optimizer.step()

        with torch.no_grad():

            #Disable things like dropout, if they exist
            model.train(False)

            #calculating epoch training and test loss
            train_loss = loss_fn(model(torch_train_x), torch_train_y).cpu().numpy()
            y_pred_test = model(torch_test_x)
            test_loss = loss_fn(y_pred_test, torch_test_y).cpu().numpy()

            train_hist.append(train_loss)
            test_hist.append(test_loss)

            #computing test accuracy
            matches = np.equal(np.argmax(y_pred_test.cpu().numpy(), axis=1), np.argmax(torch_test_y.cpu().numpy(), axis=1))
            test_accuracy.append(matches.sum()/len(matches))

    import matplotlib.pyplot as plt
    plt.plot(train_hist, label = 'train loss')
    plt.plot(test_hist, label = 'test loss')
    plt.legend()
    plt.show()
    plt.plot(test_accuracy, label = 'test accuracy')
    plt.legend()
    plt.show()

    maxacc = max(test_accuracy)
    print('max accuracy: {}'.format(maxacc))
    
    return maxacc

supervised_maxacc = supervised_train(model_baseline)

5) Определение дополнений

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

import torch
import torchvision.transforms as T

class Augment:
   """
   A stochastic data augmentation module
   Transforms any given data example randomly
   resulting in two correlated views of the same example,
   denoted x ̃i and x ̃j, which we consider as a positive pair.
   """

   def __init__(self):

       blur = T.GaussianBlur((3, 3), (0.1, 2.0))

       self.train_transform = torch.nn.Sequential(
           T.RandomAffine(degrees = (-50,50), translate = (0.1,0.1), scale=(0.5,1.5), shear=0.2),
           T.RandomPerspective(0.4,0.5),
           T.RandomPerspective(0.2,0.5),
           T.RandomPerspective(0.2,0.5),
           T.RandomApply([blur], p=0.25),
           T.RandomApply([blur], p=0.25)
       )

   def __call__(self, x):
       return self.train_transform(x), self.train_transform(x)

"""
Generating Test Augmentation
"""
a = Augment()
aug = a(torch_train_unlabled[0:100])

i=1
f, axarr = plt.subplots(2,2)
#positive pair
axarr[0,0].imshow(aug[0].cpu().detach().numpy()[i,0])
axarr[0,1].imshow(aug[1].cpu().detach().numpy()[i,0])
#another positive pair
axarr[1,0].imshow(aug[0].cpu().detach().numpy()[i+1,0])
axarr[1,1].imshow(aug[1].cpu().detach().numpy()[i+1,0])
plt.show()

6) Определение потери контраста

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

class ContrastiveLoss(nn.Module):
   """
   Vanilla Contrastive loss, also called InfoNceLoss as in SimCLR paper
   """
   def __init__(self, batch_size, temperature=0.5):
       """
       Defining certain constants used between calculations. The mask is important
       in understanding which are positive and negative examples. For more
       information see https://theaisummer.com/simclr/
       """
       super().__init__()
       self.batch_size = batch_size
       self.temperature = temperature
       self.mask = (~torch.eye(batch_size * 2, batch_size * 2, dtype=bool)).float().to(device)

   def calc_similarity_batch(self, a, b):
       """
       Defines the cosin similarity between one example, and all other examples.
       For more information see https://theaisummer.com/simclr/
       """
       representations = torch.cat([a, b], dim=0)
       return F.cosine_similarity(representations.unsqueeze(1), representations.unsqueeze(0), dim=2)

   def forward(self, proj_1, proj_2):
       """
       The actual loss function, where proj_1 and proj_2 are embeddings from the
       projection head. This function calculates the cosin similarity between
       all vectors, and rewards closeness between examples which come from the
       same example, and farness for examples which do not. For more information
       see https://theaisummer.com/simclr/
       """
       batch_size = proj_1.shape[0]
       z_i = F.normalize(proj_1, p=2, dim=1)
       z_j = F.normalize(proj_2, p=2, dim=1)

       similarity_matrix = self.calc_similarity_batch(z_i, z_j)

       sim_ij = torch.diag(similarity_matrix, batch_size)
       sim_ji = torch.diag(similarity_matrix, -batch_size)

       positives = torch.cat([sim_ij, sim_ji], dim=0)

       nominator = torch.exp(positives / self.temperature)

       denominator = self.mask * torch.exp(similarity_matrix / self.temperature)

       all_losses = -torch.log(nominator / torch.sum(denominator, dim=1))
       loss = torch.sum(all_losses) / (2 * self.batch_size)
       return loss

"""
testing
"""
loss = ContrastiveLoss(torch_train_x.shape[0]).forward
fake_proj_0, fake_proj_1 = a(torch_train_x)
fake_proj_0 = fake_proj_0[:,0,:,0]
fake_proj_1 = fake_proj_1[:,0,:,0]
loss(fake_proj_0, fake_proj_1)

7) Обучение с самоконтролем

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

from torch.optim.lr_scheduler import ExponentialLR

#degining a new model
model = Model()
model.train()

#defining key hyperparameters
batch_size = 512
epoch_size = round(torch_train_unlabled.shape[0]/batch_size)-1
num_epochs = 100
patience = 5
cutoff_ratio = 0.001

#defining key learning functions
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
num_examples = train_unlabled.shape[0]
lossfn = ContrastiveLoss(batch_size).forward
augmentfn = Augment() #augment function

#for book keeping
loss_hist = []
improvement_hist = []
schedule_hist = []

#for exponentially decreasing learning rate
scheduler = ExponentialLR(optimizer,
                          gamma = 0.95)

#for early stopping
patience_count=0

#Training Loop
avg_loss = 1e10
for i in range(num_epochs):

    print('epoch {}/{}'.format(i,num_epochs))

    total_loss = 0
    loss_change = 0

    for j in tqdm(range(epoch_size)):

        #getting random batch
        X = torch_train_unlabled[j*batch_size: (j+1)*batch_size]

        #creating pairs of augmented batches
        X_aug_i, X_aug_j = augmentfn(X)

        #ensuring gradients are zero
        optimizer.zero_grad()

        #passing through the model
        z_i = model(X_aug_i)
        z_j = model(X_aug_j)

        #calculating loss on the model embeddings, and computing gradients
        loss = lossfn(z_i, z_j)
        loss.backward()

        # Adjust learning weights
        optimizer.step()

        #checking to see if backpropegation resulted in a reduction of the loss function
        if True:
            #passing through the model, now that parameters have been updated
            z_i = model(X_aug_i)
            z_j = model(X_aug_j)

            #calculating new loss value
            new_loss = lossfn(z_i, z_j)

            loss_change += new_loss.cpu().detach().numpy() - loss.cpu().detach().numpy()

        total_loss += loss.cpu().detach().numpy()

        #step learning rate scheduler
        schedule_hist.append(scheduler.get_last_lr())

    scheduler.step()

    #calculating percentage loss reduction
    new_avg_loss = total_loss/epoch_size
    per_loss_reduction = (avg_loss-new_avg_loss)/avg_loss
    print('Percentage Loss Reduction: {}'.format(per_loss_reduction))

    #deciding to stop if loss is not decreasing fast enough
    if per_loss_reduction < cutoff_ratio:
        patience_count+=1
        print('patience counter: {}'.format(patience_count))
        if patience_count > patience:
            break
    else:
        patience_count = 0

    #setting new loss as previous loss
    avg_loss = new_avg_loss

    #book keeping
    avg_improvement = loss_change/epoch_size
    loss_hist.append(avg_loss)
    improvement_hist.append(avg_improvement)
    print('Average Loss: {}'.format(avg_loss))
    print('Average Loss change (if calculated): {}'.format(avg_im

8) Прогресс самоконтроля обучения

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

plt.plot(schedule_hist, label='learning rate')
plt.legend()
plt.show()
plt.plot(loss_hist, label = 'loss')
plt.legend()
plt.show()

9) Точная настройка модели с самостоятельным наблюдением с контролируемым обучением

Использование контролируемой функции ранее для обучения самоконтролируемой модели на контролируемых данных. Это делается дважды; один раз с оригинальной обучающейся головкой с самоконтролем и один с новой произвольно инициализируемой головкой.

import copy

#creating duplicate models for finetuning
model_same_head = copy.deepcopy(model)
model_new_head = copy.deepcopy(model)

#replacing the projection head with a randomly initialized head
#for one of the models
model_new_head.head = Head()

#training models
same_head_maxacc = supervised_train(model_same_head)
new_head_maxacc = supervised_train(model_new_head)

10) Обсуждение

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

Эти результаты носят чисто демонстративный характер; не было значительной оптимизации гиперпараметров, которая была бы необходима в производстве. Тем не менее, этот ноутбук подтверждает теоретическую полезность самоконтроля и важность осторожного использования проекционной головки.

  • Только контролируемое обучение: точность 52,5 %
  • SSL и контролируемый заголовок SSL: точность 59,7 %
  • SSL и курируются на новой голове: 63,6%

Точность 63,6 %, если учитывать только 200 помеченных изображений, впечатляет!

Следите за новостями!

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

Поставьте лайк, поделитесь и подпишитесь. Ваша поддержка как независимого автора действительно имеет огромное значение!

Атрибуция. Все изображения в этом документе были созданы Дэниелом Уорфилдом, если не указан иной источник. Вы можете использовать любые изображения в этом посте в своих некоммерческих целях, если вы ссылаетесь на эту статью, https://danielwarfield.dev или на то и другое.