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

Основанная в 2016 году Адамом Пашке, Сэмом Гроссом, Сумит Чинтала и Грегори Ченаном, ее использование идиоматического Python делает ее очень простой в реализации.

В приведенном ниже коде показана умная интеграция PyTorch с идиоматическим Python.

В этом посте мы продемонстрируем логистическую регрессию в PyTorch, используя некоторые данные Car Crash (нажмите здесь, чтобы загрузить).

Наша цель — предсказать уровень ПОВРЕЖДЕНИЙ автомобиля. Это разделено на три категории (что делает это проблемой классификации):

  1. $500 или менее в ущерб
  2. $501 — $1,500
  3. Более 1500 долларов США

1. Подготовка данных

Во-первых, давайте подготовим наши данные для моделирования.

Мы собираемся загрузить данные в виде Pandas DataFrame, выполнить предварительную обработку, а затем преобразовать в тензорный формат, который будет использоваться PyTorch.

import pandas as pd
pd.set_option('display.max_columns', None)
data = pd.read_csv('traffic_crashes_chicago.csv')

Давайте проверим наши значения NaN:

data.isna().sum()
CRASH_RECORD_ID                       0
RD_NO                              3917
CRASH_DATE_EST_I                 446607
CRASH_DATE                            0
POSTED_SPEED_LIMIT                    0
TRAFFIC_CONTROL_DEVICE                0
DEVICE_CONDITION                      0
WEATHER_CONDITION                     0
LIGHTING_CONDITION                    0
FIRST_CRASH_TYPE                      0
TRAFFICWAY_TYPE                       0
LANE_CNT                         283902
ALIGNMENT                             0
ROADWAY_SURFACE_COND                  0
ROAD_DEFECT                           0
REPORT_TYPE                       11746
CRASH_TYPE                            0
INTERSECTION_RELATED_I           373918
NOT_RIGHT_OF_WAY_I               460154
HIT_AND_RUN_I                    340916
DAMAGE                                0
DATE_POLICE_NOTIFIED                  0
PRIM_CONTRIBUTORY_CAUSE               0
SEC_CONTRIBUTORY_CAUSE                0
STREET_NO                             0
STREET_DIRECTION                      3
STREET_NAME                           1
BEAT_OF_OCCURRENCE                    5
PHOTOS_TAKEN_I                   476801
STATEMENTS_TAKEN_I               473134
DOORING_I                        481317
WORK_ZONE_I                      479748
WORK_ZONE_TYPE                   480406
WORKERS_PRESENT_I                482118
NUM_UNITS                             0
MOST_SEVERE_INJURY                  978
INJURIES_TOTAL                      967
INJURIES_FATAL                      967
INJURIES_INCAPACITATING             967
INJURIES_NON_INCAPACITATING         967
INJURIES_REPORTED_NOT_EVIDENT       967
INJURIES_NO_INDICATION              967
INJURIES_UNKNOWN                    967
CRASH_HOUR                            0
CRASH_DAY_OF_WEEK                     0
CRASH_MONTH                           0
LATITUDE                           2670
LONGITUDE                          2670
LOCATION                           2670
dtype: int64

Теперь мы отбросим все тяжелые столбцы NaN и столбцы, которые не имеют никакой прогностической ценности:

data.drop(labels=['CRASH_RECORD_ID', 'RD_NO', 'CRASH_DATE_EST_I',
                 'LANE_CNT', 'NOT_RIGHT_OF_WAY_I', 'HIT_AND_RUN_I',
                 'DATE_POLICE_NOTIFIED', 'STREET_NO', 
                 'STREET_DIRECTION', 'STREET_NAME', 
                 'BEAT_OF_OCCURRENCE', 'PHOTOS_TAKEN_I', 
                 'STATEMENTS_TAKEN_I', 'DOORING_I', 'WORK_ZONE_I', 
                 'CRASH_DATE', 'REPORT_TYPE', 
                 'INTERSECTION_RELATED_I', 'WORK_ZONE_TYPE', 
                 'LOCATION', 'WORKERS_PRESENT_I'], 
                 axis=1, inplace=True)

Поскольку оставшиеся значения NaN занимают лишь небольшую часть оставшегося набора данных, мы удалим все строки, содержащие значения NaN:

data.dropna(inplace=True)

Теперь давайте разобьем данные на наборы training и test, используя sklearn . В PyTorch также есть вариант:

from torch.utils.data import random_split

Это может выполнить эту операцию, но для этого примера мы будем использовать sklearn :

X = data.drop(labels='DAMAGE', axis=1)
y = data['DAMAGE']
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y)

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

from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
numerical_train = X_train[['LATITUDE', 'LONGITUDE']]
num_X_train_scaled = pd.DataFrame(ss.fit_transform(numerical_train), columns =numerical_train.columns)
categorical_train = X_train.drop(labels=['LATITUDE', 'LONGITUDE'], axis=1)
# Using OneHotEncoder to get converted categorical variables
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(sparse=False)
X_tr_dummies = pd.DataFrame(ohe.fit_transform(categorical_train), columns = ohe.get_feature_names())
X_tr_transformed = pd.concat([X_tr_dummies, num_X_train_scaled], axis=1)

А для наших тестовых данных

numerical_test = X_test[['LATITUDE', 'LONGITUDE']]
num_X_test_scaled = pd.DataFrame(ss.transform(numerical_test), columns=numerical_test.columns)
categorical_test = X_test.drop(labels=['LATITUDE', 'LONGITUDE'], axis=1)
X_te_dummies = pd.DataFrame(ohe.transform(categorical_test), columns = ohe.get_feature_names())
X_te_transformed = pd.concat([X_te_dummies, numerical_test], axis=1)

