每日算法 — 使用java实现自编码器:编码解码结构与隐空间特征学习

引言

自编码器(Autoencoder)是深度学习领域最重要的无监督学习模型之一,通过编码器-解码器结构学习数据的高效表示。自编码器不仅能实现数据降维和特征提取,更是生成模型、异常检测和表示学习的基础组件。本文将用Java从零实现一个多层自编码器,深入讲解前向传播、反向传播和隐空间特征学习机制。

算法原理

网络结构

自编码器由两部分组成:
– 编码器(Encoder):$h = f(W_e x + b_e)$,将输入压缩为低维隐表示
– 解码器(Decoder):$\hat{x} = g(W_d h + b_d)$,从隐表示重构输入

损失函数

自编码器的目标是最小化重构误差:

$$L = \frac{1}{n} \sum_{i=1}^{n} |x_i – \hat{x}_i|^2$$

反向传播

编码器前向传播:

$$z_e = W_e x + b_e$$
$$h = \sigma(z_e)$$

解码器前向传播:

$$z_d = W_d h + b_d$$
$$\hat{x} = \sigma(z_d)$$

损失函数对解码器参数的梯度:

$$\frac{\partial L}{\partial W_d} = \frac{2}{n} (\hat{x} – x) \odot \sigma'(z_d) \cdot h^T$$

$$\frac{\partial L}{\partial b_d} = \frac{2}{n} \sum_i (\hat{x}_i – x_i) \odot \sigma'(z_d)$$

损失函数对编码器参数的梯度(通过链式法则):

$$\frac{\partial L}{\partial W_e} = \delta_h \cdot x^T$$

其中 $\delta_h = W_d^T \delta_d \odot \sigma'(z_e)$,$\delta_d = \frac{2}{n}(\hat{x} – x) \odot \sigma'(z_d)$。

Java实现

项目结构

src/
├── Autoencoder.java         // 自编码器核心类
├── ActivationFunction.java  // 激活函数接口
├── SigmoidActivation.java   // Sigmoid激活函数
├── ReLUActivation.java      // ReLU激活函数
├── MatrixUtils.java         // 矩阵运算工具类
├── AutoencoderDemo.java     // 演示入口
└── DatasetGenerator.java    // 数据集生成器

激活函数接口与实现

/**
 * 激活函数接口
 */
public interface ActivationFunction {
    /**
     * 前向传播:计算激活值
     */
    double[] activate(double[] z);

    /**
     * 反向传播:计算导数
     */
    double[] derivative(double[] z);
}

/**
 * Sigmoid激活函数
 * σ(x) = 1 / (1 + e^(-x))
 */
class SigmoidActivation implements ActivationFunction {
    @Override
    public double[] activate(double[] z) {
        double[] result = new double[z.length];
        for (int i = 0; i < z.length; i++) {
            result[i] = 1.0 / (1.0 + Math.exp(-z[i]));
        }
        return result;
    }

    @Override
    public double[] derivative(double[] z) {
        double[] sigmoid = activate(z);
        double[] result = new double[z.length];
        for (int i = 0; i < z.length; i++) {
            result[i] = sigmoid[i] * (1 - sigmoid[i]);
        }
        return result;
    }
}

/**
 * ReLU激活函数
 * ReLU(x) = max(0, x)
 */
class ReLUActivation implements ActivationFunction {
    @Override
    public double[] activate(double[] z) {
        double[] result = new double[z.length];
        for (int i = 0; i < z.length; i++) {
            result[i] = Math.max(0, z[i]);
        }
        return result;
    }

    @Override
    public double[] derivative(double[] z) {
        double[] result = new double[z.length];
        for (int i = 0; i < z.length; i++) {
            result[i] = z[i] > 0 ? 1.0 : 0.0;
        }
        return result;
    }
}

矩阵运算工具类

import java.util.Random;

/**
 * 神经网络所需的矩阵运算工具
 */
public class MatrixUtils {
    private static Random random = new Random(42);

    /**
     * 向量逐元素相乘(Hadamard积)
     */
    public static double[] hadamard(double[] a, double[] b) {
        double[] result = new double[a.length];
        for (int i = 0; i < a.length; i++) {
            result[i] = a[i] * b[i];
        }
        return result;
    }

