• MindSpore易点通·精讲系列–网络构建之Conv2d算子


    Dive Into MindSpore – Conv2d Operator For Network Construction

    MindSpore易点通·精讲系列–网络构建之Conv2d算子

    本文开发环境

    • Ubuntu 20.04
    • Python 3.8
    • MindSpore 1.7.0

    本文内容摘要

    • 先看文档
    • 普通卷积
    • 深度卷积
    • 空洞卷积
    • 数据格式
    • 填充方式
    • 输出维度
    • 本文总结
    • 本文参考

    1. 先看文档

    老传统,先看官方文档。
    api

    参数解读:

    • in_channels – 输入通道数
    • out_channels – 输出通道数
    • kernel_size – 二维卷积核的高度和宽度。值应该为整型(代表高度和宽度均为该值)或两个整型的tuple(分别代表高度和宽度值)
    • stride – 二维卷积的移动步长
    • pad_mode – 填充模式
    • padding – 填充数量
    • dilation – 二维卷积核膨胀尺寸,输入值同kernel_size。空洞卷积参数
    • group – 将过滤器拆分为组。深度卷积参数
    • has_bias – 是否添加偏置
    • data_format – 输入数据的数据格式,NCHWNHWC

    2. 普通卷积

    普通卷积,又可以称为常规卷积。由于是在深度学习相关课程中最先接触到的CNN卷积方式,本文不再对其原理展开介绍。下面通过一个实例来介绍MindSpore中的用法。

    例如:

    对于二维的8×8原始图像,图像格式为RGB(即通道数为3),可以认为这是一个3维图片,数据维度为 3×8×8(NCHW)或8×8×3(NHWC)。

    假设我们对上述图片进行普通卷积操作,卷积核大小为3×3,步长为1,卷积后的输出通道数为4,padding方式为same,即输入和输出的高和宽一致。

    其示意图如下所示:
    common_conv

    如何用MindSpore来定义这样的普通卷积呢,示例代码如下:

    这里的批数为2

    import numpy as np
    
    from mindspore import nn
    from mindspore.common import dtype as mstype
    from mindspore.common import Tensor
    
    
    def common_conv_demo():
        img_data = np.random.rand(2, 3, 8, 8)
    
        ms_in = Tensor(img_data, dtype=mstype.float32)
        conv_op = nn.Conv2d(3, 4, 3, 1)
        ms_out = conv_op(ms_in)
    
        print("in shape: {}".format(ms_in.shape), flush=True)
        print("out shape: {}".format(ms_out.shape), flush=True)
    
    
    def main():
        common_conv_demo()
    
    
    if __name__ == "__main__":
        main()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    代码解读:

    核心代码为nn.Conv2d(3, 4, 3, 1)

    • 参数数字3表示输入通道

    • 参数数字4表示输出通道。

    • 参数数字3表示卷积核大小,这里因为高&宽的卷积值相等,所以使用整型表示。

    • 参数数字1表示卷积移动步长。

    • nn.Conv2d默认卷积方式为same,故没有在这里的参数中体现。

    将上述代码保存到common_conv2d.py文件,使用如下命令运行:

    python3 common_conv2d.py
    
    • 1

    输出内容为:

    可以看到输出的通道为4,因为填充方式为same,输出的高度和宽度与输入数据相同。

    in shape: (2, 3, 8, 8)
    out shape: (2, 4, 8, 8)
    
    • 1
    • 2

    3. 深度卷积

    深度卷积(Depthwise Convolution)的一个卷积核负责一个通道,一个通道只被一个卷积核卷积,可以看出其卷积方式与普通卷积明显不同。深度卷积一般与逐点卷积(Pointwise Convolution)结合,组成深度可分离卷积(Depthwise Separable Convolution),当然也可以单独使用,比如,经典的MobileNet网络就用到了深度可分离卷积。

    那么在MindSpore中如何实现深度卷积呢,我们先从文档说起。

    • group (int) – Splits filter into groups, in_channels and out_channels must be divisible by group. If the group is equal to in_channels and out_channels, this 2D convolution layer also can be called 2D depthwise convolution layer. Default: 1.
    • group (int) – 将过滤器拆分为组, in_channels 和 out_channels 必须可被 group 整除。如果组数等于 in_channels 和 out_channels ,这个二维卷积层也被称为二维深度卷积层。默认值:1.

    从文档可以看出,当in_channelsout_channelsgroup三个参数的值相等时,可以认为即为2D的深度卷积。下面通过一个案例来进一步讲解。

    例如:

    对于二维的8×8原始图像,图像格式为RGB(即通道数为3),可以认为这是一个3维图片,数据维度为 3×8×8(NCHW)或8×8×3(NHWC)。

    假设我们对上述图片进行深度卷积操作,卷积核大小为3×3,步长为1,卷积后的输出通道数为3(与输入通道一致),padding方式为same,即输入和输出的高和宽一致。

    其示意图如下所示:
    depthwise_conv

    MindSpore示例代码如下:

    import numpy as np
    
    from mindspore import nn
    from mindspore.common import dtype as mstype
    from mindspore.common import Tensor
    
    
    def depthwise_conv_demo():
        img_data = np.random.rand(2, 3, 8, 8)
    
        ms_in = Tensor(img_data, dtype=mstype.float32)
        conv_op = nn.Conv2d(3, 3, 3, 1, group=3)
        ms_out = conv_op(ms_in)
    
        print("in shape: {}".format(ms_in.shape), flush=True)
        print("out shape: {}".format(ms_out.shape), flush=True)
    
    
    def main():
        depthwise_conv_demo()
    
    
    if __name__ == "__main__":
        main()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    代码解读:

    核心代码为nn.Conv2d(3, 3, 3, 1, group=3)

    • 参数数字3表示输入通道。

    • 参数数字4表示输出通道。

    • 参数数字3表示卷积核大小,这里因为高&宽的卷积值相等,所以使用整型表示。

    • 参数数字1表示卷积移动步长。

    • nn.Conv2d默认卷积方式为same,故没有在这里的参数中体现。

    • 参数gropu=3与前面的输入通道3和输出通道3一致,是这里实现深度卷积的关键参数。

    将上述代码保存到depthwise_conv2d.py文件,使用如下命令运行:

    python3 depthwise_conv2d.py
    
    • 1

    输出内容为:

    可以看到输出的通道为3(与输入数据通道数一致),因为填充方式为same,输出的高度和宽度与输入数据相同。

    in shape: (2, 3, 8, 8)
    out shape: (2, 3, 8, 8)
    
    • 1
    • 2

    一点补充

    细心的读者可能会问,逐点卷积如何实现呢?这里逐点卷积可以看成普通卷积的特例,即卷积核为1×1的普通卷积(其他参数视具体而定),再参考第2节的内容,就可以很容易的实现出来了。

    4. 空洞卷积

    空洞卷积(Dilated Convolution),又称扩张卷积、膨胀卷积,是在标准的卷积核中注入空洞,以此来增加模型的感受野(reception field)。相比原来的正常卷积操作,扩张卷积多了一个参数: dilation rate,指的是卷积核的点的间隔数量,比如常规的卷积操作dilatation rate为1。
    dilated_conv

    (a)图对应3x3的1-dilated conv,和普通的卷积操作一样,(b)图对应3x3的2-dilated conv,实际的卷积kernel size还是3x3,但是空洞为1,也就是对于一个7x7的图像patch,只有9个红色的点和3x3的kernel发生卷积操作,其余的点略过。也可以理解为kernel的size为7x7,但是只有图中的9个点的权重不为0,其余都为0。 可以看到虽然kernel size只有3x3,但是这个卷积的感受野已经增大到了7x7(如果考虑到这个2-dilated conv的前一层是一个1-dilated conv的话,那么每个红点就是1-dilated的卷积输出,所以感受野为3x3,所以1-dilated和2-dilated合起来就能达到7x7的conv),©图是4-dilated conv操作,同理跟在两个1-dilated和2-dilated conv的后面,能达到15x15的感受野。对比传统的conv操作,3层3x3的卷积加起来,stride为1的话,只能达到(kernel-1)*layer+1=7的感受野,也就是和层数layer成线性关系,而dilated conv的感受野是指数级的增长。

    空洞卷积的好处是不做pooling损失信息的情况下,加大了感受野,让每个卷积输出都包含较大范围的信息。在图像需要全局信息或者语音文本需要较长的sequence信息依赖的问题中,都能很好的应用Dilated Convolution,比如语音合成WaveNet、机器翻译ByteNet中。

    一点补充

    • 在上图的(b)中,对于kernel_size为3×3,dilation rate=2的情况,其实际kernel_size大小为7×7。
    • 但是在MindSpore(Pytorch)框架内,其计算公式为dilation∗(kernelsize−1)+1,即实际kernel_size大小为5×5。
    • 可以看出,上图中对卷积核的周边做了同样的膨胀,而框架在具体实现时,只对卷积核内部做膨胀。

    下面通过一段代码示例,来看看MindSpore中的具体实现。代码如下:

    为了方便观察输出数据的高度和宽度,这里将padding方式设置为valid

    import numpy as np
    
    from mindspore import nn
    from mindspore.common import dtype as mstype
    from mindspore.common import Tensor
    
    
    def dilated_conv_demo():
        img_data = np.random.rand(2, 3, 8, 8)
    
        ms_in = Tensor(img_data, dtype=mstype.float32)
    
        common_conv_op_0 = nn.Conv2d(3, 4, 3, 1, pad_mode="valid")
        common_conv_op_1 = nn.Conv2d(3, 4, 5, 1, pad_mode="valid")
        dilated_conv_op = nn.Conv2d(3, 4, 3, 1, pad_mode="valid", dilation=2)
    
        common_out_0 = common_conv_op_0(ms_in)
        common_out_1 = common_conv_op_1(ms_in)
        dilated_out = dilated_conv_op(ms_in)
    
        print("common out 0 shape: {}".format(common_out_0.shape), flush=True)
        print("common out 1 shape: {}".format(common_out_1.shape), flush=True)
        print("dilated out shape: {}".format(dilated_out.shape), flush=True)
    
    
    def main():
        dilated_conv_demo()
    
    
    if __name__ == "__main__":
        main()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    代码解读:

    • common_conv_op_0common_conv_op_1皆为普通卷积,其卷积核大小分别为3×3和5×5。
    • dilated_conv_op为空洞卷积,卷积核为3×3,但dilation设置为2。
    • 根据公式dilation∗(kernelsize−1)+1可知,dilated_conv_op就卷积核大小来看,效果类似于5×5普通卷积。验证参见输出数据的数据维度。

    将上述代码保存到dilated_conv2d.py文件,使用如下命令运行:

    python3 dilated_conv2d.py
    
    • 1

    输出内容为:

    可以看出common out 1 shapedilated out shape相等,验证了代码解读的第三条。

    common out 0 shape: (2, 4, 6, 6)
    common out 1 shape: (2, 4, 4, 4)
    dilated out shape: (2, 4, 4, 4)
    
    • 1
    • 2
    • 3

    5. 数据格式

    特别注意:NHWC数据格式目前只支持在GPU硬件下使用。

    Conv2d中,输入数据的数据格式可选值有NHWCNCHW,默认值为NCHW。其中各个字母的含义如下:

    • N – 批数
    • C – 通道数
    • H – 高度
    • W – 宽度

    那么两种数据格式又有什么区别呢,先从一段错误代码讲起:

    在下面的代码中,我们创建数据img_data,并且将通道放置到了最后一个维度,即数据格式为NHWC。但是Conv2d中默认的数据格式为NCHW,那么运行起来如何呢?

    import numpy as np
    
    from mindspore import nn
    from mindspore.common import dtype as mstype
    from mindspore.common import Tensor
    
    def data_format_demo():
        img_data = np.random.rand(2, 8, 8, 3)
        ms_in = Tensor(img_data, dtype=mstype.float32)
    
        common_conv_op = nn.Conv2d(3, 4, 3, 1)
        ms_out = common_conv_op(ms_in)
    
        print("common out shape: {}".format(ms_out.shape), flush=True)
    
    
    def main():
        data_format_demo()
    
    
    if __name__ == "__main__":
        main()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    将上述代码保存到format_conv2d.py文件,使用如下命令运行:

    python3 format_conv2d.py
    
    • 1

    会输出报错信息,报错内容如下:

    错误信息中并没有显式提示是数据格式问题,所以对于新手来说这个问题可能具有迷惑性。

    WARNING: Logging before InitGoogleLogging() is written to STDERR
    [CRITICAL] CORE(29160,0x102270580,Python):2022-07-31-16:35:26.406.143 [build/mindspore/merge/mindspore/core/ops_merge.cc:6753] Conv2dInferShape] For 'Conv2D', 'C_in' of input 'x' shape divide by parameter 'group' should be equal to 'C_in' of input 'weight' shape: 3, but got 'C_in' of input 'x' shape: 8, and 'group': 1
    [WARNING] UTILS(29160,0x102270580,Python):2022-07-31-16:35:26.409.046 [mindspore/ccsrc/utils/comm_manager.cc:78] GetInstance] CommManager instance for CPU not found, return default instance.
    Traceback (most recent call last):
      File "/Users/kaierlong/Codes/OpenI/kaierlong/Dive_Into_MindSpore/code/chapter_02/01_conv2d.py", line 74, in <module>
        main()
      File "/Users/kaierlong/Codes/OpenI/kaierlong/Dive_Into_MindSpore/code/chapter_02/01_conv2d.py", line 70, in main
        data_format_demo()
      File "/Users/kaierlong/Codes/OpenI/kaierlong/Dive_Into_MindSpore/code/chapter_02/01_conv2d.py", line 64, in data_format_demo
        ms_out = common_conv_op(ms_in)
      File "/Users/kaierlong/Pyenvs/env_mix_dl/lib/python3.9/site-packages/mindspore/nn/cell.py", line 586, in __call__
        out = self.compile_and_run(*args)
      File "/Users/kaierlong/Pyenvs/env_mix_dl/lib/python3.9/site-packages/mindspore/nn/cell.py", line 964, in compile_and_run
        self.compile(*inputs)
      File "/Users/kaierlong/Pyenvs/env_mix_dl/lib/python3.9/site-packages/mindspore/nn/cell.py", line 937, in compile
        _cell_graph_executor.compile(self, *inputs, phase=self.phase, auto_parallel_mode=self._auto_parallel_mode)
      File "/Users/kaierlong/Pyenvs/env_mix_dl/lib/python3.9/site-packages/mindspore/common/api.py", line 1006, in compile
        result = self._graph_executor.compile(obj, args_list, phase, self._use_vm_mode())
    RuntimeError: build/mindspore/merge/mindspore/core/ops_merge.cc:6753 Conv2dInferShape] For 'Conv2D', 'C_in' of input 'x' shape divide by parameter 'group' should be equal to 'C_in' of input 'weight' shape: 3, but got 'C_in' of input 'x' shape: 8, and 'group': 1
    The function call stack (See file '/Users/kaierlong/Codes/OpenI/kaierlong/Dive_Into_MindSpore/code/chapter_02/rank_0/om/analyze_fail.dat' for more details):
    # 0 In file /Users/kaierlong/Pyenvs/env_mix_dl/lib/python3.9/site-packages/mindspore/nn/layer/conv.py(286)
            if self.has_bias:
    # 1 In file /Users/kaierlong/Pyenvs/env_mix_dl/lib/python3.9/site-packages/mindspore/nn/layer/conv.py(285)
            output = self.conv2d(x, self.weight)
                     ^
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    那么如何才能正常运行呢,有两种做法,一种是修改输入数据的数据格式;一种是对算子中的data_format参数进行调整。展开来说,可以有三种方案。

    方案1:

    • 在数据预处理部分就将输入数据的数据格式规范成NCHW

    方案2:

    • ms_in 数据做一次转置操作(Transpose),将数据调整为NCHW

    方案3:

    • data_format设置为NHWC。特别注意,这一设置只在GPU下可用,CPUAscend下目前不可用。

    6. 填充方式

    Conv2d中,填充模式(pad_mode)可选值为samevalidpad,默认值:same。下面来介绍这三种填充方式。

    6.1 same

    对于same填充方式,官方描述如下:

    输出的高度和宽度分别与输入整除 stride 后的值相同。若设置该模式,padding 的值必须为0。

    具体示例代码参见第2小节。

    特别注意

    1. PytorchConv2d的区别,在Pytorch中,填充方式为same时,只允许stride为1,而MindSpore可以允许大于1的整数值。
    2. 由于stride允许1之外的整数,所以same模式下输出数据的高度和宽度未必和输入数据一致,这一点一定要谨记,至于输出数据的高度和宽度请参考第7小节。

    6.2 valid

    对于valid填充方式,官方描述如下:

    在不填充的前提下返回有效计算所得的输出。不满足计算的多余像素会被丢弃。如果设置此模式,则 padding 的值必须为0。

    具体示例代码参见第4小节。

    6.3 pad

    本节重点来讲解一下pad填充方式,对于pad填充方式,官方描述如下:

    对输入进行填充。在输入的高度和宽度方向上填充 padding 大小的0。如果设置此模式, padding 必须大于或等于0。

    pad填充方式配合使用的,还有padding参数。下面来看一下官方对padding参数的描述:

    输入的高度和宽度方向上填充的数量。数据类型为int或包含4个整数的tuple。如果 padding 是一个整数,那么上、下、左、右的填充都等于 padding 。如果 padding 是一个有4个整数的tuple,那么上、下、左、右的填充分别等于 padding[0] 、 padding[1] 、 padding[2] 和 padding[3] 。值应该要大于等于0,默认值:0。

    padding参数解读:

    • 允许两种数据形式

      • 一个整数 – 此时表示上下左右填充值皆为padding

      • tuple,且tuple内含四个整数 – 此时表示上、下、左、右的填充分别等于 padding[0] 、 padding[1] 、 padding[2] 和 padding[3]

    • 这里的上、下、左、右表示的高和宽,通俗解释即为上高、下高、左宽、右宽。

    下面通过两个示例来讲解两种数据形式。

    6.3.1 padding为一个整数

    例如,对于二维的8×8原始图像,图像格式为RGB(即通道数为3),可以认为这是一个3维图片,数据维度为 3×8×8(NCHW)或8×8×3(NHWC)。

    假设我们对上述图片进行普通卷积操作,卷积核大小为3×3,步长为1,卷积后的输出通道数为4,要求输出数据的高度和宽度与输入数据一致。

    其示意图如下所示:
    common_conv

    简单分析:上面的案例要求第2节中的代码就可以实现,在第2节中采用的pad_modesame,那么如果采用pad_modepad呢,代码如下:

    import numpy as np
    
    from mindspore import nn
    from mindspore.common import dtype as mstype
    from mindspore.common import Tensor
    
    
    def pad_demo_01():
        img_data = np.random.rand(2, 3, 8, 8)
    
        ms_in = Tensor(img_data, dtype=mstype.float32)
        conv_op = nn.Conv2d(3, 4, 3, 1, pad_mode="pad", padding=1)
        ms_out = conv_op(ms_in)
    
        print("in shape: {}".format(ms_in.shape), flush=True)
        print("out shape: {}".format(ms_out.shape), flush=True)
    
    
    def main():
        pad_demo_01()
    
    
    if __name__ == "__main__":
        main()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    代码解读:

    • 在卷积核大小为3×3,卷积步长为1的情况下,要想保证输出数据的高宽值与输入数据一致,在pad_modepad模式下,padding的值应该设置1。
    • 这里计算padding数值的公式参加第7小节输出维度部分。

    将上述代码保存到pad_conv2d_01.py文件,使用如下命令运行:

    python3 pad_conv2d_01.py
    
    • 1

    输出内容为:

    in shape: (2, 3, 8, 8)
    out shape: (2, 4, 8, 8)
    
    • 1
    • 2

    6.3.2 padding为四个整数tuple

    "padding为四个整数tuple"是"padding为一个整数"的一般情况。下面我们通过一个示例进行讲解。

    例如:输入数据仍然保持同6.3.1中一致,但是这次我们输出数据的高度和宽度要求有所变化,要求高度与输入数据一致,宽度为7(输入数据为8),这种情况下应该如何设定padding呢?

    实例代码如下:

    import numpy as np
    
    from mindspore import nn
    from mindspore.common import dtype as mstype
    from mindspore.common import Tensor
    
    
    def pad_demo_02():
        img_data = np.random.rand(2, 3, 8, 8)
    
        ms_in = Tensor(img_data, dtype=mstype.float32)
        conv_op = nn.Conv2d(3, 4, 3, 1, pad_mode="pad", padding=(1, 1, 1, 0))
        ms_out = conv_op(ms_in)
    
        print("in shape: {}".format(ms_in.shape), flush=True)
        print("out shape: {}".format(ms_out.shape), flush=True)
    
    
    def main():
        pad_demo_02()
    
    
    if __name__ == "__main__":
        main()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    代码解读:

    • 还记得padding[0] 、 padding[1] 、 padding[2] 和 padding[3] 这四个参数的意义,不记得没关系,再来一遍:通俗解释即为上高、下高、左宽、右宽。

    • 这里的要求是输出数据的高度与输入数据一致,宽度为7(输入数据为8)。所以上高、下高的padding6.3.1中一致,即1;左宽、右宽加起来的padding为1,因为不能存在非整数,这里我们分别设置为1、0(这里没有特别要求,也可以设置为0、1)。

    • 这里计算padding数值的公式参加第7小节输出维度部分。

    • 最终的核心代码即为nn.Conv2d(3, 4, 3, 1, pad_mode="pad", padding=(1, 1, 1, 0))

    将上述代码保存到pad_conv2d_02.py文件,使用如下命令运行:

    python3 pad_conv2d_02.py
    
    • 1

    输出内容为:

    可以看到输出数据的高度和宽度符合我们的上面的要求。

    in shape: (2, 3, 8, 8)
    out shape: (2, 4, 8, 7)
    
    • 1
    • 2

    7. 输出维度

    本节来单独介绍一下Conv2d中数据输出维度的计算,在前面的6小节中,我们已经对部分做了铺垫。

    各种情况下的输出维度见下图公式。
    output_shape

    在面对具体情况时,将相关参数带入公式即可算到要计算的部分。这里不再对公式展开解释。

    本文总结

    本文重点介绍了MindSpore中的Conv2d算子。通过几种不同卷积模式(普通卷积、深度卷积、空洞卷积)的具体实现,以及数据格式、填充方式、输出维度多个角度来深入讲解Conv2d算子的具体应用。

    本文参考

    本文为原创文章,版权归作者所有,未经授权不得转载!

  • 相关阅读:
    Oracle数据库表空间数据删除以及数据库重启
    【Python自学笔记】报错No module Named Wandb
    typescript使用入门及react+ts实战
    spark常用的调参详解
    day07-缓存套餐
    <C++>类和对象下|初始化列表|explicit static|友元|内部类|匿名对象|构造函数的优化
    linux rm 删除找回的几种方法 工具介绍3之testdisk以及Photorec xfs文件系统格式的
    【C语言】指针和数组的深入理解(第二期)
    SQL基础理论篇(九):存储过程
    vs2019生成c++库文件警告dll链接不一致
  • 原文地址:https://blog.csdn.net/kaierlong/article/details/126173904