深度学习与PyTroch(二)

文章目录

  • 神经网络
  • 神经网络工具包
    • Module模块
    • 线性层
    • 卷积层
      • 输入层
      • 过滤器(卷积核)
  • 池化层
    • 池化层作用
  • BatchNorm层
  • 激活层
    • Sigmoid函数
    • Tanh函数
    • ReLU函数
    • LeakyReLU
    • Softmax函数

神经网络

神经网络是神经系统运转方式的简单模型。其基本单元是 神经元 ,通常将其组织到 层 中,如下面的图所示。卷积神经网络主要由这几类层构成:输入层、卷积层,ReLU层、池化(Pooling)层和全连接层。
深度学习与PyTroch(二)_第1张图片

  • 神经网络是模拟人类大脑处理信息方式的简化模型。此模型的工作方式为模拟大量类似于神经元的抽象形式的互连处理单元。这些处理单元都位于层中。神经网络通常包含三个部分:输入层,其中的单元表示输入字段;一个或多个隐藏层;一个输出层,带有一个或多个表示目标字段的单元。这些单元通过可变的连接强度(或权重 )连接。输入数据 显示在第一层,其值从每个神经元传播到下一层的每个神经元。最终从输出层中输出结果。
  • 该网络可通过以下过程进行学习,即检查单个记录,然后为每条记录生成预测,并且当生成的预测不正确时,对权重进行调整。在满足一个或多个停止标准之前,此过程会不断重复,而网络会持续提高其预测准确度。
  • 最初,所有的权重都是随机生成的,并且从网络输出的结果很可能没有意义的。网络可通过训练来学习。向该网络重复应用已知道结果的示例,并将网络给出的结果与已知的结果进行比较。从此比较中得出的信息会传递回网络,并逐渐改变权重。随着训练的进行,该网络对已知结果的复制会变得越来越准确。一旦训练完毕,就可以将网络应用到未知结果的未来案例中。

链接: https://www.zhihu.com/question/447419811

神经网络工具包

  • 如果要使用的功能是带有可学习参数的,比如卷积层,最好是使用Conv2d类,并且要在网络的__init__函数中定义,这样Conv2d中的参数就会自动被纳入整个网络的参数中去。
  • 如果是没有可学习参数的功能,比如ReLU激活层,两种方式可以任选
  • 如果网络中需要对权重进行某种特殊处理,使用torch.nn.functional来实现更加方便。
    如果使用torch.nn.functional中的函数来定义带参数的网络层,需要在__init__方法中用torch.nn.Parameter对可学习的权重进行封装,然后再传入函数,这样也能起到类似torch.nn.Module的效果。这种方法在自己定义网络层的时候有可能会用到。

Module模块

#coding=utf-8
import cv2
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np

class net(nn.Module):
    def __init__(self):
        super(net,self).__init__()
        self.fc1=nn.Linear(1,10)
        self.fc2=nn.Linear(10,1)
    
    def forward(self,x):
        #在forword中依次进行两个线性层的前向推理
        x=self.fc1(x)
        x=self.fc2(x)
        return x
print(net())

在定义模型时,需要选在子类的__init__方法中初始化父类的__init__方法,从nn.Module类的源码中可以看到,其初始化方法会创建几个模型参数,并把模型设置为训练模式:

 def __init__(self):
        super(net,self).__init__()
        self.fc1=nn.Linear(1,10)
        self.fc2=nn.Linear(10,1)
        self._construct()
        self.training=True
    def _construct(self):
    ##nn.Module中的构造函数
    torch._C._log_api_usage_once("python.nn_module")
    self._backend=thnn_bckend
    self._parameters=OrderedDict()
    self._buffers=OrderedDict()
    self._backward_hooks=OrderDict()
    self._forward_hooks=OrderDict()
    self._forward_pre_hooks=OrderDict()
    self._modules=OrderedDict()

线性层

