华为深度学习面试手撕题:手写nn.Conv2d()函数

题目

只允许利用numpy包,实现Pytorch二维卷积函数nn.Conv2d()

解答

此代码考察二维卷积的概念,详见:

6.2. 图像卷积 — 动手学深度学习 2.0.0 documentation

6.3. 填充和步幅 — 动手学深度学习 2.0.0 documentation

6.4. 多输入多输出通道 — 动手学深度学习 2.0.0 documentation

代码实现:

import numpy as np
import torch
import torch.nn as nn

def conv2d(input, weight, bias=None, stride=1, padding=0):
    """
    实现二维卷积操作
    
    参数:
        input:  输入数据, 形状为 (batch_size, in_channels, height, width)
        weight: 卷积核, 形状为 (out_channels, in_channels, kernel_h, kernel_w)
        bias:   偏置项, 形状为 (out_channels,)
        stride: 步长, 可以是整数或元组 (stride_h, stride_w)
        padding: 填充, 可以是整数或元组 (pad_h, pad_w)
    
    返回:
        输出特征图, 形状为 (batch_size, out_channels, out_h, out_w)
    """
    # 解析步长和填充参数
    if isinstance(stride, int):
        stride_h = stride_w = stride
    else:
        stride_h, stride_w = stride
    
    if isinstance(padding, int):
        pad_h = pad_w = padding
    else:
        pad_h, pad_w = padding
    
    # 获取输入尺寸
    batch_size, in_channels, in_h, in_w = input.shape
    out_channels, _, kernel_h, kernel_w = weight.shape
    
    # 计算输出尺寸
    out_h = (in_h + 2 * pad_h - kernel_h) // stride_h + 1
    out_w = (in_w + 2 * pad_w - kernel_w) // stride_w + 1
    
    # 添加填充
    if pad_h > 0 or pad_w > 0:
        # 使用零填充
        padded_input = np.pad(
            input, 
            ((0, 0), (0, 0), (pad_h, pad_h), (pad_w, pad_w)),
            mode='constant'
        )
    else:
        padded_input = input
    
    # 初始化输出数组
    output = np.zeros((batch_size, out_channels, out_h, out_w))
    
    # 执行卷积操作
    for b in range(batch_size):
        for c_out in range(out_channels):
            for h_out in range(out_h):
                for w_out in range(out_w):
                    # 计算输入窗口位置
                    h_start = h_out * stride_h
                    w_start = w_out * stride_w
                    h_end = h_start + kernel_h
                    w_end = w_start + kernel_w
                    
                    # 提取输入窗口
                    window = padded_input[b, :, h_start:h_end, w_start:w_end]
                    
                    # 计算点积 (卷积操作)
                    conv_val = np.sum(window * weight[c_out])
                    
                    # 添加偏置
                    if bias is not None:
                        conv_val += bias[c_out]
                    
                    # 存储结果
                    output[b, c_out, h_out, w_out] = conv_val
    
    return output

import torch
import torch.nn as nn