И, наконец, что не менее важно, мы должны преобразовать наш Pandas Dataframe в тензор PyTorch:

# X_train values
X_tr_tensor = torch.tensor(X_tr_transformed.values)
# X_test values
X_te_tensor = torch.tensor(X_te_transformed.values)

2. Разделите наши данные на «пакеты»

Вкратце это процесс логистической регрессии PyTorch:

  1. Получите случайную выборку из данных («партия»).
  2. Вычислите потери (в этом примере мы будем использовать функцию потерь перекрестной энтропии).
  3. Вычислите градиент, выполнив «обратный проход» (loss.backward())
  4. Используйте функцию оптимизатора, чтобы предпринять шаг для минимизации потерь (в этом примере мы будем использовать SGD, стохастический градиентный спуск).
  5. Промойте и повторите для любого заданного количества эпох.

Давайте начнем!

import torch
X_tr_tensor = torch.tensor(x_tr_transformed.values)
X_tr_tensor.shape
>>> torch.Size([359417, 387])
X_te_tensor = torch.tensor(x_te_transformed.values)
X_te_tensor.shape
>>> torch.Size([210273, 387])

Давайте разделим наши данные на 100 пакетов одинакового размера:

from torch.utils.data import DataLoader
batch_size = 100
train_loader = DataLoader(X_tr_tensor, batch_size, shuffle=True)
test_loader = DataLoader(X_te_tensor, batch_size)

3. Создайте экземпляр нашей модели!

import torch.nn as nn
input_size = X_tr_tensor.shape[1]
num_classes = 3
# Logistic regression model
model = nn.Linear(input_size, num_classes)

Давайте посмотрим на наши начальные веса и смещения:

model.weight.shape
>>> torch.Size([3, 387])
model.weight
>>> Parameter containing:
tensor([[ 0.0189,  0.0020, -0.0448,  ...,  0.0175,  0.0464,  0.0140],
        [-0.0335,  0.0505, -0.0031,  ..., -0.0387, -0.0364, -0.0502],
        [ 0.0013,  0.0317,  0.0103,  ..., -0.0373,  0.0377,  0.0122]],
       requires_grad=True)
model.bias.shape
>>> torch.Size([3]) # 3 biases for 3 classes, a ternary classifier
model.bias
>>> Parameter containing:
tensor([-0.0396, -0.0158, -0.0029], requires_grad=True)
# Alternative:
list(model.parameters())
>>> [Parameter containing:
 tensor([[ 0.0189,  0.0020, -0.0448,  ...,  0.0175,  0.0464,  0.0140],
         [-0.0335,  0.0505, -0.0031,  ..., -0.0387, -0.0364, -0.0502],
         [ 0.0013,  0.0317,  0.0103,  ..., -0.0373,  0.0377,  0.0122]],
        requires_grad=True),
 Parameter containing:
 tensor([-0.0396, -0.0158, -0.0029], requires_grad=True)]

4. Создавайте прогнозы:

outputs = model(X_tr_tensor.float())

В sklearn мы можем генерировать вероятности, используя метод predict_proba(). В PyTorch мы будем использовать следующее:

import torch.nn.functional as F
probs = F.softmax(outputs, dim=1)
print("Here are some of our newly converted probabilities:\n", probs[:2].data)
>>> Here are our sample probabilities:
 tensor([[0.2804, 0.3906, 0.3291],
        [0.3087, 0.3869, 0.3043]])
print(torch.sum(probs[0]).item())
>>> Sum:  1.0

Этот метод softmax() преобразует значения логита в положительные вероятности:

Прежде чем мы сможем рассчитать, насколько хороша наша модель, мы должны преобразовать наши данные y_train и y_test в тензоры:

# Converting y values into their respective classes (0,1,2)
conditions_train = [(y_train == 'OVER $1,500'), 
                    (y_train == '$501 - $1,500'), 
                    (y_train == '$500 OR LESS')]
conditions_test = [(y_test == 'OVER $1,500'), (y_test == '$501 - $1,500'), (y_test == '$500 OR LESS')]
choices = [0,1,2]
y_train = np.select(conditions_train, choices)
y_test = np.select(conditions_test, choices)
#Typecasting into tensors:
y_tr_tensor = torch.tensor(y_train)
y_te_tensor = torch.tensor(y_test)

5. Рассчитайте убытки

loss_fn = F.cross_entropy
loss = loss_fn(outputs, y_tr_tensor)
print(loss)
>>> tensor(1.1407, grad_fn=<NllLossBackward>)

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

import math
math.exp(-loss)
>>> 0.3196105865388012

6. Создайте/внедрите функцию или подкласс для повторения процесса для всех пакетов и эпох:

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

def fit(epochs, lr, model, train_loader, 
        val_loader, opt_func=torch.optim.SGD
        ):
    optimizer = opt_func(model.parameters(), lr)
    history = [] # for recording epoch-wise results
    
    for epoch in range(epochs):
        
        # Training Phase 
        for batch in train_loader:
            loss = model.training_step(batch)
            loss.backward()
            optimizer.step()
            optimizer.zero_grad()
        
        # Validation phase
        result = evaluate(model, val_loader)
        model.epoch_end(epoch, result)
        history.append(result)
    return history

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

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

# MNIST (Modified National Institute of Standards and Technology)
# handwritten digits 0-9 . 
from torchvision.datasets import MNIST
dataset = MNIST(root='data/', download=True)

Чтобы просмотреть другие встроенные наборы данных, нажмите здесь. Другие наборы данных включают CelebA (крупномасштабный набор данных атрибутов лиц с более чем 200 тыс. изображений знаменитостей), CIFAR (распознавание объектов) и Cityscapes (изображения городских уличных сцен из 50 разных городов).

Другие источники