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

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

Почему PyTorch?

Хотя PyTorch пока не пользуется популярностью в отличие от других фреймворков глубокого обучения, таких как Keras и Tensorflow, благодаря своим возможностям он стал популярным. Однако он более подробный, чем его конкурирующие фреймворки. Следовательно, это лучший фреймворк для понимания внутреннего устройства нейронной сети, как я ее вижу. С другой стороны, Tesla использует PyTorch для автоматического вождения автомобилей, - не сомневайтесь в возможностях фреймворка.

Набор данных

Мы собираемся использовать популярный набор данных Iris для обучения и тестирования нейронной сети. Для получения дополнительной информации обратитесь к этому документу из Репозитория машинного обучения UCI.

В двух словах, цветок ирис насчитывает около 280 видов. В этом наборе данных мы рассматриваем только около 3 из них. Это Ирис сетоса, Ирис вирджинский и Ирис разноцветный. Каждый из этих видов мы можем отличить по ширине и длине чашелистиков и лепестков.

Я использую Панды для загрузки и управления набором данных. Вот как выглядит образец набора данных после выполнения нижеприведенного оператора;

df = pd.read_csv('data/iris.csv')

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

plt.figure(figsize=(7, 5))
plt.scatter(df['sepal_length'].values, 
            df['sepal_width'].values,
            c=df['species_category'].values)
plt.colorbar(ticks=[0, 1, 2], 
             format=plt.FuncFormatter(lambda i, *args: labels[i]))
plt.xlabel('sepal length(cm)')
plt.ylabel('sepal width(cm)')
plt.tight_layout()
plt.show()

Как вы, возможно, уже знаете, нейронные сети не могут обрабатывать сложные типы данных, кроме чисел. В этом наборе данных размеры чашелистика и лепестка указаны в сантиметрах (см) и, очевидно, в числах. Но species, который мы собираемся спрогнозировать (а также вычислить потери), не в числах, а в строковом названии вида. Следовательно, мы можем сделать этот небольшой дополнительный шаг для map строкового имени вида в числа (существует лишь небольшое количество различных типов, поэтому мы можем просто сопоставить их с числами).

df['species_category'] = df['species'].map(
    {'setosa': 0, 'versicolor': 1, 'virginica': 2}
)

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

# List index represent the reference species.
labels = ['Iris setosa', 'Iris versicolor', 'Iris virginica']

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

Итак, входными данными будут sepal_length, sepal_width, petal_length и petal_width.

Результатом будет просто species (точнее species_category).

X = df.iloc[:, 0:4].values  # Input values.
y = df.iloc[:, 5].values    # Output values(species categories)

Теперь у нас есть массив NumPy входных и выходных значений, который имеет форму (150, 4) и (150,) соответственно.

Всегда полезно протестировать нейронную сеть после тренировки. Кроме того, нельзя использовать тот же набор данных для тестирования / проверки, который использовался для обучения. Это не помогло бы лучше понять, как обучение проходило с набором данных. Таким образом, всегда лучше иметь отдельный набор данных для тестирования. Поскольку в этом случае у нас есть только один набор данных, мы можем использовать scikit-learn, чтобы разделить набор данных на обучающий и тестовый.

X_train, X_test, y_train, y_test = train_test_split(X, y,  test_size=0.2, random_state=42)

X_train и y_train можно использовать для обучения нейронной сети, а X_test и y_test можно использовать для проверки сети. X_test и y_test - это размер 20% исходного набора данных, как определено в test_size=0.2, а random_state устанавливает случайное начальное число для перемешивания набора данных перед разделением.

Если вы проверите тип данных всех этих наборов данных, они будут массивами NumPy, а PyTorch ожидает, что типом ввода будут объекты Tensor. Поэтому преобразуйте все эти массивы NumPy в объекты torch.Tensor.

X_train = torch.FloatTensor(X_train)
X_test = torch.FloatTensor(X_test)
y_train = torch.LongTensor(y_train)
y_test = torch.LongTensor(y_test)

Нейронная сеть

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

class NeuralNet(nn.Module):
    
    def __init__(self, in_features=4, out_features=3):
        super().__init__()
        self.fc1 = nn.Linear(in_features=in_features,
                             out_features=120)
        self.fc2 = nn.Linear(in_features=120, 
                             out_features=84)
        self.fc3 = nn.Linear(in_features=84,  
                             out_features=out_features)
        
    def forward(self, X):
        X = F.relu(self.fc1(X))
        X = F.relu(self.fc2(X))
        return self.fc3(X)

Создайте экземпляр этой модели для обучения.

model = NeuralNet()

Обучение

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

Функция потерь / затрат, которую я собираюсь использовать, - это потеря кросс-энтропии, поэтому это проблема классификации, и оптимизатором будет Адам.

criteriotn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

Давай, продолжим обучение;

epochs = 50
losses = []
for i in range(epochs):
    i += 1
    
    y_pred = model(X_train)
    loss = criterion(y_pred, y_train)
    
    losses.append(loss)
    
    if i % 10 == 0:
        print(f'epoch: {i} -> loss: {loss}')
        
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

После 50 эпох график потерь по эпохам выглядит так;

plt.figure(figsize=(7, 5))
plt.plot(losses)
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.show()

Глядя на сюжет, можно было понять, что этого достаточно для достижения минимальной потери 50 эпох. Однако к 30-й эпохе потери достигают минимума. Достигнув минимума, дальнейшее обучение рискует переобучиться.

Проверка

Хотя для проверки у нас есть только 20% исходного набора данных, это совершенно новые данные, которые нейронная сеть никогда не видела. Следовательно, используя этот набор данных, мы сможем понять, есть ли переоснащение или недостаточное.

Валидация должна выполняться в блоке torch.no_grad(), поэтому мы не хотим нарушать обученные веса и смещения сети.

with torch.no_grad():
    
    y_pred = model(X_test)
    preds = torch.max(y_pred, dim=1)[1]
    correct = (preds == y_test).sum()
    
print(f'{correct} out of {y_test.shape[0]} is correct : {correct.item() / y_test.shape[0] * 100}%')

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

Результат - 30 out of 30 is correct : 100.0% из приведенного выше фрагмента. Это означает, что все элементы правильно предсказываются сетью.

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

with torch.no_grad():
    
    correct = 0
    
    for i, X in enumerate(X_test):
        
        y_pred = model(X)
        
        if y_pred.argmax().item() == y_test[i]:
            correct += 1
            
print(f'{correct} out of {y_test.shape[0]} is correct : {correct / y_test.shape[0] * 100}%')

Применяйте неизвестные данные и получайте результаты

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

@torch.no_grad()
def predict_unknown(X_unknown):
    
    y_pred = model(X_unknown)
    return labels[y_pred.argmax()]

Теперь, если у нас есть следующие данные, мы можем попытаться их предсказать.

unknown_iris = torch.tensor([5.6, 3.7, 2.2, 0.5])

Вызов predict_unknown(mystery_iris) даст нам результат Iris setosa. Мы не можем проверить, верен ли этот прогноз или нет, потому что у нас нет записи для подтверждения для этой точки данных. Но у нас могла бы быть идея, глядя на рассредоточенный график, который мы использовали ранее. Итак, мы могли бы попробовать сопоставить эту точку данных с этим рассредоточенным графиком.

Глядя на рассеянный граф, мы можем подтвердить, что прогноз нейронной сети верен.

Заключение

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

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