关于两层神经网络dW2梯度计算(矩阵转置)的疑问与扩展咨询
两层全连接神经网络梯度计算疑问(核心为dW2)
我搞不懂这段代码里的梯度计算逻辑,特别是涉及矩阵转置的部分,核心疑问是dW2的计算逻辑,也希望能扩展讨论其他梯度的计算。我自己的数学推导和这段GitHub上的可靠代码实现有差异,想知道两者差异的原因。
相关代码
from __future__ import print_function from builtins import range from builtins import object import numpy as np import matplotlib.pyplot as plt from past.builtins import xrange class TwoLayerNet(object): """ A two-layer fully-connected neural network. The net has an input dimension of D* (correction), a hidden layer dimension of H, and performs classification over C classes. We train the network with a softmax loss function and L2 regularization on the weight matrices. The network uses a ReLU nonlinearity after the first fully connected layer. In other words, the network has the following architecture: input - fully connected layer - ReLU - fully connected layer - softmax The outputs of the second fully-connected layer are the scores for each class. """ def __init__(self, input_size, hidden_size, output_size, std=1e-4): """ Initialize the model. Weights are initialized to small random values and biases are initialized to zero. Weights and biases are stored in the variable self.params, which is a dictionary with the following keys: W1: First layer weights; has shape (D, H) b1: First layer biases; has shape (H,) W2: Second layer weights; has shape (H, C) b2: Second layer biases; has shape (C,) Inputs: - input_size: The dimension D of the input data. - hidden_size: The number of neurons H in the hidden layer. - output_size: The number of classes C. """ self.params = {} self.params['W1'] = std * np.random.randn(input_size, hidden_size) self.params['b1'] = np.zeros(hidden_size) self.params['W2'] = std * np.random.randn(hidden_size, output_size) self.params['b2'] = np.zeros(output_size) def loss(self, X, y=None, reg=0.0): """ Compute the loss and gradients for a two layer fully connected neural network. Inputs: - X: Input data of shape (N, D). Each X[i] is a training sample. - y: Vector of training labels. y[i] is the label for X[i], and each y[i] is an integer in the range 0 <= y[i] < C. This parameter is optional; if it is not passed then we only return scores, and if it is passed then we instead return the loss and gradients. - reg: Regularization strength. Returns: If y is None, return a matrix scores of shape (N, C) where scores[i, c] is the score for class c on input X[i]. If y is not None, instead return a tuple of: - loss: Loss (data loss and regularization loss) for this batch of training samples. - grads: Dictionary mapping parameter names to gradients of those parameters with respect to the loss function; has the same keys as self.params. """ # Unpack variables from the params dictionary W1, b1 = self.params['W1'], self.params['b1'] W2, b2 = self.params['W2'], self.params['b2'] N, D = X.shape # Compute the forward pass scores = None ############################################################################# # TODO: Perform the forward pass, computing the class scores for the input. # # Store the result in the scores variable, which should be an array of # # shape (N, C). # ############################################################################# # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)***** # perform the forward pass and compute the class scores for the input # input - fully connected layer - ReLU - fully connected layer - softmax # define lamba function for relu relu = lambda x: np.maximum(0, x) # a1 = X x W1 = (N x D) x (D x H) = N x H a1 = relu(X.dot(W1) + b1) # activations of fully connected layer #1 # store the result in the scores variable, which should be an array of # shape (N, C). # scores = a1 x W2 = (N x H) x (H x C) = N x C scores = a1.dot(W2) + b2 # output of softmax # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)***** # If the targets are not given then jump out, we're done if y is None: return scores # Compute the loss loss = None ############################################################################# # TODO: Finish the forward pass, and compute the loss. This should include # # both the data loss and L2 regularization for W1 and W2. Store the result # # in the variable loss, which should be a scalar. Use the Softmax # # classifier loss. # ############################################################################# # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)***** # shift values for 'scores' for numeric reasons (over-flow cautious) # figure out the max score across all classes # scores.shape is N x C scores -= scores.max(axis = 1, keepdims = True) # probs.shape is N x C probs = np.exp(scores)/np.sum(np.exp(scores), axis = 1, keepdims = True) loss = -np.log(probs[np.arange(N), y]) # loss is a single number loss = np.sum(loss) # Right now the loss is a sum over all training examples, but we want it # to be an average instead so we divide by N. loss /= N # Add regularization to the loss. loss += reg * (np.sum(W1 * W1) + np.sum(W2 * W2)) # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)***** # Backward pass: compute gradients grads = {} ############################################################################# # TODO: Compute the backward pass, computing the derivatives of the weights # # and biases. Store the results in the grads dictionary. For example, # # grads['W1'] should store the gradient on W1, and be a matrix of same size # ############################################################################# # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)***** # since dL(i)/df(k) = p(k) - 1 (if k = y[i]), where f is a vector of scores for the given example # i is the training sample and k is the class dscores = probs.reshape(N, -1) # dscores is (N x C) dscores[np.arange(N), y] -= 1 # since scores = a1.dot(W2), we get dW2 by multiplying a1.T and dscores # W2 is H x C so dW2 should also match those dimensions # a1.T x dscores = (H x N) x (N x C) = H x C dW2 = np.dot(a1.T, dscores) # Right now the gradient is a sum over all training examples, but we want it # to be an average instead so we divide by N. dW2 /= N # b2 gradient: sum dscores over all N and C db2 = dscores.sum(axis = 0)/N # since a1 = X.dot(W1), we get dW1 by multiplying X.T and da1 # W1 is D x H so dW1 should also match those dimensions # X.T x da1 = (D x N) x (N x H) = D x H # first get da1 using scores = a1.dot(W2) # a1 is N x H so da1 should also match those dimensions # dscores x W2.T = (N x C) x (C x H) = N x H da1 = dscores.dot(W2.T) da1[a1 == 0] = 0 # set gradient of units that did not activate to 0 dW1 = X.T.dot(da1) # Right now the gradient is a sum over all training examples, but we want it # to be an average instead so we divide by N. dW1 /= N # b1 gradient: sum da1 over all N and H db1 = da1.sum(axis = 0)/N # Add regularization loss to the gradient dW1 += 2 * reg * W1 dW2 += 2 * reg * W2 grads = {'W1': dW1, 'b1': db1, 'W2': dW2, 'b2': db2} # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)***** return loss, grads def train(self, X, y, X_val, y_val, learning_rate=1e-3, learning_rate_decay=0.95, reg=5e-6, num_iters=100, batch_size=200, verbose=False): """ Train this neural network using stochastic gradient descent. Inputs: - X: A numpy array of shape (N, D) giving training data. - y: A numpy array f shape (N,) giving training labels; y[i] = c means that X[i] has label c, where 0 <= c < C. - X_val: A numpy array of shape (N_val, D) giving validation data. - y_val: A numpy array of shape (N_val,) giving validation labels. - learning_rate: Scalar giving learning rate for optimization. - learning_rate_decay: Scalar giving factor used to decay the learning rate after each epoch. - reg: Scalar giving regularization strength. - num_iters: Number of steps to take when optimizing. - batch_size: Number of training examples to use per step. - verbose: boolean; if true print progress during optimization. """ num_train = X.shape[0] iterations_per_epoch = max(num_train / batch_size, 1) # Use SGD to optimize the parameters in self.model loss_history = [] train_acc_history = [] val_acc_history = [] for it in range(num_iters): X_batch = None y_batch = None ######################################################################### # TODO: Create a random minibatch of training data and labels, storing # # them in X_batch and y_batch respectively. # ######################################################################### # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)***** # generate random indices indices = np.random.choice(num_train, batch_size) X_batch, y_batch = X[indices], y[indices] # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)***** # Compute loss and gradients using the current minibatch loss, grads = self.loss(X_batch, y=y_batch, reg=reg) loss_history.append(loss) ######################################################################### # TODO: Use the gradients in the grads dictionary to update the # # parameters of the network (stored in the dictionary self.params) # # using stochastic gradient descent. You'll need to use the gradients # # stored in the grads dictionary defined above. # ######################################################################### # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)***** self.params['W1'] -= learning_rate * grads['W1'] self.params['W2'] -= learning_rate * grads['W2'] self.params['b1'] -= learning_rate * grads['b1'] self.params['b2'] -= learning_rate * grads['b2'] # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)***** if verbose and it % 100 == 0: print('iteration %d / %d: loss %f' % (it, num_iters, loss)) # Every epoch, check train and val accuracy and decay learning rate. if it % iterations_per_epoch == 0: # Check accuracy train_acc = (self.predict(X_batch) == y_batch).mean() val_acc = (self.predict(X_val) == y_val).mean() train_acc_history.append(train_acc) val_acc_history.append(val_acc) # Decay learning rate learning_rate *= learning_rate_decay return { 'loss_history': loss_history, 'train_acc_history': train_acc_history, 'val_acc_history': val_acc_history, } def predict(self, X): """ Use the trained weights of this two-layer network to predict labels for data points. For each data point we predict scores for each of the C classes, and assign each data point to the class with the highest score. Inputs: - X: A numpy array of shape (N, D) giving N D-dimensional data points to classify. Returns: - y_pred: A numpy array of shape (N,) giving predicted labels for each of the elements of X. For all i, y_pred[i] = c means that X[i] is predicted to have class c, where 0 <= c < C. """ y_pred = None ########################################################################### # TODO: Implement this function; it should be VERY simple! # ########################################################################### # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)***** # define lamba function for relu relu = lambda x: np.maximum(0, x) # activations of fully connected layer #1 a1 = relu(X.dot(self.params['W1']) + self.params['b1']) # output of softmax # scores = a1 x W2 = (N x H) x (H x C) = N x C scores = a1.dot(self.params['W2']) + self.params['b2'] y_pred = np.argmax(scores, axis = 1) # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)***** return y_pred
推导差异说明
我的疑问
代码中dW2通过a1.T.dot(dscores)计算,但我推导得到的是dscores.dot(a1.T),两者形状完全不同,搞不懂为什么会有这个差异。
我的推导过程
- 损失L对第二层分数scores的梯度dscores:每个样本i的dscores[i][k] = p(i,k) - 1(当k是样本i的真实标签y[i]时),否则为p(i,k)。
- 第二层权重W2的梯度dW2:根据链式法则,dW2 = dL/dW2 = (dL/dscores) * (dscores/dW2)。scores = a1 @ W2,我推导时认为对于每个样本,dscores_i/dW2 = a1_i(行向量),所以整体的dW2应该是dscores(N×C)和a1(N×H)的某种乘积,但得到的形状和代码里的H×C不符。
差异解释与梯度逻辑梳理
核心差异:批量梯度的维度匹配
代码实现是正确的,问题出在你推导时没考虑批量样本的累加逻辑,以及梯度维度必须和参数维度一致:
- 单个样本的梯度计算
对于单个样本i:- a1_i是1×H的行向量,scores_i是1×C的行向量,满足
scores_i = a1_i @ W2 - 单个样本的dW2_i(必须和W2维度一致,即H×C)=
a1_i.T @ dscores_i(a1_i.T是H×1,dscores_i是1×C,相乘得到H×C)
- a1_i是1×H的行向量,scores_i是1×C的行向量,满足
- 批量样本的梯度累加
批量N个样本的dW2是所有单个样本dW2_i的平均,把所有a1堆叠成N×H的矩阵,dscores堆叠成N×C的矩阵,批量计算就是a1.T @ dscores(H×N 乘 N×C = H×C),再除以N取平均,这和代码完全一致。 - 你的推导错误点
你把dscores和a1的顺序搞反了,dscores.dot(a1.T)得到的是N×H的矩阵,和W2的H×C维度完全不匹配,显然不符合梯度的定义(梯度维度必须和参数维度相同,才能用于参数更新)。
其他梯度的计算逻辑
- db2:scores对b2的偏导是全1,所以db2是dscores在样本维度(N)上求和后取平均,即
dscores.sum(axis=0)/N,对应每个类别偏置的梯度。 - da1:由链式法则,
dL/da1 = dscores @ W2.T(N×C 乘 C
相关产品推荐
相关产品推荐