线性层也叫全连接层,使用nn.Linear类实现,其内部是简单的矩阵运算。线性层的神经网络模型中可以充当分类器,既可以放在网络的输出部分,也可以充当维度转换器,比如自然语言处理中的注意力模型就经常利用线性层进行维度转换。
全连接层,是每一个结点都与上一层的所有结点相连,用来把前边提取到的特征综合起来。由于其全相连的特性,一般全连接层的参数也是最多的。
深度学习与PyTroch(二)_第2张图片

nn.Linear(in_features, out_features, bias=True)
功能:对一维向量(信号)进行线性组合
参数:
in_features: 输入节点数
out_features: 输出节点数
bias: 是否需要偏置,默认为True
计算公式:y=xW^T+biasy=xW^T+bias(W^T表示转置)

根据上图及其计算公式,使用线性层进行计算

inputs=torch.tensor([[1.,2,3]])
linear_layer=nn. Linear(3,4)
linear_layer.weight.data=torch. tensor([[1.,1.,1.],
[2.,2.,2.], [3.,3.,3.],[4.,4.,4.]])
linear_layer.bias.data. fill_(0.)
output=linear_layer(inputs)
print(inputs, inputs. shape)
print(linear_layer.weight.data,linear_layer.weight.data.shape)
print(output, output.shape)

深度学习与PyTroch(二)_第3张图片

卷积层

卷积神经网络中每层卷积层(Convolutional layer)由若干卷积单元组成,每个卷积单元的参数都是通过反向传播算法最佳化得到的。卷积运算的目的是提取输入的不同特征,第一层卷积层可能只能提取一些低级的特征如边缘、线条和角等层级,更多层的网路能从低级特征中迭代提取更复杂的特征。
在卷积神经网络中,卷积运算是对两个矩阵进行的。如下图,左侧为输入矩阵M,中间为过滤器F(也叫卷积核),F以一定步长在M上进行移动,进行点积运算,得到右侧的输出矩阵O。这个就是卷积神经网络中卷积层最基础的运算。在实际的操作中,还存在一些额外的操作。

输入层

在使用卷积神经网络进行图像识别时,输入为进行过转换的图片数据,一张宽为w,高为h,深度为d的图片,表示为hwd。这里,深度为图像存储每个像素所用的位数,比如彩色图像,其一个像素有RGB三个分量,其深度为3。从数学的角度来看,hwd的图片即为d个h*w的矩阵。例如6x16x3的图片,其对应3个6x16的矩阵。在大部分运用中,输入图片的大小h和w,一般是相等的。

过滤器(卷积核)

在卷积运算时,会给定一个大小为FF的方阵,称为过滤器,又叫做卷积核,该矩阵的大小又称为感受野。过滤器的深度d和输入层的深度d维持一致,因此可以得到大小为FFd的过滤器,从数学的角度出发,其为d个FF的矩阵。在实际的操作中,不同的模型会确定不同数量的过滤器,其个数记为K,每一个K包含d个FF的矩阵,并且计算生成一个输出矩阵。一定大小的输入和一定大小的过滤器,再加上一些额外参数,会生成确定大小的输出矩阵。以下先介绍这些参数。
__1)Padding。__在进行卷积运算时,输入矩阵的边缘会比矩阵内部的元素计算次数少,且输出矩阵的大小会在卷积运算中相比较于输入变小。因此,可在输入矩阵的四周补零,称为padding,其大小为P。比如当P=1时,原5
5的矩阵如下,绿色框中为原矩阵,周围使用0作为padding。
深度学习与PyTroch(二)_第4张图片
2)进行卷积运算时,过滤器在输入矩阵上移动,进行点积运算。移动的步长stride,记为S。当S=2时,过滤器每次移动2个单元。如下图,红色框为第一步计算,蓝色框为S=2时的第二步运算。
深度学习与PyTroch(二)_第5张图片
有了以上两个参数P和S,再加上参数W(输入矩阵的大小),过滤器的大小F,输出矩阵的大小为(W-F+2P)/S+1。

对于5x5的输入矩阵,过滤器大小F=3,P=1,S=1,其输出矩阵的大小为(5-3+2)/1+1=5。可见,在步长S为1,且进行了P=1的padding后,其输出矩阵的大小和输入一致。

