每日算法 — 使用java实现GAN:生成对抗网络与纳什均衡训练

引言

生成模型是人工智能中最具创造性的方向之一,它让机器学会”想象”和”创造”全新的数据样本。生成对抗网络(Generative Adversarial Network, GAN)由Ian Goodfellow于2014年提出,通过一场生成器与判别器之间的”博弈”来学习数据分布。这种对抗训练机制灵感来源于博弈论中的纳什均衡,是深度学习领域最具革命性的创新之一。本文将用Java实现一个简化版的GAN,以二维数据生成为例,深入讲解对抗训练的核心思想。

算法原理

生成器与判别器的博弈

GAN由两个神经网络组成:
– 生成器(Generator, G):接收随机噪声z,尝试生成与真实数据分布相似的假样本 G(z)
– 判别器(Discriminator, D):接收真实样本x或生成样本G(z),判断其真伪

目标函数

GAN的优化是一个 minimax 博弈:
$$\min_G \max_D V(D, G) = \mathbb{E}{x \sim p{data}}[\log D(x)] + \mathbb{E}_{z \sim p_z}[\log(1 – D(G(z)))]$$

  • 判别器D试图最大化正确分类真实样本和生成样本的概率
  • 生成器G试图最小化log(1-D(G(z))),即让判别器将生成样本误判为真实样本

训练过程

  1. 固定G,训练D区分真实样本和生成样本
  2. 固定D,训练G欺骗D
  3. 交替迭代直至收敛(理论上达到纳什均衡)

Java实现

import java.util.*;

/**
 * GAN生成对抗网络简化实现
 * 
 * 生成器学习生成符合目标分布的二维数据点
 * 判别器学习区分真实数据和生成数据
 */
public class SimpleGAN {

    // 数据维度
    private int dataDim = 2;
    private int noiseDim = 2;

    // 网络结构参数
    private int hiddenDim = 16;

    // 生成器参数
    private double[][] G_W1;  // 输入层到隐藏层
    private double[] G_b1;
    private double[][] G_W2;  // 隐藏层到输出层
    private double[] G_b2;

    // 判别器参数
    private double[][] D_W1;  // 输入层到隐藏层
    private double[] D_b1;
    private double[][] D_W2;  // 隐藏层到输出层
    private double[] D_b2;

    // 学习率
    private double gLearningRate = 0.01;
    private double dLearningRate = 0.01;

    private Random rand;

    public SimpleGAN() {
        rand = new Random(42);
        initWeights();
    }

    /**
     * 初始化网络权重
     */
    private void initWeights() {
        // 生成器权重
        G_W1 = initMatrix(hiddenDim, noiseDim);
        G_b1 = initVector(hiddenDim);
        G_W2 = initMatrix(dataDim, hiddenDim);
        G_b2 = initVector(dataDim);

        // 判别器权重
        D_W1 = initMatrix(hiddenDim, dataDim);
        D_b1 = initVector(hiddenDim);
        D_W2 = initMatrix(1, hiddenDim);
        D_b2 = initVector(1);
    }