    /**
     * 向量减法
     */
    public static double[] subtract(double[] a, double[] b) {
        double[] result = new double[a.length];
        for (int i = 0; i < a.length; i++) {
            result[i] = a[i] - b[i];
        }
        return result;
    }

    /**
     * 向量数乘
     */
    public static double[] scale(double[] a, double scalar) {
        double[] result = new double[a.length];
        for (int i = 0; i < a.length; i++) {
            result[i] = a[i] * scalar;
        }
        return result;
    }

    /**
     * 向量点积
     */
    public static double dot(double[] a, double[] b) {
        double sum = 0;
        for (int i = 0; i < a.length; i++) {
            sum += a[i] * b[i];
        }
        return sum;
    }

    /**
     * 矩阵与向量乘法(矩阵在左)
     */
    public static double[] matVecMul(double[][] W, double[] v) {
        double[] result = new double[W.length];
        for (int i = 0; i < W.length; i++) {
            result[i] = dot(W[i], v);
        }
        return result;
    }

    /**
     * 向量与向量外积(生成矩阵)
     */
    public static double[][] outerProduct(double[] a, double[] b) {
        double[][] result = new double[a.length][b.length];
        for (int i = 0; i < a.length; i++) {
            for (int j = 0; j < b.length; j++) {
                result[i][j] = a[i] * b[j];
            }
        }
        return result;
    }

    /**
     * 矩阵转置与向量乘法
     */
    public static double[] transposedMatVecMul(double[][] W, double[] v) {
        int cols = W[0].length;
        double[] result = new double[cols];
        for (int j = 0; j < cols; j++) {
            for (int i = 0; i < W.length; i++) {
                result[j] += W[i][j] * v[i];
            }
        }
        return result;
    }

    /**
     * 矩阵加法(原地更新)
     */
    public static void addInPlace(double[][] A, double[][] B, double learningRate) {
        for (int i = 0; i < A.length; i++) {
            for (int j = 0; j < A[0].length; j++) {
                A[i][j] -= learningRate * B[i][j];
            }
        }
    }

    /**
     * 向量加法(原地更新)
     */
    public static void addInPlace(double[] a, double[] b, double learningRate) {
        for (int i = 0; i < a.length; i++) {
            a[i] -= learningRate * b[i];
        }
    }

    /**
     * 计算均方误差
     */
    public static double mse(double[] a, double[] b) {
        double sum = 0;
        for (int i = 0; i < a.length; i++) {
            double diff = a[i] - b[i];
            sum += diff * diff;
        }
        return sum / a.length;
    }

    /**
     * Xavier初始化权重
     */
    public static double[][] xavierInit(int rows, int cols) {
        double[][] W = new double[rows][cols];
        double scale = Math.sqrt(2.0 / (rows + cols));
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                W[i][j] = random.nextGaussian() * scale;
            }
        }
        return W;
    }

    /**
     * 零初始化向量
     */
    public static double[] zeros(int size) {
        return new double[size];
    }
}

自编码器核心类

import java.util.ArrayList;
import java.util.List;

/**
 * 多层自编码器实现
 * 支持自定义编码器/解码器层数和隐空间维度
 */
public class Autoencoder {
    // 网络结构参数
    private int inputDim;
    private int[] encoderLayerSizes;  // 编码器各层大小(从输入到隐空间)
    private int bottleneckDim;        // 隐空间维度(瓶颈层)

    // 网络权重和偏置
    private double[][][] encoderWeights;  // [layer][output][input]
    private double[][] encoderBiases;     // [layer][output]
    private double[][][] decoderWeights;
    private double[][] decoderBiases;

    // 激活函数
    private ActivationFunction encoderActivation;
    private ActivationFunction decoderActivation;

    // 训练参数
    private double learningRate;
    private int maxEpochs;

    // 存储前向传播中间结果(用于反向传播)
    private List<double[]> encoderZ;      // 编码器各层线性组合
    private List<double[]> encoderA;      // 编码器各层激活值
    private List<double[]> decoderZ;
    private List<double[]> decoderA;

