Table of Contents

8. Autoenkodery

Autoenkodery (autokodery) są to takie typy sieci neuronowych, które przekształcają dane wejściowe do postaci skróconej (skompresowanej, latentnej), a następnie je z tej postaci odtwarzają (rekonstruują). Sieci te składają się z dwóch części: enkodera i dekodera.

Enkoder (kompresor) przekształca dane wejściowe w skompresowaną reprezentację o niższej wymiarowości, zwaną warstwą latentną.

Dekoder (dekompresor) stara się na podstawie danych uzyskanych z warstwy latentnej jak najwierniej zrekonstruować pierwotne dane wejściowe, tak aby funkcja straty przyjmowała jak najniższą wartość.

Warstwa latentna (ang. bottleneck layer) jest kluczowym elementem autoenkodera i zmusza sieć do zidentyfikowania przez nią istotnych cech danych wejściowych.

Autoenkodery uczą się reprezentacji danych w sposób nienadzorowany, a więc nie wymagają etykiet podczas procesu uczenia. Sieć sama odkrywa struktury i wzorce w danych wejściowych, ucząc się kompresować i rekonstruować informacje.

Schemat autoenkodera

Rysunek 8.1. Schemat autoenkodera (źródło: Wikipedia, https://pl.wikipedia.org/wiki/Autoenkoder)

Note

Prawidłowym tłumaczeniem angielskiego słowa autoencoder jest autokoder. Będę jednak używać sformułowania autoenkoder, ponieważ przyjęło się ono w literaturze dotyczącej sieci neuronowych.

8.1. Klasa bazowa autoenkodera

W dalszej części rozdziału przedstawię dwie przykładowe implementacje autoenkoderów w C# z wykorzystaniem naszej biblioteki NeuralNetworks:

  • autoenkodera składającego się z warstw gęstych oraz
  • autoenkodera składającego się z warstw konwolucyjnych.

Oba modele zostaną oparte na klasie bazowej AutoencoderModel<T>, przedstawionej poniżej.

Dwie wyróżnione właściwości - BottleneckLayer i FirstDecoderLayer - reprezentują odpowiednio warstwę latentną oraz pierwszą warstwę dekodera.

public abstract class AutoencoderModel<T>(Loss<T>? defaultLossFunction, SeededRandom? random, string? modelFilePath)
    : BaseModel<T, T>(defaultLossFunction, random, modelFilePath)
    where T : notnull
{
    protected DenseLayer? BottleneckLayer { get; set; }

    protected DenseLayer? FirstDecoderLayer { get; set; }

    public float[,] GetEncodedRepresentation()
    {
        return BottleneckLayer?.Output
            ?? throw new InvalidOperationException("Bottleneck layer output is not available.");
    }

    public T Decode(float[,] encoded)
    {
        return FirstDecoderLayer is null
            ? throw new InvalidOperationException("Decoder layer is not initialized.")
            : InferFromLayer(FirstDecoderLayer, encoded);
    }

    public Operation GetBottleneckActivationFunction()
    {
        return BottleneckLayer?.GetActivationFunction()
            ?? throw new InvalidOperationException("Bottleneck layer is not initialized.");
    }
}

Listing 8.1. Definicja klasy bazowej autoenkodera

8.2. Autoenkodery z warstwami gęstymi

Poniżej przedstawiłem przykład implementacji autoenkodera z warstwami gęstymi. Model ten dziedziczy po powyższej klasie bazowej AutoencoderModel. Składa się on z (oznaczonych odpowiednimi liniami komentarzy) warstw enkodera, warstw dekodera oraz warstwy latentnej. W przykładzie tym zastosowano Leaky ReLU, Softsign i Tanh jako funkcje aktywacji oraz MSE (średni błąd kwadratowy) jako funkcję straty.

internal class AutoencoderDenseModel(int bottleneckDim, SeededRandom? random, string? modelFilePath = null)
    : AutoencoderModel<float[,]>(new MeanSquaredErrorLoss(MseReduction.ElementMean), random, modelFilePath)
{
    protected override LayerListBuilder<float[,], float[,]> CreateLayerListBuilder()
    {
        ParamInitializer initializer = new GlorotInitializer(Random);

        return
            // Encoder layers
            AddLayer(new DenseLayer(178, new LeakyReLU2D(), initializer))
            .AddLayer(new DenseLayer(46, new LeakyReLU2D(), initializer))

            // Bottleneck
            .AddLayer(BottleneckLayer = new DenseLayer(bottleneckDim, new Softsign(), initializer))

            // Decoder layers
            .AddLayer(FirstDecoderLayer = new DenseLayer(46, new LeakyReLU2D(), initializer))
            .AddLayer(new DenseLayer(178, new LeakyReLU2D(), initializer))
            .AddLayer(new DenseLayer(784, new Tanh2D(), initializer));
    }
}

Listing 8.2. Definicja modelu autoenkodera z warstwami gęstymi

Note

Powyższy kod w pełnej wersji znajduje się na GitHub.

Trzy poniższe warstwy:

new DenseLayer(178, new LeakyReLU2D(), initializer)
new DenseLayer(46, new LeakyReLU2D(), initializer)
new DenseLayer(bottleneckDim, new Softsign(), initializer)

przekształcają dane wejściowe ze zbioru MNIST o wymiarze 784 (28x28) na reprezentację latentną o wymiarze bottleneckDim. W dalszej części rozdziału zobaczymy jak wartości zmiennej bottleneckDim (5, 10, 15) wpływają na jakość rekonstrukcji danych wejściowych.

Wartości wyjściowe warstwy latentnej, po przejściu przez powższe 3 warstwy, otrzymano stosując Softsign jako funkcję aktywacji. Funkcja ta przyjmuje wartości z przedziału (-1, 1), co będzie nam ułatwiało późniejsze eksperymenty, polegające na generowaniu losowych reprezentacji latentnych i obserwowaniu, jak dekoder rekonstruuje dane wejściowe (sekcja 8.4.4.).

Wartości z przedziału (-1, 1) można również uzyskać za pomocą funkcji aktywacji Tanh i taka też funkcja pojawia się w przykładach, które można znaleźć w literaturze. Jednak z moich doświadczeń wynika, że lepsze rezultaty uzyskuje się stosując Softsign, szczególnie w przypadku autoenkoderów z wąskim gardłem. Może to wynikać z tego, że Softsign ma łagodniejsze nachylenie, co sprzyja stabilniejszemu uczeniu się reprezentacji latentnych. Polecam zapoznanie się z wykresami umieszczonymi w tym rozdziale.

8.3. Autoenkodery z warstwami konwolucyjnymi

Autoenkodery mogą również wykorzystywać warstwy konwolucyjne, co pozwala im lepiej uchwycić lokalne zależności w danych graficznych. Poniżej przedstawiono przykład implementacji autoenkodera z takimi właśnie warstwami.

internal class AutoencoderConvModel(int bottleneckDim, SeededRandom? random, string? modelFilePath = null)
    : AutoencoderModel<float[,,,]>(new MeanSquaredErrorLoss4D(MseReduction.ElementMean), random, modelFilePath)
{

    protected override LayerListBuilder<float[,,,], float[,,,]> CreateLayerListBuilder()
    {
        ParamInitializer initializer = new GlorotInitializer(Random);

        return
            // Encoder layers
            AddLayer(new Conv2DLayer(
                kernels: 32,
                kernelHeight: 3,
                kernelWidth: 3,
                activationFunction: new Tanh4D(),
                paramInitializer: initializer
            ))
            .AddLayer(new MaxPooling2DLayer(2, 2))
            .AddLayer(new FlattenLayer())

            // Bottleneck
            .AddLayer(BottleneckLayer = new DenseLayer(bottleneckDim, new Softsign(), initializer))

            // Decoder layers
            .AddLayer(FirstDecoderLayer = new DenseLayer(32 * 14 * 14, new LeakyReLU2D(), initializer))
            .AddLayer(new UnflattenLayer(32, 14, 14))
            .AddLayer(new Upsample2DLayer(2, 2))
            .AddLayer(new Conv2DLayer(
                kernels: 1,
                kernelHeight: 3,
                kernelWidth: 3,
                activationFunction: new Tanh4D(),
                paramInitializer: initializer
            ));
    }
}

Listing 8.3. Definicja modelu autoenkodera z warstwami konwolucyjnymi

Note

Powyższy kod w pełnej wersji znajduje się na GitHub.

Analizując modele z listingów 8.1., 8.2. i 8.3. można zauważyć, że format danych wejściowych (wektor, tablica wielowymiarowa) jest zawsze taki sam jak danych wyjściowych. Warstwa latentna (bottleneck) natomiast jest zawsze jednowymiarowa (wektor).

8.4. Proces i wyniki trenowania

Proces trenowania autoenkoderów polega na minimalizacji funkcji straty, która w przypadku naszych autoenkoderów jest średnim błędem kwadratowym MSE pomiędzy danymi wejściowymi a ich rekonstrukcją. Kody źródłowe implementujące ten proces znajdują się na GitHub tu (warstwy gęste) oraz tu (konwolucja).

Minimalizowaną podczas treningu funkcję straty można zapisać jako (por. wzór 1.8)

\[ \mathcal{L}(x,x') = \operatorname{MSE}(x,x') = \frac{1}{n} \sum_{i=1}^{n} (x_i - x'_i)^2 \]

gdzie

\[ x' = g(f(x)). \]

Symbol \(x\) oznacza dane wejściowe, natomiast \(x'\) — ich rekonstrukcję uzyskaną przez autoenkoder. Funkcja \(f\) reprezentuje enkoder, który mapuje dane wejściowe na reprezentację latentną, a funkcja \(g\) reprezentuje dekoder, który na podstawie tej reprezentacji odtwarza dane wejściowe.

Proces trenowania autoenkodera

Rysunek 8.2. Przykładowy screen z procesu trenowania autoenkodera

8.4.1. Rekonstrukcja danych wejściowych

Poniższe dwie tabele przedstawiają wyniki trenowania wyżej opisanych autoenkoderów. Zamieszczone w tych tabelach grafiki prezentują rekonstrukcje dokonane przez wytrenowaną sieć dla różnych wymiarów warstwy latentnej w odniesieniu do oryginalnych obrazów wejściowych.

Wymiar warstwy latentnej Rekonstruowane obrazy (warstwy gęste)
5 Image 5Image 5Image 5Image 5Image 5Image 5
10 Image 10Image 10Image 10Image 10Image 10Image 10
15 Image 15Image 15Image 15Image 15Image 15Image 15
Oryginał OryginałOryginałOryginałOryginałOryginałOryginał

Tabela 8.1. Wyniki trenowania autoenkodera z warstwami gęstymi (epochs: 30, batch size: 400, learning rate: od 1e-2 do 7e-3)

Wymiar warstwy latentnej Rekonstruowane obrazy (warstwy konwolucyjne)
5 Image 5Image 5Image 5Image 5Image 5Image 5
10 Image 10Image 10Image 10Image 10Image 10Image 10
15 Image 15Image 15Image 15Image 15Image 15Image 15
Oryginał OryginałOryginałOryginałOryginałOryginałOryginał

Tabela 8.2. Wyniki trenowania autoenkodera z warstwami konwolucyjnymi (epochs: 10, batch size: 200, learning rate: od 1e-2 do 5e-4)

Widzimy, że im większy wymiar warstwy latentnej, tym lepsza jakość rekonstruowanych obrazów. Dla niskich wartości wymiaru latentnego rekonstrukcje są nieco dokładniejsze w przypadku autoenkodera z warstwami konwolucyjnymi.

Poniżej przedstawiono fragment kodu odpowiedzialnego za wczytanie modelu autoenkodera (z warstwami gęstymi) oraz rekonstrukcję obrazów z danych treningowych (choć zapewne powinny to być dane testowe - na razie zachowajmy ten fragment do późniejszej modyfikacji).

AutoencoderDenseModel model = new(bottleneckDim, null, modelPath);
float[,] xTrain = LoadTrainingData();

// Normalize the pixel values from [0, 255] to [-1, 1].
xTrain.ScaleToInPlace(-1f, 1f);

float[,] reconstructedImages = model.Forward(xTrain, true);

// Rescale the pixel values back to [0, 255] for visualization purposes.
reconstructedImages.ScaleToInPlace(0f, 255f);

Listing 8.4. Rekonstrukcja obrazów przy użyciu wczytanego modelu autoenkodera

8.4.2. Wykresy t-SNE

Technika t-SNE (t-Distributed Stochastic Neighbor Embedding) jest metodą nieliniowej redukcji wymiarowości, wykorzystywaną przede wszystkim do wizualizacji danych wielowymiarowych w przestrzeni dwu- lub trójwymiarowej. Na wejściu przyjmuje zbiór punktów reprezentowanych w przestrzeni o wysokiej liczbie wymiarów, np. wektorów z warstwy latentnej autoenkodera lub wektorów osadzeń (embeddings). Następnie, w procesie optymalizacji iteracyjnej, wyznacza ich reprezentację w przestrzeni o niższej wymiarowości, starając się zachować lokalne relacje między punktami, w szczególności ich sąsiedztwo. Należy przy tym pamiętać, że t-SNE nie zachowuje (bo nie jest to możliwe) wiernie wszystkich odległości między punktami, a odległości i układ skupisk widocznych na wizualizacji nie muszą odzwierciedlać rzeczywistych zależności ilościowych w przestrzeni wejściowej.

Do techniki tej powrócimy w dalszych rozdziałach, omawiając redukcję wymiarowości wektorów osadzeń / wektorów cech. W kontekście autoenkoderów t-SNE może służyć do wizualnej oceny struktury reprezentacji w przestrzeni latentnej, w tym do sprawdzenia, czy punkty należące do różnych klas tworzą odrębne skupiska. Taka wizualizacja pozwala wstępnie ocenić, na ile reprezentacje latentne ujawniają strukturę związaną z przynależnością do klas.

W poniższej tabeli przedstawiono wykresy t-SNE dla różnych wymiarów warstwy latentnej (5, 10, 15) dla obu omówionych typów enkoderów (warstwy gęste i konwolucyjne). Widzimy, że wraz ze wzrostem wymiaru warstwy latentnej klastry stają się lepiej i sensowniej rozmieszczone. Dla wyższych wymiarów poszczególne cyfry (oznaczone różnymi kolorami opisanymi w legendzie) są pogrupowane w wyraźne klastry, co świadczy o tym, że autoenkoder skutecznie oddziela różne klasy danych w przestrzeni warstwy latentnej.

Wymiar warstwy latentnej Wykres t-SNE (warstwy gęste) Wykres t-SNE (warstwy konwolucyjne)
5 t-SNE 5 t-SNE 5
10 t-SNE 10 t-SNE 10
15 t-SNE 15 t-SNE 15

Tabela 8.3. Wykresy t-SNE dla różnych wymiarów warstwy latentnej

Do utworzenia powyższych wykresów wykorzystaliśmy klasę TSNE z biblioteki Accord.NET. Jej kod źródłowy znajduje się tutaj. Poniższy listing przedstawia przykładowy sposób jej użycia.

TSNE tsne = new()
{
    NumberOfOutputs = 2,
    Perplexity = 30
};

double[][] reduced = tsne.Transform(encodedDouble);

Listing 8.5. Przykładowe użycie klasy TSNE

8.4.3. Histogramy wartości warstwy latentnej

W naszych przykładach warstwa latentna przyjmuje wartości z przedziału (-1, 1), co jest wynikiem zastosowania funkcji aktywacji Softsign. Na poniższych wykresach widzimy rozkład wartości w tym zakresie. Najgorszy rozkład wartości (dwa wyraźne piki w okolicach -1 i 1) obserwujemy dla 5-wymiarowej warstwy latentnej w sieci gęstej. Rozkład ten staje się coraz bardziej równomierny wraz ze wzrostem wymiaru warstwy latentnej, ale nadal obserwujemy piki w okolicach -0,5 i 0,5. Dla warstw konwolucyjnych występuje podobny trend, choć rozkład wartości jest nieco bardziej równomierny już dla 5-wymiarowej warstwy latentnej.

W miarę zwiększania wymiaru warstwy latentnej histogramy stają się coraz bardziej płaskie, co wskazuje na lepsze wykorzystanie przestrzeni latentnej przez autoenkoder konwolucyjny.

Wymiar warstwy latentnej Histogram wartości (warstwy gęste) Histogram wartości (warstwy konwolucyjne)
5 Histogram 5 Histogram 5
10 Histogram 10 Histogram 10
15 Histogram 15 Histogram 15

Tabela 8.4. Histogramy wartości warstwy latentnej

8.4.4. Generowanie losowych próbek

W tej sekcji dowiemy się jak wyglądają obrazy powstałe w wyniku dekodowania losowo wygenerowanych próbek z przestrzeni latentnej. Próbki te są tworzone poprzez losowanie wartości z przedziału [-1, 1) dla każdego wymiaru warstwy latentnej, a następnie są one dekodowane za pomocą części "dekoderowej" autoenkodera, jak na poniższym listingu:

Random random = new(randomSeed);
float[,] randomEncoded = new float[imageCount, bottleneckDim];
for (int i = 0; i < imageCount; i++)
{
    for (int j = 0; j < bottleneckDim; j++)
    {
        randomEncoded[i, j] = (float)(random.NextDouble() * 2 - 1); // Random values in [-1, 1)
    }
}
float[,] randomlyGeneratedImages = model.Decode(randomEncoded);

Listing 8.6. Generowanie losowych próbek z przestrzeni latentnej i dekodowanie ich za pomocą autoenkodera

Poniższe przykłady pokazują wygenerowane obrazy dla różnych wymiarów warstwy latentnej w sieciach gęstych i konwolucyjnych.

Wymiar warstwy latentnej Losowe próbki (warstwy gęste) Losowe próbki (warstwy konwolucyjne)
5 Random 5Random 5Random 5Random 5Random 5 Random 5Random 5Random 5Random 5Random 5
10 Random 10Random 10Random 10Random 10Random 10 Random 10Random 10Random 10Random 10Random 10
15 Random 15Random 15Random 15Random 15Random 15 Random 15Random 15Random 15Random 15Random 15

Tabela 8.5. Losowe próbki wygenerowane z przestrzeni latentnej

Co ciekawe, im wyższy wymiar warstwy latentnej, tym rzadziej generowane obrazy przypominają rzeczywiste cyfry. Można to zapewne tłumaczyć tym, że w większych wymiarach przestrzeń latentna nie jest w pełni wykorzystana do reprezentowania wszystkich wymaganych wariantów danych i koduje również obrazy, które nie odpowiadają rzeczywistym danym.

8.4.5. "Doprawienie" modelu autoenkodera

Jak zobaczyliśmy powyżej, model złożony z warstw gęstych produkuje dla rozmiaru warstwy latentnej równego 5 dalece niezadowalające wyniki w zakresie generowania obrazów. Również wykres histogramu oraz t-SNE wskazują na jego słabe działanie. Wystarczy jednak wprowadzić do naszego modelu małe zmiany, aby znacząco poprawić jego jakość bez zmiany rozmiaru warstwy latentnej. Modyfikacje te przedstawiłem na poniższym listingu.

protected override LayerListBuilder<float[,], float[,]> CreateLayerListBuilder()
{
    ParamInitializer initializer = new GlorotInitializer(Random);

    CompositeRegularizer regularizer = new(
        new L2Regularizer(0.01f), 
        new MaxNormRegularizer(2.8f)
    );

    Dropout2D dropout = new(0.95f, Random);

    return
        // Encoder
        AddLayer(new DenseLayer(178, new LeakyReLU2D(), initializer, dropout))
        .AddLayer(new DenseLayer(46, new LeakyReLU2D(), initializer))

        // Bottleneck
        .AddLayer(BottleneckLayer = new DenseLayer(bottleneckDim, new Softsign(), initializer, weightsRegularizer: regularizer))

        // Decoder
        .AddLayer(FirstDecoderLayer = new DenseLayer(46, new LeakyReLU2D(), initializer))
        .AddLayer(new DenseLayer(178, new LeakyReLU2D(), initializer))
        .AddLayer(new DenseLayer(784, new Tanh2D(), initializer));
}

Listing 8.7. Zmodyfikowany model autoenkodera z warstwami gęstymi

Po wprowadzeniu regularyzacji na wartwie latentnej oraz zastosowaniu dropout w jednej z warstw enkodera, jakość działania modelu poprawiła się znacząco, co widać na poniższych wykresach i wynikach generowania obrazów.

Opis Wykres
Histogram wartości w przestrzeni latentnej Histogram
Wizualizacja wartości w przestrzeni latentnej za pomocą t-SNE Wizualizacja t-SNE

Tabela 8.6. Histogram i wizualizacja wartości w przestrzeni latentnej dla modelu z listingu 8.7

Opis Obrazy
Rekonstrukcja - model z listingu 8.2 (bez regularyzacji i dropout), wymiar warstwy latentnej = 5 Image 5Image 5Image 5Image 5Image 5Image 5
Rekonstrukcja - model z listingu 8.7 (z regularyzacją i dropout), wymiar warstwy latentnej = 5 Image 5Image 5Image 5Image 5Image 5Image 5
Oryginał OryginałOryginałOryginałOryginałOryginałOryginał

Tabela 8.7. Porównanie jakości rekonstrukcji obrazów przez dwa różne modele autoenkoderów

8.5. Podsumowanie

W niniejszym rozdziale omówiłem koncepcję autoenkoderów, ich implementację w sieciach gęstych i konwolucyjnych oraz sposób wykorzystania przestrzeni latentnej do generowania nowych próbek danych. Przedstawiłem też wpływ wymiaru warstwy latentnej oraz zastosowania regularyzacji i dropoutu na jakość generowanych obrazów.


Created: 2026-05-15

Last modified: 2026-10-21

Title: 8. Autoenkodery

Tags: [C#] [Sieci neuronowe] [Biblioteka] [NeuralNetworks] [MNIST] [CNN] [Convolutional Neural Networks] [Autoencoders] [Autoenkodery]