现在考虑当输入有多个深度时的情况。当输入为5x5x3,P=1,并且有K个过滤器时,每一个过滤器都为3x3x3。这里,我们把输入的3个7x7矩阵(5x5进行padding后得到7x7)命名为M1,M2,M3,第k个过滤器(0

  1. 在M1中,从最左上角,取感受野大小F*F的子矩阵,与F1进行点积运算,即对应位置元素相乘,再求和得到结果OUT1.

  2. M2和F2进行同样的运算,得到结果OUT2;M3和F3得到OUT3.

  3. OUT1+OUT2+OUT3相加,再加上偏移量b0,得到输出矩阵OUT左上角的第一个元素.

  4. 按照步长S,从M1,M2,M3中获取另外一个感受野大小的区域,对应F1,F2,F3进行步骤1~3的计算,最终得到完整的输出矩阵OUT

  5. 更换过滤器k+1,重复1~4的运算,得到K个输出矩阵。

总结以上过程,输出矩阵的每一个元素,是由对应过滤器不同深度的矩阵,作用于相应深度输入矩阵的不同位置,进行点积运算,再加上偏移量bias所得。

在斯坦福的卷积神经网络课程有,有一个很典型的例子如下。此处分析两个步骤,完整的例子见以下链接。
http://cs231n.github.io/convolutional-networks/

在这个例子中,输入矩阵为553,即W=5,填充P为1,过滤器有K=2个,每个过滤器的大小为333,即F=3,同时设定计算步长S=2。这样可得到输出中单个矩阵的大小为(5-3+21)/2+1=3,由于K=2,所以输出的33矩阵有2个。下面为具体的计算过程

  1. 首先从输入矩阵的最左边开始取得3*3的感受野,每一个深度的输入矩阵对应每一个深度的过滤器,进行点积运算,然后加上偏移Bias,得到第一个输出矩阵的第一个元素。详细过程为

输入矩阵1:r1 = 0x0+0x1+0x1+0x(-1)+1x0+0x0+0x1+0x0+1x0=0

输入矩阵2:r2 = 0x0+0x0+0x0+0x1+0x0+0x1+0x0+2x0+0x0 = 0

输入矩阵3:r3 = 0x(-1)+0x(-1)+0x0+0x0+0x0+2x(-1)+0x(-1)+0x0+2x0 = -2

输出矩阵元素(绿框中元素)OUT1 = r1+r2+r3+b0 = -1

深度学习与PyTroch(二)_第6张图片
2) 然后将感受野在3个输入矩阵上同时移动2个步长,如蓝框所示,重复1)中描述的运算,得到OUT2=-1,计算过程此处不再赘述。
深度学习与PyTroch(二)_第7张图片

3)将感受野在输入矩阵中依次移动,当完成第一个输出矩阵的计算后,使用第二个过滤器再重复一次,得到第二个输出矩阵。卷积计算完成。

在上面的计算中,每一个深度上的输入矩阵,其每一个步长的计算都是用同一个过滤器矩阵,这个现象被称为参数共享(parameter sharing)。其实这是一种简化,在未简化的情况下,同一深度矩阵上每一个步长的卷积计算,都需要使用不同的过滤器,这样会造成神经网络中参数过多,所以在实际操作中,会采取如上所述的参数共享策略,减少参数个数。
卷积层的文章采用此文章链接:https://blog.csdn.net/tjlakewalker/article/details/83275322