    private double[][] initMatrix(int rows, int cols) {
        double[][] m = 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++) {
                m[i][j] = rand.nextGaussian() * scale;
            }
        }
        return m;
    }

    private double[] initVector(int size) {
        double[] v = new double[size];
        for (int i = 0; i < size; i++) {
            v[i] = rand.nextGaussian() * 0.01;
        }
        return v;
    }

    /**
     * LeakyReLU激活函数
     */
    private double leakyReLU(double x) {
        return x > 0 ? x : 0.01 * x;
    }

    private double leakyReLUDerivative(double x) {
        return x > 0 ? 1.0 : 0.01;
    }

    /**
     * Sigmoid激活函数
     */
    private double sigmoid(double x) {
        if (x > 10) return 1.0;
        if (x < -10) return 0.0;
        return 1.0 / (1.0 + Math.exp(-x));
    }

    /**
     * 矩阵向量乘法
     */
    private double[] matVecMul(double[][] m, double[] v) {
        int rows = m.length;
        double[] result = new double[rows];
        for (int i = 0; i < rows; i++) {
            double sum = 0;
            for (int j = 0; j < m[0].length; j++) {
                sum += m[i][j] * v[j];
            }
            result[i] = sum;
        }
        return result;
    }

    /**
     * 向量加法
     */
    private double[] vecAdd(double[] a, double[] b) {
        double[] r = new double[a.length];
        for (int i = 0; i < a.length; i++) r[i] = a[i] + b[i];
        return r;
    }

    /**
     * 逐元素乘法
     */
    private double[] vecMul(double[] a, double[] b) {
        double[] r = new double[a.length];
        for (int i = 0; i < a.length; i++) r[i] = a[i] * b[i];
        return r;
    }

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

    /**
     * 标量乘法
     */
    private double[] vecScale(double[] v, double s) {
        double[] r = new double[v.length];
        for (int i = 0; i < v.length; i++) r[i] = v[i] * s;
        return r;
    }

    /**
     * 对向量应用leakyReLU
     */
    private double[] applyLeakyReLU(double[] v) {
        double[] r = new double[v.length];
        for (int i = 0; i < v.length; i++) r[i] = leakyReLU(v[i]);
        return r;
    }

    /**
     * 生成器前向传播
     */
    public double[] generatorForward(double[] noise) {
        double[] h1 = vecAdd(matVecMul(G_W1, noise), G_b1);
        double[] a1 = applyLeakyReLU(h1);
        double[] output = vecAdd(matVecMul(G_W2, a1), G_b2);
        return output;
    }

    /**
     * 判别器前向传播(返回概率)
     */
    public double discriminatorForward(double[] data) {
        double[] h1 = vecAdd(matVecMul(D_W1, data), D_b1);
        double[] a1 = applyLeakyReLU(h1);
        double[] output = vecAdd(matVecMul(D_W2, a1), D_b2);
        return sigmoid(output[0]);
    }

    /**
     * 生成随机噪声
     */
    private double[] sampleNoise() {
        double[] z = new double[noiseDim];
        for (int i = 0; i < noiseDim; i++) {
            z[i] = rand.nextGaussian();
        }
        return z;
    }

    /**
     * 生成真实数据(以圆环分布为例)
     */
    private double[] sampleRealData() {
        double angle = rand.nextDouble() * 2 * Math.PI;
        double radius = 2.0 + rand.nextGaussian() * 0.3;
        return new double[] {
            radius * Math.cos(angle),
            radius * Math.sin(angle)
        };
    }

    /**
     * 训练判别器一个batch
     */
    private double trainDiscriminator(int batchSize) {
        double totalLoss = 0;

        // 存储梯度
        double[][] dW1Grad = new double[hiddenDim][dataDim];
        double[] db1Grad = new double[hiddenDim];
        double[][] dW2Grad = new double[1][hiddenDim];
        double[] db2Grad = new double[1];

        for (int i = 0; i < batchSize; i++) {
            // 真实样本
            double[] real = sampleRealData();
            double realProb = discriminatorForward(real);
            double realLoss = -Math.log(realProb + 1e-8);
            totalLoss += realLoss;

            // 反向传播(真实样本)
            double dOut = -(1.0 / (realProb + 1e-8)) * realProb * (1 - realProb);

            // 前向中间结果
            double[] h1 = vecAdd(matVecMul(D_W1, real), D_b1);
            double[] a1 = applyLeakyReLU(h1);

            // 输出层梯度
            for (int j = 0; j < hiddenDim; j++) {
                dW2Grad[0][j] += dOut * a1[j];
            }
            db2Grad[0] += dOut;

            // 隐藏层梯度
            double[] da1 = new double[hiddenDim];
            for (int j = 0; j < hiddenDim; j++) {
                da1[j] = dOut * D_W2[0][j] * leakyReLUDerivative(h1[j]);
            }
            for (int j = 0; j < hiddenDim; j++) {
                for (int k = 0; k < dataDim; k++) {
                    dW1Grad[j][k] += da1[j] * real[k];
                }
                db1Grad[j] += da1[j];
            }

            // 生成样本
            double[] noise = sampleNoise();
            double[] fake = generatorForward(noise);
            double fakeProb = discriminatorForward(fake);
            double fakeLoss = -Math.log(1 - fakeProb + 1e-8);
            totalLoss += fakeLoss;

            // 反向传播(生成样本,固定生成器)
            dOut = -(1.0 / (1 - fakeProb + 1e-8)) * (-fakeProb * (1 - fakeProb));

            h1 = vecAdd(matVecMul(D_W1, fake), D_b1);
            a1 = applyLeakyReLU(h1);

            for (int j = 0; j < hiddenDim; j++) {
                dW2Grad[0][j] += dOut * a1[j];
            }
            db2Grad[0] += dOut;

            da1 = new double[hiddenDim];
            for (int j = 0; j < hiddenDim; j++) {
                da1[j] = dOut * D_W2[0][j] * leakyReLUDerivative(h1[j]);
            }
            for (int j = 0; j < hiddenDim; j++) {
                for (int k = 0; k < dataDim; k++) {
                    dW1Grad[j][k] += da1[j] * fake[k];
                }
                db1Grad[j] += da1[j];
            }
        }

        // 更新判别器参数
        for (int i = 0; i < hiddenDim; i++) {
            for (int j = 0; j < dataDim; j++) {
                D_W1[i][j] -= dLearningRate * dW1Grad[i][j] / batchSize;
            }
            D_b1[i] -= dLearningRate * db1Grad[i] / batchSize;
        }
        for (int j = 0; j < hiddenDim; j++) {
            D_W2[0][j] -= dLearningRate * dW2Grad[0][j] / batchSize;
        }
        D_b2[0] -= dLearningRate * db2Grad[0] / batchSize;

        return totalLoss / batchSize;
    }

    /**
     * 训练生成器一个batch
     */
    private double trainGenerator(int batchSize) {
        double totalLoss = 0;

        double[][] gW1Grad = new double[hiddenDim][noiseDim];
        double[] gb1Grad = new double[hiddenDim];
        double[][] gW2Grad = new double[dataDim][hiddenDim];
        double[] gb2Grad = new double[dataDim];

        for (int i = 0; i < batchSize; i++) {
            double[] noise = sampleNoise();

            // 生成器前向
            double[] gH1 = vecAdd(matVecMul(G_W1, noise), G_b1);
            double[] gA1 = applyLeakyReLU(gH1);
            double[] fake = vecAdd(matVecMul(G_W2, gA1), G_b2);

            // 判别器前向
            double[] dH1 = vecAdd(matVecMul(D_W1, fake), D_b1);
            double[] dA1 = applyLeakyReLU(dH1);
            double[] dOut = vecAdd(matVecMul(D_W2, dA1), D_b2);
            double prob = sigmoid(dOut[0]);

            double loss = -Math.log(prob + 1e-8);
            totalLoss += loss;

            // 反向传播
            double dLoss = -(1.0 / (prob + 1e-8)) * prob * (1 - prob);

            // 判别器部分梯度
            double[] ddA1 = new double[hiddenDim];
            for (int j = 0; j < hiddenDim; j++) {
                ddA1[j] = dLoss * D_W2[0][j] * leakyReLUDerivative(dH1[j]);
            }

            double[] dfake = new double[dataDim];
            for (int j = 0; j < dataDim; j++) {
                for (int k = 0; k < hiddenDim; k++) {
                    dfake[j] += ddA1[k] * D_W1[k][j];
                }
            }

            // 生成器输出层梯度
            for (int j = 0; j < dataDim; j++) {
                for (int k = 0; k < hiddenDim; k++) {
                    gW2Grad[j][k] += dfake[j] * gA1[k];
                }
                gb2Grad[j] += dfake[j];
            }

            // 生成器隐藏层梯度
            double[] dgA1 = new double[hiddenDim];
            for (int j = 0; j < hiddenDim; j++) {
                for (int k = 0; k < dataDim; k++) {
                    dgA1[j] += dfake[k] * G_W2[k][j];
                }
                dgA1[j] *= leakyReLUDerivative(gH1[j]);
            }

            for (int j = 0; j < hiddenDim; j++) {
                for (int k = 0; k < noiseDim; k++) {
                    gW1Grad[j][k] += dgA1[j] * noise[k];
                }
                gb1Grad[j] += dgA1[j];
            }
        }

        // 更新生成器参数
        for (int i = 0; i < hiddenDim; i++) {
            for (int j = 0; j < noiseDim; j++) {
                G_W1[i][j] -= gLearningRate * gW1Grad[i][j] / batchSize;
            }
            G_b1[i] -= gLearningRate * gb1Grad[i] / batchSize;
        }
        for (int i = 0; i < dataDim; i++) {
            for (int j = 0; j < hiddenDim; j++) {
                G_W2[i][j] -= gLearningRate * gW2Grad[i][j] / batchSize;
            }
            G_b2[i] -= gLearningRate * gb2Grad[i] / batchSize;
        }

        return totalLoss / batchSize;
    }

    /**
     * 训练GAN
     */
    public void train(int epochs, int dSteps, int gSteps, int batchSize) {
        System.out.println("开始训练GAN...");

        for (int epoch = 0; epoch < epochs; epoch++) {
            double dLoss = 0, gLoss = 0;

            // 训练判别器
            for (int i = 0; i < dSteps; i++) {
                dLoss += trainDiscriminator(batchSize);
            }

            // 训练生成器
            for (int i = 0; i < gSteps; i++) {
                gLoss += trainGenerator(batchSize);
            }

            if ((epoch + 1) % 1000 == 0) {
                System.out.printf("Epoch %d: D_loss=%.4f, G_loss=%.4f%n", 
                    epoch + 1, dLoss / dSteps, gLoss / gSteps);

                // 生成样本展示
                double[] sample = generatorForward(sampleNoise());
                System.out.printf("  生成样本: (%.3f, %.3f)%n", sample[0], sample[1]);
            }
        }
    }

    /**
     * 生成并打印多个样本
     */
    public void generateSamples(int n) {
        System.out.println("\n=== 生成样本 ===");
        for (int i = 0; i < n; i++) {
            double[] sample = generatorForward(sampleNoise());
            System.out.printf("样本%d: (%.4f, %.4f)%n", i + 1, sample[0], sample[1]);
        }
    }

    public static void main(String[] args) {
        SimpleGAN gan = new SimpleGAN();
        gan.train(20000, 2, 1, 32);
        gan.generateSamples(10);

        // 验证生成数据分布
        System.out.println("\n=== 验证生成数据分布 ===");
        double sumR = 0;
        for (int i = 0; i < 1000; i++) {
            double[] s = gan.generatorForward(gan.sampleNoise());
            double r = Math.sqrt(s[0]*s[0] + s[1]*s[1]);
            sumR += r;
        }
        System.out.printf("平均半径: %.4f (目标: ~2.0)%n", sumR / 1000);
    }
}