    public Autoencoder(int inputDim, int[] encoderLayerSizes, int bottleneckDim,
                        double learningRate, int maxEpochs) {
        this.inputDim = inputDim;
        this.encoderLayerSizes = encoderLayerSizes;
        this.bottleneckDim = bottleneckDim;
        this.learningRate = learningRate;
        this.maxEpochs = maxEpochs;
        this.encoderActivation = new ReLUActivation();
        this.decoderActivation = new SigmoidActivation();

        initializeNetwork();
    }

    /**
     * 初始化网络参数
     */
    private void initializeNetwork() {
        int nEncoderLayers = encoderLayerSizes.length;

        // 编码器权重初始化
        encoderWeights = new double[nEncoderLayers + 1][][];
        encoderBiases = new double[nEncoderLayers + 1][];

        // 输入层 -> 第一个隐藏层
        encoderWeights[0] = MatrixUtils.xavierInit(encoderLayerSizes[0], inputDim);
        encoderBiases[0] = MatrixUtils.zeros(encoderLayerSizes[0]);

        // 隐藏层之间
        for (int i = 1; i < nEncoderLayers; i++) {
            encoderWeights[i] = MatrixUtils.xavierInit(encoderLayerSizes[i], encoderLayerSizes[i-1]);
            encoderBiases[i] = MatrixUtils.zeros(encoderLayerSizes[i]);
        }

        // 最后一个隐藏层 -> 瓶颈层
        encoderWeights[nEncoderLayers] = MatrixUtils.xavierInit(bottleneckDim, encoderLayerSizes[nEncoderLayers-1]);
        encoderBiases[nEncoderLayers] = MatrixUtils.zeros(bottleneckDim);

        // 解码器权重初始化(对称结构)
        decoderWeights = new double[nEncoderLayers + 1][][];
        decoderBiases = new double[nEncoderLayers + 1][];

        // 瓶颈层 -> 第一个解码隐藏层
        decoderWeights[0] = MatrixUtils.xavierInit(encoderLayerSizes[nEncoderLayers-1], bottleneckDim);
        decoderBiases[0] = MatrixUtils.zeros(encoderLayerSizes[nEncoderLayers-1]);

        // 解码隐藏层之间
        for (int i = 1; i < nEncoderLayers; i++) {
            decoderWeights[i] = MatrixUtils.xavierInit(
                encoderLayerSizes[nEncoderLayers - 1 - i],
                encoderLayerSizes[nEncoderLayers - i]);
            decoderBiases[i] = MatrixUtils.zeros(encoderLayerSizes[nEncoderLayers - 1 - i]);
        }

        // 最后一个解码隐藏层 -> 输出层
        decoderWeights[nEncoderLayers] = MatrixUtils.xavierInit(inputDim, encoderLayerSizes[0]);
        decoderBiases[nEncoderLayers] = MatrixUtils.zeros(inputDim);
    }

    /**
     * 编码:将输入压缩为隐表示
     */
    public double[] encode(double[] input) {
        double[] current = input.clone();

        for (int i = 0; i < encoderWeights.length; i++) {
            double[] z = MatrixUtils.matVecMul(encoderWeights[i], current);
            for (int j = 0; j < z.length; j++) {
                z[j] += encoderBiases[i][j];
            }
            current = encoderActivation.activate(z);
        }

        return current;
    }

    /**
     * 解码:从隐表示重构输入
     */
    public double[] decode(double[] latent) {
        double[] current = latent.clone();

        for (int i = 0; i < decoderWeights.length; i++) {
            double[] z = MatrixUtils.matVecMul(decoderWeights[i], current);
            for (int j = 0; j < z.length; j++) {
                z[j] += decoderBiases[i][j];
            }
            // 最后一层使用sigmoid,其他层使用ReLU
            if (i == decoderWeights.length - 1) {
                current = decoderActivation.activate(z);
            } else {
                current = encoderActivation.activate(z);
            }
        }

        return current;
    }

    /**
     * 前向传播:编码+解码
     */
    public double[] forward(double[] input) {
        return decode(encode(input));
    }