卷积层

  • 卷积就是使用一个卷积核在图片上进行扫描,每扫描一步就将相应位置的像素与卷积核元素对应相乘并相加。
  • 卷积层使用了nn.Conv2d类,nn.Conv2d类中经常会使用到的参数如下。
  • in_channels:输入的特征图的通道数量
  • out_channels:输出的特征图的通道数量
  • kernel_size:卷积核的尺寸
  • stride:卷积核滑动的步长
  • padding:在输入卷积图周围补零的数量
    经过卷积运算,图片的尺寸可能会发生变化,尺寸变化公式为:
    outputsize=(inputsize-kernelsize+2xpadding)/stride+1
    卷积层的优点及作用:
  • 卷积层相比线性层有一个显著的优势,就是在卷积运算中存在对相邻像素的运算,因此在训练过程中,可以更好地处理相邻像素之间的关系。而线性层中每个像素之间是相互独立的,显然不符合图像处理的原则。
  • 卷积运算的作用:减少神经网络的运算量和提取特征。卷积网络的特征提取过程与传统人工图片特征提取算法HOG有点相似,HOG使用block滑窗提取梯度信息特征,卷积使用卷积核滑窗提取特征,图片中的像素都会被相同的卷积核扫描到,这叫作权值共享。这样操作相比全连接层而言,大大减少了参数量。
convert的转化模式有九种不同模式: 1,L,P,RGB,RGBA,CMYK,YCbCr,I,F。

# 为二值图像,非黑即白。每个像素用8个bit表示,0表示黑,255表示白。
image.convert('1')
 
# 为灰度图像,每个像素用8个bit表示,0表示黑,255表示白,其他数字表示不同的灰度。
# 转换公式:L = R * 299/1000 + G * 587/1000+ B * 114/1000。
image.convert('L')
from PIL import Image
def convert_P():
    image = Image.open("图片路径")
    image_L = image.convert('L')
    image.show()
    image_L.show()
原文链接:https://blog.csdn.net/MasterCayman/article/details/118707553

torch.nn.parameter.Parameter(data=None,requires_grad=True)
其中:
data (Tensor) – parameter tensor. —— 输入得是一个tensor
requires_grad (bool, optional)if the parameter requires gradient. See Locally disabling gradient computation for more details. Default: True —— 这个不用解释,需要注意的是nn.Parameter()默认有梯度。

原文链接:https://blog.csdn.net/weixin_44878336/article/details/124733598
#coding=utf-8
import cv2
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image

#图片路径
img_path="img2.jpg"
img=Image.open(img_path).convert("L")
# transforms.ToTensor()的操作对象有PIL格式的图像以及numpy
# (即cv2读取的图像也可以)这两种。对象不能是tensor格式的,因为是要转换为tensor的。

#实现Tensor与PILImage对象之间的互转
to_tensor=transforms.ToTensor()
to_img=transforms.ToPILImage()
#显示图片
img.show()
#将图片转为Tensor
img=to_tensor(img)
#增加维度,便于卷积计算
img=torch.unsqueeze(img,0)
#定于卷积核
c=nn.Conv2d(1,1,kernel_size=3,padding=1)

#将卷积参数替换成用于边缘检测的sobel算子(此处为y方向检测算子)
#weight需要定义成torch.nn.Parameter()对象
c.weight=torch.nn.Parameter(torch.Tensor(
    [[[[-1,0,1],[-2,0,2],[-1,0,1]]]]
))

#卷积运算
img=c(img)
#减少一个维度
img=torch.squeeze(img,0)
img=to_img(img)
#展示卷积后的图片
img.show()

深度学习与PyTroch(二)_第8张图片
深度学习与PyTroch(二)_第9张图片
卷积操作提取出了图形的大致边缘轮廓。在训练模型的过程中,模型会不断调整卷积核中的参数,使其能够提取更多的特征。

池化层

通常在连续的卷积层之间会周期性地插入一个池化层。它的作用是逐渐降低数据体的空间尺寸,这样的话就能减少网络中参数的数量,使得计算资源耗费变少,也能有效控制过拟合。
池化操作通过池化模板和步长两个关键性变量构成,模板描述了提取信息区域的大小,一般是一个方形窗口,步长(stride)描述了窗口在卷积层输出特征图上的移动步长,一般和模板边长相等(即模板移动前后不重叠)。

池化层作用

池化层的作用是提取局部均值与最大值,根据计算出来的值不一样就分为均值池化层与最大值池化层,一般常见的多为最大值池化层。池化的时候同样需要提供filter的大小、步长、下面就是3x3步长为1的filter在5x5的输入图像上均值池化计算过程与输出结果。
深度学习与PyTroch(二)_第10张图片
改用最大值做池化的过程与结果如下:
深度学习与PyTroch(二)_第11张图片

