<jupyter_start><jupyter_text>Возьмем готовый претренированный ResNet50<jupyter_code>transfer_model = models.resnet50(pretrained=True)<jupyter_output><empty_output><jupyter_text>Фризим слои, которые мы не хотим обновлять в дообучении. Для этого мы не даем накапливаться градиентам спмощью requires_grad() для всех параметров сети (доступны через метод paraneters())Кроме того, не будем фризить блоки BatchNorm чтобьы не терять данные на нормализации<jupyter_code>for name, param in transfer_model.named_parameters():
    if("bn" not in name):
        param.requires_grad = False<jupyter_output><empty_output><jupyter_text>Заменим последний классификационный блок своим хвостом. В данном случае двумя линейными слоями, релу и дропаутом. Пайторч хранит последний блок как переменную экземпляра, поэтому ее можно переопределить.Переменная in_features позволяет захватывать несколько активаций, входящих в слой. Аналог для исходящих out_features. Они используются в случае, когда вход следующего слоя ожидает от выхода предыдущего одну размерность, а получает другую, что вызывает ошибку.<jupyter_code>transfer_model.fc = nn.Sequential(nn.Linear(transfer_model.fc.in_features, 500), 
                                  nn.ReLU(), 
                                  nn.Dropout(), 
                                  nn.Linear(500, 2))<jupyter_output><empty_output><jupyter_text>Обучение<jupyter_code>def train(model, optimizer, loss_fn, train_loader, val_loader, epochs=20, device="cpu"):
    for epoch in range(epochs):
        training_loss = 0.0
        valid_loss = 0.0
        model.train()
        for batch in train_loader:
            optimizer.zero_grad()
            inputs, targets = batch
            inputs = inputs.to(device)
            targets = targets.to(device)
            output = model(inputs)
            loss = loss_fn(output, targets)
            loss.backward()
            optimizer.step()
            training_loss += loss.data.item() * inputs.size(0)
        training_loss /= len(train_loader.dataset)
        
        model.eval()
        num_correct = 0 
        num_examples = 0
        for batch in val_loader:
            inputs, targets = batch
            inputs = inputs.to(device)
            output = model(inputs)
            targets = targets.to(device)
            loss = loss_fn(output,targets) 
            valid_loss += loss.data.item() * inputs.size(0)
            correct = torch.eq(torch.max(F.softmax(output, dim=1), dim=1)[1], targets)
            num_correct += torch.sum(correct).item()
            num_examples += correct.shape[0]
        valid_loss /= len(val_loader.dataset)

        print('Epoch: {}, Training Loss: {:.2f}, Validation Loss: {:.2f}, accuracy = {:.2f}'.format(epoch, training_loss,
        valid_loss, num_correct / num_examples))
def check_image(path):
    try:
        im = Image.open(path)
        return True
    except:
        return False
img_transforms = transforms.Compose([
    transforms.Resize((64,64)),    
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                    std=[0.229, 0.224, 0.225])
    ])

train_data_path = "pytorch_learning/train/"
train_data = torchvision.datasets.ImageFolder(root=train_data_path,
                                              transform=img_transforms, 
                                              is_valid_file=check_image)
val_data_path = "pytorch_learning/val/"
val_data = torchvision.datasets.ImageFolder(root=val_data_path,
                                            transform=img_transforms, 
                                            is_valid_file=check_image)
# не используем
test_data_path = "pytorch_learning/test/"
test_data = torchvision.datasets.ImageFolder(root=test_data_path, 
                                             transform=img_transforms, 
                                             is_valid_file=check_image)

batch_size=64
train_data_loader = torch.utils.data.DataLoader(train_data, 
                                                batch_size=batch_size,
                                                shuffle=True)
val_data_loader  = torch.utils.data.DataLoader(val_data, 
                                               batch_size=batch_size, 
                                               shuffle=True)
test_data_loader = torch.utils.data.DataLoader(test_data, 
                                               batch_size=batch_size, 
                                               shuffle=True)

if torch.cuda.is_available():
    device = torch.device("cuda") 
else:
    device = torch.device("cpu")
transfer_model.to(device)
optimizer = optim.Adam(transfer_model.parameters(), lr=0.001)
train(transfer_model, 
      optimizer,
      torch.nn.CrossEntropyLoss(), 
      train_data_loader,
      val_data_loader, 
      epochs=10, 
      device=device)
labels = ['cat','fish']
def predictor(image_for_predict):
    img = Image.open(image_for_predict) 
    img = img_transforms(img).to(device)
    # сеть ожидает 4d тензор, т.к. используются пакеты 
    # (первое значение в тензоре - число изображений в пакете)
    # т.к. у нас пакетов нет, создаем пакет длиной 1 с помощью unsqueeze(0)
    # который добавляет новую размерность в начало тензора
    img = img.unsqueeze(0)

    prediction = F.softmax(transfer_model(img), dim=1)
    prediction = prediction.argmax()
    print(labels[prediction])
# cat
predictor("pytorch_learning/test/cat/2041806579_e4a7f31b32.jpg")
# fish
predictor("pytorch_learning/test/fish/1609947018_bfa1fcd6b2.jpg")
# fish
predictor("pytorch_learning/test/fish/2869804396_c4127ccec6.jpg")<jupyter_output>fish<jupyter_text>Вычисление скорости обученияМетод описан [тут](https://arxiv.org/abs/1506.01186) и предполагает, что в течении эпохи, скорость обучения начинается с небольшой, увеличивается с каждым минибачем, в итоге получается высокая в конце эпохи. Задача заключается в нахождении отношения скорости к потерям и затем в выборе такого показателя скорости, при котором происходит наибольшее снижение потерь (это можно сделать например визуально по графику)<jupyter_code># упрощенный пример реализации метода в open.ai
def find_lr(model, loss_fn, optimizer, train_loader, init_value=1e-8, final_value=10.0, device="cpu"):
    number_in_epoch = len(train_loader) - 1
    update_step = (final_value / init_value) ** (1 / number_in_epoch)
    lr = init_value
    optimizer.param_groups[0]["lr"] = lr
    best_loss = 0.0
    batch_num = 0
    losses = []
    log_lrs = []
    for data in train_loader:
        batch_num += 1
        inputs, targets = data
        inputs = inputs.to(device)
        targets = targets.to(device)
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = loss_fn(outputs, targets)

        # Crash out if loss explodes

        if batch_num > 1 and loss > 4 * best_loss:
            if(len(log_lrs) > 20):
                return log_lrs[10:-5], losses[10:-5]
            else:
                return log_lrs, losses

        # Record the best loss

        if loss < best_loss or batch_num == 1:
            best_loss = loss

        # Store the values
        losses.append(loss.item())
        log_lrs.append((lr))

        # Do the backward pass and optimize

        loss.backward()
        optimizer.step()

        # Update the lr for the next step and store

        lr *= update_step
        optimizer.param_groups[0]["lr"] = lr
    if(len(log_lrs) > 20):
        return log_lrs[10:-5], losses[10:-5]
    else:
        return log_lrs, losses
(lrs, losses) = find_lr(transfer_model, 
                        torch.nn.CrossEntropyLoss(),
                        optimizer, 
                        train_data_loader,
                        device=device)
plt.plot(lrs, losses)

plt.xscale("log")
plt.xlabel("Learning rate")
plt.ylabel("Loss")
plt.show()<jupyter_output><empty_output><jupyter_text>**Функция фактически обучает модель и связана с настройками оптимизатора. Поэтому необходимо предварительно сохранить и перегрузить модель, чтобы вернуться в состояние, в котором она находилась до вызова find_lr(). Кроме того придется повторно инициализировать оптимизатор.**  Дифференциальная скорость обученияМожно дообучить слои предобученной сети, не используя обучение слоев с нуля. Для этого в pytorch можно задать специфическую скорость обучения для этих слоев и разморозитиь их.В данном случае сделаем это с последним и предпоследним слоем перед добавленным нами классификатором<jupyter_code># базируемся на вычисленной скорости обучения 
# (на самом деле сетья не перегружал и выбрал значение на шару - просто лемонстрация)
found_lr = 1e-7
optimizer = optim.Adam([
    {'params': transfer_model.layer4.parameters(), 'lr': found_lr/3},
    {'params': transfer_model.layer3.parameters(), 'lr': found_lr/9}
], lr= found_lr)
unfreeze_layers = [transfer_model.layer3, transfer_model.layer4]
for layer in unfreeze_layers:
    for param in layer.parameters():
        param.requires_grad = True
train(transfer_model, 
      optimizer,
      torch.nn.CrossEntropyLoss(), 
      train_data_loader,
      val_data_loader, 
      epochs=10, 
      device=device)<jupyter_output>Epoch: 0, Training Loss: 0.02, Validation Loss: 0.27, accuracy = 0.94
Epoch: 1, Training Loss: 0.02, Validation Loss: 0.29, accuracy = 0.94
Epoch: 2, Training Loss: 0.02, Validation Loss: 0.28, accuracy = 0.94
Epoch: 3, Training Loss: 0.01, Validation Loss: 0.28, accuracy = 0.94
Epoch: 4, Training Loss: 0.02, Validation Loss: 0.27, accuracy = 0.93
Epoch: 5, Training Loss: 0.01, Validation Loss: 0.28, accuracy = 0.94
Epoch: 6, Training Loss: 0.01, Validation Loss: 0.29, accuracy = 0.93
Epoch: 7, Training Loss: 0.01, Validation Loss: 0.28, accuracy = 0.94
Epoch: 8, Training Loss: 0.01, Validation Loss: 0.29, accuracy = 0.94
Epoch: 9, Training Loss: 0.00, Validation Loss: 0.29, accuracy = 0.94<jupyter_text>Аугментация данныхМетоды для аугментации доступны в torchvision.Методы могут возрващать PIL-images, тензоры или конвертить тензоры в изображения и наоборот.Есть два способа - преобразования из коробки и кастомные преобразования с использованием пользовательских функций[Подробнее](https://pytorch.org/docs/stable/torchvision/transforms.html)<jupyter_code># трансформация из коробки осуществляется через стандартный папйплайн pytorch с вызовом определенных функций
img_augment = transforms.Compose([
    transforms.CenterCrop(10),
    transforms.ToTensor(),
    ])<jupyter_output><empty_output><jupyter_text>Для аугментации в разных цветовых форматах у pytorch методов нет (RGB, HSV, YUV, LAB). Но есть обертка, с помощью которой можно написать свой метод для конвертера изображений.[transforms.Lambda()](https://pytorch.org/docs/stable/torchvision/transforms.htmlgeneric-transforms)<jupyter_code>def _random_color_apace(x):
    output = x.convert('HSV') # метод PIL конвертирует в нужный формат
    return output
# все изображения в пакете
color_transforms = transforms.Lambda(lambda x: _random_color_apace(x))
# сдучайные изображения в пакете с вероятностью 0.5
random_color_transforms = transforms.RandomApply([color_transforms])<jupyter_output><empty_output><jupyter_text>Когда анонимной функции недостаточно, можно создать свой класс, который будет производить преобразования над PILL-изображениями или над тензорами. Такой класс должен определять два метода \__call\__ в котором будет определен конвеер и \__repr\__, который вернет строковое представление преобразования, а так-же состояния для тестирования. **Ограничений на методы в call нет - можно хоть в другую сеть пихать**Для примера - добавление к тензору случайного гауссового шума.<jupyter_code>class Noise():
    """Adds gaussian noise to a tensor.
    
    Example:
        >>> transforms.Compose([
        >>>     transforms.ToTensor(),
        >>>     Noise(0.1, 0.05)),
        >>> ])
    
    """
    def __init__(self, mean, stddev):
        self.mean = mean
        self.stddev = stddev

    def __call__(self, tensor):
        noise = torch.zeros_like(tensor).normal_(self.mean, self.stddev)
        return tensor.add_(noise)
    
    def __repr__(self):
        repr_ = f"{self.__class__.__name__  }(mean={self.mean},sttdev={self.stddev})"
        return repr_
custom_transform_pipeline = transforms.Compose([
    random_color_transforms, 
    Noise(0.1, 0.05)
])<jupyter_output><empty_output><jupyter_text>Еще один метод улучшить качество обучения модели - обучиться вначале на изображениях маленького формата, затем обученную сеть еще раз прогнать на изображдениях большего формата и затем еще раз на самых больших.<jupyter_code>resize_pipeline = transforms.Compose([
    transforms.Resize(64),
    # other augmentation
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                    std=[0.229, 0.224, 0.225])
])<jupyter_output><empty_output><jupyter_text>Этот метод правда замедлит обучение, так как каждый раз в пайплайне будет выполняться ресайз. Можно подготовить картинки заранее, но это требует места на диске.Кроме того, можно повышать глубину сети, к примеру двигаясь от ResNet34, к 101 и 152 Еще один способ - использовать несколько моделей, а затем построить их ансамбль. В данном случае мы прост овыберем среднее из двух резнетов. stack объединяет массив тензоров моделей. mean вычисляет среднее (в данном случае по первой размерности тензора). argmax выбирает индекс тензора с наивысшей размерностью. Естественно это самый банальный из возможных примеров.<jupyter_code>models_ensemble = [models.resnet50().to(device), models.resnet50().to(device)]
predictions = [F.softmax(m(torch.rand(1,3,224,244).to(device))) for m in models_ensemble] 
avg_prediction = torch.stack(predictions).mean(0).argmax()
avg_prediction
torch.stack(predictions)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09121326738434682, 'fraction_numerical': 0.023639802153040443, 'mean_word_length': 3.1152349595929363, 'pattern_counts': {'":': 0, '<': 44, '<?xml version=': 0, '>': 59, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 27, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '84273', 'license': 'MIT', 'n_tokens_mistral': 4898, 'n_tokens_neox': 5035, 'n_words': 1080, 'path': 'pytorch-learning-transfer.ipynb', 'repo_name': 'KonstantinKlepikov/all-python-ml-learning', 'size': '13748'}