И. Введение
Нейронные сети штурмом захватили мир, совершив революцию в области машинного обучения и открыв новые горизонты в области искусственного интеллекта. Эти мощные модели привели к прорывам, которые когда-то считались невозможными, например, 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%
В. Заключение
В этом сообщении блога мы продемонстрировали, как нейронные сети могут обучаться, сжимая данные в скрытом пространстве, обеспечивая более точное представление и повышая производительность в таких задачах, как уменьшение размерности и классификация.
Способность нейронных сетей моделировать сложные шаблоны и нелинейные отношения делает их мощным инструментом машинного обучения, а присущая им способность к сжатию в скрытых пространствах позволяет им изучать эффективные представления для различных задач.
В заключение позвольте мне попросить модель преобразования изображения в текст создать изображение, иллюстрирующее идею «сжатия в скрытое пространство». Полученное изображение, чуть ниже, кажется, отражает идею кодирования в пространстве более низкого измерения.
—
Дополнительные ресурсы
- Визуализируйте автоэнкодер во время обучения: https://cs.stanford.edu/people/karpathy/convnetjs/demo/autoencoder.html
- 3-х минутное видео об автоэнкондерах:
—
Если вам понравился этот пост, вы можете проверить некоторые из моих других работ на этой платформе по связанным темам:
- https://towardsdatascience.com/a-visual-approach-to-gradient-descent-and-other-optimization-algorithms-c82f45b7fc87
- https://towardsdatascience.com/deep-equinance-models-via-neural-odes-c25a3ac8d004
- https://towardsdatascience.com/linear-model-the-machine-learning-way-c33f9db9ac37
- https://towardsdatascience.com/artificial-neural-networks-as-universal-function-apprimators-a6ac6547a35f
- https://medium.com/towards-data-science/a-primer-on-computer-vision-with-julia-2c7068a35b32
Если у вас хорошее настроение, вы даже можете стать участником Medium по ссылке ниже. Я буду получать очень небольшую сумму (около половины эспрессо в месяц), что побудит меня размещать больше контента на этой платформе: