【Python · PyTorch】卷积神经网络(基础概念)

【Python · PyTorch】卷积神经网络 CNN(基础概念)

  • [0. 生物学相似性](#0. 生物学相似性)
  • [1. 概念](#1. 概念)
    • [1.1 定义](#1.1 定义)
    • [1.2 优势](#1.2 优势)
      • [1.2.1 权重共享](#1.2.1 权重共享)
      • [1.2.2 局部连接](#1.2.2 局部连接)
      • [1.2.3 层次结构](#1.2.3 层次结构)
    • [1.3 结构](#1.3 结构)
    • [1.4 数据预处理](#1.4 数据预处理)
      • [1.4.1 标签编码](#1.4.1 标签编码)
        • [① One-Hot编码 / 独热编码](#① One-Hot编码 / 独热编码)
        • [② Word Embedding / 词嵌入](#② Word Embedding / 词嵌入)
      • [1.4.2 归一化](#1.4.2 归一化)
        • [① Min-Max归一化(最大最小归一化)](#① Min-Max归一化(最大最小归一化))
        • [② Z-Score归一化(标准化 Standardization)](#② Z-Score归一化(标准化 Standardization))
  • [2. 基础概念](#2. 基础概念)
      • [2.2.1 卷积层](#2.2.1 卷积层)
      • [2.2.2 辅助操作](#2.2.2 辅助操作)
        • [① 卷积步长](#① 卷积步长)
        • [② 零填充](#② 零填充)
      • [2.2.3 池化层 / 汇聚层](#2.2.3 池化层 / 汇聚层)
        • [① 最大池化](#① 最大池化)
        • [② 平均池化 / 均值池化](#② 平均池化 / 均值池化)
      • [2.2.4 感受野](#2.2.4 感受野)
      • [2.2.5 空洞卷积 / 膨胀卷积](#2.2.5 空洞卷积 / 膨胀卷积)
        • [① Gridding Effect 现象](#① Gridding Effect 现象)
        • [② 如何设计膨胀系数?](#② 如何设计膨胀系数?)
      • [2.2.6 可分离卷积](#2.2.6 可分离卷积)
        • [① 空间可分离卷积](#① 空间可分离卷积)
        • [② 深度可分离卷积](#② 深度可分离卷积)
      • [2.2.7 扁平卷积](#2.2.7 扁平卷积)
      • [2.2.8 分组卷积](#2.2.8 分组卷积)
      • [2.2.9 混洗分组卷积](#2.2.9 混洗分组卷积)
      • [2.2.6 上采样 vs 下采样](#2.2.6 上采样 vs 下采样)
        • [① 上采样](#① 上采样)
        • [② 下采样](#② 下采样)
      • [2.2.7 上采样](#2.2.7 上采样)
  • [3. 评估方法](#3. 评估方法)
    • [3.1 混淆矩阵](#3.1 混淆矩阵)
    • [3.2 精确度](#3.2 精确度)
    • [3.3 其他指标](#3.3 其他指标)

0. 生物学相似性

卷积神经网络从猫视觉皮层电生理研究中获得启发,通过仿造生物的视知觉机制来构建模型。卷积网络中卷积核的设定就对应着视觉神经系统中视觉皮层对视觉空间的组织。

视觉皮层细胞从视网膜上的光感受器接收信号,但单个视觉皮层细胞不会接收光感受器的所有信号,而是只接受其所支配的刺激区域,即感受野内的信号。只有感受野内的刺激才能够激活该神经元。多个视觉皮层细胞通过系统地将感受野叠加,完整接收视网膜传递的信号并建立视觉空间 。

卷积神经网络中基于感受野设定的稀疏连接有明确对应的神经科学过程------视觉神经系统中视觉皮层(visual cortex)对视觉空间(visual space)的组织 。视觉皮层细胞从视网膜上的光感受器接收信号,但单个视觉皮层细胞不会接收光感受器的所有信号,而是只接受其所支配的刺激区域,即感受野内的信号。只有感受野内的刺激才能够激活该神经元。多个视觉皮层细胞通过系统地将感受野叠加完整接收视网膜传递的信号并建立视觉空间。

事实上机器学习的"感受野"一词即来自其对应的生物学研究。卷积神经网络中的权重共享的性质在生物学中没有明确证据,但在对与大脑学习密切相关的目标传播(target-propagation, TP)和反馈调整(feedback alignment, FA) 机制的研究中,权重共享提升了学习效果 。

1. 概念

1.1 定义

卷积神经网络(Convolutional Neural Networks, CNN)是一类包含卷积计算且具有深度结构的前馈神经网络(Feedforward Neural Networks),是深度学习(deep learning)的代表算法之一 。

卷积神经网络具有表征学习(representation learning)能力,能够按其阶层结构对输入信息进行平移不变分类(shift-invariant classification),因此也被称为"平移不变人工神经网络(Shift-Invariant Artificial Neural Networks, SIANN)" 。

应用领域:计算机视觉、音频识别

1.2 优势

1.2.1 权重共享

使用相同的权重参数对输入数据进行处理,从而减少模型的参数数量,降低计算复杂度,并提高模型的泛化能力;在卷积神经网络中主要体现在卷积核 (过滤器)

在卷积神经网络中,权值共享是指在卷积层中,同一个卷积核在不同位置上的权值是相同的,这样可以大大减少模型参数,提高模型泛化能力。

权值/权重独立与CNN的权重共享不同:

1.2.2 局部连接

在卷积神经网络中,局部连接是指在卷积层中,每个卷积核只与输入数据的一部分进行卷积运算,而不是与整个输入数据进行卷积运算,这样可以提取出局部特征,增强模型的特征提取能力。

用类比卷积神经网络的方式描述全连接:

1.2.3 层次结构

卷积神经网络是一种包含卷积计算且具有深度结构的前馈神经网络,其所包含的层次结构如下:

输入层:接收原始图像数据或其他类型的网格结构数据。

卷积层:卷积层是CNN的核心,通过卷积操作提取输入数据的局部特征。每个卷积核可以提取一种特定的特征,多个卷积核可以并行工作以提取不同类型的特征。卷积操作通过滤波器对局部输入数据进行内积计算,滤波器在数据窗口上滑动,计算每个局部数据的输出‌。

激活层:激活层用于提升网络的非线性能力,通常跟在卷积层之后,其能够解决梯度消失问题、加快收敛速度‌。

池化层:池化层对卷积层的输出进行下采样 (降维),以减少参数数量并提高计算效率。

全连接层:全连接层负责将前面层提取的特征综合起来,用于分类或回归等任务。全连接层的每个神经元都与前一层的所有神经元相连‌。

BatchNorm层:BN层通过规范化手段将每层神经网络的输入值的分布强行拉回到均值为0,方差为1的标准正态分布,以加快训练速度并减小图像之间的绝对差异‌。

1.3 结构

输入层:输入接收数据集,处理原始图像数据。

卷积层:通过卷积操作提取图像特征,卷积操作使用‌滤波器 (卷积核) 对图像的局部区域进行内积运算,提取特征。

‌激活层‌:对卷积层的输出应用非线性激活函数,引入非线性以增强网络的表达能力。

池化层:通过下采样减少数据的维度和参数数量,通常有两种方式:最大池化和平均池化。

全连接层:在网络的最后部分,将前面的特征图展平后进行分类/回归任务。

1.4 数据预处理

1.4.1 标签编码

① One-Hot编码 / 独热编码

One-Hot编码 / 独热编码:一种常用于机器学习中的特征编码方式,主要用于处理分类数据。它的基本思想是将分类变量转换为机器学习算法易于处理的形式。

  • 例如:MNIST数据集共有10个分类结果,若利用独热编码其可表示为:

    ↓ 编码下标 \ 分类结果 → 0 1 2 3 4 5 6 7 8 9
    0 1 0 0 0 0 0 0 0 0 0
    1 0 1 0 0 0 0 0 0 0 0
    2 0 0 1 0 0 0 0 0 0 0
    3 0 0 0 1 0 0 0 0 0 0
    4 0 0 0 0 1 0 0 0 0 0
    5 0 0 0 0 0 1 0 0 0 0
    6 0 0 0 0 0 0 1 0 0 0
    7 0 0 0 0 0 0 0 1 0 0
    8 0 0 0 0 0 0 0 0 1 0
    9 0 0 0 0 0 0 0 0 0 1

用途

  • 更易于表示分类
  • 适用于多种算法
② Word Embedding / 词嵌入

Word Embedding / 词嵌入:一种自然语言处理机器学习任务中常用的特征编码方式,便于机器学习算法读取文本信息并理解相关内容。通过词嵌入将单词转变为在预定义的向量空间中的实数向量,每个单词都映射到一个向量。

目的:通过数字替代字母,从而便于程序进行运算,还可完成词语间相似度、词语间关联等计算。而独热编码无法胜任此项工作,故利用词嵌入实现。

常见的词嵌入模型:EmbeddingLayer算法、Word2Vec模型、Glove模型、FastText模型

举例:"猫"对应的向量为 a ⃗ = [ 0.1 0.2 0.3 ] \vec{a}=\begin{bmatrix}0.1\\0.2 \\ 0.3\end{bmatrix} a = 0.10.20.3 ,"虎"对应的向量为 b ⃗ = [ 0.2 0.2 0.4 ] \vec{b}=\begin{bmatrix}0.2\\0.2\\0.4\end{bmatrix} b = 0.20.20.4 ,"鹰"对应的映射为 c ⃗ = [ − 0.4 − 0.5 − 0.2 ] \vec{c}=\begin{bmatrix}-0.4\\-0.5\\ -0.2\end{bmatrix} c = −0.4−0.5−0.2

计算 猫&虎、猫&鹰 间的余弦相似度,余弦相似度公式如下:
c o s < a ⃗ , b ⃗ > = a ⃗ ⋅ b ⃗ ∣ ∣ a ⃗ ∣ ∣ ∣ ∣ b ⃗ ∣ ∣ cos\left<\vec{a},\vec{b}\right>=\frac{\vec{a}\cdot\vec{b}}{||\vec{a}||\space||\vec{b}||} cos⟨a ,b ⟩=∣∣a ∣∣ ∣∣b ∣∣a ⋅b

余弦相似度的取值范围为 [ − 1 , 1 ] [-1,1] [−1,1],当两个向量的夹角为180°时,余弦相似度为-1。‌

余弦相似度衡量的是两个向量之间的夹角,其值的符号表示向量之间的方向关系,数值的大小表示它们的相似程度。

当两个向量的夹角为0度时,即完全重合,余弦相似度为1;当两个向量的夹角为90度时,即正交或无关,余弦相似度为0;当两个向量的夹角为180度时,即完全相反,余弦相似度为-1。

计算过程:

python 复制代码
import torch
import torch.nn as nn
 

a = torch.tensor([0.1, 0.2, 0.3], dtype=torch.float)
b = torch.tensor([0.2, 0.2, 0.4], dtype=torch.float)
c = torch.tensor([-0.4, -0.5, -0.2], dtype=torch.float)
 
# 计算两个余弦值的余弦相似度
cos = nn.CosineSimilarity(dim=0, eps=1e-6)
 
output1 = cos(a, b)
output2 = cos(a, c)
output1, output2

计算结果:

python 复制代码
(tensor(0.9820), tensor(-0.7968))

由此可得出结论:猫与虎更接近,猫与鹰完全相反。

1.4.2 归一化

归一化:归一化是一种常见的数据预处理技术,用于将数据转换为统一的比例范围,以消除不同变量之间的量纲差异

用途

  • 消除量纲差异:消除单位差异
  • 避免权重不平衡:避免因数值范围差异大引起的权重不平衡问题
  • 加速模型收敛
① Min-Max归一化(最大最小归一化)

将数据线性地映射至一个区间(例如:[0,1] 或 [-1,1])
x ∗ = x − M i n ( x ) M a x ( x ) − M i n ( x ) x^*=\frac{x-Min(x)}{Max(x)-Min(x)} x∗=Max(x)−Min(x)x−Min(x)

其中, x x x是原始数据, x ∗ x^* x∗是归一化后数据。

python 复制代码
x # 评估器导入
from sklearn.preprocessing import MinMaxScaler

#评估器实例化
scaler = MinMaxScaler() 							# 评估器实例化:默认映射[0.1]区间
scaler = scaler.fit(data) 							# 计算min(x)、max(x)
# scaler = MinMaxScaler(feature_range=[5,10]) 		# 映射至除[0.1]其他区间
result = scaler.transform(data)
② Z-Score归一化(标准化 Standardization)

将数据映射至 均值为0 标准差为1 的标准正态分布的一个区间。
x ∗ = x − μ σ x^*=\frac{x-\mu}{\sigma} x∗=σx−μ

其中, x x x是原始数据, x ∗ x^* x∗是标准化后的数据, μ \mu μ是原始数据的均值, σ \sigma σ是原始数据的标准差。

python 复制代码
# 评估器导入
from sklearn.preprocessing import StandardScaler

# 评估器实例化
scaler = StandardScaler()
scaler.fit(data)
scaler.transform(X)

2. 基础概念

2.2.1 卷积层

卷积(Convolution),分析数学中的一种运算。在信号或图像处理中,经常使用一维或二维卷积。

卷积运算是卷积神经网络的核心,它通过在音频/图像上滑动卷积核,计算核与图像的局部区域的点积,从而形成 特征图(Feature Map)

卷积运算捕捉了图像的局部关联并保留了图像的空间关系,卷积神经网络依靠卷积运算实现图像平移不变性。

卷积意义

卷积数学公式:
∫ 0 t f ( x ) g ( t − x ) d x \int^t_0f(x)g(t-x)dx ∫0tf(x)g(t−x)dx

在卷积运算中,可理解为 瞬时行为的持续性后果,可类比蝴蝶效应。

在图像识别领域中, f ( ⋅ ) f(·) f(⋅)等同于图像, g ( ⋅ ) g(·) g(⋅)等同于卷积核,通过卷积运算可求得周围像素对中间像素的影响程度,或被扫描区域对卷积核/过滤器过滤的响应程度。滑动过程中所遇两者外形越相似,则卷积得到的响应越高。卷积神经网络中,卷积核实际上在提取某种局部特征,图片中与该卷积核过滤所需 越相似的区域响应越高。

卷积 三种意义:

  • 一个系统 + 输入不稳定 + 输出稳定 + 用卷积求系统存量
    • 输入不稳定:输入数据存在差异(如 x x x和 x \mathscr{x} x)
    • 输出稳定:变化趋势稳定
    • 用卷积求系统存量:求变化后的系统总存量,这一过程持续故采用积分形式
  • 周围像素点如何影响中间像素点
    • 周围像素点与中间像素点共同构成局部特征
    • 通过神经网络对局部特征进行归纳识别从而得出分类/预测结果
  • 对周围位置的试探
    • 过滤器过滤保留有用特征,交由神经网络判断结果

在图像识别领域中,像素为离散点,故将积分更换为求和,两种维度卷积情况如后文所示。


① 一维卷积

一维卷积一般用于 音频/时序数据 的处理,一维卷积公式如下:
y t = ∑ k = 1 K w k x t − k + 1 y_t=\sum^{K}{k=1}w_kx{t-k+1} yt=k=1∑Kwkxt−k+1

其中: x t − k + 1 x_{t-k+1} xt−k+1表示输入的信号序列, w k w_k wk表示 滤波器 (卷积核) , y t y_t yt表示输出。

向量形式
y = w ∗ x \boldsymbol{y}=\boldsymbol{w} \ast \boldsymbol{x} y=w∗x

其中: w \boldsymbol{w} w和 x \boldsymbol{x} x分别为滤波器和输入信号的向量形式, ∗ \ast ∗表示卷积运算, y \boldsymbol{y} y表示输出。

② 二维卷积

二维卷积一般用于 图像/视频数据 的处理,二维卷积公式如下:
y i j = ∑ u = 1 U ∑ v = 1 V w u v x i − u + 1 , j − v + 1 y_{ij}=\sum^{U}{u=1}\sum^{V}{v=1}w_{uv}x_{i-u+1,j-v+1} yij=u=1∑Uv=1∑Vwuvxi−u+1,j−v+1

矩阵形式
Y = W ∗ X \boldsymbol{Y}=\boldsymbol{W} \ast \boldsymbol{X} Y=W∗X

其中: W \boldsymbol{W} W和 X \boldsymbol{X} X分别为滤波器和输入信号的矩阵形式, ∗ \ast ∗表示卷积运算, Y \boldsymbol{Y} Y表示输出。

单通道单卷积核

卷积也可以拥有偏置,这种单一卷积核的情况称为单通道单卷积核。

单通道多卷积核

某些情况下,可使用多个卷积核完成不同类型特征的提取。

卷积核可以认为是具有识别某一类元素(特征)的能力,而对于一些复杂结构的数据来说仅仅只是通过一类特征来进行辨识往往是不够的,因此,通常来说我们都会通过多个不同的卷积核来对输入进行特征提取得到多个特征图,然再输入到后续的网络中进行后续任务。

多通道单卷积核

多通道的单卷积核天生针对每个通道进行操作,所以实际上还是视作单个卷积核。

RGB合成示意图

多通道多卷积核

多通道也可以利用多卷积核针对不同通道分别提取多种不同种类的特征。

③ 互相关

**互相关 (Cross-Correlation) **→ 不翻转卷积:用滑动窗口的点积 → 衡量两个序列的相关性

它与卷积的区别仅仅在于是否翻转:翻转指所有维度颠倒次序 → 即旋转180°(二维 → 上下颠倒+左右颠倒)
y i j = ∑ u = 1 U ∑ v = 1 V w u v x i + u − 1 , j + v − 1 y_{ij}=\sum^{U}{u=1}\sum^{V}{v=1}w_{uv}x_{i+u-1,j+v-1} yij=u=1∑Uv=1∑Vwuvxi+u−1,j+v−1
矩阵形式
Y = W ⊗ X = r o t 180 ( W ) ∗ X \begin{equation} \begin{aligned} Y &= \boldsymbol{W} \otimes \boldsymbol{X} \\ &= rot180(\boldsymbol{W}) \ast \boldsymbol{X} \end{aligned} \end{equation} Y=W⊗X=rot180(W)∗X

2.2.2 辅助操作

① 卷积步长

卷积步长:卷积核每次扫描间距,一般用字母 k k k或 K K K表示。

一维情况

卷积步长 s = 2 s=2 s=2

二维情况

卷积步长 s = 2 s=2 s=2

② 零填充

零填充:为原始输入特征周围填充若干圈零元素,一般用字母 p p p或P表示。

卷积类型

  • 窄卷积 (Narrow Convolution) :步长 s = 1 s=1 s=1,两端不补零 p = 0 p=0 p=0,卷积后输出长度为 m − k + 1 m-k+1 m−k+1。
  • 宽卷积 (Wide Convolution) :步长 s = 1 s=1 s=1,两端补零 p = k − 1 p=k-1 p=k−1,卷积后输出长度为 m + k − 1 m+k-1 m+k−1。
  • 等宽卷积 (Equal-Width Convolution) :步长 s = 1 s=1 s=1,两端补零 p = k − 1 2 ,卷积后输出长度为 p=\frac{k-1}{2},卷积后输出长度为 p=2k−1,卷积后输出长度为m。

2.2.3 池化层 / 汇聚层

池化层 (Pooling Layer):又称汇聚层

**池化 (Pooling):**属于下采样(Down Sampling)


作用:特征筛选 → 减少特征数量 → 减少后续层参数数量

① 最大池化

最大池化 (Maximum Pooling):取区域内所有神经元最大激活值

② 平均池化 / 均值池化

平均池化 (Mean Pooling):取区域内所有神经元激活值均值

2.2.4 感受野

感受野 (Receptive Field):卷积神经网络每一层输出的特征图 (Feature Map) 上的像素点在输入图片上映射的区域大小。

生物学 - 感受野 :视觉皮层细胞从视网膜上的光感受器接收信号,但单个视觉皮层细胞不会接收光感受器的所有信号,而是只接受其所支配的刺激区域,即感受野内的信号。只有感受野内的刺激才能够激活该神经元。多个视觉皮层细胞通过系统地将感受野叠加,完整接收视网膜传递的信号并建立视觉空间 。

感受野计算公式:
l k = l k − 1 + [ ( f k − 1 ) ∗ ∏ i = 0 k − 1 s i ] l_k=l_{k-1}+[(f_k-1) \ast \prod^{k-1}_{i=0}s_i] lk=lk−1+[(fk−1)∗i=0∏k−1si]

其中 , l k − 1 l_{k-1} lk−1为第 k − 1 k − 1 k−1层对应的感受野大小, f k f_k fk为第 k k k层卷积层的卷积核大小 / 第 k k k层池化层的池化尺寸大小, s i s_i si为步长stride。

感受野绘制工具 (开源)(作者:WZMIAOMIAO)

该工具代码如下:

python 复制代码
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap


def dilated_conv_one_pixel(center: (int, int),
                           feature_map: np.ndarray,
                           k: int = 3,
                           r: int = 1,
                           v: int = 1):
    """
    膨胀卷积核中心在指定坐标center处时,统计哪些像素被利用到,
    并在利用到的像素位置处加上增量v
    Args:
        center: 膨胀卷积核中心的坐标
        feature_map: 记录每个像素使用次数的特征图
        k: 膨胀卷积核的kernel大小
        r: 膨胀卷积的dilation rate
        v: 使用次数增量
    """
    assert divmod(3, 2)[1] == 1

    # left-top: (x, y)
    left_top = (center[0] - ((k - 1) // 2) * r, center[1] - ((k - 1) // 2) * r)
    for i in range(k):
        for j in range(k):
            feature_map[left_top[1] + i * r][left_top[0] + j * r] += v


def dilated_conv_all_map(dilated_map: np.ndarray,
                         k: int = 3,
                         r: int = 1):
    """
    根据输出特征矩阵中哪些像素被使用以及使用次数,
    配合膨胀卷积k和r计算输入特征矩阵哪些像素被使用以及使用次数
    Args:
        dilated_map: 记录输出特征矩阵中每个像素被使用次数的特征图
        k: 膨胀卷积核的kernel大小
        r: 膨胀卷积的dilation rate
    """
    new_map = np.zeros_like(dilated_map)
    for i in range(dilated_map.shape[0]):
        for j in range(dilated_map.shape[1]):
            if dilated_map[i][j] > 0:
                dilated_conv_one_pixel((j, i), new_map, k=k, r=r, v=dilated_map[i][j])

    return new_map


def plot_map(matrix: np.ndarray):
    plt.figure()

    c_list = ['white', 'blue', 'red']
    new_cmp = LinearSegmentedColormap.from_list('chaos', c_list)
    plt.imshow(matrix, cmap=new_cmp)

    ax = plt.gca()
    ax.set_xticks(np.arange(-0.5, matrix.shape[1], 1), minor=True)
    ax.set_yticks(np.arange(-0.5, matrix.shape[0], 1), minor=True)

    # 显示color bar
    plt.colorbar()

    # 在图中标注数量
    thresh = 5
    for x in range(matrix.shape[1]):
        for y in range(matrix.shape[0]):
            # 注意这里的matrix[y, x]不是matrix[x, y]
            info = int(matrix[y, x])
            ax.text(x, y, info,
                    verticalalignment='center',
                    horizontalalignment='center',
                    color="white" if info > thresh else "black")
    ax.grid(which='minor', color='black', linestyle='-', linewidth=1.5)
    plt.show()
    plt.close()


def main():
    # bottom to top
    dilated_rates = [1, 2, 3]
    # init feature map
    size = 31
    m = np.zeros(shape=(size, size), dtype=np.int32)
    center = size // 2
    m[center][center] = 1
    # print(m)
    # plot_map(m)

    for index, dilated_r in enumerate(dilated_rates[::-1]):
        new_map = dilated_conv_all_map(m, r=dilated_r)
        m = new_map
    print(m)
    plot_map(m)


if __name__ == '__main__':
    main()

2.2.5 空洞卷积 / 膨胀卷积

空洞卷积 (Atrous Convolution) / 膨胀卷积 (Dilated Convolution)

作用:增大感受野、保持原输入特征图W、H(Padding后)

卷积神经网络 - 4个核心参数:

  • 卷积间隙 (膨胀因子 / 膨胀系数) r r r
  • 卷积核大小 k k k
  • 填充 p p p
  • 步长 s s s

空洞卷积 在语义分割中的应用:

  • 图片下采样提取后再上采样还原尺寸
  • 下采样倍率不能过大,VGG maxpooling丢失细节信息/目标,上采样无法还原
  • maxpooling层去除得到特征图感受野变小,影响后面的卷积层
  • 此时需要用到膨胀卷积:既能增加感受野、输入输出矩阵高宽不发生改变
① Gridding Effect 现象

Gridding Effect 现象:感受野间存在不连续间隔

实验一

r 1 = 2 , r 2 = 2 , r 3 = 2 ; k = 3 r_1=2,\space r_2=2,\space r_3=2;\space k=3 r1=2, r2=2, r3=2; k=3

像素利用热度:累加计算L4利用L1像素的次数

r r r相同 → 连续堆叠3层:L4利用L1像素数据并非连续 / 存在间隔 → 丢失细节信息 → 出现 Grid Effect


实验二

r 1 = 1 , r 2 = 1 , r 3 = 1 ; k = 3 r_1=1,\space r_2=1,\space r_3=1;\space k=3 r1=1, r2=1, r3=1; k=3

感受野:累加计算 L4 利用 L1(原始图像) 像素的次数,即为其连续时所能覆盖的大小

该实验中 感受野大小: R F ( R e c e p t i v e F i e l d ) = 7 × 7 RF(Receptive Field) = 7 \times 7 RF(ReceptiveField)=7×7

普通卷积: r = 1 r=1 r=1相同 → 连续堆叠3层:L4利用L1像素数据连续 → 感受野较小


实验三

r 1 = 1 , r 2 = 2 , r 3 = 3 ; k = 3 r_1=1,\space r_2=2,\space r_3=3;\space k=3 r1=1, r2=2, r3=3; k=3

该实验中 感受野大小: R F ( R e c e p t i v e F i e l d ) = 13 × 13 RF(Receptive Field) = 13 \times 13 RF(ReceptiveField)=13×13

r r r相异 → 连续堆叠3层:L4利用L1像素数据连续 → 避免 Grid Effect → 感受野大


结论:当卷积核大小相同时,为空洞卷积设置合适的膨胀系数可增大感受野。

② 如何设计膨胀系数?

论文链接

将上述选取 r r r值过程连续,假设连续堆叠 n n n 个 k × k k \times k k×k 膨胀卷积。

Hybrid Dilated Convolution (HDC) 问题目标:通过一系列堆叠膨胀卷积,最终形成覆盖底层无孔洞方形区域;最终目标 M 2 ≤ K M_2 \le K M2≤K。

若给出待验证的膨胀系数 r ⃗ = [ r 1 , r 2 , ... , r n ] \vec{r}=[r_1,r_2,\dots,r_n] r =[r1,r2,...,rn],令 M n = r n M_n=r_n Mn=rn,则从第 n n n层系数一次计算至第 1 1 1层:

M i = m a x [ M i + 1 − 2 r i , M i + 1 − 2 ( M i + 1 − r i ) , r i ] M_i=max[M_{i+1}-2r_i,M_{i+1}-2(M_{i+1}-r_i),r_i] Mi=max[Mi+1−2ri,Mi+1−2(Mi+1−ri),ri]

其中: M M M为第 i i i层两非零元素距离最大值, r i r_i ri为需验证的第 i i i层膨胀系数。


选值一

r 1 = 1 , r 2 = 2 , r 3 = 5 ; k = 3 r_1=1,\space r_2=2,\space r_3=5;\space k=3 r1=1, r2=2, r3=5; k=3

令 M 3 = r 3 = 5 M_3=r_3=5 M3=r3=5,则:
M 2 = m a x [ 5 − 4 , 5 − 2 ( 5 − 2 ) , 2 ] = 2 M 1 = m a x [ 2 − 2 , 2 − 2 ( 2 − 2 ) , 1 ] = 1 \begin{equation} \begin{aligned} M_2 &= max[5-4,5-2(5-2),2]=2 \\ M_1 &= max[2-2,2-2(2-2),1]=1 \end{aligned} \end{equation} M2M1=max[5−4,5−2(5−2),2]=2=max[2−2,2−2(2−2),1]=1

此时:满足条件 M 2 ≤ k M_2 \le k M2≤k,故不存在Grid Effect现象。

选值二

r 1 = 1 , r 2 = 2 , r 3 = 9 ; k = 3 r_1=1,\space r_2=2,\space r_3=9;\space k=3 r1=1, r2=2, r3=9; k=3

令 M 3 = r 3 = 9 M_3=r_3=9 M3=r3=9,则:
M 2 = m a x [ 9 − 4 , 9 − 2 ( 9 − 2 ) , 2 ] = 5 M 1 = m a x [ 5 − 2 , 5 − 2 ( 5 − 2 ) , 1 ] = 3 \begin{equation} \begin{aligned} M_2 &= max[9-4,9-2(9-2),2]=5 \\ M_1 &= max[5-2,5-2(5-2),1]=3 \end{aligned} \end{equation} M2M1=max[9−4,9−2(9−2),2]=5=max[5−2,5−2(5−2),1]=3

此时:不满足条件 M 2 ≤ k M_2 \le k M2≤k,故存在Grid Effect现象。

原因:我们希望高层特征图利用底层所有像素, M 1 M_1 M1在 M 2 − 2 r 1 M_2-2r_1 M2−2r1、 M 2 − 2 ( M 2 − r 1 ) M_2-2(M_2-r_1) M2−2(M2−r1)和 r 1 r_1 r1中取最大,第1层 M 1 = 1 M_1=1 M1=1时无间隙,则 r 1 r_1 r1必须 = 1 =1 =1,倘若 r 1 > 1 r_1>1 r1>1,则 M 1 > 1 M_1>1 M1>1 会出现间隙。

公约数大于1,也会出现 Grid Effect问题,即每层的利用区域未能实现 "交错"。

结论:公约数不能大于1,否则会出现 Grid Effect问题。

选值三(公约数2大于1)

r 1 = 2 , r 2 = 4 , r 3 = 8 ; k = 3 r_1=2,\space r_2=4,\space r_3=8;\space k=3 r1=2, r2=4, r3=8; k=3

令 M 3 = r 3 = 8 M_3=r_3=8 M3=r3=8,则:
M 2 = m a x [ 8 − 8 , 8 − 2 ( 8 − 4 ) , 8 ] = 8 M 1 = m a x [ 4 − 2 , 4 − 2 ( 4 − 2 ) , 2 ] = 2 \begin{equation} \begin{aligned} M_2 &= max[8-8,8-2(8-4),8]=8 \\ M_1 &= max[4-2,4-2(4-2),2]=2 \end{aligned} \end{equation} M2M1=max[8−8,8−2(8−4),8]=8=max[4−2,4−2(4−2),2]=2

此时:不满足条件 M 2 ≤ k M_2 \le k M2≤k,故存在Grid Effect现象。

文章推荐可使用诸如 [ 1 , 2 , 3 , 1 , 2 , 3 ] [1,2,3,1,2,3] [1,2,3,1,2,3]的锯齿结构设计。下图是是否使用准则训练效果的对比情况,可以看出使用准则效果明显优于未使用准则。

2.2.6 可分离卷积

可分离卷积 (Separable Convolutions) :包括 空间可分离卷积 (Spatially Separable Convolutions) 和 深度可分离卷积(depthwise separable convolution)。

① 空间可分离卷积

空间可分离卷积 (Spatially Separable Convolutions):将卷积核分解为两项独立的核分别进行操作。

利用向量乘法,将3×3卷积核分解为如下两个小卷积核:
[ − 1 0 1 − 2 0 2 − 1 0 1 ] = [ 1 2 1 ] ⋅ [ − 1 0 1 ] \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \\ \end{bmatrix}= \begin{bmatrix} 1 \\ 2 \\ 1 \\ \end{bmatrix} \cdot \begin{bmatrix} -1 & 0 & 1 \end{bmatrix} −1−2−1000121 = 121 ⋅[−101]

计算流程:先用 3 × 1 3\times1 3×1的卷积核作横向扫描计算,再用 1 × 3 1\times3 1×3的卷积核作纵向扫描计算,最后得到结果,其计算量相较于标准卷积更小。

② 深度可分离卷积

深度可分离卷积 (Depthwise Separable Convolutions) :由深度卷积和 1 × 1 1\times1 1×1卷积组成。

深度可分卷积(depthwise separable convolution)是卷积神经网络中对标准的卷积计算进行改进所得到的算法,其通过拆分空间维度和通道(深度)维度的相关性,减少了卷积计算所需要的参数个数,并在一些研究中被证实提升了卷积核参数的使用效率。
深度可分卷积的原型可认为来自于卷积神经网络中的Inception模块,其卷积计算分为两部分,首先对通道(深度)分别进行空间卷积(depthwise convolution),并对输出进行拼接,随后使用单位卷积核进行通道卷积(pointwise convolution)以得到特征图。
在应用方面,深度可分卷积被用于微型神经网络的搭建,也被用于大规模卷积神经网络的结构优化。使用深度可分卷积的深度学习算法包括Xception和MobileNet。

第一步:深度卷积

以彩色图像为例,使用3个卷积核分别对输入层的3个通道作卷积计算,再堆叠在一起。

第二步: 1 × 1 1\times1 1×1卷积

1 × 1 1\times1 1×1卷积可以得到只有1个通道的结果,将 1 × 1 1\times1 1×1卷积过程重复多次。

2.2.7 扁平卷积

扁平卷积 (Flattened convolutions) :将标准卷积核拆分为3个 1 × 1 1\times1 1×1的卷积核,再分别对输入层进行卷积计算。

2.2.8 分组卷积

分组卷积 (Grouped Convolution):2012年AlexNet论文中最先提出的概念,当时以解决GPU显存不足问题为目的,将卷积分组后放到两个GPU并行执行。

在分组卷积中,卷积核被分成不同的组,每组负责对相应的输入层进行卷积计算,最后再进行合并。

2.2.9 混洗分组卷积

在分组卷积中,卷积核被分成多个组后,输入层卷积计算的结果仍按照原先的顺序进行合并组合,这就阻碍了模型在训练期间特征信息在通道组之间流动,同时还削弱了特征表示。

混洗分组卷积 (Shuffled Grouped Convolution) 将分组卷积后的计算结果混合交叉在一起输出。

如下图,在第一层分组卷积 (GConv1) 计算后,得到的特征图先进行拆组,再混合交叉,形成新的结果输入到第二层分组卷积 (GConv2)中。

2.2.6 上采样 vs 下采样

① 上采样

上采样:将低分辨率的图像或特征图放大到原始分辨率的过程。

计算机视觉中,上采样用于图像分割、目标检测和图像生成等任务,可帮助提高模型的准确性和性能。

常见的上采样方法:转置卷积、双线性插值/最近邻插值等。

其中:双线性插值/最近邻插值是较简单的上采样方法,转置卷积则专用于卷积神经网络。

② 下采样

下采样:将高分辨率的图像或特征图缩小到较低分辨率的过程。

计算机视觉中,下采样用于图像分类、目标检测和图像分割等任务,可帮助减少计算量和内存消耗,加快模型训练及推理速度。

作用:一是减少计算量,防止过拟合

二是增大感受野,使得后面的卷积核能够学到更加全局的信息。

常见的下采样方法:卷积操作、平均池化/最大池化等。

其中:池化操作是一种常用的下采样方法,卷积操作也可实现下采样 ( 步长卷积 / 空洞卷积 )

2.2.7 上采样

常见的上采样方法:转置卷积、双线性插值/最近邻插值等。

① 转置卷积

转置卷积 (Transposed Convolution):即 分数步长卷积 (Fractionally-strided Convolution) / 反卷积 (Deconvolution),一种上采样方法,可增大图像的尺寸。转置卷积不是卷积的逆运算,而是通过特定的操作来恢复图像的原始尺寸。

微步卷积

微步卷积 (Fractionally-Strided Convolution): "步长"$ < 1$ 的 转置卷积

实现方式:间隙填充 → 在输入特征间插入0 → 间接减少步长

转置卷积默认参数 s = 1 s=1 s=1,参数 s > 1 s > 1 s>1时则转置卷积称为步长为 1 s \frac{1}{s} s1微步卷积,需在输入特征间插入 s − 1 s − 1 s−1行列的0元素使其移动速度变慢。


运算步骤

  1. ‌间隙 填充 ‌:在输入特征图元素间填充 s − 1 s-1 s−1行、 s − 1 s-1 s−1列的 0元素
  2. 四周 填充 ‌:在输入特征图四周填充 k − p − 1 k-p-1 k−p−1行、 k − p − 1 k-p-1 k−p−1列的 0元素
  3. 转置卷积核‌:将卷积核参数 上下+左右翻转
  4. 卷积操作‌:做正常卷积运算

参数一

当 s = 1 , p = 0 , k = 3 s=1,\space p=0,\space k=3 s=1, p=0, k=3 时,执行如下图所示的 转置卷积。

经计算 s − 1 = 0 s-1=0 s−1=0、 k − p − 1 = 2 k-p-1=2 k−p−1=2,故间隙不填充0元素,四周填充2行列0元素。

随后将卷积核参数上下左右翻转,在此基础继续作 步长1 且 填充0 的 正常卷积运算

参数二

当 s = 2 , p = 0 , k = 3 s=2,\space p=0,\space k=3 s=2, p=0, k=3 时,执行如下图所示的 微步卷积。

经计算 s − 1 = 1 s-1=1 s−1=1、 k − p − 1 = 2 k-p-1=2 k−p−1=2,故间隙填充1行列0元素,四周填充2行列0元素。

随后将卷积核参数上下左右翻转,在此基础继续作 步长1 且 填充0 的 正常卷积运算

参数三

当 s = 2 , p = 1 , k = 3 s=2,\space p=1,\space k=3 s=2, p=1, k=3 时,执行如下图所示的 微步卷积。

经计算 s − 1 = 1 s-1=1 s−1=1、 k − p − 1 = 1 k-p-1=1 k−p−1=1,故间隙填充1行列0元素,四周填充1行列0元素。

随后将卷积核参数上下左右翻转,在此基础继续作 步长1 且 填充0 的 正常卷积运算

Pytorch 中 2D转置卷积 实现:

python 复制代码
import tortch.nn as nn

# 函数参数:in_channels, out_channels, kernel_size, stride, padding, output_padding, dilation, groups
# in_channels: 输入图像特征尺寸
# out_channels: 转置卷积后输出图像特征尺寸
# kernel_size: 转置卷积核尺寸
# stride: 步长  默认值1
# padding: 填充  默认值0 【dilation * (kernel_size - 1) - padding zero-padding will be added to both sides of each dimension in the input. Default: 0】
# bias: 偏置 默认值True
# dilation: 膨胀卷积
nn.ConvTranspose2d()

转置卷积的应用场景

  1. 无监督学习‌:用于找到一组核和特征图,以重建图片。
  2. CNN可视化‌:将CNN中的特征图还原到像素空间,观察特定的特征图对哪些图案敏感。
  3. 上采样‌:在像素级预测任务中,如图像分割和图像生成中,通过上采样还原到原始图片尺寸。

由下图可见,转置卷积增大了图像的尺寸。

② 插值

插值:利用已知数据估计未知位置数值。

最近邻插值

最近邻插值 (Nearest Neighbor Interpolation):对于未知位置,直接采用与它最邻近的像素点的值为其赋值,即缩放前后等比填充。

前提假设:在一个连续变量的空间中,一个点的值可以由离它最近的已知点来估计,其在图像处理中被应用于图像的缩放。

双线性插值

双线性插值 (Bilinear Interpolation):不仅可实现图像缩放;其本质是利用参考周围元素按坐标进行双向线性计算后插值。

坐标转换公式:
s r c X = d s t X ∗ ( s r c W i d t h / d s t W i d t h ) s r c Y = d s t Y ∗ ( s r c H e i g h t / d s t H e i g h t ) srcX=dstX* (srcWidth/dstWidth) \\ srcY = dstY * (srcHeight/dstHeight) srcX=dstX∗(srcWidth/dstWidth)srcY=dstY∗(srcHeight/dstHeight)

其中, s r c X srcX srcX表示原图插值临时坐标, d s t X dstX dstX表示新图坐标, s r c H e i g h t srcHeight srcHeight、 s r c W i d t h srcWidth srcWidth 分别表示原图长宽, d s t H e i g h t dstHeight dstHeight、 d s t W i d t h dstWidth dstWidth 分别表示目标长宽。

双线性插值就是做两次线性变换,先在X轴上做一次线性变换,求出每一行的R点, f ( ⋅ ) f(·) f(⋅)表示对应坐标所在数值:
f ( R 1 ) ≈ x 2 − x x 2 − x 1 f ( Q 11 ) + x − x 1 x 2 − x 1 f ( Q 12 ) W h e r e R 1 = f ( x , y 1 ) f ( R 2 ) ≈ x 2 − x x 2 − x 1 f ( Q 12 ) + x − x 1 x 2 − x 1 f ( Q 22 ) W h e r e R 1 = f ( x , y 2 ) f(R_1) \approx \frac{x_2-x}{x_2-x_1}f(Q_{11})+\frac{x-x_1}{x_2-x_1}f(Q_{12}) \space\space\space\space Where \space R_1=f(x,y_1) \\ f(R_2) \approx \frac{x_2-x}{x_2-x_1}f(Q_{12})+\frac{x-x_1}{x_2-x_1}f(Q_{22}) \space\space\space\space Where \space R_1=f(x,y_2) f(R1)≈x2−x1x2−xf(Q11)+x2−x1x−x1f(Q12) Where R1=f(x,y1)f(R2)≈x2−x1x2−xf(Q12)+x2−x1x−x1f(Q22) Where R1=f(x,y2)

再通过一次线性变换求出在该区域中的P点:
f ( P ) ≈ y 2 − y y 2 − y 1 f ( R 1 ) + y − y 1 y 2 − y 1 f ( R 2 ) f(P) \approx \frac{y_2-y}{y_2-y_1}f(R_1)+\frac{y-y_1}{y_2-y_1}f(R_2) f(P)≈y2−y1y2−yf(R1)+y2−y1y−y1f(R2)

整合所有公式,可得以下计算公式:
f ( x , y ) ≈ ( x 2 − x ) ( y 2 − y ) ( x 2 − x 1 ) ( y 2 − y 1 ) f ( Q 11 ) + ( x − x 1 ) ( y 2 − y ) ( x 2 − x 1 ) ( y 2 − y 1 ) f ( Q 21 ) + ( x 2 − x ) ( y − y 1 ) ( x 2 − x 1 ) ( y 2 − y 1 ) f ( Q 12 ) + ( x − x 1 ) ( y − y 1 ) ( x 2 − x 1 ) ( y 2 − y 1 ) f ( Q 22 ) f(x,y) \approx \frac{(x_2-x)(y_2-y)}{(x_2-x_1)(y_2-y_1)}f(Q_{11})+\frac{(x-x_1)(y_2-y)}{(x_2-x_1)(y_2-y_1)}f(Q_{21})+\frac{(x_2-x)(y-y_1)}{(x_2-x_1)(y_2-y_1)}f(Q_{12})+\frac{(x-x_1)(y-y_1)}{(x_2-x_1)(y_2-y_1)}f(Q_{22}) f(x,y)≈(x2−x1)(y2−y1)(x2−x)(y2−y)f(Q11)+(x2−x1)(y2−y1)(x−x1)(y2−y)f(Q21)+(x2−x1)(y2−y1)(x2−x)(y−y1)f(Q12)+(x2−x1)(y2−y1)(x−x1)(y−y1)f(Q22)

如果计算出的点在f中是边缘点,则使用单线性插值,如果不是边缘点,则使用双线性插值。

双三次插值

双三次插值 (Bicubic Interpolation)

Bicubic函数公式:

W ( d ) = { ( a + 2 ) ∣ d ∣ 3 − ( a + 3 ) ∣ d ∣ 2 + 1 f o r ∣ d ∣ ≤ 1 a ∣ d ∣ 3 − 5 a ∣ d ∣ 2 + 8 a ∣ d ∣ − 4 a f o r 1 < ∣ d ∣ < 2 0 o t h e r w i s e W(d)= \begin{equation} \left\{ \begin{array}{ll} (a+2)|d|^3-(a+3)|d|^2+1 & for \space |d| \le 1 \\ a|d|^3-5a|d|^2+8a|d|-4a & for \space 1 \lt|d| \lt 2 \\ 0 & otherwise \end{array} \right. \end{equation} W(d)=⎩ ⎨ ⎧(a+2)∣d∣3−(a+3)∣d∣2+1a∣d∣3−5a∣d∣2+8a∣d∣−4a0for ∣d∣≤1for 1<∣d∣<2otherwise

函数图像如下所示:

假设存在以下图像,需在 P P P点插值。

根据坐标转换公式,求得点P坐标,并将其分割为整数部分和小数部分。

假设 P P P的坐标为 P ( x + u , y + v ) P(x+u,y+v) P(x+u,y+v),其中 x , y x,y x,y分别表示整数部分, u , v u,v u,v分别表示小数部分。

可得到如图所示的最近16个像素的位置,用 a ( i , j ) ( i , j = 0 , 1 , 2 , 3 ) a(i,j)\space \space(i,j=0,1,2,3) a(i,j) (i,j=0,1,2,3) 来表示。

求出BiCubic函数中的参数d,从而获得上面所说的16个像素所对应的权重W(d)。

BiCubic基函数是一元函数,而图像是二维图像,故将像素点的行与列分开计算。

BiCubic函数中的参数 d d d表示该像素点到 P P P点的距离。

例如: ( 0 , 0 ) (0,0) (0,0)距离 P ( x + u , y + v ) P(x+u,y+v) P(x+u,y+v)的距离为 ( 1 + u , 1 + v ) (1+u,1+v) (1+u,1+v),因此a00的横坐标权重 i 0 = W ( 1 + u ) i_0=W(1+u) i0=W(1+u),纵坐标权重 j 0 = W ( 1 + v ) j_0=W(1+v) j0=W(1+v), ( 0 , 0 ) (0,0) (0,0)对 B ( X , Y ) B(X,Y) B(X,Y)的贡献值为: Q 00 ∗ i 0 ∗ j 0 Q_{00} * i_0* j_0 Q00∗i0∗j0。

因此,横坐标权重分别为 W ( 1 + u ) W(1+u) W(1+u), W ( u ) W(u) W(u), W ( 1 − u ) W(1-u) W(1−u), W ( 2 − u ) W(2-u) W(2−u);纵坐标权重分别为 W ( 1 + v ) W(1+v) W(1+v), W ( v ) W(v) W(v), W ( 1 − v ) W(1-v) W(1−v), W ( 2 − v ) W(2-v) W(2−v)。

B ( X , Y ) B(X,Y) B(X,Y)像素值为:
B ( X , Y ) = ∑ i = 0 3 ∑ j = 0 3 a i j × W ( i ) × W ( j ) B(X,Y)=\sum^{3}{i=0}\sum^{3}{j=0}a_{ij}\times W(i)\times W(j) B(X,Y)=i=0∑3j=0∑3aij×W(i)×W(j)

③ 反池化

反池化 (Unpooling) :池化 (Pooling) 的逆操作,包括 反最大池化反平均池化

在池化过程中,数据被压缩以减少维度,而在反池化过程中,数据通过恢复被压缩的数据来增加维度。

反池化并不能完全恢复原始数据,它只恢复主要信息,并舍去部分信息。‌

反最大池化

在池化过程中记录最大激活值的坐标位置,最大反池化时,将最大激活值所在位置坐标值激活,其他的值设置为0。

反平均池化

先还原成原来的大小,再将池化结果中的每个值都填入其对应原始数据区域中相应位置即可。

3. 评估方法

3.1 混淆矩阵

展示模型在各个类别上的预测正确与预测错误的数量,非常直观地反映模型在各类别上的性能,以帮助改进模型。

3.2 精确度

总的正确预测数除以总的预测数。

  • 注意:在类别不平衡的情况下,精确度不是唯一的评估标准。

3.3 其他指标

  • 召回率 (Recall)
  • 精确率 (Precision)
  • F1 分数 (F1 Score):精确率和召回率的调和平均。
相关推荐
云空2 小时前
《解锁 Python 数据分析的强大力量》
python·数据挖掘·数据分析
MUTA️3 小时前
专业版pycharm与服务器连接
人工智能·python·深度学习·计算机视觉·pycharm
xuanfengwuxiang3 小时前
安卓帧率获取
android·python·测试工具·adb·性能优化·pycharm
觅远4 小时前
python+PyMuPDF库:(一)创建pdf文件及内容读取和写入
开发语言·python·pdf
MinIO官方账号4 小时前
使用亚马逊针对 PyTorch 和 MinIO 的 S3 连接器实现可迭代式数据集
人工智能·pytorch·python
四口鲸鱼爱吃盐4 小时前
Pytorch | 利用IE-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击
人工智能·pytorch·python·深度学习·计算机视觉
四口鲸鱼爱吃盐4 小时前
Pytorch | 利用EMI-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击
人工智能·pytorch·python
游客5205 小时前
自动化办公-合并多个excel
开发语言·python·自动化·excel
豌豆花下猫5 小时前
Python 潮流周刊#83:uv 的使用技巧(摘要)
后端·python·ai
凡人的AI工具箱5 小时前
每天40分玩转Django:Django部署概述
开发语言·数据库·后端·python·django