    /**
     * 训练自编码器
     */
    public void train(double[][] data) {
        int n = data.length;

        System.out.println("=== 自编码器训练开始 ===");
        System.out.println("输入维度: " + inputDim);
        System.out.println("隐空间维度: " + bottleneckDim);
        System.out.println("编码器层数: " + encoderWeights.length);
        System.out.println("样本数: " + n);
        System.out.println("学习率: " + learningRate);

        for (int epoch = 0; epoch < maxEpochs; epoch++) {
            double totalLoss = 0;

            for (int i = 0; i < n; i++) {
                // 前向传播并记录中间结果
                double[] output = forwardWithCache(data[i]);

                // 计算损失
                totalLoss += MatrixUtils.mse(output, data[i]);

                // 反向传播
                backward(data[i]);
            }

            double avgLoss = totalLoss / n;

            if ((epoch + 1) % 100 == 0 || epoch < 5) {
                System.out.printf("Epoch %d: 平均重构损失 = %.6f%n", epoch + 1, avgLoss);
            }
        }

        System.out.println("\n训练完成!");
    }

    /**
     * 带缓存的前向传播(用于反向传播)
     */
    private double[] forwardWithCache(double[] input) {
        encoderZ = new ArrayList<>();
        encoderA = new ArrayList<>();
        decoderZ = new ArrayList<>();
        decoderA = new ArrayList<>();

        // 编码器前向传播
        double[] current = input.clone();
        encoderA.add(current); // 输入作为第0层激活

        for (int i = 0; i < encoderWeights.length; i++) {
            double[] z = MatrixUtils.matVecMul(encoderWeights[i], current);
            for (int j = 0; j < z.length; j++) {
                z[j] += encoderBiases[i][j];
            }
            encoderZ.add(z);
            current = encoderActivation.activate(z);
            encoderA.add(current);
        }

        // 解码器前向传播
        decoderA.add(current); // 瓶颈层激活

        for (int i = 0; i < decoderWeights.length; i++) {
            double[] z = MatrixUtils.matVecMul(decoderWeights[i], current);
            for (int j = 0; j < z.length; j++) {
                z[j] += decoderBiases[i][j];
            }
            decoderZ.add(z);

            if (i == decoderWeights.length - 1) {
                current = decoderActivation.activate(z);
            } else {
                current = encoderActivation.activate(z);
            }
            decoderA.add(current);
        }

        return current;
    }

    /**
     * 反向传播
     */
    private void backward(double[] input) {
        int nLayers = decoderWeights.length;

        // 输出层误差
        double[] output = decoderA.get(decoderA.size() - 1);
        double[] delta = MatrixUtils.scale(
            MatrixUtils.subtract(output, input),
            2.0 / input.length
        );

        // 解码器反向传播
        List<double[]> decoderDeltas = new ArrayList<>();

        for (int i = nLayers - 1; i >= 0; i--) {
            double[] z = decoderZ.get(i);
            double[] activationDerivative;

            if (i == nLayers - 1) {
                activationDerivative = decoderActivation.derivative(z);
            } else {
                activationDerivative = encoderActivation.derivative(z);
            }

            delta = MatrixUtils.hadamard(delta, activationDerivative);
            decoderDeltas.add(0, delta);

            // 传播到前一层
            if (i > 0) {
                delta = MatrixUtils.transposedMatVecMul(decoderWeights[i], delta);
            }
        }

        // 编码器反向传播(从瓶颈层开始)
        delta = MatrixUtils.transposedMatVecMul(decoderWeights[0], decoderDeltas.get(0));

        List<double[]> encoderDeltas = new ArrayList<>();

        for (int i = encoderWeights.length - 1; i >= 0; i--) {
            double[] z = encoderZ.get(i);
            double[] activationDerivative = encoderActivation.derivative(z);

            delta = MatrixUtils.hadamard(delta, activationDerivative);
            encoderDeltas.add(0, delta);

            if (i > 0) {
                delta = MatrixUtils.transposedMatVecMul(encoderWeights[i], delta);
            }
        }

        // 更新解码器参数
        for (int i = 0; i < nLayers; i++) {
            double[] prevActivation = decoderA.get(i);
            double[][] gradW = MatrixUtils.outerProduct(decoderDeltas.get(i), prevActivation);
            MatrixUtils.addInPlace(decoderWeights[i], gradW, learningRate);
            MatrixUtils.addInPlace(decoderBiases[i], decoderDeltas.get(i), learningRate);
        }

        // 更新编码器参数
        for (int i = 0; i < encoderWeights.length; i++) {
            double[] prevActivation = encoderA.get(i);
            double[][] gradW = MatrixUtils.outerProduct(encoderDeltas.get(i), prevActivation);
            MatrixUtils.addInPlace(encoderWeights[i], gradW, learningRate);
            MatrixUtils.addInPlace(encoderBiases[i], encoderDeltas.get(i), learningRate);
        }
    }

