算法概述
循环神经网络(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展示了序列建模的核心思想,是理解更高级变体的基础。