MaxPool2d 这个类的实现十分简单。
我们先来看一下基本参数,一共六个:
kernel_size :表示做最大池化的窗口大小,可以是单个值,也可以是tuple元组
stride :步长,可以是单个值,也可以是tuple元组
padding :填充,可以是单个值,也可以是tuple元组
dilation :控制窗口中元素步幅
return_indices :布尔类型,返回最大值位置索引
ceil_mode :布尔类型,为True,用向上取整的方法,计算输出形状;默认是向下取整。
原文链接:https://blog.csdn.net/weixin_38481963/article/details/109962715
#coding=utf-8
import cv2
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image

img_path="img2.jpg"
#实现Tensor与PILImage对象之间的互转
to_tensor = transforms.ToTensor()
to_img = transforms.ToPILImage()

m=nn.MaxPool2d(kernel_size=4,stride=4)
img=Image.open(img_path).convert("L")#转化为灰度图
img.resize((50,50))
img.show()
img=to_tensor(img)
#减少维度
img=torch.unsqueeze(img,0)
img=m(img)
#增加维度
img=torch.squeeze(img)
#转化为图片
img=to_img(img)
img.show()

经池化层计算后,图片的尺寸下降导致清晰度下降,这样可以减少神经网络的运算量。在卷积神经网络的设计中,特征图的尺寸逐渐缩小的同时,特征图的通道数量会逐渐增大,特征图的通道数量会逐渐增大,特征图的总信息量仍在增大,并不会因尺寸缩小而造成信息损失。

BatchNorm层

BatchNorm的设计初衷是解决深度神经网络计算过程中数据分布变化的问题。尤其是经过类似ReLU这样的激活函数计算之后,数据分布会发生很大的变化,而每一层的输出数据分布都不同,这会给训练带来很大的困难,并且输出值的尺度变化过大也会对训练产生影响。因此引入了BatchNorm进行批量归一化,并且加入了可学习的参数,尽可能保留数据的特征。
BatchNorm通常作用于特征图,不改变特征图的形状,且只有一个需要设定的参数,即num_features,它对应着特征图(N,C,H,W)中的通道C。

torch.nn.BatchNorm2d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
num_features:一般输入参数为batch_size×num_features×height×width,即为其中特征的数量
eps:分母中添加的一个值,目的是为了计算的稳定性,默认为:1e-5
momentum:一个用于运行过程中均值和方差的一个估计参数
affine:当设为true时,会给定可以学习的系数矩阵gamma和beta
原文链接:https://blog.csdn.net/woxiangxinwang/article/details/122788012


m=torch.nn.BatchNorm2d(16)
a=torch.rand((1,16,100,100))
b=m(a)
print(b.shape)

激活层

激活函数通常是非线性函数,在神经网络中添加激活函数是为了让神经网络具备学习非线性关系的能力。如果没有激活层,无论网络迭代多少层,都只能表示线性关系。

Sigmoid函数

Sigmoid激活函数应该是深度神经网络最先使用的激活函数,主要用于逻辑回归(logistic regression),实现二分类功能。sigmoid函数的公式如下:
深度学习与PyTroch(二)_第12张图片
sigmoid函数y值的取值范围是(0,1)。

#coding=utf-8
import cv2
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
x=[-4,-2,0,2,4]
print(x)
x=np.array(x)
y=1/(1+np.exp(-x))
plt.plot(x,y)
plt.show()

深度学习与PyTroch(二)_第13张图片

Tanh函数

Tanh函数的公式:
深度学习与PyTroch(二)_第14张图片
其函数图像可以通过如下代码绘制,绘制出的图像。

#coding=utf-8
import cv2
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
x=[-4,-2,0,2,4]
x=np.array(x)
y=(np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x))
plt.plot(x,y)
plt.show()

