引言
卷积神经网络(CNN)是深度学习处理网格结构数据(尤其是图像)的核心架构。本文用纯Java实现一个简化版CNN,包含卷积层、ReLU激活、最大池化与全连接层,从零构建前向传播与反向传播,帮助读者理解参数共享、局部连接与特征层次化提取的本质。
一、核心组件设计
1.1 张量工具类
public class Tensor {
public final int depth, height, width;
public final double[][][] data;
public Tensor(int depth, int height, int width) {
this.depth = depth;
this.height = height;
this.width = width;
this.data = new double[depth][height][width];
}
public Tensor(double[][][] data) {
this.depth = data.length;
this.height = data[0].length;
this.width = data[0][0].length;
this.data = deepCopy(data);
}
private static double[][][] deepCopy(double[][][] src) {
double[][][] dst = new double[src.length][][];
for (int i = 0; i < src.length; i++) {
dst[i] = new double[src[i].length][];
for (int j = 0; j < src[i].length; j++) {
dst[i][j] = src[i][j].clone();
}
}
return dst;
}
/**
* 填充零边界
*/
public Tensor pad(int padH, int padW) {
Tensor result = new Tensor(depth, height + 2 * padH, width + 2 * padW);
for (int d = 0; d < depth; d++) {
for (int h = 0; h < height; h++) {
System.arraycopy(data[d][h], 0, result.data[d][h + padH], padW, width);
}
}
return result;
}
}
二、卷积层实现
2.1 卷积运算
卷积核在输入上滑动,局部加权求和:
public class ConvolutionLayer {
public final int numFilters; // 卷积核数量(输出通道数)
public final int filterSize; // 卷积核尺寸(正方形)
public final int stride; // 步长
public final int padding; // 零填充大小
// 参数: [输出通道][输入通道][核高][核宽]
public final double[][][][] filters;
public final double[] biases;
// 缓存用于反向传播
private Tensor inputCache;
private Tensor outputCache;
public ConvolutionLayer(int numFilters, int filterSize,
int stride, int padding, int inputDepth) {
this.numFilters = numFilters;
this.filterSize = filterSize;
this.stride = stride;
this.padding = padding;
// Xavier初始化
double scale = Math.sqrt(2.0 / (inputDepth * filterSize * filterSize));
this.filters = new double[numFilters][inputDepth][filterSize][filterSize];
for (int f = 0; f < numFilters; f++) {
for (int d = 0; d < inputDepth; d++) {
for (int i = 0; i < filterSize; i++) {
for (int j = 0; j < filterSize; j++) {
filters[f][d][i][j] = (Math.random() - 0.5) * 2 * scale;
}
}
}
}
this.biases = new double[numFilters];
}
/**
* 前向传播: 对输入执行卷积
*/
public Tensor forward(Tensor input) {
this.inputCache = input;
Tensor padded = input.pad(padding, padding);
int outH = (input.height + 2 * padding - filterSize) / stride + 1;
int outW = (input.width + 2 * padding - filterSize) / stride + 1;
Tensor output = new Tensor(numFilters, outH, outW);
for (int f = 0; f < numFilters; f++) {
for (int oh = 0; oh < outH; oh++) {
for (int ow = 0; ow < outW; ow++) {
double sum = biases[f];
int ihStart = oh * stride;
int iwStart = ow * stride;
for (int d = 0; d < input.depth; d++) {
for (int fh = 0; fh < filterSize; fh++) {
for (int fw = 0; fw < filterSize; fw++) {
sum += padded.data[d][ihStart + fh][iwStart + fw]
* filters[f][d][fh][fw];
}
}
}
output.data[f][oh][ow] = sum;
}
}
}
this.outputCache = output;
return output;
}
}
2.2 反向传播
public class ConvolutionLayer {
/**
* 反向传播计算梯度
* @param gradOutput 损失对输出的梯度 [numFilters][outH][outW]
* @return 损失对输入的梯度
*/
public Tensor backward(Tensor gradOutput) {
Tensor paddedInput = inputCache.pad(padding, padding);
Tensor gradInputPadded = new Tensor(paddedInput.depth,
paddedInput.height, paddedInput.width);
// 计算filter梯度和bias梯度
double[][][][] gradFilters = new double[numFilters][inputCache.depth]
[filterSize][filterSize];
double[] gradBiases = new double[numFilters];
for (int f = 0; f < numFilters; f++) {
for (int oh = 0; oh < gradOutput.height; oh++) {
for (int ow = 0; ow < gradOutput.width; ow++) {
double grad = gradOutput.data[f][oh][ow];
gradBiases[f] += grad;
int ihStart = oh * stride;
int iwStart = ow * stride;
for (int d = 0; d < inputCache.depth; d++) {
for (int fh = 0; fh < filterSize; fh++) {
for (int fw = 0; fw < filterSize; fw++) {
gradFilters[f][d][fh][fw] +=
paddedInput.data[d][ihStart + fh][iwStart + fw] * grad;
gradInputPadded.data[d][ihStart + fh][iwStart + fw] +=
filters[f][d][fh][fw] * grad;
}
}
}
}
}
}
// 更新参数 (简化版SGD)
double lr = 0.01;
for (int f = 0; f < numFilters; f++) {
biases[f] -= lr * gradBiases[f];
for (int d = 0; d < inputCache.depth; d++) {
for (int i = 0; i < filterSize; i++) {
for (int j = 0; j < filterSize; j++) {
filters[f][d][i][j] -= lr * gradFilters[f][d][i][j];
}
}
}
}
// 去除padding,返回对原始输入的梯度
return unpad(gradInputPadded);
}
private Tensor unpad(Tensor padded) {
Tensor result = new Tensor(padded.depth,
padded.height - 2 * padding,
padded.width - 2 * padding);
for (int d = 0; d < result.depth; d++) {
for (int h = 0; h < result.height; h++) {
System.arraycopy(padded.data[d][h + padding], padding,
result.data[d][h], 0, result.width);
}
}
return result;
}
}
三、激活层与池化层
3.1 ReLU激活
public class ReLULayer {
private Tensor mask; // 记录哪些位置>0,用于反向传播
public Tensor forward(Tensor input) {
this.mask = new Tensor(input.depth, input.height, input.width);
Tensor output = new Tensor(input.depth, input.height, input.width);
for (int d = 0; d < input.depth; d++) {
for (int h = 0; h < input.height; h++) {
for (int w = 0; w < input.width; w++) {
double val = input.data[d][h][w];
if (val > 0) {
output.data[d][h][w] = val;
mask.data[d][h][w] = 1;
} else {
output.data[d][h][w] = 0;
mask.data[d][h][w] = 0;
}
}
}
}
return output;
}
public Tensor backward(Tensor gradOutput) {
Tensor gradInput = new Tensor(gradOutput.depth, gradOutput.height, gradOutput.width);
for (int d = 0; d < gradOutput.depth; d++) {
for (int h = 0; h < gradOutput.height; h++) {
for (int w = 0; w < gradOutput.width; w++) {
gradInput.data[d][h][w] = gradOutput.data[d][h][w] * mask.data[d][h][w];
}
}
}
return gradInput;
}
}
3.2 最大池化层
public class MaxPoolLayer {
private final int poolSize;
private final int stride;
private int[][][] maxIndicesH; // 记录每个输出对应的最大值位置H
private int[][][] maxIndicesW; // 记录每个输出对应的最大值位置W
public MaxPoolLayer(int poolSize, int stride) {
this.poolSize = poolSize;
this.stride = stride;
}
public Tensor forward(Tensor input) {
int outH = (input.height - poolSize) / stride + 1;
int outW = (input.width - poolSize) / stride + 1;
Tensor output = new Tensor(input.depth, outH, outW);
maxIndicesH = new int[input.depth][outH][outW];
maxIndicesW = new int[input.depth][outH][outW];
for (int d = 0; d < input.depth; d++) {
for (int oh = 0; oh < outH; oh++) {
for (int ow = 0; ow < outW; ow++) {
double maxVal = Double.NEGATIVE_INFINITY;
int maxH = 0, maxW = 0;
int ihStart = oh * stride;
int iwStart = ow * stride;
for (int ph = 0; ph < poolSize; ph++) {
for (int pw = 0; pw < poolSize; pw++) {
double val = input.data[d][ihStart + ph][iwStart + pw];
if (val > maxVal) {
maxVal = val;
maxH = ihStart + ph;
maxW = iwStart + pw;
}
}
}
output.data[d][oh][ow] = maxVal;
maxIndicesH[d][oh][ow] = maxH;
maxIndicesW[d][oh][ow] = maxW;
}
}
}
return output;
}
public Tensor backward(Tensor gradOutput, int inH, int inW) {
Tensor gradInput = new Tensor(gradOutput.depth, inH, inW);
for (int d = 0; d < gradOutput.depth; d++) {
for (int oh = 0; oh < gradOutput.height; oh++) {
for (int ow = 0; ow < gradOutput.width; ow++) {
int mh = maxIndicesH[d][oh][ow];
int mw = maxIndicesW[d][oh][ow];
gradInput.data[d][mh][mw] += gradOutput.data[d][oh][ow];
}
}
}
return gradInput;
}
}
四、全连接层与Softmax
4.1 全连接层
public class FullyConnectedLayer {
public final int inputSize, outputSize;
public final double[][] weights;
public final double[] biases;
private double[] inputCache;
private double[] outputCache;
public FullyConnectedLayer(int inputSize, int outputSize) {
this.inputSize = inputSize;
this.outputSize = outputSize;
double scale = Math.sqrt(2.0 / inputSize);
this.weights = new double[outputSize][inputSize];
for (int i = 0; i < outputSize; i++) {
for (int j = 0; j < inputSize; j++) {
weights[i][j] = (Math.random() - 0.5) * 2 * scale;
}
}
this.biases = new double[outputSize];
}
public double[] forward(double[] input) {
this.inputCache = input.clone();
double[] output = new double[outputSize];
for (int i = 0; i < outputSize; i++) {
double sum = biases[i];
for (int j = 0; j < inputSize; j++) {
sum += weights[i][j] * input[j];
}
output[i] = sum;
}
this.outputCache = output.clone();
return output;
}
public double[] backward(double[] gradOutput, double lr) {
double[] gradInput = new double[inputSize];
for (int i = 0; i < outputSize; i++) {
biases[i] -= lr * gradOutput[i];
for (int j = 0; j < inputSize; j++) {
weights[i][j] -= lr * gradOutput[i] * inputCache[j];
gradInput[j] += gradOutput[i] * weights[i][j];
}
}
return gradInput;
}
}
4.2 Softmax与交叉熵损失
public class SoftmaxCrossEntropy {
private double[] probs; // 缓存softmax概率
/**
* Softmax前向 + 交叉熵损失计算
* @param logits 全连接层输出
* @param label 真实类别索引
* @return 损失值
*/
public double forward(double[] logits, int label) {
// Softmax
double maxLogit = Double.NEGATIVE_INFINITY;
for (double v : logits) maxLogit = Math.max(maxLogit, v);
double sumExp = 0;
probs = new double[logits.length];
for (int i = 0; i < logits.length; i++) {
probs[i] = Math.exp(logits[i] - maxLogit); // 数值稳定性
sumExp += probs[i];
}
for (int i = 0; i < probs.length; i++) {
probs[i] /= sumExp;
}
// 交叉熵: -log(prob[label])
return -Math.log(probs[label] + 1e-10);
}
/**
* 反向传播: 返回对logits的梯度
*/
public double[] backward(int label) {
double[] grad = probs.clone();
grad[label] -= 1.0; // one-hot差分
return grad;
}
}
五、网络组装与训练
public class SimpleCNN {
private final ConvolutionLayer conv1;
private final ReLULayer relu1;
private final MaxPoolLayer pool1;
private final FullyConnectedLayer fc;
private final SoftmaxCrossEntropy lossLayer;
// 假设输入: 1x28x28 (单通道28x28图像,如MNIST)
// conv1: 8个3x3卷积核 -> 8x28x28 (padding=1)
// pool1: 2x2最大池化 -> 8x14x14
// flatten: 8*14*14 = 1568
// fc: 1568 -> 10 (10分类)
public SimpleCNN() {
conv1 = new ConvolutionLayer(8, 3, 1, 1, 1);
relu1 = new ReLULayer();
pool1 = new MaxPoolLayer(2, 2);
fc = new FullyConnectedLayer(8 * 14 * 14, 10);
lossLayer = new SoftmaxCrossEntropy();
}
/**
* 前向传播
*/
public double forward(Tensor input, int label) {
Tensor t1 = conv1.forward(input);
Tensor t2 = relu1.forward(t1);
Tensor t3 = pool1.forward(t2);
// Flatten
double[] flat = flatten(t3);
double[] logits = fc.forward(flat);
return lossLayer.forward(logits, label);
}
/**
* 反向传播
*/
public void backward() {
double[] gradLogits = lossLayer.backward(-1); // label已在forward时确定,这里简化
double[] gradFlat = fc.backward(gradLogits, 0.01);
Tensor gradPool = unflatten(gradFlat, 8, 14, 14);
Tensor gradRelu = pool1.backward(gradPool, 28, 28);
Tensor gradConv = relu1.backward(gradRelu);
conv1.backward(gradConv);
}
private double[] flatten(Tensor t) {
double[] result = new double[t.depth * t.height * t.width];
int idx = 0;
for (int d = 0; d < t.depth; d++) {
for (int h = 0; h < t.height; h++) {
for (int w = 0; w < t.width; w++) {
result[idx++] = t.data[d][h][w];
}
}
}
return result;
}
private Tensor unflatten(double[] flat, int d, int h, int w) {
Tensor t = new Tensor(d, h, w);
int idx = 0;
for (int i = 0; i < d; i++) {
for (int j = 0; j < h; j++) {
for (int k = 0; k < w; k++) {
t.data[i][j][k] = flat[idx++];
}
}
}
return t;
}
}
六、演示:特征可视化
public class CNNDemo {
public static void main(String[] args) {
// 构造一个简单输入: 边缘检测特征
double[][][] input = new double[1][8][8];
// 左半部分为0,右半部分为1,形成垂直边缘
for (int h = 0; h < 8; h++) {
for (int w = 0; w < 8; w++) {
input[0][h][w] = w < 4 ? 0.0 : 1.0;
}
}
Tensor inputTensor = new Tensor(input);
// 创建一个手动设定的边缘检测卷积核
ConvolutionLayer edgeDetector = new ConvolutionLayer(1, 3, 1, 1, 1);
// Sobel垂直边缘核近似: [-1, 0, 1; -2, 0, 2; -1, 0, 1]
edgeDetector.filters[0][0][0][0] = -1; edgeDetector.filters[0][0][0][1] = 0; edgeDetector.filters[0][0][0][2] = 1;
edgeDetector.filters[0][0][1][0] = -2; edgeDetector.filters[0][0][1][1] = 0; edgeDetector.filters[0][0][1][2] = 2;
edgeDetector.filters[0][0][2][0] = -1; edgeDetector.filters[0][0][2][1] = 0; edgeDetector.filters[0][0][2][2] = 1;
edgeDetector.biases[0] = 0;
Tensor convOut = edgeDetector.forward(inputTensor);
System.out.println("卷积输出 (边缘响应):");
printTensor(convOut);
// ReLU激活
ReLULayer relu = new ReLULayer();
Tensor reluOut = relu.forward(convOut);
System.out.println("\nReLU后 (抑制负响应):");
printTensor(reluOut);
// 最大池化
MaxPoolLayer pool = new MaxPoolLayer(2, 2);
Tensor poolOut = pool.forward(reluOut);
System.out.println("\n池化后 (降采样):");
printTensor(poolOut);
}
private static void printTensor(Tensor t) {
for (int h = 0; h < t.height; h++) {
for (int w = 0; w < t.width; w++) {
System.out.printf("%6.2f ", t.data[0][h][w]);
}
System.out.println();
}
}
}
七、复杂度分析
| 层 | 参数量 | 前向时间 | 反向时间 | 说明 |
|---|---|---|---|---|
| 卷积层 | F×D×K² + F | O(F×D×K²×H×W) | O(F×D×K²×H×W) | F=输出通道,D=输入通道,K=核尺寸 |
| ReLU | 0 | O(D×H×W) | O(D×H×W) | 逐元素操作 |
| 最大池化 | 0 | O(D×H×W×P²) | O(D×H×W) | P=池化尺寸 |
| 全连接 | O×I + O | O(O×I) | O(O×I) | O=输出,I=输入 |
| Softmax | 0 | O(C) | O(C) | C=类别数 |
关键优势: 卷积层通过参数共享(同一卷积核在整幅图滑动)将参数量从O(F×D×K²×H×W)降至O(F×D×K²),大幅减少过拟合风险。
八、扩展方向
- 批量归一化(BatchNorm): 在卷积后添加,加速收敛
- Dropout: 全连接层随机失活,增强泛化
- 残差连接(ResNet): 跳跃连接缓解梯度消失
- Adam优化器: 替代SGD,自适应学习率
总结
本文用Java从零实现了CNN的核心组件:卷积层、ReLU激活、最大池化、全连接与Softmax交叉熵损失。读者通过代码可以理解参数共享机制、反向传播中梯度的链式流转,以及卷积核作为特征检测器的本质。建议进一步阅读Batch Normalization与ResNet等现代架构,将基础CNN扩展为深度网络。