NNDL 实验六 卷积神经网络(1)卷积

目录

5.1 卷积 

5.1.1 二维卷积运算 

5.1.2 二维卷积算子 

5.1.3 二维卷积的参数量和计算量 

5.1.4 感受野  

5.1.5 卷积的变种  

5.1.5.1 步长(Stride) 

5.1.5.2 零填充(Zero Padding)  

5.1.6 带步长和零填充的二维卷积算子  

5.1.7 使用卷积运算完成图像边缘检测任务 

选做题

 1.实现一些传统边缘检测算子,如:Roberts、Prewitt、Sobel、Scharr、Kirsch、Robinson、Laplacian。

边缘检测系列2:简易的 Canny 边缘检测器 

 边缘检测系列3:【HED】 Holistically-Nested 边缘检测 

 边缘检测系列4:【RCF】基于更丰富的卷积特征的边缘检测 

边缘检测系列5:【CED】添加了反向细化路径的 HED 模型 

总结


卷积神经网络(Convolutional Neural Network,CNN)

  • 受生物学上感受野机制的启发而提出。
  • 一般是由卷积层、汇聚层和全连接层交叉堆叠而成的前馈神经网络
  • 有三个结构上的特性:局部连接、权重共享、汇聚
  • 具有一定程度上的平移、缩放和旋转不变性。
  • 和前馈神经网络相比,卷积神经网络的参数更少
  • 主要应用在图像和视频分析的任务上,其准确率一般也远远超出了其他的神经网络模型。
  • 近年来卷积神经网络也广泛地应用到自然语言处理、推荐系统等领域。

5.1 卷积 

5.1.1 二维卷积运算 

 

5.1.2 二维卷积算子 

在本章面的实现中,算子都继承torch.nn.Module,并使用支持反向传播的pytorchAPI进行实现,这样我们就可以不用手工写backword()的代码实现。 

码实现如下: 

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


class Conv2D(nn.Module):
    def __init__(self, kernel_size):
        super(Conv2D, self).__init__()
        w = torch.tensor(np.array([[0., 1.], [2., 3.]], dtype='float32').reshape([kernel_size, kernel_size]))
        self.weight = torch.nn.Parameter(w, requires_grad=True)

    def forward(self, X):
        u, v = self.weight.shape
        output = torch.zeros([X.shape[0], X.shape[1] - u + 1, X.shape[2] - v + 1])
        for i in range(output.shape[1]):
            for j in range(output.shape[2]):
                output[:, i, j] = torch.sum(X[:, i:i + u, j:j + v] * self.weight, axis=[1, 2])
        return output


# 随机构造一个二维输入矩阵
inputs = torch.as_tensor([[[1., 2., 3.], [4., 5., 6.], [7., 8., 9.]]])
conv2d = Conv2D(kernel_size=2)
outputs = conv2d(inputs)
print("input: {}, \noutput: {}".format(inputs, outputs))

输出

 

5.1.3 二维卷积的参数量和计算量 

随着隐藏层神经元数量的变多以及层数的加深,

使用全连接前馈网络处理图像数据时,参数量会急剧增加。

如果使用卷积进行图像处理,相较于全连接前馈网络,参数量少了非常多。

参数量

由于二维卷积的运算方式为在一个图像(或特征图)上滑动一个卷积核,通过卷积操作得到一组新的特征。所以参数量仅仅与卷积核的尺寸有关,对于一个输入矩阵X\in \mathbb{R} ^{M\times N}和一个滤波器W\in \mathbb{R} ^{U\times V},卷积核的参数量为U\times V

假设有一幅大小为32×32的图像,如果使用全连接前馈网络进行处理,即便第一个隐藏层神经元个数为1,此时该层的参数量也高达1025个,此时该层的计算过程如下图所示。

 

可以想像,随着隐藏层神经元数量的变多以及层数的加深,使用全连接前馈网络处理图像数据时,参数量会急剧增加。如果使用卷积进行图像处理,当卷积核为3×3时,参数量仅为9相较于全连接前馈网络,参数量少了非常多。

计算量

在卷积神经网络中运算时,通常会统计网络总的乘加运算次数作为计算量(FLOPs,floating point of operations),来衡量整个网络的运算速度。对于单个二维卷积,计算量的统计方式为:

FLOPs=M′×N′×U×V。(5.4)

其中M′×N′表示输出特征图的尺寸,即输出特征图上每个点都要与卷积核W\in \mathbb{R} ^{U\times V}进行U×V次乘加运算。对于一幅大小为32×32的图像,使用3×3的卷积核进行运算可以得到以下的输出特征图尺寸:

M′=M−U+1=30M′=M−U+1=30

N′=N−V+1=30N′=N−V+1=30

此时,计算量为:

FLOPs=M′×N′×U×V=30×30×3×3=8100

5.1.4 感受野  

输出特征图上每个点的数值,是由输入图片上大小为U×V的区域的元素与卷积核每个元素相乘再相加得到的,所以输入图像上U×V区域内每个元素数值的改变,都会影响输出点的像素值。我们将这个区域叫做输出特征图上对应点的感受野。感受野内每个元素数值的变动,都会影响输出点的数值变化。比如3×3卷积对应的感受野大小就是3×3,如下图所示。

 

而当通过两层3×3的卷积之后,感受野的大小将会增加到5×5,如下图所示。 

 

因此,当增加卷积网络深度的同时,感受野将会增大,输出特征图中的一个像素点将会包含更多的图像语义信息。 

5.1.5 卷积的变种  

在卷积的标准定义基础上,还可以引入卷积核的滑动步长和零填充来增加卷积的多样性,从而更灵活地进行特征抽取。 

5.1.5.1 步长(Stride) 

在卷积运算的过程中,有时会希望跳过一些位置来降低计算的开销,也可以把这一过程看作是对标准卷积运算输出的下采样

在计算卷积时,可以在所有维度上每间隔S个元素计算一次,S称为卷积运算的步长(Stride),也就是卷积核在滑动时的间隔。

此时,对于一个输入矩阵X\in \mathbb{R} ^{M\times N}和一个滤波器W\in \mathbb{R} ^{U\times V},它们的卷积为

                                                    y_{i,j}=\sum_{u=0}^{U-1}\sum_{v=0}^{V-1} w_{uv}x_{i\times S} +u,j\times S+v,(5.5)

 在二维卷积运算中,当步长S=2时,计算过程如下图所示。 

 

5.1.5.2 零填充(Zero Padding)  

在卷积运算中,还可以对输入用零进行填充使得其尺寸变大。根据卷积的定义,如果不进行填充,当卷积核尺寸大于1时,输出特征会缩减。对输入进行零填充则可以对卷积核的宽度和输出的大小进行独立的控制。

在二维卷积运算中,零填充(Zero Padding)是指在输入矩阵周围对称地补上P个0。图 为使用零填充的示例。

 

图5.7:padding=1的零填充

 对于一个输入矩阵X\in \mathbb{R} ^{M\times N}和一个滤波器W\in \mathbb{R} ^{U\times V},,步长为S,对输入矩阵进行零填充,那么最终输出矩阵大小则为:

                                                   {M}'=\frac{M+2P-U}{S}+1

                                                   {N}'=\frac{N+2P-V}{S}+1

引入步长和零填充后的卷积,参数量和计算量的统计方式与之前一致,参数量与卷积核的尺寸有关,为:U×V,计算量与输出特征图和卷积核的尺寸有关,为:

FLOPs={M}'\times {N}'\times U\times V=(\frac{M+2P-U}{S}+1)\times (\frac{N+2P-V}{S}+1)\times U\times V

一般常用的卷积有以下三类: 

1.窄卷积:步长S=1,两端不补零P=0,卷积后输出尺寸为:

                                                        M′=M−U+1,

                                                        N′=N−V+1.

2.宽卷积:步长S=1,两端补零P=U−1=V−1,卷积后输出尺寸为:

                                                        M′=M+U−1,

                                                        N′=N+V−1.

3.等宽卷积:步长S=1,两端补零P=\frac{(U-1)}{2}=\frac{(V-1)}{2},卷积后输出尺寸为:

                                                         M′=M,

                                                         N′=N.

通常情况下,在层数较深的卷积神经网络,比如:VGG、ResNet中,会使用等宽卷积保证输出特征图的大小不会随着层数的变深而快速缩减。例如:当卷积核的大小为3×3时,会将步长设置为S=1,两端补零P=1,此时,卷积后的输出尺寸就可以保持不变。在本章后续的案例中,会使用ResNet进行实验。

