И. Введение

Нейронные сети штурмом захватили мир, совершив революцию в области машинного обучения и открыв новые горизонты в области искусственного интеллекта. Эти мощные модели привели к прорывам, которые когда-то считались невозможными, например, ChatGPT, который может генерировать ответы, подобные человеческим, DALL-E, который создает потрясающие изображения из текстовых подсказок (см. заключение этого поста для примера), и AlphaGo Zero. , который освоил древнюю игру Го без предварительного знания человеческого опыта.

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

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

II. PCA, нейронные сети иавтоэнкодеры

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

Нейронные сети, с другой стороны, могут изучать нелинейные отношения и сложные шаблоны, что делает их более мощными и гибкими, чем PCA. Общей архитектурой, используемой для уменьшения размерности, является автоэнкодер, нейронная сеть с кодировщиком, который отображает входные данные в низкоразмерное представление скрытого пространства, и декодером, который реконструирует входные данные из скрытого пространства. Автоэнкодер учится сжимать данные в скрытом пространстве, сводя к минимуму «ошибку реконструкции», которая измеряет разницу между вводом и выводом. Ключевым элементом автоэнкодера является «узкое место», обозначенное буквой «h» на схеме ниже.

III. Автоэнкодер с Python

Ниже приведена реализация автоэнкодера с использованием Python и pytorch. Чтобы все было легко и понятно, давайте воспользуемся набором данных MNIST, который содержит изображения рукописных цифр.

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

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from torchvision import datasets, transforms
import torch
import torch.nn as nn
from torch.utils.data import DataLoader

# Check if GPU is available and set the device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Load the MNIST dataset
transform = transforms.Compose([transforms.ToTensor()])
mnist_train = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
mnist_test = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

# Create DataLoader for training and testing sets
batch_size = 128
train_loader = DataLoader(mnist_train, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(mnist_test, batch_size=batch_size, shuffle=False)

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

#Architecture choice based on the following post: https://medium.com/dataseries/convolutional-autoencoder-in-pytorch-on-mnist-dataset-d65145c132ac
class Encoder(nn.Module):
    def __init__(self, encoded_space_dim,fc2_input_dim):
        super().__init__()
        
        ### Convolutional section
        self.encoder_cnn = nn.Sequential(
            nn.Conv2d(1, 8, 3, stride=2, padding=1),
            nn.ReLU(True),
            nn.Conv2d(8, 16, 3, stride=2, padding=1),
            nn.BatchNorm2d(16),
            nn.ReLU(True),
            nn.Conv2d(16, 32, 3, stride=2, padding=0),
            nn.ReLU(True)
        )
        
        ### Flatten layer
        self.flatten = nn.Flatten(start_dim=1)
        ### Linear section
        self.encoder_lin = nn.Sequential(
            nn.Linear(3 * 3 * 32, 128),
            nn.ReLU(True),
            nn.Linear(128, encoded_space_dim)
        )
        
    def forward(self, x):
        x = self.encoder_cnn(x)
        x = self.flatten(x)
        x = self.encoder_lin(x)
        return x
      
class Decoder(nn.Module):
    def __init__(self, encoded_space_dim,fc2_input_dim):
        super().__init__()
        self.decoder_lin = nn.Sequential(
            nn.Linear(encoded_space_dim, 128),
            nn.ReLU(True),
            nn.Linear(128, 3 * 3 * 32),
            nn.ReLU(True)
        )

        self.unflatten = nn.Unflatten(dim=1, 
        unflattened_size=(32, 3, 3))

        self.decoder_conv = nn.Sequential(
            nn.ConvTranspose2d(32, 16, 3, 
            stride=2, output_padding=0),
            nn.BatchNorm2d(16),
            nn.ReLU(True),
            nn.ConvTranspose2d(16, 8, 3, stride=2, 
            padding=1, output_padding=1),
            nn.BatchNorm2d(8),
            nn.ReLU(True),
            nn.ConvTranspose2d(8, 1, 3, stride=2, 
            padding=1, output_padding=1)
        )
        
    def forward(self, x):
        x = self.decoder_lin(x)
        x = self.unflatten(x)
        x = self.decoder_conv(x)
        x = torch.sigmoid(x)
        return x

class ConvAutoencoder(nn.Module):
    def __init__(self):
        super(ConvAutoencoder,self).__init__()
        self.encoder = Encoder(encoded_space_dim=10,fc2_input_dim=128)
        self.decoder = Decoder(encoded_space_dim=10,fc2_input_dim=128)
    def forward(self, x):
        x = self.encoder(x)
        x = self.decoder(x)
        return x

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

# Instantiate the autoencoder and optimizer
model = ConvAutoencoder().to(device)

optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.MSELoss()

# Train the autoencoder
num_epochs = 20

for epoch in range(num_epochs):
    for batch_features, _ in train_loader:
        batch_features = batch_features.to(device)
        optimizer.zero_grad()
        outputs = model(batch_features)
        loss = criterion(outputs, batch_features)
        loss.backward()
        optimizer.step()

    print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}")