代码解析

生成器架构

生成器是一个简单的全连接网络:噪声(2维)→ 隐藏层(16维,LeakyReLU)→ 输出层(2维)。它将随机噪声映射到数据空间,学习模拟真实数据的分布。

判别器架构

判别器同样是全连接网络:数据(2维)→ 隐藏层(16维,LeakyReLU)→ 输出层(1维,Sigmoid)。输出值表示输入为真实数据的概率。

对抗训练循环

代码中每轮先训练判别器2步,再训练生成器1步。这种不平衡的训练策略有助于稳定GAN的训练——判别器太强会导致生成器梯度消失,判别器太弱则无法提供有效指导。

损失函数

判别器损失是真实样本的交叉熵损失与生成样本的交叉熵损失之和。生成器损失则是 -log(D(G(z))),即让判别器对生成样本的输出概率尽可能接近1。

复杂度分析

指标 复杂度 说明
单次生成 O(H × (N + D)) H为隐藏层大小,N为噪声维度,D为数据维度
单次判别 O(H × (D + 1)) 判别器前向传播
训练时间 O(E × B × (H × D + H × N)) E为轮数,B为batch大小

GAN的演进与挑战

  1. DCGAN:将全连接层替换为卷积层,生成更高质量的图像
  2. WGAN:使用Wasserstein距离替代JS散度,训练更稳定
  3. CGAN:引入条件信息,实现可控生成
  4. StyleGAN:分层风格控制,生成超逼真的人脸图像
  5. CycleGAN:无需配对数据即可实现图像域转换

训练难点

GAN训练 notoriously 不稳定,常见挑战包括:
– 模式崩溃(Mode Collapse):生成器只生成少数几种样本
– 梯度消失:判别器过强时生成器梯度趋近于零
– 不收敛:生成器和判别器难以达到纳什均衡

本文的简化实现以二维数据为例,避开了图像生成中的复杂挑战,但完整呈现了GAN对抗训练的核心机制。

总结

本文通过Java实现了一个简化版GAN,展示了生成器与判别器之间的对抗博弈过程。GAN的精妙之处在于将生成问题转化为一场双人博弈,两个网络在相互对抗中共同进化。虽然训练稳定性仍是研究热点,但GAN在图像生成、数据增强、风格迁移等领域的成功应用,证明了这一框架的强大潜力。