5.1.6 带步长和零填充的二维卷积算子  

引入步长和零填充后,二维卷积算子代码实现如下: 

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


class Conv2D(nn.Module):
    def __init__(self, kernel_size,stride=1, padding=0):
        super(Conv2D, self).__init__()
        w = torch.tensor(np.array([[0., 1., 2.], [3., 4. ,5.],[6.,7.,8.]], dtype='float32').reshape([kernel_size, kernel_size]))
        self.weight = torch.nn.Parameter(w, requires_grad=True)
        self.stride = stride
        self.padding = padding

    def forward(self, X):
        # 零填充
        new_X = torch.zeros([X.shape[0], X.shape[1] + 2 * self.padding, X.shape[2] + 2 * self.padding])
        new_X[:, self.padding:X.shape[1] + self.padding, self.padding:X.shape[2] + self.padding] = X
        u, v = self.weight.shape
        output_w = (new_X.shape[1] - u) // self.stride + 1
        output_h = (new_X.shape[2] - v) // self.stride + 1
        output = torch.zeros([X.shape[0], output_w, output_h])
        for i in range(0, output.shape[1]):
            for j in range(0, output.shape[2]):
                output[:, i, j] = torch.sum(
                    new_X[:, self.stride * i:self.stride * i + u, self.stride * j:self.stride * j + v] * self.weight,
                    axis=[1, 2])
        return output


inputs = torch.randn(size=[2, 8, 8])
conv2d_padding = Conv2D(kernel_size=3, padding=1)
outputs = conv2d_padding(inputs)
print("When kernel_size=3, padding=1 stride=1, input's shape: {}, output's shape: {}".format(inputs.shape, outputs.shape))
conv2d_stride = Conv2D(kernel_size=3, stride=2, padding=1)
outputs = conv2d_stride(inputs)
print("When kernel_size=3, padding=1 stride=2, input's shape: {}, output's shape: {}".format(inputs.shape, outputs.shape))

 输出

 从输出结果看出,使用3×3大小卷积,padding为1,当stride=1时,模型的输出特征图可以与输入特征图保持一致;当stride=2时,输出特征图的宽和高都缩小一倍。  

5.1.7 使用卷积运算完成图像边缘检测任务 

 

在图像处理任务中,常用拉普拉斯算子对物体边缘进行提取,拉普拉斯算子为一个大小为3×3的卷积核,中心元素值是8,其余元素值是−1。 

考虑到边缘其实就是图像上像素值变化很大的点的集合,因此可以通过计算二阶微分得到,当二阶微分为0时,像素值的变化最大。此时,对x方向和y方向分别求取二阶导数:

                                                 \frac{\delta ^{2}I}{\delta x^{2}}=I(i,j+1)-2I(i,j)+I(i,j-1)

                                                 \frac{\delta ^{2}I}{\delta y^{2}}=I(i+1,j)-2I(i,j)+I(i-1,j)

完整的二阶微分公式为: 

            \bigtriangledown ^{2}I=\frac{\delta ^{2}I}{\delta x^{2}}+ \frac{\delta ^{2}I}{\delta y^{2}}=-4I(i,j)+I(i,j-1)+I(i,j+1)+I(i+1,j)+I(i-1,j)

上述公式也被称为拉普拉斯算子,对应的二阶微分卷积核为:

                                                       \begin{bmatrix} 0& 1 & 0\\ 1& -4 & 1\\ 0 & 1 &0 \end{bmatrix}

对上述算子全部求反也可以起到相同的作用,此时,该算子可以表示为: 

                                                     \begin{bmatrix} 0& -1 & 0\\ -1& 4 & -1\\ 0 & -1 &0 \end{bmatrix}

也就是一个点的四邻域拉普拉斯的算子计算结果是自己像素值的四倍减去上下左右的像素的和,将这个算子旋转45°后与原算子相加,就变成八邻域的拉普拉斯算子,也就是一个像素自己值的八倍减去周围一圈八个像素值的和,做为拉普拉斯计算结果,此时,该算子可以表示为: 

                                                       \begin{bmatrix} -1& -1 & -1\\ -1& 8 & -1\\ -1 & -1 &-1 \end{bmatrix}

