引言
自编码器(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实现完整展示了多层网络的前向传播、反向传播和参数更新过程。掌握自编码器后,读者可以进一步探索变分自编码器、生成对抗网络等更先进的深度学习模型。