    /**
     * 在数据集上评估重构误差
     */
    public double evaluate(double[][] data) {
        double totalMse = 0;
        for (double[] sample : data) {
            double[] reconstructed = forward(sample);
            totalMse += MatrixUtils.mse(reconstructed, sample);
        }
        return totalMse / data.length;
    }

    /**
     * 获取隐空间表示
     */
    public double[][] getLatentRepresentations(double[][] data) {
        double[][] latent = new double[data.length][bottleneckDim];
        for (int i = 0; i < data.length; i++) {
            latent[i] = encode(data[i]);
        }
        return latent;
    }

    public int getBottleneckDim() {
        return bottleneckDim;
    }
}

数据集生成器

import java.util.Random;

/**
 * 生成用于自编码器演示的数据集
 */
public class DatasetGenerator {
    private Random random;

    public DatasetGenerator(long seed) {
        this.random = new Random(seed);
    }

    /**
     * 生成带有结构的高维数据(实际自由度较低)
     * 适合自编码器学习紧凑表示
     */
    public double[][] generateStructuredData(int n, int dim) {
        double[][] data = new double[n][dim];

        for (int i = 0; i < n; i++) {
            // 生成3个隐变量
            double h1 = random.nextGaussian();
            double h2 = random.nextGaussian();
            double h3 = random.nextGaussian();

            // 通过非线性映射生成高维数据
            for (int d = 0; d < dim; d++) {
                double angle = 2 * Math.PI * d / dim;
                data[i][d] = sigmoid(
                    h1 * Math.cos(angle) +
                    h2 * Math.sin(angle) +
                    h3 * Math.cos(2 * angle) +
                    random.nextGaussian() * 0.1
                );
            }
        }

        return data;
    }

    /**
     * 生成简单的模式数据(如带噪声的图像块)
     */
    public double[][] generatePatternData(int n, int dim) {
        double[][] data = new double[n][dim];
        int side = (int) Math.sqrt(dim);

        for (int i = 0; i < n; i++) {
            // 随机选择模式类型
            int pattern = random.nextInt(3);

            for (int d = 0; d < dim; d++) {
                int row = d / side;
                int col = d % side;
                double value;

                switch (pattern) {
                    case 0: // 水平条纹
                        value = (row % 2 == 0) ? 0.8 : 0.2;
                        break;
                    case 1: // 垂直条纹
                        value = (col % 2 == 0) ? 0.8 : 0.2;
                        break;
                    default: // 对角线
                        value = ((row + col) % 2 == 0) ? 0.8 : 0.2;
                        break;
                }

                // 添加噪声
                value += random.nextGaussian() * 0.05;
                data[i][d] = Math.max(0, Math.min(1, value));
            }
        }

        return data;
    }

    private double sigmoid(double x) {
        return 1.0 / (1.0 + Math.exp(-x));
    }
}

演示入口

import java.util.Arrays;

/**
 * 自编码器算法演示
 */
public class AutoencoderDemo {
    public static void main(String[] args) {
        demoDimensionalityReduction();
        System.out.println("\n" + "=".repeat(60) + "\n");
        demoPatternReconstruction();
    }

