#P4239. 第3题-反向传播实现
-
ID: 3315
Tried: 61
Accepted: 16
Difficulty: 7
所属公司 :
华为
时间 :2025年10月17日-AI方向
-
算法标签>反向传播
第3题-反向传播实现
解题思路
这道题要求实现一个多层前馈神经网络的反向传播算法,计算损失函数对每层权重矩阵和偏置向量的梯度。
核心算法是反向传播(Backpropagation),其基本思想是利用链式法则从输出层向输入层逐层计算梯度。具体实现包括以下步骤:
第一步是前向传播。从输入层开始,逐层计算每一层的线性组合结果Z和激活值A。前K-1层使用ReLU激活函数,最后一层使用Softmax激活函数得到类别概率分布。在前向传播过程中,需要保存所有的Z和A值,供反向传播使用。
第二步是计算输出层的梯度。对于交叉熵损失函数配合Softmax激活函数的组合,其对Z[K]的梯度有简化形式:dZ[K] = (output_probabilities - Y_true) / N,其中Y_true是真实标签的one-hot编码。
第三步是反向传播计算各层梯度。从输出层向输入层反向遍历,对于每一层,根据链式法则计算梯度。对于使用ReLU激活的层,需要根据前向传播时的Z值判断ReLU的导数(大于0为1,否则为0)。每层的权重梯度等于上一层激活值的转置与该层Z梯度的矩阵乘积,偏置梯度等于该层Z梯度在样本维度上的求和。
第四步是将计算得到的梯度按指定格式输出。需要注意梯度矩阵的形状必须与对应的权重矩阵形状一致。
实现过程中需要特别注意的是Softmax函数的数值稳定性问题,应该采用减去最大值的技巧避免指数运算溢出。
复杂度分析
时间复杂度为O(N × sum(h[i] × h[i+1])),其中N是批次大小,h[i]是每层的维度。前向传播和反向传播都需要遍历所有层,每层的矩阵乘法运算复杂度为O(N × h[i] × h[i+1])。
空间复杂度为O(K × N × max(h[i])),需要存储K层的所有中间激活值和梯度值,每层最多需要存储N × h[i]大小的矩阵。
代码实现
Python
import sys
import numpy as np
# 打印矩阵,保留 4 位小数
def print_formatted_matrix(matrix):
for row in matrix:
print(" ".join(f"{x:.4f}" for x in row))
# 打印向量,保留 4 位小数
def print_formatted_vector(vector):
print(" ".join(f"{x:.4f}" for x in vector))
# 数值稳定的 Softmax 实现
def stable_softmax(z):
# 减去每行的最大值,防止指数溢出
z_shifted = z - np.max(z, axis=1, keepdims=True)
exps = np.exp(z_shifted)
return exps / np.sum(exps, axis=1, keepdims=True)
# 反向传播函数
def backprop(dZK, K, A, M, Z):
"""
dZK: 最后一层 softmax+交叉熵 的梯度
K: 网络层数
A: 每层激活输出(包含输入 A[0])
M: 每层权重矩阵
Z: 每层线性变换结果
"""
grad_M = [None] * (K + 1) # 存放每层对 M 的梯度
grad_b = [None] * (K + 1) # 存放每层对 b 的梯度
# 最后一层梯度计算
grad_M[K] = A[K - 1].T @ dZK # dL/dM[K] = A[K-1]^T * dZ[K]
grad_b[K] = np.sum(dZK, axis=0) # dL/db[K] = sum(dZ[K])
dA_prev = dZK @ M[K].T # 传播到前一层
# 逐层反向传播(从 K-1 到 1)
for i in range(K - 1, 0, -1):
# ReLU 的导数:Z>0 时为 1,否则为 0
dZi = dA_prev * (Z[i] > 0).astype(float)
grad_M[i] = A[i - 1].T @ dZi # dL/dM[i]
grad_b[i] = np.sum(dZi, axis=0) # dL/db[i]
dA_prev = dZi @ M[i].T # 向前传播梯度
return grad_M, grad_b
def solve():
try:
# 读取层数 K
K_str = sys.stdin.readline()
if not K_str.strip():
return
K = int(K_str)
# 读取每层维度(例如:4 5)
h_str = sys.stdin.readline()
h = list(map(int, h_str.split()))
dims = h + [10] # 最后一层固定输出维度为 10 类
# 初始化权重矩阵和偏置向量
M = [None] * (K + 1)
b = [None] * (K + 1)
# 读取每层权重矩阵 M[i]
for i in range(1, K + 1):
rows, cols = dims[i-1], dims[i]
M[i] = np.array(
[list(map(float, sys.stdin.readline().split())) for _ in range(rows)],
dtype=float
).reshape(rows, cols)
# 读取每层偏置向量 b[i]
for i in range(1, K + 1):
b[i] = np.array(
list(map(float, sys.stdin.readline().split())),
dtype=float
).reshape(1, dims[i])
# 读取 batch size N
N = int(sys.stdin.readline())
# 读取输入样本矩阵 x
x = np.array([list(map(float, sys.stdin.readline().split())) for _ in range(N)], dtype=float)
# 读取真实标签(整数形式)
Y_labels = [int(sys.stdin.readline()) for _ in range(N)]
# 转换为 one-hot 编码
Y_true_onehot = np.zeros((N, 10), dtype=float)
Y_true_onehot[np.arange(N), Y_labels] = 1.0
# 前向传播
A = [None] * (K + 1)
Z = [None] * (K + 1)
A[0] = x
# 前 K-1 层使用 ReLU
for i in range(1, K):
Z[i] = A[i - 1] @ M[i] + b[i]
A[i] = np.maximum(0.0, Z[i])
# 最后一层使用 Softmax
Z[K] = A[K - 1] @ M[K] + b[K]
A[K] = stable_softmax(Z[K])
output_probabilities = A[K]
# Softmax + CrossEntropy 的梯度
dZK = (output_probabilities - Y_true_onehot) / N
# 反向传播计算梯度
grad_M, grad_b = backprop(dZK, K, A, M, Z)
# 输出每层梯度矩阵和偏置向量
for i in range(1, K + 1):
print_formatted_matrix(grad_M[i])
print_formatted_vector(grad_b[i])
except (IOError, ValueError, IndexError):
# 捕获输入或计算错误,防止程序崩溃
return
# 程序入口
if __name__ == "__main__":
solve()
Java
import java.util.*;
import java.io.*;
public class Main {
// 数值稳定的softmax实现
static double[][] stableSoftmax(double[][] z) {
int n = z.length;
int m = z[0].length;
double[][] result = new double[n][m];
for (int i = 0; i < n; i++) {
double maxVal = z[i][0];
for (int j = 1; j < m; j++) {
maxVal = Math.max(maxVal, z[i][j]);
}
double sum = 0;
for (int j = 0; j < m; j++) {
result[i][j] = Math.exp(z[i][j] - maxVal);
sum += result[i][j];
}
for (int j = 0; j < m; j++) {
result[i][j] /= sum;
}
}
return result;
}
// 矩阵乘法
static double[][] matmul(double[][] a, double[][] b) {
int n = a.length;
int m = b[0].length;
int k = a[0].length;
double[][] result = new double[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
for (int p = 0; p < k; p++) {
result[i][j] += a[i][p] * b[p][j];
}
}
}
return result;
}
// 矩阵转置
static double[][] transpose(double[][] a) {
int n = a.length;
int m = a[0].length;
double[][] result = new double[m][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
result[j][i] = a[i][j];
}
}
return result;
}
// 反向传播计算梯度
static void backprop(double[][] dZK, int K, double[][][] A, double[][][] M, double[][][] Z,
double[][][] gradM, double[][] gradB) {
double[][] dZ = dZK;
for (int i = K; i >= 1; i--) {
// 计算权重梯度
gradM[i] = matmul(transpose(A[i - 1]), dZ);
// 计算偏置梯度
int cols = dZ[0].length;
gradB[i] = new double[cols];
for (int j = 0; j < dZ.length; j++) {
for (int k = 0; k < cols; k++) {
gradB[i][k] += dZ[j][k];
}
}
if (i > 1) {
// 计算对前一层的梯度
double[][] dA = matmul(dZ, transpose(M[i]));
// 应用ReLU导数
dZ = new double[dA.length][dA[0].length];
for (int j = 0; j < dA.length; j++) {
for (int k = 0; k < dA[0].length; k++) {
dZ[j][k] = Z[i - 1][j][k] > 0 ? dA[j][k] : 0;
}
}
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 读取网络层数
int K = sc.nextInt();
// 读取每层维度
int[] dims = new int[K + 1];
for (int i = 0; i < K; i++) {
dims[i] = sc.nextInt();
}
dims[K] = 10;
// 读取权重矩阵
double[][][] M = new double[K + 1][][];
for (int i = 1; i <= K; i++) {
M[i] = new double[dims[i - 1]][dims[i]];
for (int j = 0; j < dims[i - 1]; j++) {
for (int k = 0; k < dims[i]; k++) {
M[i][j][k] = sc.nextDouble();
}
}
}
// 读取偏置向量
double[][] b = new double[K + 1][];
for (int i = 1; i <= K; i++) {
b[i] = new double[dims[i]];
for (int j = 0; j < dims[i]; j++) {
b[i][j] = sc.nextDouble();
}
}
// 读取batch size
int N = sc.nextInt();
// 读取输入数据
double[][] X = new double[N][dims[0]];
for (int i = 0; i < N; i++) {
for (int j = 0; j < dims[0]; j++) {
X[i][j] = sc.nextDouble();
}
}
// 读取真实标签
double[][] yTrue = new double[N][10];
for (int i = 0; i < N; i++) {
int label = sc.nextInt();
yTrue[i][label] = 1;
}
// 前向传播
double[][][] A = new double[K + 1][][];
double[][][] Z = new double[K + 1][][];
A[0] = X;
for (int i = 1; i <= K; i++) {
// 线性变换
Z[i] = matmul(A[i - 1], M[i]);
for (int j = 0; j < N; j++) {
for (int k = 0; k < dims[i]; k++) {
Z[i][j][k] += b[i][k];
}
}
if (i < K) {
// ReLU激活
A[i] = new double[N][dims[i]];
for (int j = 0; j < N; j++) {
for (int k = 0; k < dims[i]; k++) {
A[i][j][k] = Math.max(0, Z[i][j][k]);
}
}
} else {
// Softmax激活
A[i] = stableSoftmax(Z[i]);
}
}
// 计算输出层梯度
double[][] dZK = new double[N][10];
for (int i = 0; i < N; i++) {
for (int j = 0; j < 10; j++) {
dZK[i][j] = (A[K][i][j] - yTrue[i][j]) / N;
}
}
// 反向传播
double[][][] gradM = new double[K + 1][][];
double[][] gradB = new double[K + 1][];
backprop(dZK, K, A, M, Z, gradM, gradB);
// 输出梯度
for (int i = 1; i <= K; i++) {
for (int j = 0; j < gradM[i].length; j++) {
for (int k = 0; k < gradM[i][j].length; k++) {
if (k > 0) System.out.print(" ");
System.out.printf("%.4f", gradM[i][j][k]);
}
System.out.println();
}
for (int j = 0; j < gradB[i].length; j++) {
if (j > 0) System.out.print(" ");
System.out.printf("%.4f", gradB[i][j]);
}
System.out.println();
}
}
}
C++
#include <iostream>
#include <vector>
#include <cmath>
#include <iomanip>
#include <algorithm>
using namespace std;
// 数值稳定的softmax实现
vector<vector<double>> stableSoftmax(const vector<vector<double>>& z) {
int n = z.size();
int m = z[0].size();
vector<vector<double>> result(n, vector<double>(m));
for (int i = 0; i < n; i++) {
double maxVal = *max_element(z[i].begin(), z[i].end());
double sum = 0;
for (int j = 0; j < m; j++) {
result[i][j] = exp(z[i][j] - maxVal);
sum += result[i][j];
}
for (int j = 0; j < m; j++) {
result[i][j] /= sum;
}
}
return result;
}
// 矩阵乘法
vector<vector<double>> matmul(const vector<vector<double>>& a,
const vector<vector<double>>& b) {
int n = a.size();
int m = b[0].size();
int k = a[0].size();
vector<vector<double>> result(n, vector<double>(m, 0));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
for (int p = 0; p < k; p++) {
result[i][j] += a[i][p] * b[p][j];
}
}
}
return result;
}
// 矩阵转置
vector<vector<double>> transpose(const vector<vector<double>>& a) {
int n = a.size();
int m = a[0].size();
vector<vector<double>> result(m, vector<double>(n));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
result[j][i] = a[i][j];
}
}
return result;
}
// 反向传播计算梯度
void backprop(vector<vector<double>> dZK, int K,
vector<vector<vector<double>>>& A,
vector<vector<vector<double>>>& M,
vector<vector<vector<double>>>& Z,
vector<vector<vector<double>>>& gradM,
vector<vector<double>>& gradB) {
vector<vector<double>> dZ = dZK;
for (int i = K; i >= 1; i--) {
// 计算权重梯度
gradM[i] = matmul(transpose(A[i - 1]), dZ);
// 计算偏置梯度
int cols = dZ[0].size();
gradB[i].resize(cols, 0);
for (int j = 0; j < dZ.size(); j++) {
for (int k = 0; k < cols; k++) {
gradB[i][k] += dZ[j][k];
}
}
if (i > 1) {
// 计算对前一层的梯度
vector<vector<double>> dA = matmul(dZ, transpose(M[i]));
// 应用ReLU导数
dZ = vector<vector<double>>(dA.size(), vector<double>(dA[0].size()));
for (int j = 0; j < dA.size(); j++) {
for (int k = 0; k < dA[0].size(); k++) {
dZ[j][k] = Z[i - 1][j][k] > 0 ? dA[j][k] : 0;
}
}
}
}
}
int main() {
// 读取网络层数
int K;
cin >> K;
// 读取每层维度
vector<int> dims(K + 1);
for (int i = 0; i < K; i++) {
cin >> dims[i];
}
dims[K] = 10;
// 读取权重矩阵
vector<vector<vector<double>>> M(K + 1);
for (int i = 1; i <= K; i++) {
M[i].resize(dims[i - 1], vector<double>(dims[i]));
for (int j = 0; j < dims[i - 1]; j++) {
for (int k = 0; k < dims[i]; k++) {
cin >> M[i][j][k];
}
}
}
// 读取偏置向量
vector<vector<double>> b(K + 1);
for (int i = 1; i <= K; i++) {
b[i].resize(dims[i]);
for (int j = 0; j < dims[i]; j++) {
cin >> b[i][j];
}
}
// 读取batch size
int N;
cin >> N;
// 读取输入数据
vector<vector<double>> X(N, vector<double>(dims[0]));
for (int i = 0; i < N; i++) {
for (int j = 0; j < dims[0]; j++) {
cin >> X[i][j];
}
}
// 读取真实标签
vector<vector<double>> yTrue(N, vector<double>(10, 0));
for (int i = 0; i < N; i++) {
int label;
cin >> label;
yTrue[i][label] = 1;
}
// 前向传播
vector<vector<vector<double>>> A(K + 1);
vector<vector<vector<double>>> Z(K + 1);
A[0] = X;
for (int i = 1; i <= K; i++) {
// 线性变换
Z[i] = matmul(A[i - 1], M[i]);
for (int j = 0; j < N; j++) {
for (int k = 0; k < dims[i]; k++) {
Z[i][j][k] += b[i][k];
}
}
if (i < K) {
// ReLU激活
A[i] = Z[i];
for (int j = 0; j < N; j++) {
for (int k = 0; k < dims[i]; k++) {
A[i][j][k] = max(0.0, Z[i][j][k]);
}
}
} else {
// Softmax激活
A[i] = stableSoftmax(Z[i]);
}
}
// 计算输出层梯度
vector<vector<double>> dZK(N, vector<double>(10));
for (int i = 0; i < N; i++) {
for (int j = 0; j < 10; j++) {
dZK[i][j] = (A[K][i][j] - yTrue[i][j]) / N;
}
}
// 反向传播
vector<vector<vector<double>>> gradM(K + 1);
vector<vector<double>> gradB(K + 1);
backprop(dZK, K, A, M, Z, gradM, gradB);
// 输出梯度
cout << fixed << setprecision(4);
for (int i = 1; i <= K; i++) {
for (int j = 0; j < gradM[i].size(); j++) {
for (int k = 0; k < gradM[i][j].size(); k++) {
if (k > 0) cout << " ";
cout << gradM[i][j][k];
}
cout << "\n";
}
for (int j = 0; j < gradB[i].size(); j++) {
if (j > 0) cout << " ";
cout << gradB[i][j];
}
cout << "\n";
}
return 0;
}
题目内容
给定K层前馈网络模型的权重矩阵M[i]、偏移向量b[i],以及一批输入数据X和对应的真实分类标签Y_true_labels,请计算出总损失L对每一个权重矩阵M[i]和每一个偏移向量b[i]的梯度。
模型架构
模型有K个权重矩阵,第i个矩阵是M[i]。还有K个偏移向量,第i个向量是b[i]。
网络的计算过程(前向传播)如下:
- 输入是一个 N×w 的矩阵 x,其中 N 是
batch size
(本次输入数据的个数),w 是初始特征的数量。 - 网络的计算分为K层。我们用 A[i] 代表第 i 层的输出(激活值)。初始输入 A[0]=X。
- 对于第 i 层(从 i=1 到 K−1 ):
- 线性计算:Z[i]=A[i−1]⋅M[i]+b[i]
- 激活函数:A[i]=ReLU(Z[i])
- 对于最后一层(第 K 层),使用 Softmax 激活函数来输出每个类别的概率:
- 线性计算:Z[K]=A[K−1]⋅M[K]+b[K]
- 激活函数:A[K]=Softmax(Z[K])
- 最终的输出为 A[K],我们称之为
output_probabilities
。这是一个 N×10 的矩阵。
为了衡量模型预测的好坏,使用了交叉熵损失(Cross-Entropy Loss),具体计算公式参见提示部分。
输入描述
-
第一行是一个整数K,代表网络的层数。
-
第二行是K个整数,依次代表每层的维度h[0],h[1],h[2],…,h[K−1]。 最后一层的输出维度固定为10,代表10个类别。其中h[0]=W,h[K]=10。
-
M[i]的形状是h[i−1]×h[i](其中h即为w),1≤i≤K。
-
b[i]的形状是1×h[i]。
-
接下来是 K 个权重矩阵 M[1],…,M[K] 的数据。第 i 矩阵的数字分为 h[i−1] 行、每行 h[i] 个浮点数、第 i 行j列表示 M[i] 矩阵的第 i 行j列 1≤i≤K)。
-
接下来是K个偏移向量b[1],…,b[K]的数据。 分为K行,第i行有h[i]个数字,表示b[i]的权重(1≤i≤K)。
-
之后是一个整数N,代表batch size。
-
接下来是N行,每行w个浮点数,代表输入矩阵X。
-
最后是N行,每行一个整数y∈{0,1,…,9},代表真实的类别标签。
输入保证输入的所有浮点数绝对值不超过100,且小数点后最多2位数字。 即任意输入浮点数f,有∣f∣≤100,且100⋅f是整数。
输出描述
-
依次输出K个权重矩阵的梯度和K个偏移向量的梯度。
-
对于第i层(从1到K):
- 首先,输出梯度矩阵∂M[i]∂L,分为h[i−1]行输出,每行h[i]个实数,用一个空格分隔,行末不能有空格。
- 然后,输出平均梯度向量∂b[i]∂L,一行有h[i]个数字,用一个空格分隔,行末不能有空格。
-
所有浮点数保留4位小数。
输入保证计算出来的输出梯度不会有NAN,且可以被双精度浮点数表示(即不会炸梯度,不会出现NAN/INF)。
样例1
输入
2
4 5
1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0
0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
0.1 0.1 0.1 0.1 0.1
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2
0.5 0.5 0.5 0.5
0.5 0.5 0.5 0.5
2
8
输出
0.0000 0.0000 0.0000 0.0000 0.0000
0.0000 0.0000 0.0000 0.0000 0.0000
0.0000 0.0000 0.0000 0.0000 0.0000
0.0000 0.0000 0.0000 0.0000 0.0000
0.0000 0.0000 0.0000 0.0000 0.0000
0.2100 0.2100 -0.8400 0.2100 0.2100 0.2100 0.2100 0.2100 -0.8400 0.2100
0.2100 0.2100 -0.8400 0.2100 0.2100 0.2100 0.2100 0.2100 -0.8400 0.2100
0.2100 0.2100 -0.8400 0.2100 0.2100 0.2100 0.2100 0.2100 -0.8400 0.2100
0.2100 0.2100 -0.8400 0.2100 0.2100 0.2100 0.2100 0.2100 -0.8400 0.2100
0.2100 0.2100 -0.8400 0.2100 0.2100 0.2100 0.2100 0.2100 -0.8400 0.2100
0.1000 0.1000 -0.4000 0.1000 0.1000 0.1000 0.1000 0.1000 -0.4000 0.1000
说明
输入样例是2个样本,特征相同,但是标签不同。
样例2
输入
1
2
1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0
0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0
0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08 0.09 0.1
1
50.0 60.0
4
输出
0.0000 0.0000 0.0000 0.0000 -50.0000 0.0000 0.0000 0.0000 0.0000 50.0000
0.0000 0.0000 0.0000 0.0000 -60.0000 0.0000 0.0000 0.0000 0.0000 60.0000
0.0000 0.0000 0.0000 0.0000 -1.0000 0.0000 0.0000 0.0000 0.0000 1.0000
说明
一层网络,一个样本。
Z[1] = [56.01 112.02 168.03 224.04 280.05 336.06 392.07 448.08 504.09 560.1]
A[1] = [1.1925994847839173e-219, 2.519582300056682e-195, 5.323073712302622e-171, 1.1245956818306658e-146, 2.3759119560363354e-122, 5.019544102861018e-98, 1.060469557238993e-73, 2.240433909505195e-49, 4.733322204863164e-25, 1.0]
Ground Truth是[0,0,0,0,1,0,0,0,0,0]
提示
Loss计算公式
对于一批(batch)大小为N的输入,每个输入都有一个真实的类别标签
yj∈{0,1,…,9}
首先,我们将真实标签yj转换为one-hot向量(Ytrue)j。例如,如果真实标签为2,其one-hot向量为
[0,0,1,0,0,0,0,0,0,0]。
总损失L的计算方式是批次中所有样本损失的平均值:
$$L = -\frac{1}{N} \sum_{j=1}^{N} \sum_{l=0}^{9} (Y_{true})_{jl} \log(output\_probabilities_{jl}) $$其中(output_probabilities)jl是第j个样本的预测输出向量(经过Softmax后)的第l个元素。
数据范围
1≤K≤5
1≤w,h[i]≤100
1≤N≤100
所有输入浮点数的绝对值不超过100,且浮点数小数点后最多两位小数。
提示
提示
- 提示1:
$\frac{\partial L}{\partial b[i]_j} = \frac{\partial Z[i]_j}{\partial b[i]_j} \cdot \frac{\partial L}{\partial Z[i]_j} = \frac{\partial L}{\partial Z[i]_j}$
- 提示2:
$\frac{\partial L}{\partial M[i]_{kj}} = \frac{\partial Z[i]_j}{\partial M[i]_{kj}} \cdot \frac{\partial L}{\partial Z[i]_j} = A[i-1][k] \cdot \frac{\partial L}{\partial Z[i]_j}$
- 提示3:
根据链式法则,dM[i]=A[i−1]T×dZ[i](T表示转置)