卷积神经网络的基本原理

卷积核

  笔者在学会了如何运用卷积神经网路后,突然有一天萌发了很多问题,为什么要用卷积核?卷积核具体完成了什么工作?带着这些疑问,笔者开始查询资料,其中一段视频(从“卷积”、到“图像卷积操作”、再到“卷积神经网络”,“卷积”意义的3次改变)对我的帮助很大。接下来,我们将略去卷积的数学意义,只从神经网路的角度来看卷积核。
  卷积核其实就是一个具有学习能力的过滤器(也可以叫特征提取器,实际都是一个意思,过滤器就是过滤掉干扰,只留下特征)。卷积核在特征图上滑动,每移动一个步长,卷积核会与输入特征图出现重合区域,重合区域对应元素相乘、求和再加上偏置项得到输出图的一个像素点。
在这里插入图片描述
  上图中,蓝色的为 5 × 5 5\times5 5×5 的输入特征图,在其上滑动的为 3 × 3 3\times3 3×3 的卷积核。绿色的为输出图。可以看到输入特征图与输出图的尺寸不一致,为了使两者保持一致,常常在输入特征图的四周填充(padding)像素点(取值通常为 0 0 0),如下图所示。当然,填充不是强制的,在常见的几款深度学习框架中,它们提供了 same 和 valid 两种填充模式。same 模式会自动进行填充以保持输入与输出的图像尺寸一致;valid 模式不会进行任何填充,图像尺寸会被缩小。
在这里插入图片描述
  通过上图我们也可发现,输入特征图与输出图中对应像素点之间的区别就是,输出图中的像素点包含了输入特征图对应像素点及其周围 8 8 8 个像素点的信息。换个角度来说, 3 × 3 3\times3 3×3 的卷积核将 1 1 1 个像素点及其周围 1 1 1 圈像素点的信息融合为了 1 1 1 个像素点。以此类推, 5 × 5 5\times5 5×5 的卷积核将 1 1 1 个像素点及其周围 2 2 2 圈像素点的信息进行了融合。
  通过卷积核的运算,原始输入特征图上的一片区域可以映射到输出图中的一个像素点,这一片区域称为该像素点的感受野(receptive field)。如上图中,输出图中每个像素点在输入特征图中感受野的大小为 3 × 3 3\times3 3×3。卷积层的叠加可以增大感受野,例如:两个 3 × 3 3\times3 3×3 的卷积核叠加,感受野可以扩大到 5 × 5 5\times5 5×5
  此外,卷积核在图像上移动的步长也可以进行设定。下图就是左右步长为 2 2 2、上下步长为 1 1 1 的卷积示意图。
在这里插入图片描述
  以上我们讨论的图像都是单通道图像(灰度图像),那么多通道图像(如:RGB 图像)该如何进行卷积呢?事实上,当图像的通道增加时,卷积核的 “厚度” 也会增加。例如:在 RGB 图像上应用 3 × 3 3\times3 3×3 的卷积核,卷积核的实际尺寸为 3 × 3 × 3 3\times3\times3 3×3×3,核内会有 27 27 27 个权重值。根据卷积核的运算过程,此时三通道的 RGB 图像最终卷积成了单通道的输出图。
  我们也可以在一张图片上应用多个卷积核,让不同的卷积核提取不同的特征。在输出时,我们让每个卷积核的结果单独作为一个输出通道。这样我们在一张图片上应用 n n n 个卷积核就会产生 n n n 通道的输出图。

卷积

  接下来,我们介绍几种常见的 3 × 3 3\times3 3×3 卷积核,分别让它们在灰度图上进行卷积操作,来直观的了解卷积的作用。

  • 平滑卷积
    1 9 [ 1 1 1 1 1 1 1 1 1 ] \frac{1}{9} \left[ \begin{matrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \\ \end{matrix} \right] 91 111111111   通过以上表达式可以推测出:平滑卷积会让图像更加平滑,降低锐化。平滑卷积的作用效果如下:
    在这里插入图片描述

  • 锐化卷积
    [ − 1 − 1 − 1 − 1 9 − 1 − 1 − 1 − 1 ] \left[ \begin{matrix} -1 & -1 & -1 \\ -1 & 9 & -1 \\ -1 & -1 & -1 \\ \end{matrix} \right] 111191111   该卷积核会利用周边像素信息来增强对比度,从而起到锐化的效果。锐化卷积的作用效果如下:
    在这里插入图片描述

  • 垂直梯度卷积
    [ − 1 0 1 − 1 0 1 − 1 0 1 ] \left[ \begin{matrix} -1 & 0 & 1 \\ -1 & 0 & 1 \\ -1 & 0 & 1 \\ \end{matrix} \right] 111000111   该卷积核会增强图片中的垂直线条。其作用效果如下:
    在这里插入图片描述

  • 水平梯度卷积
    [ − 1 − 1 − 1 0 0 0 1 1 1 ] \left[ \begin{matrix} -1 & -1 & -1 \\ 0 & 0 & 0 \\ 1 & 1 & 1 \\ \end{matrix} \right] 101101101   该卷积核会增强图片中的水平线条。其作用效果如下:
    在这里插入图片描述

正向传播

  在对卷积核进行简单介绍后,我们需要将其应用到神经网络中。首先要解决的就是实现上文中动画所展示的计算过程。作为程序员的第一直觉肯定是嵌套几个循环来解决。笔者一开始也是这样想的,但查阅过一些资料后,才发现这种做法虽然能得到正确的结果,但效率太低,无法利用矩阵运算资源。
  设单通道 4 × 4 4\times4 4×4 特征图 I I I,尺寸为 3 × 3 3\times3 3×3 的卷积核 C C C,以及输出图 O O O。可使用矩阵将其表达为如下形式:
I = [ x 1 x 2 x 3 x 4 x 5 x 6 x 7 x 8 x 9 x 10 x 11 x 12 x 13 x 14 x 15 x 16 ] C = [ w 1 , 1 w 1 , 2 w 1 , 3 w 2 , 1 w 2 , 2 w 2 , 3 w 3 , 1 w 3 , 2 w 3 , 3 ] O = [ y 1 y 2 y 3 y 4 ] \begin{matrix} I= \left[ \begin{matrix} x_{1} & x_{2} & x_{3} & x_{4} \\ x_{5} & x_{6} & x_{7} & x_{8} \\ x_{9} & x_{10} & x_{11} & x_{12} \\ x_{13} & x_{14} & x_{15} & x_{16} \end{matrix} \right] & C= \left[ \begin{matrix} w_{1,1} & w_{1,2} & w_{1,3} \\ w_{2,1} & w_{2,2} & w_{2,3} \\ w_{3,1} & w_{3,2} & w_{3,3} \end{matrix} \right] & O= \left[ \begin{matrix} y_{1} & y_{2} \\ y_{3} & y_{4} \end{matrix} \right] \end{matrix} I= x1x5x9x13x2x6x10x14x3x7x11x15x4x8x12x16 C= w1,1w2,1w3,1w1,2w2,2w3,2w1,3w2,3w3,3 O=[y1y3y2y4]  显然 O = C ⋅ I O=C\cdot I O=CI 不符合矩阵运算规则,也无法完成卷积计算过程。我们现在的想法是,寻找一种能完成卷积计算且符合矩阵运算规则的方法。因此我们分别对 I I I C C C O O O 进行如下变形:
I → flatten I ~ = [ x 1 x 2 x 3 x 4 x 5 x 6 x 7 x 8 x 9 x 10 x 11 x 12 x 13 x 14 x 15 x 16 ] I \xrightarrow{\text{flatten}} \widetilde{I} = \left[ \begin{matrix} x_{1} \\ x_{2} \\ x_{3} \\ x_{4} \\ x_{5} \\ x_{6} \\ x_{7} \\ x_{8} \\ x_{9} \\ x_{10} \\ x_{11} \\ x_{12} \\ x_{13} \\ x_{14} \\ x_{15} \\ x_{16} \end{matrix} \right] Iflatten I = x1x2x3x4x5x6x7x8x9x10x11x12x13x14x15x16 C → C ~ = [ w 1 , 1 w 1 , 2 w 1 , 3 0 w 2 , 1 w 2 , 2 w 2 , 3 0 w 3 , 1 w 3 , 2 w 3 , 3 0 0 0 0 0 0 w 1 , 1 w 1 , 2 w 1 , 3 0 w 2 , 1 w 2 , 2 w 2 , 3 0 w 3 , 1 w 3 , 2 w 3 , 3 0 0 0 0 0 0 0 0 w 1 , 1 w 1 , 2 w 1 , 3 0 w 2 , 1 w 2 , 2 w 2 , 3 0 w 3 , 1 w 3 , 2 w 3 , 3 0 0 0 0 0 0 w 1 , 1 w 1 , 2 w 1 , 3 0 w 2 , 1 w 2 , 2 w 2 , 3 0 w 3 , 1 w 3 , 2 w 3 , 3 ] C \rightarrow \widetilde{C} = \left[ \begin{matrix} w_{1,1} & w_{1,2} & w_{1,3} & 0 & w_{2,1} & w_{2,2} & w_{2,3} & 0 & w_{3,1} & w_{3,2} & w_{3,3} & 0 & 0 & 0 & 0 & 0 \\ 0 & w_{1,1} & w_{1,2} & w_{1,3} & 0 & w_{2,1} & w_{2,2} & w_{2,3} & 0 & w_{3,1} & w_{3,2} & w_{3,3} & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & w_{1,1} & w_{1,2} & w_{1,3} & 0 & w_{2,1} & w_{2,2} & w_{2,3} & 0 & w_{3,1} & w_{3,2} & w_{3,3} & 0 & \\ 0 & 0 & 0 & 0 & 0 & w_{1,1} & w_{1,2} & w_{1,3} & 0 & w_{2,1} & w_{2,2} & w_{2,3} & 0 & w_{3,1} & w_{3,2} & w_{3,3} \\ \end{matrix} \right] CC = w1,1000w1,2w1,100w1,3w1,2000w1,300w2,10w1,10w2,2w2,1w1,2w1,1w2,3w2,2w1,3w1,20w2,30w1,3w3,10w2,10w3,2w3,1w2,2w2,1w3,3w3,2w2,3w2,20w3,30w2,300w3,1000w3,2w3,100w3,3w3,2000w3,3 O → flatten O ~ = [ y 1 y 2 y 3 y 4 ] O \xrightarrow{\text{flatten}} \widetilde{O} = \left[ \begin{matrix} y_{1} \\ y_{2} \\ y_{3} \\ y_{4} \end{matrix} \right] Oflatten O = y1y2y3y4   这样 O ~ = C ~ ⋅ I ~ \widetilde{O} = \widetilde{C} \cdot \widetilde{I} O =C I 既符合矩阵运算规则又完成了卷积运算过程。此外,大多数卷积核内还设有一个偏置,但在上述公式中并未体现。 O ~ \widetilde{O} O 只需再做一次向量加法即可完成偏置的计算,这里不再展开。

反向传播

  将卷积应用到神经网路,反向传播的过程也是必不可少的,只有这样才能完成卷积核内权重和偏置的更新。通过神经网路的基本原理一文,我们知道反向传播中每层需要完成两个计算工作。接下来,我们首先完成 “第一个工作” —— 根据损失值对本层的输出值的偏导,求损失值对上一层输出值的偏导。
  我们继续沿用正向传播中使用的假设,以保持整体思路的连贯性。我们首先考虑求 ∂ ℓ ∂ x 1 \frac{\partial\ell}{\partial x_1} x1。通过正向传播中的矩阵公式,我们发现 y 1 y_1 y1 y 2 y_2 y2 y 3 y_3 y3 y 4 y_4 y4 都对 x 1 x_1 x1 有偏导,导致这个偏导关系的就是矩阵 C ~ \widetilde{C} C 的第一列。我们将 C ~ \widetilde{C} C 的第一列从上到下记作 c 1 , 1 c_{1,1} c1,1 c 2 , 1 c_{2,1} c2,1 c 3 , 1 c_{3,1} c3,1 c 4 , 1 c_{4,1} c4,1,那么 ∂ y 1 ∂ x 1 = c 1 , 1 \frac{\partial y_1}{\partial x_1}=c_{1,1} x1y1=c1,1 ∂ y 2 ∂ x 1 = c 2 , 1 \frac{\partial y_2}{\partial x_1}=c_{2,1} x1y2=c2,1 ∂ y 3 ∂ x 1 = c 3 , 1 \frac{\partial y_3}{\partial x_1}=c_{3,1} x1y3=c3,1 ∂ y 4 ∂ x 1 = c 4 , 1 \frac{\partial y_4}{\partial x_1}=c_{4,1} x1y4=c4,1。因此有 ∂ ℓ ∂ x 1 = c 1 , 1 ⋅ ∂ ℓ ∂ y 1 + c 2 , 1 ⋅ ∂ ℓ ∂ y 2 + c 3 , 1 ⋅ ∂ ℓ ∂ y 3 + c 4 , 1 ⋅ ∂ ℓ ∂ y 4 \frac{\partial \ell}{\partial x_1}=c_{1,1} \cdot \frac{\partial \ell}{\partial y_1} + c_{2,1} \cdot \frac{\partial \ell}{\partial y_2} + c_{3,1} \cdot \frac{\partial \ell}{\partial y_3} + c_{4,1} \cdot \frac{\partial \ell}{\partial y_4} x1=c1,1y1+c2,1y2+c3,1y3+c4,1y4。依次类推,我们发现如下规律: ∂ ℓ ∂ x i = c 1 , i ⋅ ∂ ℓ ∂ y 1 + c 2 , i ⋅ ∂ ℓ ∂ y 2 + c 3 , i ⋅ ∂ ℓ ∂ y 3 + c 4 , i ⋅ ∂ ℓ ∂ y 4 \frac{\partial \ell}{\partial x_i}=c_{1,i} \cdot \frac{\partial \ell}{\partial y_1} + c_{2,i} \cdot \frac{\partial \ell}{\partial y_2} + c_{3,i} \cdot \frac{\partial \ell}{\partial y_3} + c_{4,i} \cdot \frac{\partial \ell}{\partial y_4} xi=c1,iy1+c2,iy2+c3,iy3+c4,iy4,其中 i = 1 , 2 , ⋯   , 16 i = 1, 2, \cdots, 16 i=1,2,,16。综上,我们发现有如下矩阵公式成立:
[ ∂ ℓ ∂ x 1 ∂ ℓ ∂ x 2 ⋮ ∂ ℓ ∂ x 16 ] = C ~ ⊤ ⋅ [ ∂ ℓ ∂ y 1 ∂ ℓ ∂ y 2 ∂ ℓ ∂ y 3 ∂ ℓ ∂ y 4 ] \left[ \begin{matrix} \frac{\partial \ell}{\partial x_1} \\ \frac{\partial \ell}{\partial x_2} \\ \vdots \\ \frac{\partial \ell}{\partial x_{16}} \end{matrix} \right] = \widetilde{C}^\top \cdot \left[ \begin{matrix} \frac{\partial \ell}{\partial y_1} \\ \frac{\partial \ell}{\partial y_2} \\ \frac{\partial \ell}{\partial y_3} \\ \frac{\partial \ell}{\partial y_4} \end{matrix} \right] x1x2x16 =C y1y2y3y4   通过以上公式我们可以发现与多层前馈神经网路反向传播相同的地方,多层前馈神经网路反向传播使用的权重矩阵也是正向传播权重矩阵的转置。这里应该是有数学公式存在的,笔者能力有限,感兴趣的读者可以查找相关资料。
  对于 “第二个任务” 我们这里不再做详细推导,直接给出结果。以损失值对输出图的偏导组成的矩阵 O ′ O' O 为卷积核,在输入特征图 I I I 上做卷积运算,得出的输出矩阵即为对权重的偏导组成的矩阵 W ′ W' W。笔者暂时将 A @ B A@B A@B 称为以 A A A 为卷积核在 B B B 上做卷积运算,下面给出 W ′ = O ′ @ I W'=O'@I W=O@I 的具体公式:
[ ∂ ℓ ∂ w 1 , 1 ∂ ℓ ∂ w 1 , 2 ∂ ℓ ∂ w 1 , 3 ∂ ℓ ∂ w 2 , 1 ∂ ℓ ∂ w 2 , 2 ∂ ℓ ∂ w 2 , 3 ∂ ℓ ∂ w 3 , 1 ∂ ℓ ∂ w 3 , 2 ∂ ℓ ∂ w 3 , 3 ] = [ ∂ ℓ ∂ y 1 ∂ ℓ ∂ y 2 ∂ ℓ ∂ y 3 ∂ ℓ ∂ y 4 ] @ [ x 1 x 2 x 3 x 4 x 5 x 6 x 7 x 8 x 9 x 10 x 11 x 12 x 13 x 14 x 15 x 16 ] \left[ \begin{matrix} \frac{\partial \ell}{\partial w_{1,1}} & \frac{\partial \ell}{\partial w_{1,2}} & \frac{\partial \ell}{\partial w_{1,3}} \\ \frac{\partial \ell}{\partial w_{2,1}} & \frac{\partial \ell}{\partial w_{2,2}} & \frac{\partial \ell}{\partial w_{2,3}} \\ \frac{\partial \ell}{\partial w_{3,1}} & \frac{\partial \ell}{\partial w_{3,2}} & \frac{\partial \ell}{\partial w_{3,3}} \\ \end{matrix} \right] = \left[ \begin{matrix} \frac{\partial \ell}{\partial y_1} & \frac{\partial \ell}{\partial y_2} \\ \frac{\partial \ell}{\partial y_3} & \frac{\partial \ell}{\partial y_4} \end{matrix} \right] @ \left[ \begin{matrix} x_{1} & x_{2} & x_{3} & x_{4} \\ x_{5} & x_{6} & x_{7} & x_{8} \\ x_{9} & x_{10} & x_{11} & x_{12} \\ x_{13} & x_{14} & x_{15} & x_{16} \end{matrix} \right] w1,1w2,1w3,1w1,2w2,2w3,2w1,3w2,3w3,3 =[y1y3y2y4]@ x1x5x9x13x2x6x10x14x3x7x11x15x4x8x12x16

池化

  与卷积操作类似的是池化也有一个池化核,也可以在特征图上移动。但二者也有诸多不同点,不同点如下:

  • 池化核内没有任何参数。卷积核是使用落入核内的像素数据以及核内参数完成运算,而池化核仅使用落入核内的像素数据完成运算。
  • 池化核的 “厚度” 恒为一。池化核仅作用在一个图层上,当多通道图像输入时,每个通道上都会有一个池化核。这样经过池化操作的图像其通道数不变。

  当前常见的池化操作主要有最大池化和平均池化两种。最大池化就是选取落入池化核的像素数据中的最大值作为输出值;平均池化就是落入池化核的像素数据的平均值作为输出值。

参考文献

1998 LeNet《Gradient-Based Learning Applied to Document Recognition
2012 AlexNet 《ImageNet Classification with Deep Convolutional Neural Networks
2014 VGGNet 《VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION
2014 InceptionNet v1 《Going deeper with convolutions
2015 InceptionNet v2 v3 《Rethinking the Inception Architecture for Computer Vision
2015 ResNet 《Deep Residual Learning for Image Recognition
2017 ResNeXt 《Aggregated Residual Transformations for Deep Neural Networks

附录

  生成卷积核动态演示图的代码如下:

import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation

in_map_size = (5, 5)
kernel_size = (3, 3)
stride = (1, 2)
padding = (0, 0)

in_map_with_padding_size = (2 * padding[0] + in_map_size[0], 2 * padding[1] + in_map_size[1])
out_map_size = ((in_map_with_padding_size[0] - kernel_size[0]) // stride[0] + 1,
                (in_map_with_padding_size[1] - kernel_size[1]) // stride[1] + 1)
out_map_top_left = ((in_map_with_padding_size[0] - out_map_size[0]) // 2,
                    (in_map_with_padding_size[1] - out_map_size[1]) // 2)

ax = plt.axes(projection='3d')


def update(frame):
    kernel_top_left = ((frame // out_map_size[1]) * stride[0], (frame % out_map_size[1]) * stride[1])
    kernel_bottom_right = (kernel_top_left[0] + kernel_size[0], kernel_top_left[1] + kernel_size[1])
    kernel_top_right = (kernel_top_left[0], kernel_bottom_right[1])
    kernel_bottom_left = (kernel_bottom_right[0], kernel_top_left[1])
    pixel_top_left = ((frame // out_map_size[1]) + out_map_top_left[0], (frame % out_map_size[1]) + out_map_top_left[1])
    pixel_bottom_right = (pixel_top_left[0] + 1, pixel_top_left[1] + 1)
    pixel_top_right = (pixel_top_left[0], pixel_bottom_right[1])
    pixel_bottom_left = (pixel_bottom_right[0], pixel_top_left[1])
    ax.clear()
    ax.axis(False)
    x, y = np.meshgrid(np.arange(0, in_map_with_padding_size[0] + 1),
                       np.arange(0, in_map_with_padding_size[1] + 1))
    z = np.zeros(x.shape)
    ax.plot_surface(x, y, z, color='None', linestyle='--', edgecolor='black', zorder=1)
    x, y = np.meshgrid(np.arange(padding[0], padding[0] + in_map_size[0] + 1),
                       np.arange(padding[1], padding[1] + in_map_size[1] + 1))
    z = np.zeros(x.shape)
    ax.plot_surface(x, y, z, edgecolor='black', zorder=2, alpha=0.5)
    x, y = np.meshgrid(np.arange(kernel_top_left[0], kernel_bottom_right[0] + 1),
                       np.arange(kernel_top_left[1], kernel_bottom_right[1] + 1))
    z = np.zeros(x.shape)
    ax.plot_surface(x, y, z, zorder=3)
    x, y = np.meshgrid(np.arange(out_map_top_left[0], out_map_top_left[0] + out_map_size[0] + 1),
                       np.arange(out_map_top_left[1], out_map_top_left[1] + out_map_size[1] + 1))
    z = np.ones(x.shape)
    ax.plot_surface(x, y, z, edgecolor='black', zorder=4, alpha=0.5)
    x, y = np.meshgrid([pixel_top_left[0], pixel_bottom_right[0]], [pixel_top_left[1], pixel_bottom_right[1]])
    z = np.ones(x.shape)
    ax.plot_surface(x, y, z, zorder=5)
    x, y, z = [kernel_top_left[0], pixel_top_left[0]], [kernel_top_left[1], pixel_top_left[1]], [0, 1]
    ax.plot(x, y, z, color='black', zorder=6)
    x, y, z = [kernel_bottom_right[0], pixel_bottom_right[0]], [kernel_bottom_right[1], pixel_bottom_right[1]], [0, 1]
    ax.plot(x, y, z, color='black', zorder=6)
    x, y, z = [kernel_top_right[0], pixel_top_right[0]], [kernel_top_right[1], pixel_top_right[1]], [0, 1]
    ax.plot(x, y, z, color='black', zorder=6)
    x, y, z = [kernel_bottom_left[0], pixel_bottom_left[0]], [kernel_bottom_left[1], pixel_bottom_left[1]], [0, 1]
    ax.plot(x, y, z, color='black', zorder=6)


ani = animation.FuncAnimation(plt.gcf(), update, interval=500, frames=out_map_size[0] * out_map_size[1])
ani.save(r'test.gif')
plt.show()

  生成卷积效果图的代码如下:

import matplotlib.image
import matplotlib.pyplot as plt
import numpy as np
import torch

conv = torch.nn.Conv2d(1, 1, (3, 3), bias=False)
# conv.weight.data = torch.ones(1, 1, 3, 3) / 9
# conv.weight.data = torch.Tensor([[[
#     [-1, -1, -1],
#     [-1, 9, -1],
#     [-1, -1, -1]
# ]]])
# conv.weight.data = torch.Tensor([[[
#     [-1, 0, 1],
#     [-1, 0, 1],
#     [-1, 0, 1]
# ]]])
conv.weight.data = torch.Tensor([[[
    [-1, -1, -1],
    [0, 0, 0],
    [1, 1, 1]
]]])

image = matplotlib.image.imread(r'C:\Users\11191\Desktop\10.jpg')   # 输入的必须是灰度图像
source = image.astype(np.float32)
source = torch.from_numpy(source[np.newaxis, np.newaxis, :, :])
result = conv(source)[0][0].detach().numpy()
result = result.astype(np.uint8)

axes1 = plt.subplot(121)
axes2 = plt.subplot(122)
axes1.axis(False)
axes2.axis(False)
axes1.imshow(image, cmap='gray')
axes2.imshow(result, cmap='gray')
axes1.set_title('Source')
axes2.set_title('Result')
plt.show()
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值