下面我们利用上面定义的Conv2D算子,构造一个简单的拉普拉斯算子,并对一张输入的灰度图片进行边缘检测,提取出目标的外形轮廓。

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


class Conv2D(nn.Module):
    def __init__(self, kernel_size,stride=1, padding=0):
        super(Conv2D, self).__init__()
        # 设置卷积核参数
        w = np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], dtype='float32').reshape((3,3))
        w=torch.from_numpy(w)
        self.weight = torch.nn.Parameter(w, requires_grad=True)
        self.stride = stride
        self.padding = padding

    def forward(self, X):
        # 零填充
        new_X = torch.zeros([X.shape[0], X.shape[1] + 2 * self.padding, X.shape[2] + 2 * self.padding])
        new_X[:, self.padding:X.shape[1] + self.padding, self.padding:X.shape[2] + self.padding] = X
        u, v = self.weight.shape
        output_w = (new_X.shape[1] - u) // self.stride + 1
        output_h = (new_X.shape[2] - v) // self.stride + 1
        output = torch.zeros([X.shape[0], output_w, output_h])
        for i in range(0, output.shape[1]):
            for j in range(0, output.shape[2]):
                output[:, i, j] = torch.sum(
                    new_X[:, self.stride * i:self.stride * i + u, self.stride * j:self.stride * j + v] * self.weight,
                    axis=[1, 2])
        return output

import matplotlib.pyplot as plt
from PIL import Image
# 读取图片
img = Image.open('cat.png').convert('L')
img = np.array(img, dtype='float32')
im = torch.from_numpy(img.reshape((img.shape[0],img.shape[1])))

# 创建卷积算子,卷积核大小为3x3,并使用上面的设置好的数值作为卷积核权重的初始化参数
conv = Conv2D(kernel_size=3, stride=1, padding=0)

# 将读入的图片转化为float32类型的numpy.ndarray
inputs = np.array(im).astype('float32')
print("bf as_tensor, inputs:",inputs)
# 将图片转为Tensor
inputs = torch.as_tensor(inputs)
print("bf unsqueeze, inputs:",inputs)
inputs = torch.unsqueeze(inputs, axis=0)
print("af unsqueeze, inputs:",inputs)
outputs = conv(inputs)
print(outputs)
# outputs = outputs.data.squeeze().numpy()
# # 可视化结果
plt.subplot(121).set_title('input image', fontsize=15)
plt.imshow(img.astype('uint8'),cmap='gray')
plt.subplot(122).set_title('output feature map', fontsize=15)
plt.imshow(outputs.squeeze().detach().numpy(), cmap='gray')
plt.savefig('conv-vis.pdf')
plt.show()

 输出

 

选做题

Pytorch实现1、2;阅读3、4、5写体会。

 1.实现一些传统边缘检测算子,如:Roberts、Prewitt、Sobel、Scharr、Kirsch、Robinson、Laplacian。

import cv2
import numpy as np

# 加载图像
image = cv2.imread('cat.png', 0)
image = cv2.resize(image, (800, 800))
# 自定义卷积核
# Roberts边缘算子
kernel_Roberts_x = np.array([
    [1, 0],
    [0, -1]
])
kernel_Roberts_y = np.array([
    [0, -1],
    [1, 0]
])
# Sobel边缘算子
kernel_Sobel_x = np.array([
    [-1, 0, 1],
    [-2, 0, 2],
    [-1, 0, 1]])
kernel_Sobel_y = np.array([
    [1, 2, 1],
    [0, 0, 0],
    [-1, -2, -1]])
# Prewitt边缘算子
kernel_Prewitt_x = np.array([
    [-1, 0, 1],
    [-1, 0, 1],
    [-1, 0, 1]])
kernel_Prewitt_y = np.array([
    [1, 1, 1],
    [0, 0, 0],
    [-1, -1, -1]])