# Visualize the results
model.eval()

with torch.no_grad():
    for batch_features, _ in test_loader:
        batch_features = batch_features.to(device)
        outputs = model(batch_features)
        outputs = outputs.cpu()
        break

fig, axes = plt.subplots(2, 10, figsize=(20, 4))
for i in range(10):
    axes[0][i].imshow(batch_features[i].cpu().squeeze().numpy(), cmap='gray')
    axes[1][i].imshow(outputs[i].squeeze().numpy(), cmap='gray')
    axes[0][i].axis('off')
    axes[1][i].axis('off')

plt.savefig('encoding_decoding.png')
plt.show()

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

Следующий блок кода создает двумерное представление скрытого пространства с использованием t-SNE (t-распределенное стохастическое встраивание соседей), которое представляет собой метод уменьшения размерности, который особенно хорошо подходит для визуализации многомерных изображений. данные в 2D или 3D. Он работает, поддерживая попарное сходство точек данных из пространства высокой размерности в пространстве низкой размерности.

from sklearn.manifold import TSNE
import seaborn as sns
import pandas as pd
from sklearn.decomposition import PCA

# Function to extract latent vectors 
# (projection of input images into the low-dimensional latent space)
def extract_latent_space(model, loader):
    model.eval()
    with torch.no_grad():
        latent_vectors = []
        labels = []

        for batch_features, batch_labels in loader:
            latent_vectors.append(model.encoder(batch_features.to(device)).view(batch_features.size(0), -1).cpu().numpy())
            labels.extend(batch_labels)

        latent_vectors = np.vstack(latent_vectors)
        labels = np.array(labels)

    return latent_vectors, labels

latent_vectors, labels = extract_latent_space(model, test_loader)
tsne = TSNE(n_components=2, random_state=42)
latent_2D = tsne.fit_transform(latent_vectors)

# Function to plot the latent space
def plot_latent_space(latent_2D, labels, title):
    df = pd.DataFrame(latent_2D, columns=["x", "y"])
    df["label"] = labels

    plt.figure(figsize=(10, 8))
    sns.scatterplot(data=df, x="x", y="y", hue="label", palette="tab10", legend="full", alpha=0.8)
    plt.title(title)
    plt.show()

plot_latent_space(latent_2D, labels, "2D Visualization of Latent Space (MNIST)")

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

IV. Автоэнкодер с классификатором

Чтобы еще больше проиллюстрировать силу уменьшения размерности, теперь мы можем заменить декодер классификатором. Теперь цель состоит не в том, чтобы воспроизвести ввод, а в том, чтобы предсказать класс рукописной цифры (от 0 до 9).

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

class Classifier(nn.Module):
    def __init__(self, encoder, num_classes=10):
        super(Classifier, self).__init__()
        self.encoder = encoder
        self.classifier = nn.Sequential(
            nn.Linear(10, 10),
            nn.Softmax(dim=1)
        )
        
    def forward(self, x):
        x = self.encoder(x)
        x = self.classifier(x)
        return x

# Instantiate the classifier using the pre-trained encoder
classifier = Classifier(model.encoder).to(device)

criterion_classifier = nn.CrossEntropyLoss()
optimizer_classifier = torch.optim.Adam(classifier.parameters(), lr=1e-3) 

num_epochs_classifier = 20
train_losses = []
train_accuracies = []

# Train classifier
for epoch in range(num_epochs_classifier):
    running_loss = 0.0
    running_correct = 0
    total_samples = 0
    
    for batch_features, batch_labels in train_loader:
        batch_features = batch_features.to(device)
        batch_labels = batch_labels.to(device)

        optimizer_classifier.zero_grad()
        outputs = classifier(batch_features)
        loss = criterion_classifier(outputs, batch_labels)
        loss.backward()
        optimizer_classifier.step()

        _, predicted = torch.max(outputs.data, 1)
        total_samples += batch_labels.size(0)
        running_correct += (predicted == batch_labels).sum().item()
        running_loss += loss.item()

    train_accuracy = 100 * running_correct / total_samples
    train_loss = running_loss / len(train_loader)
    
    train_losses.append(train_loss)
    train_accuracies.append(train_accuracy)

    print(f"Epoch [{epoch+1}/{num_epochs_classifier}], Loss: {train_loss:.4f}, Train Accuracy: {train_accuracy:.2f}%")

# Plot the loss and accuracy
plt.style.use('ggplot')
epochs = list(range(1, num_epochs_classifier+1))

plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(epochs, train_losses, label='Train Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(epochs, train_accuracies, label='Train Accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.title('Training Accuracy')
plt.legend()

plt.savefig("encoder_classifier.png")
plt.show()

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

correct = 0
total = 0

# Measure accuracy on the test set
with torch.no_grad():
    for batch_features, batch_labels in test_loader:
        batch_features = batch_features.to(device)
        batch_labels = batch_labels.to(device)

        outputs = classifier(batch_features)
        _, predicted = torch.max(outputs.data, 1)

        total += batch_labels.size(0)
        correct += (predicted == batch_labels).sum().item()

print(f"Accuracy of the classifier on the test set: {100 * correct / total:.2f}%")
Accuracy of the classifier on the test set: 98.80%

В качестве последнего упражнения я заменю кодировщик нейронной сети кодировщиком PCA. В этой новой архитектуре изображения проецируются в другое скрытое пространство с использованием простого PCA, при этом размер скрытого пространства остается неизменным (d=10). Как показано на графике ниже, результаты менее впечатляющие. Сохраняя количество эпох постоянным, я получаю точность 80,29% на тестовом наборе.

# Flatten and normalize the training images
train_images_flat = mnist_train.data.view(-1, 28 * 28).float() / 255

# Perform PCA
encoded_space_dim = 10
pca = PCA(n_components=encoded_space_dim)
pca.fit(train_images_flat.numpy())

class PC_Encoder(nn.Module):
    def __init__(self, pca):
        super().__init__()
        self.pca = pca

    def forward(self, x):
        x_flat = x.view(x.size(0), -1)  # Flatten the input
        x_pca = torch.tensor(self.pca.transform(x_flat.cpu().numpy()), dtype=torch.float).to(device)  # Apply PCA
        return x_pca

# Instantiate the PCA encoder
encoder = PC_Encoder(pca).to(device)

class ConvAutoencoder(nn.Module):
    def __init__(self, encoder):
        super(ConvAutoencoder, self).__init__()
        self.encoder = encoder
        self.classifier = nn.Sequential(
            nn.Linear(10, 10),
            nn.Softmax(dim=1)
        )

    def forward(self, x):
        x = self.encoder(x)
        x = self.classifier(x)
        return x

# Instantiate the autoencoder
classifier = ConvAutoencoder(encoder).to(device)

criterion_classifier = nn.CrossEntropyLoss()
optimizer_classifier = torch.optim.Adam(classifier.parameters(), lr=1e-3) #only train classifier's parameters

num_epochs_classifier = 20
train_losses = []
train_accuracies = []

# Train the PCA-classifier
for epoch in range(num_epochs_classifier):
    running_loss = 0.0
    running_correct = 0
    total_samples = 0
    
    for batch_features, batch_labels in train_loader:
        batch_features = batch_features.to(device)
        batch_labels = batch_labels.to(device)

        optimizer_classifier.zero_grad()
        outputs = classifier(batch_features)
        loss = criterion_classifier(outputs, batch_labels)
        loss.backward()
        optimizer_classifier.step()

        _, predicted = torch.max(outputs.data, 1)
        total_samples += batch_labels.size(0)
        running_correct += (predicted == batch_labels).sum().item()
        running_loss += loss.item()

    train_accuracy = 100 * running_correct / total_samples
    train_loss = running_loss / len(train_loader)
    
    train_losses.append(train_loss)
    train_accuracies.append(train_accuracy)

    print(f"Epoch [{epoch+1}/{num_epochs_classifier}], Loss: {train_loss:.4f}, Train Accuracy: {train_accuracy:.2f}%")

# Plot the loss and accuracy
plt.style.use('ggplot')
epochs = list(range(1, num_epochs_classifier+1))

plt.figure(figsize=(16, 6))
plt.subplot(1, 2, 1)
plt.plot(epochs, train_losses, label='Train Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(epochs, train_accuracies, label='Train Accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.title('Training Accuracy')
plt.legend()

plt.savefig("PCA_classifier.png")
plt.show()

# Test the PCA classifier
correct = 0
total = 0

# Measure accuracy on the test set
with torch.no_grad():
    for batch_features, batch_labels in test_loader:
        batch_features = batch_features.to(device)
        batch_labels = batch_labels.to(device)

        outputs = classifier(batch_features)
        _, predicted = torch.max(outputs.data, 1)

        total += batch_labels.size(0)
        correct += (predicted == batch_labels).sum().item()

print(f"Accuracy of the PCA-classifier on the test set: {100 * correct / total:.2f}%")
Accuracy of the PCA-classifier on the test set: 80.29%

В. Заключение

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

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

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

Дополнительные ресурсы

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

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