深度学习与PyTroch(二)_第15张图片
Tanh函数图像也称为双曲正切曲线,与Sigmoid函数相比,虽然它也存在梯度弥散问题,但是因为Tanh函数图像说关于原点对称的,所以在实际应用中,Tanh函数要比Sigmoid函数更好。

ReLU函数

ReLU函数的公式为:
在这里插入图片描述
其函数图像可以通过如下代码绘制。

#coding=utf-8
import cv2
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
x=[-4,-2,0,2,4]
x=np.array(x)
y=[max(0,item) for item in x]
plt.plot(x,y)
plt.show()

深度学习与PyTroch(二)_第16张图片
ReLU是一个分段函数,又称线性整流函数,x=0右侧是一个正比例函数,x=0左侧是一个常数值。在ReLU函数图像中,所有正数对应的梯度都相等,不会遇到类似Sigmoid和Tanh的困难,所以在训练的时候模型收敛速度会快很多。
ReLU函数的缺点就是训练过程中网络会变得稀疏,部分网络结点的梯度可能一直是0,永远不会更新,这种结点称为死亡节点。

LeakyReLU

为了避免出现死亡结点,LeakyReLU函数给所有的负数赋予了一个非零斜率,其公式为:
深度学习与PyTroch(二)_第17张图片
LeakyReLU的函数图像可以通过如下代码描绘

#coding=utf-8
import cv2
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
x=[-4,-2,0,2,4]
x=np.array(x)
def f(x):
    if x>=0:
        return x
    else:
        return x/5
y=[f(item) for item in x]
plt.plot(x,y)
plt.show()

深度学习与PyTroch(二)_第18张图片

Softmax函数

Softmax函数的公式是:
深度学习与PyTroch(二)_第19张图片
Softmax函数适用于输出多分类神经网络,将网络输出结点的值映射成结点的概率值(所有结点值得总和映射成1)。

#coding=utf-8
import cv2
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
from sklearn.datasets import make_blobs
x,y=make_blobs(n_samples=500,centers=4,n_features=2,cluster_std=1.2,random_state=10)
makers=['o','v','x','.']
colors=['r','g','y','b']
for i,label in enumerate(y):
    plt.scatter(x[i,0],x[i,1],marker=makers[label],color=colors[label])
plt.show()

深度学习与PyTroch(二)_第20张图片
为了提高模型拟合的难度,可以将4个数据集合并成两个:

y[y==3]=0
y[y==4]=1
makers=['o','v','x','.']
colors=['r','g','y','b']
for i,label in enumerate(y):
    plt.scatter(x[i,0],x[i,1],marker=makers[label],color=colors[label])
plt.show()

深度学习与PyTroch(二)_第21张图片
接下来,建立一个包含两个全连接层的模型来学习这个数据集,代码如下:

from torch import nn
class net(nn.Module):
    def __init__(self,sig=True):
        #sig是否包含激活层
        super(net,self).__init__()
        self.fc1=nn.Linear(2,10)
        self.sig=sig
        if self.sig:
            self.act=nn.Sigmoid()
        self.fc2=nn.Linear(10,2)
    def forward(self,x):
        if self.sig:
            return self.fc2(self.act(self.fc1(x)))
        else:
            return self.fc2(self.fc1(x))
import torch
data=torch.from_numpy(x).float()
model=net(sig=True)
losses=[]
criteron=nn.CrossEntropyLoss()
optimizer=torch.optim.SGD(model.parameters(),lr=0.1)
y=torch.from_numpy(y).long()
#训练100次
for i in range(1000):
    out=model(data)
    optimizer.zero_grad()
    #计算损失
    loss=criteron(out,y)
    #反向传播
    loss.backward()
    #更新
    optimizer.step()
    if i %100==0:
        losses.append(loss.item())
label=torch.argmax(out,dim=1)
plt.title("Sigmoid")
plt.plot(losses)
plt.show()

深度学习与PyTroch(二)_第22张图片
深度学习与PyTroch(二)_第23张图片

你可能感兴趣的:(Python计算机视觉,深度学习,python,opencv,深度学习)