    /**
     * 演示1:高维数据降维
     */
    private static void demoDimensionalityReduction() {
        System.out.println("=== 演示1:高维数据降维 ===\n");

        DatasetGenerator generator = new DatasetGenerator(42);
        double[][] data = generator.generateStructuredData(500, 20);

        System.out.println("原始数据维度: 20");
        System.out.println("样本数: 500");

        // 创建自编码器:20 -> 15 -> 10 -> 5 -> 10 -> 15 -> 20
        Autoencoder autoencoder = new Autoencoder(
            20, new int[]{15, 10}, 5, 0.01, 500);

        autoencoder.train(data);

        double mse = autoencoder.evaluate(data);
        System.out.printf("\n重构均方误差: %.6f%n", mse);

        // 获取隐空间表示
        double[][] latent = autoencoder.getLatentRepresentations(data);
        System.out.println("\n隐空间表示样本(前3条):");
        for (int i = 0; i < Math.min(3, latent.length); i++) {
            System.out.printf("  样本%d: %s%n", i + 1, Arrays.toString(latent[i]));
        }

        // 对比原始数据与重构数据
        System.out.println("\n重构对比(第一条样本的前10维):");
        double[] original = data[0];
        double[] reconstructed = autoencoder.forward(original);
        System.out.print("  原始: [");
        for (int i = 0; i < 10; i++) {
            System.out.printf("%.3f", original[i]);
            if (i < 9) System.out.print(", ");
        }
        System.out.println("]");
        System.out.print("  重构: [");
        for (int i = 0; i < 10; i++) {
            System.out.printf("%.3f", reconstructed[i]);
            if (i < 9) System.out.print(", ");
        }
        System.out.println("]");
    }

    /**
     * 演示2:模式数据重构
     */
    private static void demoPatternReconstruction() {
        System.out.println("=== 演示2:模式数据重构 ===\n");

        DatasetGenerator generator = new DatasetGenerator(123);
        double[][] data = generator.generatePatternData(300, 16);

        System.out.println("生成16维模式数据(4x4图像块),共300个样本");

        // 自编码器:16 -> 12 -> 8 -> 4 -> 8 -> 12 -> 16
        Autoencoder autoencoder = new Autoencoder(
            16, new int[]{12, 8}, 4, 0.02, 400);

        autoencoder.train(data);

        double mse = autoencoder.evaluate(data);
        System.out.printf("\n重构均方误差: %.6f%n", mse);

        // 展示隐空间维度
        double[][] latent = autoencoder.getLatentRepresentations(data);
        System.out.println("\n隐空间维度: 4");
        System.out.println("压缩率: " + (4.0 / 16.0 * 100) + "%");

        System.out.println("\n隐空间表示方差分析:");
        for (int d = 0; d < 4; d++) {
            double mean = 0, var = 0;
            for (double[] l : latent) {
                mean += l[d];
            }
            mean /= latent.length;
            for (double[] l : latent) {
                var += (l[d] - mean) * (l[d] - mean);
            }
            var /= latent.length;
            System.out.printf("  维度%d: 均值=%.4f, 方差=%.4f%n", d + 1, mean, var);
        }
    }
}

复杂度分析

指标 复杂度 说明
编码前向 $O(L_e \cdot d_{max}^2)$ $L_e$为编码器层数,$d_{max}$为最大层宽度
解码前向 $O(L_d \cdot d_{max}^2)$ 对称于编码器
反向传播 $O(L \cdot d_{max}^2)$ $L$为总层数
单样本训练 $O(L \cdot d_{max}^2)$ 前向+反向的总和
总训练 $O(E \cdot n \cdot L \cdot d_{max}^2)$ $E$为epoch数,$n$为样本数
空间复杂度 $O(L \cdot d_{max}^2)$ 存储所有权重矩阵

算法扩展

去噪自编码器(Denoising Autoencoder)

在输入中添加噪声,训练自编码器从噪声输入中恢复原始数据,学习更鲁棒的特征表示。

稀疏自编码器(Sparse Autoencoder)

在损失函数中添加稀疏约束(如L1正则化或KL散度),使隐层激活稀疏化,学习更有意义的特征。

变分自编码器(VAE)

将隐空间建模为概率分布,通过重参数化技巧实现端到端的训练,可生成新样本。

总结

自编码器通过编码器-解码器结构学习数据的高效低维表示,是理解神经网络表示学习的重要入口。本文的Java实现完整展示了多层网络的前向传播、反向传播和参数更新过程。掌握自编码器后,读者可以进一步探索变分自编码器、生成对抗网络等更先进的深度学习模型。