# Kirsch 边缘检测算子
def kirsch(image):
    m, n = image.shape
    list = []
    kirsch = np.zeros((m, n))
    for i in range(2, m - 1):
        for j in range(2, n - 1):
            d1 = np.square(5 * image[i - 1, j - 1] + 5 * image[i - 1, j] + 5 * image[i - 1, j + 1] -
                           3 * image[i, j - 1] - 3 * image[i, j + 1] - 3 * image[i + 1, j - 1] -
                           3 * image[i + 1, j] - 3 * image[i + 1, j + 1])
            d2 = np.square((-3) * image[i - 1, j - 1] + 5 * image[i - 1, j] + 5 * image[i - 1, j + 1] -
                           3 * image[i, j - 1] + 5 * image[i, j + 1] - 3 * image[i + 1, j - 1] -
                           3 * image[i + 1, j] - 3 * image[i + 1, j + 1])
            d3 = np.square((-3) * image[i - 1, j - 1] - 3 * image[i - 1, j] + 5 * image[i - 1, j + 1] -
                           3 * image[i, j - 1] + 5 * image[i, j + 1] - 3 * image[i + 1, j - 1] -
                           3 * image[i + 1, j] + 5 * image[i + 1, j + 1])
            d4 = np.square((-3) * image[i - 1, j - 1] - 3 * image[i - 1, j] - 3 * image[i - 1, j + 1] -
                           3 * image[i, j - 1] + 5 * image[i, j + 1] - 3 * image[i + 1, j - 1] +
                           5 * image[i + 1, j] + 5 * image[i + 1, j + 1])
            d5 = np.square((-3) * image[i - 1, j - 1] - 3 * image[i - 1, j] - 3 * image[i - 1, j + 1] - 3
                           * image[i, j - 1] - 3 * image[i, j + 1] + 5 * image[i + 1, j - 1] +
                           5 * image[i + 1, j] + 5 * image[i + 1, j + 1])
            d6 = np.square((-3) * image[i - 1, j - 1] - 3 * image[i - 1, j] - 3 * image[i - 1, j + 1] +
                           5 * image[i, j - 1] - 3 * image[i, j + 1] + 5 * image[i + 1, j - 1] +
                           5 * image[i + 1, j] - 3 * image[i + 1, j + 1])
            d7 = np.square(5 * image[i - 1, j - 1] - 3 * image[i - 1, j] - 3 * image[i - 1, j + 1] +
                           5 * image[i, j - 1] - 3 * image[i, j + 1] + 5 * image[i + 1, j - 1] -
                           3 * image[i + 1, j] - 3 * image[i + 1, j + 1])
            d8 = np.square(5 * image[i - 1, j - 1] + 5 * image[i - 1, j] - 3 * image[i - 1, j + 1] +
                           5 * image[i, j - 1] - 3 * image[i, j + 1] - 3 * image[i + 1, j - 1] -
                           3 * image[i + 1, j] - 3 * image[i + 1, j + 1])

            # 第一种方法:取各个方向的最大值,效果并不好,采用另一种方法
            list = [d1, d2, d3, d4, d5, d6, d7, d8]
            kirsch[i, j] = int(np.sqrt(max(list)))
    for i in range(m):
        for j in range(n):
            if kirsch[i, j] > 127:
                kirsch[i, j] = 255
            else:
                kirsch[i, j] = 0
    return kirsch


# 拉普拉斯卷积核
kernel_Laplacian_1 = np.array([
    [0, 1, 0],
    [1, -4, 1],
    [0, 1, 0]])
kernel_Laplacian_2 = np.array([
    [1, 1, 1],
    [1, -8, 1],
    [1, 1, 1]])
# 下面两个卷积核不具有旋转不变性
kernel_Laplacian_3 = np.array([
    [2, -1, 2],
    [-1, -4, -1],
    [2, 1, 2]])
kernel_Laplacian_4 = np.array([
    [-1, 2, -1],
    [2, -4, 2],
    [-1, 2, -1]])
# 5*5 LoG卷积模板
kernel_LoG = np.array([
    [0, 0, -1, 0, 0],
    [0, -1, -2, -1, 0],
    [-1, -2, 16, -2, -1],
    [0, -1, -2, -1, 0],
    [0, 0, -1, 0, 0]])
