精华内容
下载资源
问答
  • 今天小编就为大家分享一篇PyTorch 普通卷积和空洞卷积实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
  • 空洞卷积实现代码

    千次阅读 2021-04-29 22:20:35
    经过几天的学习理论知识和实践,终于把unet跟空洞卷积结合了。 还没看过空洞卷积的请看下面链接 空洞卷积理论知识 代码 uent.py import numpy as np import os import skimage.io as io import skimage.transform as...

    经过几天的学习理论知识和实践,终于把unet跟空洞卷积结合了。
    还没看过空洞卷积的请看下面链接
    空洞卷积理论知识

    代码

    uent.py

    import numpy as np
    import os
    import skimage.io as io
    import skimage.transform as trans
    import numpy as np
    from tensorflow.keras.models import *
    from tensorflow.keras.layers import *
    from tensorflow.keras.optimizers import *
    from tensorflow.keras.callbacks import ModelCheckpoint, LearningRateScheduler
    from tensorflow.keras import backend as keras
    
    
    def unet(pretrained_weights=None, input_size=(256, 256, 1)):
        inputs = Input(input_size)  # 初始化keras张量
        
        #第一层卷积
        #实际上从unet的结构来看每一次卷积的padding应该是valid,也就是每次卷积后图片尺寸减少2,
        #但在这里为了避免裁剪,方便拼接,把padding设成了same,即每次卷积不会改变图片的尺寸。
        conv1 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(inputs)
        # filters:输出的维度
        # kernel_size:卷积核的尺寸
        # activation:激活函数
        # padding:边缘填充,实际上在该实验中并没有严格按照unet网络结构进行卷积,same填充在卷积完毕之后图片大小并不会改变
        # kernel_initializer:kernel权值初始化
        conv1 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv1)
        pool1 = MaxPooling2D(pool_size=(2, 2))(conv1)#采用2*2的最大池化
        
        #第二层卷积
        #参数类似于第一层卷积,只是输出的通道数翻倍
        conv2 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool1)
        conv2 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv2)
        pool2 = MaxPooling2D(pool_size=(2, 2))(conv2)
        
        #第三层卷积
        conv3 = Conv2D(256, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool2)
        conv3 = Conv2D(256, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv3)
        pool3 = MaxPooling2D(pool_size=(2, 2))(conv3)
        
        #第四层卷积
        conv4 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal',dilation_rate=2)(pool3)
        conv4 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal',dilation_rate=2)(conv4)
        drop4 = Dropout(0.5)(conv4)  # 每次训练时随机忽略50%的神经元,减少过拟合
        pool4 = MaxPooling2D(pool_size=(2, 2))(drop4)
        
        #第五层卷积
        conv5 = Conv2D(1024, 3, activation='relu', padding='same', kernel_initializer='he_normal',dilation_rate=2)(pool4)
        conv5 = Conv2D(1024, 3, activation='relu', padding='same', kernel_initializer='he_normal',dilation_rate=2)(conv5)
        drop5 = Dropout(0.5)(conv5)# 每次训练时随机忽略50%的神经元,减少过拟合
        
        #第一次反卷积
        up6 = Conv2D(512, 2, activation='relu', padding='same', kernel_initializer='he_normal')(
            UpSampling2D(size=(2, 2))(drop5))  # 先上采样放大,在进行卷积操作,相当于转置卷积
        # merge6 = merge([drop4, up6], mode='concat', concat_axis=3)
        #将第四层卷积完毕并进行Dropout操作后的结果drop4与反卷积后的up6进行拼接
        merge6 = concatenate([drop4, up6], axis=3)  # (width,heigth,channels)拼接通道数
        conv6 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge6)
        conv6 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv6)
    
        #第二次反卷积
        up7 = Conv2D(256, 2, activation='relu', padding='same', kernel_initializer='he_normal')(
            UpSampling2D(size=(2, 2))(conv6))
        # merge7 = merge([conv3, up7], mode='concat', concat_axis=3)
        #将第三层卷积完毕后的结果conv3与反卷积后的up7进行拼接
        merge7 = concatenate([conv3, up7], axis=3)
        conv7 = Conv2D(256, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge7)
        conv7 = Conv2D(256, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv7)
        
        #第三次反卷积
        up8 = Conv2D(128, 2, activation='relu', padding='same', kernel_initializer='he_normal')(
            UpSampling2D(size=(2, 2))(conv7))
        # merge8 = merge([conv2, up8], mode='concat', concat_axis=3)
        #将第二层卷积完毕后的结果conv2与反卷积后的up8进行拼接
        merge8 = concatenate([conv2, up8], axis=3)#拼接通道数
        conv8 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge8)
        conv8 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv8)
        
        
        #第四次反卷积
        up9 = Conv2D(64, 2, activation='relu', padding='same', kernel_initializer='he_normal')(
            UpSampling2D(size=(2, 2))(conv8))
        # merge9 = merge([conv1, up9], mode='concat', concat_axis=3)
        #将第一层卷积完毕后的结果conv1与反卷积后的up9进行拼接
        merge9 = concatenate([conv1, up9], axis=3)#拼接通道数
        conv9 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge9)
        conv9 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv9)
        conv9 = Conv2D(2, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv9)
        
        #进行一次卷积核为1*1的卷积操作,卷积完毕后通道数变为1,作为输出结果
        conv10 = Conv2D(1, 1, activation='sigmoid')(conv9)
    
        model = Model(inputs=inputs, outputs=conv10)
        #keras内置函数,对模型进行编译
        model.compile(optimizer=Adam(lr=1e-4), loss='binary_crossentropy', metrics=['accuracy'])
        # optimizer:优化器
        # binary_crossentropy:与sigmoid相对应的损失函数
        # metrics:评估模型在训练和测试时的性能的指标
    
        if pretrained_weights:
            model.load_weights(pretrained_weights)
    
        return model
    
    

    空洞卷积主要是在第四层和第五层实现的。

    处理前:
    在这里插入图片描述
    处理后:

    在这里插入图片描述

    总结:

    将unet和空洞卷积结合了,接下来该将之前学到的复杂卷积都实现了,但是不一定都换上空洞卷积都会有好的效果,还是需要继续努力学习新的知识,加油!

    展开全文
  • 主要介绍了Tensorflow tf.nn.atrous_conv2d如何实现空洞卷积的,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
  • 博客见:https://blog.csdn.net/qq_37534947/article/details/109727232,主要是空洞卷积以及残差网络的代码实现,包含数据集,框架是pytorch
  • TensorFlow实现卷积、反卷积和空洞卷积 TensorFlow已经实现了卷积(tf.nn.conv2d卷积函数),反卷积(tf.nn.conv2d_transpose反卷积函数)以及空洞卷积(tf.nn.atrous_conv2d空洞卷积(dilated convolution)),这...

    TensorFlow实现卷积、反卷积和空洞卷积

        TensorFlow已经实现了卷积(tf.nn.conv2d卷积函数),反卷积(tf.nn.conv2d_transpose反卷积函数)以及空洞卷积(tf.nn.atrous_conv2d空洞卷积(dilated convolution)),这三个函数的参数理解,可参考网上。比较难的是计算维度,这里提供三种方式封装卷积、反卷积和空洞卷积的方法,方面调用:


    一、卷积

    • 输入图片大小 W×W
    • Filter大小 F×F
    • 步长 S
    • padding的像素数 P

       于是我们可以得出

    N = [(W − F + 2P )/S]+1

        输出图片大小为 N×N,卷积维度计算方法:https://blog.csdn.net/qq_21997625/article/details/87252780

        可以使用TensorFlow高级的API的slim.conv2d

        net = slim.conv2d(inputs=inputs,
                          num_outputs=num_outputs,
                          weights_initializer=tf.truncated_normal_initializer(stddev=0.01),
                          weights_regularizer=reg,
                          kernel_size=[kernel, kernel],
                          activation_fn=activation_fn,
                          stride=stride,
                          padding=padding,
                          trainable=True,
                          scope=scope)

         一些特殊情况,可以自己对feature进行填充:

    def slim_conv2d(inputs,num_outputs,stride,padding,kernel,activation_fn,reg,scope):
        if padding=="VALID":
            padding_size=int(kernel /2)
            inputs = tf.pad(inputs, paddings=[[0, 0], [padding_size, padding_size], [padding_size, padding_size], [0, 0]],
                         mode='REFLECT')
            print("pad.inputs.shape:{}".format(inputs.get_shape()))
        net = slim.conv2d(inputs=inputs,
                          num_outputs=num_outputs,
                          weights_initializer=tf.truncated_normal_initializer(stddev=0.01),
                          weights_regularizer=reg,
                          kernel_size=[kernel, kernel],
                          activation_fn=activation_fn,
                          stride=stride,
                          padding=padding,
                          trainable=True,
                          scope=scope)
        print("net.{}.shape:{}".format(scope,net.get_shape()))
        return net

        下面是使用TensorFlow自己封装的卷积,与TensorFlow自带的slim.conv2d高级API类似的功能

    
    def conv2D_layer(inputs,num_outputs,kernel_size,activation_fn,stride,padding,scope,weights_regularizer):
        '''
        根据tensorflow slim模块封装一个卷积层API:包含卷积和激活函数,但不包含池化层
        :param inputs:
        :param num_outputs:
        :param kernel_size: 卷积核大小,一般是[1,1],[3,3],[5,5]
        :param activation_fn:激活函数
        :param stride: 例如:[2,2]
        :param padding: SAME or VALID
        :param scope: scope name
        :param weights_regularizer:正则化,例如:weights_regularizer = slim.l2_regularizer(scale=0.01)
        :return:
        '''
        with  tf.variable_scope(name_or_scope=scope):
            in_channels = inputs.get_shape().as_list()[3]
            # kernel=[height, width, in_channels, output_channels]
            kernel=[kernel_size[0],kernel_size[1],in_channels,num_outputs]
            strides=[1,stride[0],stride[1],1]
            # filter_weight=tf.Variable(initial_value=tf.truncated_normal(shape,stddev=0.1))
            filter_weight = slim.variable(name='weights',
                                          shape=kernel,
                                          initializer=tf.truncated_normal_initializer(stddev=0.1),
                                          regularizer=weights_regularizer)
            bias = tf.Variable(tf.constant(0.01, shape=[num_outputs]))
            inputs = tf.nn.conv2d(inputs, filter_weight, strides, padding=padding) + bias
            if not activation_fn is None:
                inputs = activation_fn(inputs)
            return inputs

    二、反卷积

         TensorFlow的高级API已经封装好了反卷积函数,分别是: tf.layers.conv2d_transpose以及slim.conv2d_transpose,其用法基本一样,如果想使用tf.nn.conv2d_transpose实现反卷积功能,那么需要自己根据padding='VALID'和‘SAME’计算输出维度,这里提供一个函数deconv_output_length函数,可以根据输入的维度,filter_size, padding, stride自动计算其输出维度。

    # -*-coding: utf-8 -*-
    """
        @Project: YNet-python
        @File   : myTest.py
        @Author : panjq
        @E-mail : pan_jinquan@163.com
        @Date   : 2019-01-10 15:51:23
    """
    import tensorflow as tf
    import tensorflow.contrib.slim as slim
    
    def deconv_output_length(input_length, filter_size, padding, stride):
      """Determines output length of a transposed convolution given input length.
      Arguments:
          input_length: integer.
          filter_size: integer.
          padding: one of SAME or VALID ,FULL
          stride: integer.
      Returns:
          The output length (integer).
      """
      if input_length is None:
        return None
      # 默认SAME
      input_length *= stride
      if padding == 'VALID':
        input_length += max(filter_size - stride, 0)
      elif padding == 'FULL':
        input_length -= (stride + filter_size - 2)
      return input_length
    
    
    
    def conv2D_transpose_layer(inputs,num_outputs,kernel_size,activation_fn,stride,padding,scope,weights_regularizer):
        '''
        实现反卷积的API:包含反卷积和激活函数,但不包含池化层
        :param inputs:input Tensor=[batch, in_height, in_width, in_channels]
        :param num_outputs:
        :param kernel_size: 卷积核大小,一般是[1,1],[3,3],[5,5]
        :param activation_fn:激活函数
        :param stride: 例如:[2,2]
        :param padding: SAME or VALID
        :param scope: scope name
        :param weights_regularizer:正则化,例如:weights_regularizer = slim.l2_regularizer(scale=0.01)
        :return:
        '''
        with  tf.variable_scope(name_or_scope=scope):
            # shape = [batch_size, height, width, channel]
            in_shape = inputs.get_shape().as_list()
            # 计算反卷积的输出维度
            output_height=deconv_output_length(in_shape[1], kernel_size[0], padding=padding, stride=stride[0])
            output_width =deconv_output_length(in_shape[2], kernel_size[1], padding=padding, stride=stride[1])
            output_shape=[in_shape[0],output_height,output_width,num_outputs]
    
            strides=[1,stride[0],stride[1],1]
    
            # kernel=[kernel_size, kernel_size, output_channel, input_channel ]
            kernel=[kernel_size[0],kernel_size[1],num_outputs,in_shape[3]]
            filter_weight = slim.variable(name='weights',
                                          shape=kernel,
                                          initializer=tf.truncated_normal_initializer(stddev=0.1),
                                          regularizer=weights_regularizer)
            bias = tf.Variable(tf.constant(0.01, shape=[num_outputs]))
            inputs = tf.nn.conv2d_transpose(value=inputs, filter=filter_weight,output_shape=output_shape, strides=strides, padding=padding) + bias
            if not activation_fn is None:
                inputs = activation_fn(inputs)
            return inputs
    
    if __name__ == "__main__":
        inputs = tf.ones(shape=[4, 100, 100, 3])
        stride=2
        kernel_size=10
        padding="SAME"
        net1 = tf.layers.conv2d_transpose(inputs=inputs,
                                          filters=32,
                                          kernel_size=kernel_size,
                                          strides=stride,
                                          padding=padding)
        net2 = slim.conv2d_transpose(inputs=inputs,
                                     num_outputs=32,
                                     kernel_size=[kernel_size, kernel_size],
                                     stride=[stride, stride],
                                     padding=padding)
        net3 = conv2D_transpose_layer(inputs=inputs,
                                      num_outputs=32,
                                      kernel_size=[kernel_size, kernel_size],
                                      activation_fn=tf.nn.relu,
                                      stride=[stride, stride],
                                      padding=padding,
                                      scope="conv2D_transpose_layer",
                                      weights_regularizer=None)
        print("net1.shape:{}".format(net1.get_shape()))
        print("net2.shape:{}".format(net2.get_shape()))
        print("net3.shape:{}".format(net3.get_shape()))
    
        with tf.Session() as sess:
            sess.run(tf.global_variables_initializer())
    
    

    三、空洞卷积:增大感受野

        Dilated/Atrous conv 空洞卷积/多孔卷积,又名扩张卷积(dilated convolutions),向卷积层引入了一个称为 “扩张率(dilation rate)”的新参数,该参数定义了卷积核处理数据时各值的间距。该结构的目的是在不用pooling(pooling层会导致信息损失)且计算量相当的情况下,提供更大的感受野

        在空洞卷积中有个重要的参数叫rate,这个参数代表了空洞的大小。 要理解空洞概念和如何操作可以从两个角度去看。 

    1)从原图角度,所谓空洞就是在原图上做采样。采样的频率是根据rate参数来设置的,当rate为1时候,就是原图不丢失任何信息采样,此时卷积操作就是标准的卷积操作,当rate>1,比如2的时候,就是在原图上每隔一(rate-1)个像素采样,如图b,可以把红色的点想象成在原图上的采样点,然后将采样后的图像与kernel做卷积,这样做其实变相增大了感受野。 

    2)从kernel角度去看空洞的话就是扩大kernel的尺寸,在kernel中,相邻点之间插入rate-1个零,然后将扩大的kernel和原图做卷积 ,这样还是增大了感受野

    3)标准卷积为了提高感受野,可以通过池化pooling下采样,降低图像尺度的同时增大感受野,但pooling本身是不可学习的,也会丢失很多细节信息。而dilated conv空洞卷积,不需要pooling,也能有较大的感受野看到更多的信息。

    4)增大卷积核的大小也可以提高感受野,但这会增加计算量

    标准卷积: 

    空洞卷积 :

        在VGG网络中就证明了使用小卷积核叠加来取代大卷积核可以起到减少参数同时达到大卷积核同样大小感受野的功效。但是通过叠加小卷积核来扩大感受野只能线性增长,公式为(kernelSize−1)∗layers+1(kernelSize−1)∗layers+1,,也就是线性增长,而空洞卷积可以以指数级增长感受野。

        参考资料:https://blog.csdn.net/silence2015/article/details/79748729

    def dilated_conv2D_layer(inputs,num_outputs,kernel_size,activation_fn,rate,padding,scope,weights_regularizer):
        '''
        使用Tensorflow封装的空洞卷积层API:包含空洞卷积和激活函数,但不包含池化层
        :param inputs:
        :param num_outputs:
        :param kernel_size: 卷积核大小,一般是[1,1],[3,3],[5,5]
        :param activation_fn:激活函数
        :param rate:
        :param padding: SAME or VALID
        :param scope: scope name
        :param weights_regularizer:正则化,例如:weights_regularizer = slim.l2_regularizer(scale=0.01)
        :return:
        '''
        with  tf.variable_scope(name_or_scope=scope):
            in_channels = inputs.get_shape().as_list()[3]
            kernel=[kernel_size[0],kernel_size[1],in_channels,num_outputs]
            # filter_weight=tf.Variable(initial_value=tf.truncated_normal(shape,stddev=0.1))
            filter_weight = slim.variable(name='weights',
                                          shape=kernel,
                                          initializer=tf.truncated_normal_initializer(stddev=0.1),
                                          regularizer=weights_regularizer)
            bias = tf.Variable(tf.constant(0.01, shape=[num_outputs]))
            # inputs = tf.nn.conv2d(inputs,filter_weight, strides, padding=padding) + bias
            inputs = tf.nn.atrous_conv2d(inputs, filter_weight, rate=rate, padding=padding) + bias
            if not activation_fn is None:
                inputs = activation_fn(inputs)
            return inputs
    

     

    展开全文
  • 深度学习之空洞卷积

    2018-03-09 13:46:35
    深度学习之空洞卷积原始出处论文,介绍了空洞卷积实现方法以及实现意义。
  • 参考博客: 空洞卷积(dilated convolution)理解 【Tensorflow】tf.nn.atrous_conv2d如何实现空洞卷积

    1. Dilated Convolution空洞卷积

    1.1 Dilated Convolution基本原理

    Dilated/Atrous Convolution(中文叫做空洞卷积或者膨胀卷积) 或者是 Convolution with holes 从字面上就很好理解,是在标准的 convolution map 里注入空洞,以此来增加 reception field。相比原来的正常convolution,dilated convolution 多了一个 hyper-parameter 称之为 dilation rate 指的是kernel的间隔数量(e.g. 正常的 convolution 是 dilatation rate 1)。

    一个简单的例子,[动态图来源: https://github.com/vdumoulin/conv_arithmetic]
    Standard Convolution with a 3 x 3 kernel (and padding) Standard Convolution with a 3 x 3 kernel (and padding)
    Dilated Convolution with a 3 x 3 kernel and dilation rate 2Dilated Convolution with a 3 x 3 kernel and dilation rate 2

    1.2 Deep CNN的缺陷

    Deep CNN 对于其他任务还有一些致命性的缺陷。较为著名的是 up-sampling 和 pooling layer 的设计。主要问题有:

    • Up-sampling / pooling layer (e.g. bilinear interpolation) is deterministic. (参数不可学习)
    • 内部数据结构丢失;空间层级化信息丢失。
    • 小物体信息无法重建 (假设有四个pooling layer 则 任何小于 2^4 = 16 pixel 的物体信息将理论上无法重建。)

    在这样问题的存在下,语义分割问题一直处在瓶颈期无法再明显提高精度, 而 dilated convolution 的设计就良好的避免了这些问题。

    在图像分割领域,图像输入到CNN(典型的网络比如FCN)中,FCN先像传统的CNN那样对图像做卷积再pooling,降低图像尺寸的同时增大感受野,但是由于图像分割预测是pixel-wise的输出,所以要将pooling后较小的图像尺寸upsampling到原始的图像尺寸进行预测(upsampling一般采用deconv反卷积操作,deconv可参见知乎答案 如何理解深度学习中的deconvolution networks?),之前的pooling操作使得每个pixel预测都能看到较大感受野信息。
    因此, 图像分割FCN中有两个关键,一个是pooling减小图像尺寸增大感受野,另一个是upsampling扩大图像尺寸。在先减小再增大尺寸的过程中,肯定有一些信息损失掉了,那么能不能设计一种新的操作,不通过pooling也能有较大的感受野看到更多的信息呢?答案就是dilated conv。

    下面看一下dilated conv原始论文[4]中的示意图:

    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的感受野是指数级的增长。

    dilated的好处是不做pooling损失信息的情况下,加大了感受野,让每个卷积输出都包含较大范围的信息。

    1.3 空洞卷积存在的问题

    潜在问题 1:The Gridding Effect

    假设我们仅仅多次叠加 dilation rate 2 的 3 x 3 kernel 的话,则会出现这个问题:
    在这里插入图片描述
    我们发现我们的 kernel 并不连续,也就是并不是所有的 pixel 都用来计算了,因此这里将信息看做 checker-board 的方式会损失信息的连续性。这对 pixel-level dense prediction 的任务来说是致命的。

    潜在问题 2:Long-ranged information might be not relevant.

    我们从 dilated convolution 的设计背景来看就能推测出这样的设计是用来获取 long-ranged information。然而光采用大 dilation rate 的信息或许只对一些大物体分割有效果,而对小物体来说可能则有弊无利了。如何同时处理不同大小的物体的关系,则是设计好 dilated convolution 网络的关键。

    1.4 通向标准化设计:Hybrid Dilated Convolution (HDC)

    对于上个 section 里提到的几个问题,图森组的文章对其提出了较好的解决的方法。他们设计了一个称之为 HDC 的设计结构。

    • 第一个特性是,叠加卷积的 dilation rate 不能有大于1的公约数。比如 [2, 4, 6] 则不是一个好的三层卷积,依然会出现 gridding effect。
    • 第二个特性是,我们将 dilation rate 设计成锯齿状结构,例如 [1, 2, 5, 1, 2, 5] 循环结构。
    • 第三个特性是,我们需要满足一下这个式子:
      在这里插入图片描述
      一个简单的例子: dilation rate [1, 2, 5] with 3 x 3 kernel (可行的方案)
      在这里插入图片描述而这样的锯齿状本身的性质就比较好的来同时满足小物体大物体的分割要求(小 dilation rate 来关心近距离信息,大 dilation rate 来关心远距离信息)。

    2. Dilated Convolution的tensorflow实现

    2.1 tf.nn.atrous_conv2d函数解析
    tf.nn.atrous_conv2d(value,filters,rate,padding,name=None

    与方法有关的一共四个主要参数:

    • value:指需要做卷积的输入图像,要求是一个4维Tensor,具有[batch, height, width, channels]这样的shape,具体含义是[训练时一个batch的图片数量, 图片高度, 图片宽度, 图像通道数]
    • filters:相当于CNN中的卷积核,要求是一个4维Tensor,具有[filter_height, filter_width, channels, out_channels]这样的shape,具体含义是[卷积核的高度,卷积核的宽度,图像通道数,卷积核个数],同理这里第三维channels,就是参数value的第四维
    • rate:要求是一个int型的正数,正常的卷积操作应该会有stride(即卷积核的滑动步长),但是空洞卷积是没有stride参数的,这一点尤其要注意。取而代之,它使用了新的rate参数,那么rate参数有什么用呢?它定义为我们在输入图像上卷积时的采样间隔,你可以理解为卷积核当中穿插了(rate-1)数量的“0”,把原来的卷积核插出了很多“洞洞”,这样做卷积时就相当于对原图像的采样间隔变大了。具体怎么插得,可以看后面更加详细的描述。此时我们很容易得出rate=1时,就没有0插入,此时这个函数就变成了普通卷积。
    • padding:string类型的量,只能是”SAME”,”VALID”其中之一,这个值决定了不同边缘填充方式。

    传统的tf.nn.conv2d函数中还包含有stride参数, 用于设置滑动步长的大小. 而在tf.nn.atrous_conv2d函数中已经默认了stride=1,也就是滑动步长无法改变,固定为1。

    结果返回一个Tensor,填充方式为“VALID”时,返回[batch,height-2*(filter_width-1),width-2*(filter_height-1),out_channels]的Tensor,填充方式为“SAME”时,返回[batch, height, width, out_channels]的Tensor。

    2.1 tf.nn.atrous_conv2d具体应用

    首先创建一张2通道图

    img = tf.constant(value=[[[[1],[2],[3],[4]],[[1],[2],[3],[4]],[[1],[2],[3],[4]],[[1],[2],[3],[4]]]],dtype=tf.float32)
    img = tf.concat(values=[img,img],axis=3)
    

    然后用一个3*3卷积核去做卷积

    filter = tf.constant(value=1, shape=[3,3,2,5], dtype=tf.float32)
    out_img = tf.nn.atrous_conv2d(value=img, filters=filter, rate=1)
    

    建立好了img和filter,就可以做卷积了

    out_img = tf.nn.conv2d(input=img, filter=filter, strides=[1,1,1,1], padding='VALID')
    

    输出5个channel,我们设置rate=1,此时空洞卷积可以看做普通的卷积,分别在SAME和VALID模式下输出如下:

    在这里插入图片描述调整rate=2,继续运行程序

    out_img = tf.nn.atrous_conv2d(value=img, filters=filter, rate=2, padding='SAME')
    

    查看输出结果

    [[[[ 16.  16.  16.  16.  16.]
       [ 24.  24.  24.  24.  24.]
       [ 16.  16.  16.  16.  16.]
       [ 24.  24.  24.  24.  24.]]
    
      [[ 16.  16.  16.  16.  16.]
       [ 24.  24.  24.  24.  24.]
       [ 16.  16.  16.  16.  16.]
       [ 24.  24.  24.  24.  24.]]
    
      [[ 16.  16.  16.  16.  16.]
       [ 24.  24.  24.  24.  24.]
       [ 16.  16.  16.  16.  16.]
       [ 24.  24.  24.  24.  24.]]
    
      [[ 16.  16.  16.  16.  16.]
       [ 24.  24.  24.  24.  24.]
       [ 16.  16.  16.  16.  16.]
       [ 24.  24.  24.  24.  24.]]]]
    

    这个结果怎么出来的呢?再用一张图
    在这里插入图片描述这里我们看到rate=2时,通过穿插“0”,卷积核由33膨胀到了55。再看看“VALID”模式下,会发生什么?

    在这里插入图片描述直接报错了。因为卷积核的大小已经超过了原图大小

    好了,看到这里相信大家对于空洞卷积有了基本的了解了。那么,填充方式为“VALID”时,返回[batch,height-2*(filter_width-1),width-2*(filter_height-1),out_channels]的Tensor,这个结果,相信大家就可以证明了。

    完整代码如下:

    import tensorflow as tf
    
    img = tf.constant(value=[[[[1],[2],[3],[4]],[[1],[2],[3],[4]],[[1],[2],[3],[4]],[[1],[2],[3],[4]]]],dtype=tf.float32)
    img = tf.concat(values=[img,img],axis=3)
    filter = tf.constant(value=1, shape=[3,3,2,5], dtype=tf.float32)
    out_img1 = tf.nn.atrous_conv2d(value=img, filters=filter, rate=1, padding='SAME')
    out_img2 = tf.nn.atrous_conv2d(value=img, filters=filter, rate=1, padding='VALID')
    out_img3 = tf.nn.atrous_conv2d(value=img, filters=filter, rate=2, padding='SAME')
    
    #error
    #out_img4 = tf.nn.atrous_conv2d(value=img, filters=filter, rate=2, padding='VALID')
    
    with tf.Session() as sess:
        print 'rate=1, SAME mode result:'
        print(sess.run(out_img1))
    
        print 'rate=1, VALID mode result:'
        print(sess.run(out_img2))
    
        print 'rate=2, SAME mode result:'
        print(sess.run(out_img3))
    
        # error
        #print 'rate=2, VALID mode result:'
        #print(sess.run(out_img4))
    

    相关论文:

    • Yu, Fisher, and Vladlen Koltun. “Multi-scale context aggregation by dilated convolutions.” arXiv preprint arXiv:1511.07122 (2015).
    • Graph WaveNet for Deep Spatial-Temporal Graph Modeling. IJCAI 2019

    参考博客:

    展开全文
  • 一、参考文献 https://blog.csdn.net/mao_xiao_feng/article/details/78003730 ... 二、概念 空洞卷积的基本概念则是扩大卷积核,例如下图(作图为标准卷积核,有图为空洞卷积核): 这里使用...

    一、参考文献

    https://blog.csdn.net/mao_xiao_feng/article/details/78003730

    https://blog.csdn.net/davincil/article/details/80902366

    二、概念

    空洞卷积的基本概念则是扩大卷积核,例如下图(作图为标准卷积核,有图为空洞卷积核):

                      

    这里使用空洞卷积的时候padding一定要注意,因为卷积核可能比输入还要打,所以尽量使用padding=‘SAME’

    这篇博客中对空洞卷积有一个详细的流程图解释:https://blog.csdn.net/mao_xiao_feng/article/details/78003730

    上图中输入是一个4*4的一个双通道的图片,在padding=same中,卷积核相乘的时候进行了填充,相反valid是不对输出进行天填充,所以最后的输出的维度是偏小的

    上图的卷积就是空洞卷积,原本3*3*2的卷积核在空洞步长rate=2的情况下变成了5*5*2的卷积核,这就是为什么要使用padding=same的原因了,因为卷积核5*5已经大于4*4的输入了,不然会报错!!

    三、Tensorflow中的空洞卷积

    函数的调用为:

    atrous_conv2d(value, filters, rate, padding, name=None)

    value: 
    指需要做卷积的输入图像,要求是一个4维Tensor,具有[batch, height, width, channels]这样的shape,具体含义是[训练时一个batch的图片数量, 图片高度, 图片宽度, 图像通道数]

    filters: 
    相当于CNN中的卷积核,要求是一个4维Tensor,具有[filter_height, filter_width, channels, out_channels]这样的shape,具体含义是[卷积核的高度,卷积核的宽度,图像通道数,卷积核个数],同理这里第三维channels,就是参数value的第四维

    rate: 
    要求是一个int型的正数,正常的卷积操作应该会有stride(即卷积核的滑动步长),但是空洞卷积是没有stride参数的,这一点尤其要注意。取而代之,它使用了新的rate参数,那么rate参数有什么用呢?它定义为我们在输入图像上卷积时的采样间隔,你可以理解为卷积核当中穿插了(rate-1)数量的“0”,把原来的卷积核插出了很多“洞洞”,这样做卷积时就相当于对原图像的采样间隔变大了。具体怎么插得,可以看后面更加详细的描述。此时我们很容易得出rate=1时,就没有0插入,此时这个函数就变成了普通卷积。如果rate=2,就把3*3的卷积核变成5*5了

    padding: 
    string类型的量,只能是”SAME”,”VALID”其中之一,这个值决定了不同边缘填充方式。
     

    四、完整代码实现

    数据集+相关原版代码链接:https://blog.csdn.net/qq_28626909/article/details/88866707

    我的空洞卷积是对上文博客中的CNN代码进行了修改实现的

    #!D:/workplace/python
    # -*- coding: utf-8 -*-
    # @File  : cifar10_atrous_conv2d.py
    # @Author: WangYe
    # @Date  : 2019/3/26
    # @Software: PyCharm
    
    # coding:utf-8
    # 导入官方cifar10模块
    #from tensorflow.image.cifar10 import cifar10
    # import cifar10
    # import tensorflow as tf
    #
    # # tf.app.flags.FLAGS是tensorflow的一个内部全局变量存储器
    # FLAGS = tf.app.flags.FLAGS
    # # cifar10模块中预定义下载路径的变量data_dir为'/tmp/cifar10_eval',预定义如下:
    # # tf.app.flags.DEFINE_string('data_dir', './cifar10_data',
    # #                           """Path to the CIFAR-10 data directory.""")
    # # 为了方便,我们将这个路径改为当前位置
    # FLAGS.data_dir = './cifar10_data'
    #
    # # 如果不存在数据文件则下载,并且解压
    # cifar10.maybe_download_and_extract()
    import cifar10,cifar10_input
    import tensorflow as tf
    import numpy as np
    import time
    
    #cifar10.maybe_download_and_extract()
    max_steps = 3000
    batch_size = 128
    data_dir = r'./cifar10_data/cifar-10-batches-bin'
    
    
    def variable_with_weight_loss(shape, stddev, w1):
        var = tf.Variable(tf.truncated_normal(shape, stddev=stddev))
        if w1 is not None:
            weight_loss = tf.multiply(tf.nn.l2_loss(var), w1, name='weight_loss')
            tf.add_to_collection('losses', weight_loss)
        return var
    
    
    images_train, labels_train = cifar10_input.distorted_inputs(data_dir=data_dir, batch_size=batch_size)
    
    images_test, labels_test = cifar10_input.inputs(eval_data=True,
                                                    data_dir=data_dir,
                                                    batch_size=batch_size)
    
    image_holder = tf.placeholder(tf.float32, [batch_size, 24, 24, 3])
    label_holder = tf.placeholder(tf.float32, [batch_size])
    #第一层
    
    
    #输入为   batch*24*24*3   卷积后为b*24*24*64   池化后为b*14*14*64   b 是batch的简写
    weight1 = variable_with_weight_loss(shape=[5, 5, 3, 64], stddev=0.05, w1=0)
    kernel1 = tf.nn.conv2d(image_holder, weight1, [1, 1, 1, 1], padding='SAME')
    bias1 = tf.Variable(tf.constant(0.0, shape=[64]))
    # conv1 = tf.nn.relu(tf.nn.bias_add(kernel1,bias1))
    conv1 = tf.nn.relu(kernel1 + bias1)
    pool1 = tf.nn.max_pool(conv1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')
    norm1 = tf.nn.lrn(pool1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75)
    
    
    
    #第二层  输入b*14*14*64  卷积后卫 b*14*14*64    池化后卫为   b*7*7*64
    weight2 = variable_with_weight_loss(shape=[5, 5, 64, 64], stddev=0.05, w1=0.0)
    kernel2 = tf.nn.conv2d(norm1, weight2, [1, 1, 1, 1], padding='SAME')
    bias2 = tf.Variable(tf.constant(0.1, shape=[64]))
    conv2 = tf.nn.relu(kernel2 + bias1)
    norm2 = tf.nn.lrn(conv2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75)
    pool2 = tf.nn.max_pool(norm2, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')
    
    
    
    #第三层   自己添加一层   Atrous Convolution卷积层  空洞卷积(膨胀卷积)
    weight3 = variable_with_weight_loss(shape=[5, 5, 64, 128], stddev=0.05, w1=0.0)
    """
    rate: 
    要求是一个int型的正数,正常的卷积操作应该会有stride(即卷积核的滑动步长),
    但是空洞卷积是没有stride参数的,这一点尤其要注意。取而代之,它使用了新的rate参数,
    那么rate参数有什么用呢?它定义为我们在输入图像上卷积时的采样间隔,
    你可以理解为卷积核当中穿插了(rate-1)数量的“0”,把原来的卷积核插出了很多“洞洞”,
    这样做卷积时就相当于对原图像的采样间隔变大了。具体怎么插得,可以看后面更加详细的描述。
    此时我们很容易得出rate=1时,就没有0插入,此时这个函数就变成了普通卷积。
    """
    #输入为  b*7*7*64  空洞卷积缺少strides参数,所以仍然卷积之后是  b*7*7*128
    kernel3 = tf.nn.atrous_conv2d(pool2,weight3,rate=2,padding='SAME')   #空洞卷积会把上面的卷积核编程  10*10的大小
    bias3 = tf.Variable(tf.constant(0.1, shape=[128]))     #输出维度128
    conv3 = tf.nn.relu(kernel3 + bias3)    #   输出为b*7*7*128
    norm3 = tf.nn.lrn(conv3, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75)
    pool3 = tf.nn.max_pool(norm3, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME') #strides的步长上下都是2,输出为b*4*4*128
    
    
    #全链接
    reshape = tf.reshape(pool3, [batch_size, -1])  #分开成为
    dim = reshape.get_shape()[1].value       #
    weight3 = variable_with_weight_loss([dim, 384], stddev=0.04, w1=0.004)
    bias3 = tf.Variable(tf.constant(0.1, shape=[384]))
    local3 = tf.nn.relu(tf.matmul(reshape, weight3) + bias3)
    #全连接层
    weight4 = variable_with_weight_loss([384, 192], stddev=0.04, w1=0.004)
    bias4 = tf.Variable(tf.constant(0.1, shape=[192]))
    local4 = tf.nn.relu(tf.matmul(local3, weight4) + bias4)
    #输出层
    weight5 = variable_with_weight_loss([192, 10], stddev=1 / 192, w1=0.0)
    bias5 = tf.Variable(tf.constant(0.0, shape=[10]))
    logits = tf.matmul(local4, weight5) + bias5
    
    
    def loss(logits, labels):
        labels = tf.cast(labels, tf.int64)
        cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=labels,
                                                                       name='cross_entropy_per_example')
        cross_entropy_mean = tf.reduce_mean(cross_entropy, name='cross_entropy')
        tf.add_to_collection('losses', cross_entropy_mean)
        return tf.add_n(tf.get_collection('losses'), name='total_loss')
    
    
    loss = loss(logits, label_holder)
    
    train_op = tf.train.AdamOptimizer(1e-3).minimize(loss)
    top_k_op = tf.nn.in_top_k(logits, tf.cast(label_holder, tf.int64), 1)
    
    # num_examples = 10000
    # import math
    # num_iter = int(math.ceil(num_examples/ batch_size))
    # true_count = 0
    # total_sample_count = num_iter * batch_size
    # step = 0
    # with tf.Session() as sess:
    #     sess.run(tf.global_variables_initializer())
    #     tf.train.start_queue_runners()
    #     for step in range(max_steps):
    #         start_time = time.time()
    #         image_batch,label_batch = sess.run([images_train,labels_train])
    #         _,loss_value = sess.run([train_op,loss],feed_dict={image_holder: image_batch,label_holder:label_batch})
    #         duration = time.time()-start_time
    #         if step % 10 == 0:
    #             examples_per_sec = batch_size /duration
    #             sec_per_batch = float(duration)
    
    #             format_str = ('step %d,loss=%.2f (%.1f examples/sec;%.3f sec/batch)')
    #             print(format_str % (step,loss_value,examples_per_sec,sec_per_batch))
    
    #     while step< num_iter:
    #         image_batch,label_batch = sess.run([images_test,labels_test])
    #         predictions = sess.run([top_k_op],feed_dict = {image_holder:image_batch,
    #                                                        label_holder:label_batch})
    #         true_count += np.sum(predictions)
    #         step+=1
    #         if step % 10 ==0:
    #             print true_count
    sess = tf.InteractiveSession()
    tf.global_variables_initializer().run()
    tf.train.start_queue_runners()
    for step in range(max_steps):
        start_time = time.time()
        image_batch, label_batch = sess.run([images_train, labels_train])
        _, loss_value = sess.run([train_op, loss], feed_dict={image_holder: image_batch, label_holder: label_batch})
        duration = time.time() - start_time
        if step % 10 == 0:
            examples_per_sec = batch_size / duration
            sec_per_batch = float(duration)
    
            format_str = ('step %d,loss=%.2f (%.1f examples/sec;%.3f sec/batch)')
            print(format_str % (step, loss_value, examples_per_sec, sec_per_batch))
    
    num_examples = 10000
    import math
    
    num_iter = int(math.ceil(num_examples / batch_size))
    true_count = 0
    total_sample_count = num_iter * batch_size
    step = 0
    # with tf.Session() as sess:
    while step < num_iter:
        image_batch, label_batch = sess.run([images_test, labels_test])
        predictions = sess.run([top_k_op], feed_dict={image_holder: image_batch,
                                                      label_holder: label_batch})
        true_count += np.sum(predictions)
        step += 1
        if step % 10 == 0:
            print(true_count)
    
    precision = float(true_count) / total_sample_count
    print('precision @ 1 =%.3f' % precision)
    

     

    截图可以看出代码是没问题的

    五、说明

    代码只是为了实现空洞卷积,所以并没有在意准确率什么的,请谅解

    展开全文
  • 空洞卷积以及Caffe的实现 1.空洞卷积的理解 空洞卷积 1.空洞卷积 Dilated Convolutions,翻译为扩张卷积或空洞卷积。扩张卷积与普通的卷积相比,除了卷积核的大小以外,还有一个扩张率(dilation rate)参数,主要用来...
  • 空洞卷积的应用处:空洞卷积(dilated convolution)是针对图像语义分割问题中下采样会降低图像分辨率、丢失信息而提出的一种卷积思路。利用添加空洞扩大感受野,让原本3x3的卷积核,在相同参数量和计算量下拥有5x5...
  • Dilated Convolutions 空洞卷积 pytorch版

    千次阅读 2018-12-14 17:48:20
    conv2 = nn.Conv2d(1, 1, 3, stride=1, bias=False, dilation=2) # dilation就是空洞率,即间隔 init.constant_(conv1.weight, 1) init.constant_(conv2.weight, 1) out1 = conv1(arr) out2 = conv2(arr) print('...
  • 之前博文已经对空洞卷积做了介绍,本文进行深入介绍《各种卷积层的理解(深度可分离卷积、分组卷积、扩张卷积、反卷积)》 诞生背景,在图像分割领域,图像输入到CNN(典型的网络比如FCN[3])中,FCN先像传统的...
  • 三维卷积&空洞卷积

    2020-07-21 12:09:22
    三维卷积 1. 大致的结构 下面就是 3D 卷积,其过滤器深度小于输入层深度(核大小<通道大小)。因此,3D 过滤器可以在所有三个方向(图像的高度、宽度、通道)上移动。在每个位置,逐元素的乘法和加法都会提供一个...
  • 二维卷积实验 卷积神经网络是一种具有局部连接、权重共享特性的深层前馈神经网络。局部连接是指通过引入卷积核,使得每次运算学习的特征只和局部输入相关,极大地减少了计算量和连接层数;权值共享是指学习的特征...
  • 上采样方法总结 unpooling 插值:线性插值、双线性插值、临近插值 反卷积(转置卷积、空洞卷积):对原始特征图周边padding或者元素之间padding然后在进行卷积。 ...
  • pytorch实现空洞卷积+残差网络实验(torch实现

    千次阅读 热门讨论 2020-11-16 19:10:11
    一:pytorch实现空洞卷积实验(torch实现) 要求: 从至少一个数据集上进行实验,同理,这里我选取了车辆分类数据集(后面的实验都是用的车辆分类数据集),主要在之前利用torch.nn实现二维卷积的基础上,为解决感受...
  • 引子: 感受野(receptive ... Dilated/Atrous Convolution(中文叫做空洞卷积或者膨胀卷积) 或者是 Convolution with holes 从字面上就很好理解,是在标准的 convolution map 里注入空洞,以此来增加 recepti...
  • 吃透空洞卷积(Dilated Convolutions)

    千次阅读 2020-12-10 15:00:00
    一、空洞卷积的提出 空洞卷积中文名也叫膨胀卷积或者扩张卷积,英文名也叫Atrous Convolution 空洞卷积最初的提出是为了解决图像分割的问题而提出的,常见的图像分割算法通常使用池化层和卷积层来增加感受野...
  • 文章来源:... 1. 扩张卷积的提出 Multi-Scale Context Aggregation by Dilated Convolutions Dilated Residual Networks 论文笔记——CVPR 2017 Dilated Residual Netwo...
  • 前两篇我们对卷积层的原理和实现做了一些介绍。这一节我们来深入了解两个常用的卷积——转置卷积和空洞卷积。传送门:卷积核的基本概况转置卷积利用神经网络去生成图片,通察情况下是从低像素、粗糙的图...
  • wavenet在做语音合成的时候,用到了dilated connvolution(空洞卷积).关于空洞卷积的介绍,知乎的这篇文章写的不错: https://www.zhihu.com/question/54149221/answer/192025860 关于在tensorflow中实现一般的卷积...
  • 指需要做卷积的输入图像,要求是一个4维Tensor,具有[batch, height, width, channels]这样的shape,具体含义是[训练时一个batch的图片数量, 图片高度, 图片宽度, 图像通道数] filters:  相当于CNN中的卷积...
  • 进而,假定输入空洞卷积的大小为 i,步长 为 s ,空洞卷积后特征图大小 o 的计算公式为: 参考资料 1、A guide to convolution arithmetic for deep learning(https://arxiv.org/abs/1603.07285) 2、如何理解深度...
  • 『算法学习』空洞卷积

    千次阅读 2018-09-21 17:27:00
    一、空洞卷积的提出 空洞卷积(atrous convolutions)又名扩张卷积(dilated convolutions),向卷积层引入了一个称为 “扩张率(dilation rate)”的新参数,该参数定义了卷积核处理数据时各值的间距。 该结构的...
  • 空洞卷积

    千次阅读 2019-03-26 09:31:00
    ... 一、含义 空洞卷积是在标准的卷积核里注入空洞,以此来增加感受野/接受域。相比原来的正常卷积运算,空洞卷积 多了一个 超参数称之为 dilation rate 指的是kernel的间隔数量(e.g. 正常的 con...
  • 1.空洞卷积 注要内容如下: 空洞卷积理解 但是呢,对于这篇博客的部分内容我不赞同。 3x3的kernel设置dialted-rate=2时,理应变为"5x5"的kernel,多出来的空洞用0填充,这也是变相的增加了感受野。也就是说,设置...
  • 卷积神经网络-空洞卷积

    千次阅读 2020-06-24 13:39:43
    一、空洞卷积的提出 空洞卷积(atrous convolutions)又名扩张卷积(dilated convolutions),向卷积层引入了一个称为 “扩张率(dilation rate)”的新参数,该参数定义了卷积核处理数据时各值的间距。 该结构的目的...
  • 吃透空洞卷积

    2021-04-13 00:53:53
    点击上方“小白学视觉”,选择加"星标"或“置顶”重磅干货,第一时间送达 本文转自:视学算法一、空洞卷积的提出空洞卷积中文名也叫膨胀卷积或者扩张卷积,英文名也叫Atrous...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,566
精华内容 1,426
关键字:

空洞卷积实现