if __name__ == "__main__":
    # 创建测试数据
    np.random.seed(42)
    
    # 输入数据: (batch_size=2, in_channels=3, height=5, width=5)
    input_data = np.random.randn(2, 3, 5, 5).astype(np.float32)
    
    # 卷积核: (out_channels=2, in_channels=3, kernel_h=3, kernel_w=3)
    weights = np.random.randn(2, 3, 3, 3).astype(np.float32)
    
    # 偏置: (out_channels=2)
    bias = np.array([0.5, -0.5], dtype=np.float32)
    
    # 转换为 PyTorch 张量
    input_torch = torch.tensor(input_data)
    weights_torch = torch.tensor(weights)
    bias_torch = torch.tensor(bias)
    
    # 测试1: 无填充, 步长=1
    print("测试1: 无填充, 步长=1")
    output1 = conv2d(input_data, weights, bias, stride=1, padding=0)
    
    # 创建 PyTorch 卷积层
    conv1_nn = nn.Conv2d(in_channels=3, out_channels=2, kernel_size=3, 
                         stride=1, padding=0, bias=True)
    # 设置权重和偏置
    with torch.no_grad():
        conv1_nn.weight.data = weights_torch
        conv1_nn.bias.data = bias_torch
    
    # 计算 PyTorch 输出
    output1_nn = conv1_nn(input_torch).detach().numpy()
    
    # 比较结果
    print("自定义实现与PyTorch输出是否一致:", np.allclose(output1, output1_nn, atol=1e-6))
    print(f"输出形状: {output1.shape}")
    print("自定义实现输出 (第一个样本的第一个通道前2x2):")
    print(output1[0, 0, :2, :2])
    print("PyTorch输出 (第一个样本的第一个通道前2x2):")
    print(output1_nn[0, 0, :2, :2])
    
    # 测试2: 填充=1, 步长=1
    print("\n测试2: 填充=1, 步长=1")
    output2 = conv2d(input_data, weights, bias, stride=1, padding=1)
    
    # 创建 PyTorch 卷积层
    conv2_nn = nn.Conv2d(in_channels=3, out_channels=2, kernel_size=3, 
                         stride=1, padding=1, bias=True)
    with torch.no_grad():
        conv2_nn.weight.data = weights_torch
        conv2_nn.bias.data = bias_torch
    
    output2_nn = conv2_nn(input_torch).detach().numpy()
    
    print("自定义实现与PyTorch输出是否一致:", np.allclose(output2, output2_nn, atol=1e-6))
    print(f"输出形状: {output2.shape}")
    print("自定义实现输出 (第一个样本的第一个通道前2x2):")
    print(output2[0, 0, :2, :2])
    print("PyTorch输出 (第一个样本的第一个通道前2x2):")
    print(output2_nn[0, 0, :2, :2])
    
    # 测试3: 无填充, 步长=2
    print("\n测试3: 无填充, 步长=2")
    output3 = conv2d(input_data, weights, bias, stride=2, padding=0)
    
    # 创建 PyTorch 卷积层
    conv3_nn = nn.Conv2d(in_channels=3, out_channels=2, kernel_size=3, 
                         stride=2, padding=0, bias=True)
    with torch.no_grad():
        conv3_nn.weight.data = weights_torch
        conv3_nn.bias.data = bias_torch
    
    output3_nn = conv3_nn(input_torch).detach().numpy()
    
    print("自定义实现与PyTorch输出是否一致:", np.allclose(output3, output3_nn, atol=1e-6))
    print(f"输出形状: {output3.shape}")
    print("自定义实现输出 (第一个样本的第一个通道):")
    print(output3[0, 0])
    print("PyTorch输出 (第一个样本的第一个通道):")
    print(output3_nn[0, 0])
    
    # 测试4: 无偏置
    print("\n测试4: 无偏置")
    output4 = conv2d(input_data, weights, None, stride=1, padding=0)
    
    # 创建 PyTorch 卷积层
    conv4_nn = nn.Conv2d(in_channels=3, out_channels=2, kernel_size=3, 
                         stride=1, padding=0, bias=False)
    with torch.no_grad():
        conv4_nn.weight.data = weights_torch
    
    output4_nn = conv4_nn(input_torch).detach().numpy()
    
    print("自定义实现与PyTorch输出是否一致:", np.allclose(output4, output4_nn, atol=1e-6))
    print("自定义实现输出 (第一个样本的第一个通道前2x2):")
    print(output4[0, 0, :2, :2])
    print("PyTorch输出 (第一个样本的第一个通道前2x2):")
    print(output4_nn[0, 0, :2, :2])

