每日算法 — 使用java实现RNN:循环神经网络与序列时间预测

算法概述

循环神经网络(Recurrent Neural Network, RNN)是一类专门处理序列数据的神经网络。与传统前馈神经网络不同,RNN具有”记忆”能力,能够将前一时刻的隐藏状态传递到当前时刻,从而捕捉序列中的时间依赖关系。RNN广泛应用于自然语言处理、语音识别、时间序列预测等领域。

RNN核心公式

RNN的前向传播由以下公式定义:

hidden_t = tanh(Wxh * x_t + Whh * hidden_{t-1} + bh)
output_t = Why * hidden_t + by

其中Wxh是输入到隐藏的权重,Whh是隐藏到隐藏的循环权重,Why是隐藏到输出的权重。

完整Java实现

import java.util.*;

/**
 * 循环神经网络(RNN)完整实现
 * 用于序列时间预测任务
 */
public class SimpleRNN {

    int inputSize;
    int hiddenSize;
    int outputSize;
    double learningRate;

    double[][] Wxh;
    double[][] Whh;
    double[][] Why;
    double[] bh;
    double[] by;

    List<double[]> xs;
    List<double[]> hs;
    List<double[]> ys;
    List<double[]> ps;

    public SimpleRNN(int inputSize, int hiddenSize, int outputSize, double learningRate) {
        this.inputSize = inputSize;
        this.hiddenSize = hiddenSize;
        this.outputSize = outputSize;
        this.learningRate = learningRate;
        Wxh = initMatrix(hiddenSize, inputSize);
        Whh = initMatrix(hiddenSize, hiddenSize);
        Why = initMatrix(outputSize, hiddenSize);
        bh = new double[hiddenSize];
        by = new double[outputSize];
    }

    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] = (Math.random() - 0.5) * 2 * scale;
        return m;
    }

    double tanh(double x) { return Math.tanh(x); }

    double tanhDerivative(double x) {
        double t = Math.tanh(x);
        return 1.0 - t * t;
    }

    double[] matVecMul(double[][] m, double[] v) {
        double[] result = new double[m.length];
        for (int i = 0; i < m.length; i++) {
            double sum = 0;
            for (int j = 0; j < v.length; j++)
                sum += m[i][j] * v[j];
            result[i] = sum;
        }
        return result;
    }

    double[] vecAdd(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;
    }

    double[] apply(double[] v, java.util.function.DoubleFunction<Double> fn) {
        double[] result = new double[v.length];
        for (int i = 0; i < v.length; i++)
            result[i] = fn.apply(v[i]);
        return result;
    }

    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;
    }

    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;
    }

    double[] transposeMatVecMul(double[][] m, double[] v) {
        double[] result = new double[m[0].length];
        for (int i = 0; i < m[0].length; i++) {
            double sum = 0;
            for (int j = 0; j < m.length; j++)
                sum += m[j][i] * v[j];
            result[i] = sum;
        }
        return result;
    }

    public List<double[]> forwardSequence(List<double[]> inputs) {
        List<double[]> outputs = new ArrayList<>();
        double[] h = new double[hiddenSize];
        xs = new ArrayList<>();
        hs = new ArrayList<>();
        ys = new ArrayList<>();
        ps = new ArrayList<>();
        hs.add(h.clone());
        for (double[] x : inputs) {
            xs.add(x);
            double[] WxhX = matVecMul(Wxh, x);
            double[] WhhH = matVecMul(Whh, h);
            double[] preAct = vecAdd(vecAdd(WxhX, WhhH), bh);
            h = apply(preAct, this::tanh);
            hs.add(h);
            double[] y = vecAdd(matVecMul(Why, h), by);
            ys.add(y);
            ps.add(y.clone());
            outputs.add(y.clone());
        }
        return outputs;
    }

    public double backward(List<double[]> targets) {
        double[][] dWxh = new double[hiddenSize][inputSize];
        double[][] dWhh = new double[hiddenSize][hiddenSize];
        double[][] dWhy = new double[outputSize][hiddenSize];
        double[] dbh = new double[hiddenSize];
        double[] dby = new double[outputSize];
        double[] dhNext = new double[hiddenSize];
        double totalLoss = 0;

        for (int t = ys.size() - 1; t >= 0; t--) {
            double[] dy = new double[outputSize];
            for (int i = 0; i < outputSize; i++) {
                dy[i] = ps.get(t)[i] - targets.get(t)[i];
                totalLoss += 0.5 * dy[i] * dy[i];
            }
            dWhy = addMatrix(dWhy, outerProduct(dy, hs.get(t + 1)));
            dby = vecAdd(dby, dy);
            double[] dh = vecAdd(transposeMatVecMul(Why, dy), dhNext);
            double[] preAct = vecAdd(vecAdd(
                matVecMul(Wxh, xs.get(t)),
                matVecMul(Whh, hs.get(t))), bh);
            double[] dtanh = hadamard(dh, apply(preAct, this::tanhDerivative));
            dWxh = addMatrix(dWxh, outerProduct(dtanh, xs.get(t)));
            dWhh = addMatrix(dWhh, outerProduct(dtanh, hs.get(t)));
            dbh = vecAdd(dbh, dtanh);
            dhNext = transposeMatVecMul(Whh, dtanh);
        }

        clipGradients(dWxh, 5.0);
        clipGradients(dWhh, 5.0);
        clipGradients(dWhy, 5.0);
        clipVector(dbh, 5.0);
        clipVector(dby, 5.0);

        Wxh = subMatrix(Wxh, scaleMatrix(dWxh, learningRate));
        Whh = subMatrix(Whh, scaleMatrix(dWhh, learningRate));
        Why = subMatrix(Why, scaleMatrix(dWhy, learningRate));
        bh = vecSub(bh, scaleVector(dbh, learningRate));
        by = vecSub(by, scaleVector(dby, learningRate));

        return totalLoss;
    }

    double[][] addMatrix(double[][] a, double[][] b) {
        double[][] result = new double[a.length][a[0].length];
        for (int i = 0; i < a.length; i++)
            for (int j = 0; j < a[0].length; j++)
                result[i][j] = a[i][j] + b[i][j];
        return result;
    }

    double[][] subMatrix(double[][] a, double[][] b) {
        double[][] result = new double[a.length][a[0].length];
        for (int i = 0; i < a.length; i++)
            for (int j = 0; j < a[0].length; j++)
                result[i][j] = a[i][j] - b[i][j];
        return result;
    }

    double[][] scaleMatrix(double[][] m, double scale) {
        double[][] result = new double[m.length][m[0].length];
        for (int i = 0; i < m.length; i++)
            for (int j = 0; j < m[0].length; j++)
                result[i][j] = m[i][j] * scale;
        return result;
    }

    double[] vecSub(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;
    }

    double[] scaleVector(double[] v, double scale) {
        double[] result = new double[v.length];
        for (int i = 0; i < v.length; i++)
            result[i] = v[i] * scale;
        return result;
    }

    void clipGradients(double[][] m, double threshold) {
        for (double[] row : m)
            clipVector(row, threshold);
    }

    void clipVector(double[] v, double threshold) {
        double norm = 0;
        for (double x : v) norm += x * x;
        norm = Math.sqrt(norm);
        if (norm > threshold) {
            double scale = threshold / norm;
            for (int i = 0; i < v.length; i++)
                v[i] *= scale;
        }
    }

    public static void main(String[] args) {
        SimpleRNN rnn = new SimpleRNN(1, 16, 1, 0.01);
        List<List<double[]>> trainSequences = new ArrayList<>();
        List<List<double[]>> trainTargets = new ArrayList<>();

        for (int seq = 0; seq < 100; seq++) {
            List<double[]> inputs = new ArrayList<>();
            List<double[]> targets = new ArrayList<>();
            double phase = Math.random() * 2 * Math.PI;
            for (int t = 0; t < 20; t++) {
                double x = Math.sin(phase + t * 0.3);
                double y = Math.sin(phase + (t + 1) * 0.3);
                inputs.add(new double[]{x});
                targets.add(new double[]{y});
            }
            trainSequences.add(inputs);
            trainTargets.add(targets);
        }

        System.out.println("开始训练RNN...");
        for (int epoch = 0; epoch < 500; epoch++) {
            double totalLoss = 0;
            for (int i = 0; i < trainSequences.size(); i++) {
                rnn.forwardSequence(trainSequences.get(i));
                totalLoss += rnn.backward(trainTargets.get(i));
            }
            if (epoch % 100 == 0) {
                System.out.println("Epoch " + epoch + ", Loss: " + (totalLoss / trainSequences.size()));
            }
        }

        System.out.println("\n测试正弦波预测:");
        List<double[]> testInput = new ArrayList<>();
        for (int t = 0; t < 10; t++) {
            testInput.add(new double[]{Math.sin(t * 0.3)});
        }
        List<double[]> predictions = rnn.forwardSequence(testInput);
        for (int t = 0; t < predictions.size(); t++) {
            double actual = Math.sin((t + 1) * 0.3);
            System.out.printf("Step %d: 预测=%.4f, 实际=%.4f%n",
                t, predictions.get(t)[0], actual);
        }
    }
}

算法复杂度分析

  • 时间复杂度:前向传播O(T * hiddenSize^2),其中T为序列长度。反向传播(BPTT)同样需要O(T * hiddenSize^2)。
  • 空间复杂度:O(T * hiddenSize)用于存储每个时间步的隐藏状态。

RNN的局限与改进

标准RNN存在梯度消失/爆炸问题,难以捕捉长距离依赖。LSTM和GRU通过门控机制解决了这一问题。本文实现的RNN展示了序列建模的核心思想,是理解更高级变体的基础。