the backward function

本文介绍了一个使用PyTorch自定义实现的Sigmoid激活函数类。该类包括前向传播和反向传播两个阶段,并优化了反向传播过程以提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

class Sigmoid(Function):

    @staticmethod

    def forward(ctx, x):

            output = 1/(1 + t.exp(-x))

            ctx.save_for_backward(output)

            return output

    @staticmethond

    def backward(ctx,  grad_output):

        output, = ctx.saved_variables

        grad_x = output * (1 - output) * grad_output

        return grad_x



def f_sigmoid(x):

    y = Sigmoid.apply(x)

    y.backward(t.ones(x.size()))



the backward part in f_sigmoid function has optimized the process of backward

<think>我们正在实现卷积层的反向传播函数conv_backward_naive。这个函数将接收上游传来的梯度dout和之前缓存的值(包括输入x、权重w、偏置b和卷积参数conv_param),然后计算相对于输入x、权重w和偏置b的梯度。 步骤: 1. 从缓存中恢复前向传播时保存的变量(x, w, b, conv_param)和参数(stride, pad)。 2. 初始化梯度:dx, dw, db,其形状分别与x, w, b相同。 3. 根据前向传播的填充方式,对dx进行相应的填充(因为我们需要计算填充区域的梯度,但最后要去掉填充部分)。 4. 按照前向传播的卷积过程,遍历每个样本、每个滤波器、每个输出位置,计算: - db: 每个滤波器的偏置梯度是dout中对应滤波器的所有位置梯度之和。 - dw: 每个滤波器的权重梯度是输入对应区域与dout对应位置的乘积之和(按通道求和)。 - dx: 输入梯度(填充后的)是每个位置由权重与dout对应位置的乘积累加(注意多个滤波器对同一个输入区域的影响需要累加)。 具体实现: 前向传播时,我们计算了: out[n, f, i, j] = np.sum(x_padded[n, :, h_start:h_end, w_start:w_end] * w[f]) + b[f] 反向传播时: - 偏置b的梯度:db[f] = sum_{n,i,j} dout[n, f, i, j] - 权重w的梯度:对于每个滤波器f,dw[f] = sum_{n,i,j} (x_padded[n, :, h_start:h_end, w_start:w_end] * dout[n, f, i, j]),注意这里是对所有样本和所有位置的累加。 - 输入x的梯度:对于每个样本n,每个位置(i,j)和每个滤波器f,dout[n,f,i,j]会乘以滤波器w[f]的每个权重,然后累加到输入x_padded对应的区域[n, :, h_start:h_end, w_start:w_end]上。由于同一个输入位置可能被多个输出位置和多个滤波器使用,因此需要累加。 注意:dx是计算在填充后的输入上的,最后需要去掉填充部分,得到原始输入的梯度。 实现细节: 1. 获取缓存中的值:x, w, b, conv_param = cache 2. 获取参数:pad = conv_param['pad'], stride = conv_param['stride'] 3. 初始化梯度: dx = np.zeros_like(x) dw = np.zeros_like(w) db = np.zeros_like(b) 4. 对输入x进行填充(和正向传播一样),但这里我们不需要填充后的输入数据,而是需要填充后的梯度数组(初始化为0),然后我们将梯度累加到这个填充数组上,最后再裁剪掉填充部分得到dx。 因此,我们创建一个填充后的dx_padded: dx_padded = np.pad(dx, ((0,0), (0,0), (pad,pad), (pad,pad)), mode='constant', constant_values=0) 5. 获取输入和输出的尺寸: N, C, H, W = x.shape F, _, HH, WW = w.shape _, _, H_out, W_out = dout.shape 6. 计算db: for f in range(F): db[f] = np.sum(dout[:, f, :, :]) 7. 计算dw和dx_padded: 遍历样本n,滤波器f,以及输出位置i, j: # 定位输入窗口 h_start = i * stride h_end = h_start + HH w_start = j * stride w_end = w_start + WW # 当前梯度值:dout[n, f, i, j] 是一个标量 # 对于dw[f]:累加 x_padded[n, :, h_start:h_end, w_start:w_end] * dout[n, f, i, j] # 对于dx_padded:将 w[f] * dout[n, f, i, j] 累加到 dx_padded[n, :, h_start:h_end, w_start:w_end] 注意:前向传播时我们使用了填充后的输入x_padded,这里反向传播同样操作在填充后的梯度dx_padded上。 8. 由于我们遍历了所有位置,所以dw[f]已经累积了所有样本和所有位置。但注意,在实现时,我们可以在循环中累加。 9. 最后,从dx_padded中裁剪出未填充的部分,得到dx: dx = dx_padded[:, :, pad:pad+H, pad:pad+W] 但是注意:前向传播时我们使用了填充,而反向传播时,我们计算的是原始输入(未填充)的梯度,所以填充部分不应该保留。因此,我们通过裁剪得到dx。 然而,我们也可以先初始化一个与x形状相同的dx(全0),然后创建一个填充版本的dx_padded(与正向传播的x_padded形状相同),然后我们在循环中更新dx_padded,最后裁剪得到dx。 具体代码结构: 步骤: - 恢复缓存 - 获取参数 - 初始化梯度 - 填充dx为dx_padded(初始为0) - 计算db - 循环计算dw和dx_padded - 裁剪dx_padded得到dx 注意:由于输入x可能有填充,而dx的梯度只需要原始区域,所以最后裁剪。 但是,在循环中,我们需要前向传播时的填充输入x_padded吗?在反向传播中,计算dw时,我们需要前向传播的输入数据(因为dw = x * dout),所以我们需要缓存x_padded吗? 实际上,在正向传播中,我们缓存的是原始的x(未填充),所以这里我们需要重新填充?或者我们在正向传播时缓存了填充后的x_padded? 回顾正向传播函数conv_forward_naive,我们缓存的是(x, w, b, conv_param),其中x是原始输入。所以这里我们需要重新填充原始输入x,用于计算dw(因为dw需要乘以输入数据)。 因此,在反向传播中,我们需要重新进行填充: x_padded = np.pad(x, ((0,0), (0,0), (pad,pad), (pad,pad)), mode='constant') 但是,为了避免重复计算,我们可以在正向传播时缓存填充后的x_padded。然而,在作业中,正向传播缓存的是(x, w, b, conv_param),所以我们只能重新填充。 所以,在反向传播中: x_padded = np.pad(x, ((0,0),(0,0),(pad,pad),(pad,pad)), mode='constant', constant_values=0) 然后,我们使用这个x_padded来计算dw。 然而,我们也可以不缓存填充后的输入,因为我们可以通过原始输入和pad参数重新构建。这样,在反向传播中,我们重新填充x得到x_padded。 因此,具体步骤调整: 1. 从缓存中取出x(原始输入,未填充),w, b, conv_param。 2. 获取pad, stride。 3. 填充x:x_padded = np.pad(...) [用于计算dw] 4. 初始化dx:np.zeros_like(x),然后创建dx_padded = np.pad(dx, ...) [注意:这里我们初始化为0,然后累加,最后裁剪] 但是,注意:dx_padded的初始值应该是0,然后我们通过循环将每个位置的反向传播梯度累加进去。 代码实现: 由于我们需要计算三个梯度:dx, dw, db。 初始化: db = 前面计算的方式 dw = np.zeros_like(w) dx = np.zeros_like(x) # 然后创建dx_padded = np.pad(dx, ...) [注意:这样dx_padded的初始值就是0,且形状是填充后的] 然后循环: for n in range(N): # 每个样本 for f in range(F): # 每个滤波器 for i in range(H_out): for j in range(W_out): # 计算窗口位置 h_start = i * stride h_end = h_start + HH w_start = j * stride w_end = w_start + WW # 计算dw[f]:需要x_padded的对应窗口 # 注意:dout[n, f, i, j] 是一个标量 dw[f] += x_padded[n, :, h_start:h_end, w_start:w_end] * dout[n, f, i, j] # 计算dx_padded:将w[f]乘以dout[n,f,i,j]加到dx_padded的对应窗口 dx_padded[n, :, h_start:h_end, w_start:w_end] += w[f] * dout[n, f, i, j] 最后,从dx_padded中取出未填充部分: dx = dx_padded[:, :, pad:pad+H, pad:pad+W] 但是,注意:这样实现会有四重循环,效率较低,但题目要求naive实现,所以可以接受。 另外,db的计算可以不用循环,但这里我们也可以放在循环外,用向量化方式计算。 优化db计算: db = np.sum(dout, axis=(0,2,3)) # 按样本、高度、宽度求和,保留滤波器维度 因此,我们可以不用循环f来计算db。 总结代码: 注意:输入x的尺寸是(N, C, H, W),dout的尺寸是(N, F, H_out, W_out) 步骤: pad = conv_param['pad'] stride = conv_param['stride'] N, C, H, W = x.shape F, C, HH, WW = w.shape H_out = (H + 2 * pad - HH) // stride + 1 W_out = (W + 2 * pad - WW) // stride + 1 # 检查dout的尺寸 assert dout.shape == (N, F, H_out, W_out) # 对输入x进行填充(用于计算dw) x_padded = np.pad(x, ((0,0), (0,0), (pad,pad), (pad,pad)), mode='constant', constant_values=0) # 初始化梯度 dx = np.zeros_like(x) dw = np.zeros_like(w) db = np.zeros_like(b) # 创建dx的填充版本(初始为0),用于累加 dx_padded = np.pad(dx, ((0,0), (0,0), (pad,pad), (pad,pad)), mode='constant', constant_values=0) # 计算db: 对每个滤波器f,对dout的所有样本和所有空间位置求和 db = np.sum(dout, axis=(0,2,3)) # 形状为(F,) # 四重循环 for n in range(N): for f in range(F): for i in range(H_out): for j in range(W_out): # 定位窗口 h_start = i * stride h_end = h_start + HH w_start = j * stride w_end = w_start + WW # 计算dw[f]: 累加 x_padded[n, :, h_start:h_end, w_start:w_end] * dout[n, f, i, j] dw[f] += x_padded[n, :, h_start:h_end, w_start:w_end] * dout[n, f, i, j] # 计算dx_padded: 累加 w[f] * dout[n, f, i, j] dx_padded[n, :, h_start:h_end, w_start:w_end] += w[f] * dout[n, f, i, j] # 裁剪dx_padded得到dx dx = dx_padded[:, :, pad:pad+H, pad:pad+W] 返回dx, dw, db 注意:我们这里使用了两个填充数组:x_padded(用于计算dw)和dx_padded(用于计算dx)。x_padded是前向传播时的输入填充,而dx_padded是反向传播时用于累加梯度的填充数组。 但是,我们也可以不用dx_padded,而是直接操作dx的填充版本?我们上面就是这样做的。 然而,还有一种更节省内存的方法:我们不需要一开始就创建dx_padded,而是在循环中直接操作一个填充数组,最后再裁剪。我们上面的做法就是先创建了一个填充数组(初始为0),然后累加,最后裁剪。 这个实现是直接的,但效率不高。不过题目要求naive实现,所以可以接受。 测试:运行数值梯度检查。 注意:由于循环较多,对于大尺寸输入可能会很慢,但测试时使用小尺寸数据。 最后返回 (dx, dw, db) 代码实现如下:</think>### 卷积层反向传播实现 在 `cs231n/layers.py` 中实现卷积层的反向传播函数 `conv_backward_naive`: ```python def conv_backward_naive(dout, cache): """ 实现卷积层的反向传播 输入: - dout: 上游梯度 (N, F, H', W') - cache: 前向传播时缓存的元组 (x, w, b, conv_param) 返回: - dx: 输入数据梯度 (N, C, H, W) - dw: 滤波器权重梯度 (F, C, HH, WW) - db: 偏置项梯度 (F,) """ # 从缓存中获取前向传播数据 x, w, b, conv_param = cache pad = conv_param.get('pad', 0) stride = conv_param.get('stride', 1) # 获取各维度尺寸 N, C, H, W = x.shape F, _, HH, WW = w.shape _, _, H_out, W_out = dout.shape # 初始化梯度 dx = np.zeros_like(x) dw = np.zeros_like(w) db = np.zeros_like(b) # 对输入进行填充(与前向传播一致) x_padded = np.pad(x, ((0, 0), (0, 0), (pad, pad), (pad, pad)), mode='constant') dx_padded = np.pad(dx, ((0, 0), (0, 0), (pad, pad), (pad, pad)), mode='constant') # 反向传播计算 for n in range(N): # 遍历每个样本 for f in range(F): # 遍历每个滤波器 for i in range(H_out): # 遍历输出高度 for j in range(W_out): # 遍历输出宽度 # 计算输入窗口位置 h_start = i * stride h_end = h_start + HH w_start = j * stride w_end = w_start + WW # 计算偏置梯度 db[f] += dout[n, f, i, j] # 计算权重梯度 dw[f] += x_padded[n, :, h_start:h_end, w_start:w_end] * dout[n, f, i, j] # 计算输入梯度 dx_padded[n, :, h_start:h_end, w_start:w_end] += w[f] * dout[n, f, i, j] # 从填充梯度中提取有效部分 dx = dx_padded[:, :, pad:pad+H, pad:pad+W] return dx, dw, db ``` ### 反向传播关键步骤说明: 1. **梯度初始化**: - `dx`: 输入数据梯度,形状与原始输入 `x` 相同 - `dw`: 滤波器权重梯度,形状与权重 `w` 相同 - `db`: 偏置项梯度,形状与偏置 `b` 相同 2. **填充处理**: - 前向传播中的填充需要在反向传播中保持一致 - 创建填充版本的 `dx_padded` 用于中间计算 3. **梯度计算**: - **偏置梯度 (db)**: 累加所有位置的上游梯度 - **权重梯度 (dw)**: 输入数据窗口 × 上游梯度 - **输入梯度 (dx)**: 滤波器权重 × 上游梯度 4. **梯度累积**: - 每个输出位置对梯度的贡献需要累加 - 同一输入位置可能被多个输出位置使用 ### 数值梯度检查方法: ```python from cs231n.gradient_check import eval_numerical_gradient_array # 生成随机测试数据 x = np.random.randn(2, 3, 8, 8) w = np.random.randn(5, 3, 3, 3) b = np.random.randn(5) dout = np.random.randn(2, 5, 4, 4) conv_param = {'stride': 2, 'pad': 1} # 前向传播获取缓存 _, cache = conv_forward_naive(x, w, b, conv_param) # 计算解析梯度 dx, dw, db = conv_backward_naive(dout, cache) # 数值梯度检查 dx_num = eval_numerical_gradient_array(lambda x: conv_forward_naive(x, w, b, conv_param)[0], x, dout) dw_num = eval_numerical_gradient_array(lambda w: conv_forward_naive(x, w, b, conv_param)[0], w, dout) db_num = eval_numerical_gradient_array(lambda b: conv_forward_naive(x, w, b, conv_param)[0], b, dout) # 比较误差 print('dx 误差:', rel_error(dx_num, dx)) print('dw 误差:', rel_error(dw_num, dw)) print('db 误差:', rel_error(db_num, db)) ``` ### 反向传播计算要点: 1. **链式法则应用**:输出梯度 → 权重/输入梯度 2. **位置对应关系**:输出位置 (i,j) 对应输入区域 (h_start:h_end, w_start:w_end) 3. **梯度累加原则**:每个输出位置对输入/权重梯度的贡献需要累加 4. **填充处理**:梯度计算需考虑前向传播的填充,最后去除填充
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值