'''
测试1: 无填充, 步长=1
自定义实现与PyTorch输出是否一致: True
输出形状: (2, 2, 3, 3)
自定义实现输出 (第一个样本的第一个通道前2x2):
[[-6.4546895  -2.49435902]
 [-6.27663374  3.31103873]]
PyTorch输出 (第一个样本的第一个通道前2x2):
[[-6.4546895 -2.4943593]
 [-6.276634   3.3110385]]

测试2: 填充=1, 步长=1
自定义实现与PyTorch输出是否一致: True
输出形状: (2, 2, 5, 5)
自定义实现输出 (第一个样本的第一个通道前2x2):
[[ 1.17402518  1.28695214]
 [-0.09722954 -6.4546895 ]]
PyTorch输出 (第一个样本的第一个通道前2x2):
[[ 1.1740253   1.2869523 ]
 [-0.09722958 -6.4546895 ]]

测试3: 无填充, 步长=2
自定义实现与PyTorch输出是否一致: True
输出形状: (2, 2, 2, 2)
自定义实现输出 (第一个样本的第一个通道):
[[-6.4546895   1.38441801]
 [ 3.1934371  -1.1537782 ]]
PyTorch输出 (第一个样本的第一个通道):
[[-6.4546895  1.3844179]
 [ 3.1934366 -1.1537789]]

测试4: 无偏置
自定义实现与PyTorch输出是否一致: True
自定义实现输出 (第一个样本的第一个通道前2x2):
[[-6.9546895  -2.99435902]
 [-6.77663374  2.81103873]]
PyTorch输出 (第一个样本的第一个通道前2x2):
[[-6.9546895 -2.9943593]
 [-6.776634   2.811039 ]]
'''
### 华为OD模式下AI算子面试中的常见问及解答 #### 常见问分析 在华为OD模式下的AI算子开发岗位中,常见的技术考察点通常围绕以下几个方面展开:深度学习框架优化、模型推理加速、硬件适配以及底层性能调优等。以下是针对这些领域可能涉及的具体问及其解答。 --- #### 参数初始化策略的重要性与实现方式 参数初始化对于神经网络训练过程至关重要,不恰当的初始值可能导致梯度消失或爆炸等问。常用的几种初始化方法如下: - **Lecun 初始化**适用于激活函数为tanh的情况,其核心思想是通过调整权重分布来保持信号传播过程中方差稳定[^1]。 - **Xavier/Glorot 初始化**进一步扩展了这一理念,在考虑输入节点数和输出节点数的基础上设定合理的标准差范围,从而更好地平衡前向传递与反向传播期间的信息流动。 - **Kaiming Initialization (He)**特别适合ReLU类非线性变换场景,它依据正向传导特性重新定义了推荐的标准偏差计算公式\[std=\sqrt{\frac{2}{n_{in}}}\]。 ```python import torch.nn.init as init def initialize_weights(m): if isinstance(m, nn.Conv2d): init.kaiming_normal_(m.weight.data) elif isinstance(m, nn.Linear): init.xavier_uniform_(m.weight.data) model.apply(initialize_weights) ``` --- #### 图像处理基础知识——ROI操作详解 区域感兴趣(Region of Interest, ROI)是指图像或者视频帧内的特定部分,该部分内容往往承载着最重要的视觉特征信息。在实际应用当中,比如目标检测任务里提取候选框对应像素数据作为后续分类器输入源便是典型例子之一。 具体到实现层面,可以采用裁剪、池化等方式完成对原始图片不同位置子集的选择工作;而在某些高级架构设计之中,则引入专门模块如RoIAlign解决传统整数索引取样带来的几何失真现象。 ```python from torchvision.ops import roi_align features = ... # 输入特征图 Tensor[N,C,H,W] rois = ... # Region Of Interests Bounding Boxes [num_rois, 5] output_size = (7, 7) # 输出尺寸 H_out x W_out roi_features = roi_align(features, rois, output_size=output_size, spatial_scale=1/8., sampling_ratio=-1) ``` --- #### 性能优化技巧探讨 为了提升AI算子运行效率,可以从多个角度出发实施改进措施: - 数据预处理阶段减少冗余运算量; - 利用半精度浮点数(FP16)代替单精度(Single Precision FP32),既节省存储空间又能加快乘加指令执行速度; - 对卷积层做Winograd快速傅立叶转换近似替代常规滑动窗口机制降低理论复杂度O(n²)->O(nlogn)。 此外还需注意结合具体部署平台特点选取最适宜的技术方案组合拳出击才能达到事半功倍的效果。 --- #### 编程实践案例分享 下面给出一段简单代码片段展示如何利用PyTorch构建自定义Layer并对其进行高效初始化的过程: ```python class CustomConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0): super(CustomConv, self).__init__() self.conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size, stride=stride, padding=padding) # 使用 Kaiming He 方法初始化 Convolutional Layer 权重矩阵 nn.init.kaiming_normal_(self.conv.weight, mode='fan_in', nonlinearity='relu') def forward(self, x): return F.relu(self.conv(x)) ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北京地铁1号线

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值