# 卷积
output_1 = cv2.filter2D(image, -1, kernel_Prewitt_x)
output_2 = cv2.filter2D(image, -1, kernel_Sobel_x)
output_3 = cv2.filter2D(image, -1, kernel_Prewitt_x)
output_4 = cv2.filter2D(image, -1, kernel_Laplacian_1)
output_5 = kirsch(image)
# 显示锐化效果
image = cv2.resize(image, (800, 600))
output_1 = cv2.resize(output_1, (800, 600))
output_2 = cv2.resize(output_2, (800, 600))
output_3 = cv2.resize(output_3, (800, 600))
output_4 = cv2.resize(output_4, (800, 600))
output_5 = cv2.resize(output_5, (800, 600))
cv2.imshow('Original Image', image)
cv2.imshow('Prewitt Image', output_1)
cv2.imshow('Sobel Image', output_2)
cv2.imshow('Prewitt Image', output_3)
cv2.imshow('Laplacian Image', output_4)
cv2.imshow('kirsch Image', output_5)
# 停顿
if cv2.waitKey(0) & 0xFF == 27:
    cv2.destroyAllWindows()

 

 

 

 

 

边缘检测系列2:简易的 Canny 边缘检测器 

算法原理
Canny 是一个经典的图像边缘检测算法,一般包含如下几个步骤:

  • 使用高斯模糊对图像进行模糊降噪处理
  • 基于图像梯度幅值进行图像边缘增强
  • 非极大值抑制处理进行图像边缘细化
  • 图像二值化和边缘连接得到最终的结果

实现步骤:

  • 用高斯滤波器平滑图像
  • 计算图像中每个像素点的梯度强度和方向
  • 对梯度幅值进行非极大值抑制
  • 用双阈值算法检测来确定真实和潜在的边缘

优点:

  • 对噪声不敏感,不容易受到噪声干扰

  • 能够检测到真正的弱边缘

  • 使用两种不同的阈值分别检测强边缘和弱边缘

  • 仅当强边缘与弱边缘相连时才将弱边缘包含在输出图像中

缺点:

  • 易使高频边缘被平滑掉,从而造成边缘丢失

代码如下:

import cv2
# 加载图像
image = cv2.imread(r'cat.png',0)
image = cv2.resize(image,(800,800))
def Canny(image,k,t1,t2):
    img = cv2.GaussianBlur(image, (k, k), 0)
    canny = cv2.Canny(img, t1, t2)
    return canny
image = cv2.resize(image, (800, 600))
cv2.imshow('Original Image', image)
output =cv2.resize(Canny(image,3,50,150),(800,600))
cv2.imshow('Canny Image', output)
# 停顿
if cv2.waitKey(0) & 0xFF == 27:
    cv2.destroyAllWindows()

 输出

 

 边缘检测系列3:【HED】 Holistically-Nested 边缘检测 

复现论文 Holistically-Nested Edge Detection,发表于 CVPR 2015
一个基于深度学习的端到端边缘检测模型。 

 

 

 边缘检测系列4:【RCF】基于更丰富的卷积特征的边缘检测 

 一个基于更丰富的卷积特征的边缘检测模型 【RCF】。

 

  • RCF 与 HED 模型一样,包含五个层级的特征提取架构,同样也是基于 VGG 16 Backbone
  • 相比 HED,RCF 模型更加充分利用对象的多尺度和多级信息来全面地执行图像到图像的预测
  • RCF 不只是使用了每个层级的输出,而是使用了每个层级中所有卷积层的输出进行融合(Conv + sum)后,作为边缘检测的输入
  • 模型结构图如下:

 

边缘检测系列5:【CED】添加了反向细化路径的 HED 模型 

 Crisp Edge Detection(CED)模型是前面介绍过的 HED 模型的另一种改进模型

 

CED 模型总体基于 HED 模型改造而来,其中做了如下几个改进:

  • 将模型中的上采样操作从转置卷积插值更换为 PixelShuffle
  • 添加了反向细化路径,即一个反向的从高层级特征逐步往低层级特征的边缘细化路径
  • 没有多层级输出,最终的输出为融合了各层级的特征的边缘检测结果

架构图如下:

正常情况下,卷积操作会使特征图的高和宽缩小。但当我们的步长即 stride = 1/r < 1 时,可以让卷积后的特征图的高和宽扩大——即分辨率增大或者叫上采样,这个新的操作叫做 PixelShuffle

总结

本次实验对卷积神经网络进行了复习和扩展,对卷积的操作有了更加清晰的认识,卷积对特征图的影响更明了。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值