CNN网络架构演进:从LeNet到DenseNet

本文主要介绍卷积神经网络(CNN) 的发展演变,包含对每个网络的结构分析,创新点总结。 内容来自自己的收集整理,还有网易云课堂吴恩达的卷积神经网络教学视频。

LeNet-5

LeNet是LeCun在1998年提出,用于解决手写数字识别(0-9) 的视觉任务。自那时起,CNN的最基本的架构就定下来了:卷积层、池化层、全连接层。如今各大深度学习框架中所使用的LeNet都是简化改进过的LeNet-5。和原始的LeNet有些许不同,比如把激活函数改为了现在很常用的ReLu
LeNet-5跟现有的conv->pool->ReLU的套路不同,它使用的方式是conv1->pool->conv2->pool2再接全连接层,但是不变的是,卷积层后紧接池化层的模式依旧不变。

模型结构

LeNet-5一共有7个层,不包含输入层,分别是

  1. 2个卷积层 (5x5,stride=1,num1=6,num2=16)
  2. 2个池化层 (2x2,stride=2,type=average)
  3. 2个全连接层 (120,84)
  4. 1个输出层10个类别(数字0-9的概率)

卷积大小的计算公式如下

模型分析

以下图改进后的LeNet-5,对其进行深入分析(原始LeNet5中图像大小是32*32

首先输入图像是单通道的28*28大小的图像,用矩阵表示就是[1,28,28]

  1. 第一个卷积层Conv1所用的卷积核尺寸为5*5,滑动步长为1,卷积核数目为20,那么经过该层后图像尺寸变为24,28-5+1=24,输出矩阵为[20,24,24]。
  2. 第一个池化层Pool1核尺寸为2*2,步长2,这是没有重叠的max pooling,池化操作后,图像尺寸减半,变为12×12,输出矩阵为[20,12,12]。
  3. 第二个卷积层Conv2的卷积核尺寸为5*5,步长1,卷积核数目为50,卷积后图像尺寸变为8,这是因为12-5+1=8,输出矩阵为[50,8,8].
  4. 第二个池化层Pool2核尺寸为2*2,步长2,这是没有重叠的max pooling,池化操作后,图像尺寸减半,变为4×4,输出矩阵为[50,4,4]。
  5. pool2后面接全连接层FC1,神经元数目为500,再接relu激活函数。
  6. 再接FC2,神经元个数为10,得到10维的特征向量,用于10个数字的分类训练,送入softmax分类,得到分类结果的概率output

模型特性

  1. 使用三个层作为一个系列:卷积,池化,非线性
  2. 使用卷积提取空间特征
  3. 使用映射到空间均值下采样(subsample)
  4. 双曲线(tanh)或S型(sigmoid)形式非线性
  5. 多层神经网络(MLP)作为最后的分类器
  6. 层与层之间的稀疏连接矩阵避免大的计算成本

AlexNet

AlexNet在2012年ImageNet竞赛中以超过第二名10.9个百分点的绝对优势一举夺冠,从此深度学习和卷积神经网络名声鹊起,深度学习的研究如雨后春笋般出现,AlexNet的出现可谓是卷积神经网络的王者归来。

模型结构

一共有8个层,前5层是卷积层,后三层是全连接层,最终softmax输出是1000类。(此处层数不算池化层)

  1. Conv1(11x11,stride=4,num=96)
  2. Conv2(5x5,stride=1,num=256,pad=2)
  3. Conv3-5(3x3,stride=1,num3=num4=384,num5=256,pad=1)
  4. FC6-8 (num6=num7=4096,num8=1000)

模型分析

Conv1

第一层输入数据为原始图像的227x227x3的图像(最开始是224x224x3,为后续处理方便必须进行调整),这个图像被11x11x3(3代表深度,例如RGB的3通道)的卷积核进行卷积运算,卷积核对原始图像的每次卷积都会生成一个新的像素。卷积核的步长为4个像素,朝着横向和纵向这两个方向进行卷积。由此,会生成新的像素;(227-11)/4+1=55个像素,由于第一层有96个卷积核,所以就会形成555596个像素层,系统是采用双GPU处理,因此分为2组数据:55x55x48的像素层数据
重叠pool池化层:这些像素层还需要经过pool运算(池化运算)的处理,池化运算的尺度由预先设定为3x3,运算的步长为2,则池化后的图像的尺寸为:(55-3)/2+1=27。即经过池化处理过的规模为27x27x96
局部响应归一化层(LRN):最后经过局部响应归一化处理,归一化运算的尺度为5x5;第一层卷积层结束后形成的图像层的规模为27x27x96。分别由96个卷积核对应生成,这96层数据氛围2组,每组48个像素层,每组在独立的GPU下运算。

Conv2

第二层输入数据为第一层输出的27x27x96的像素层(为方便后续处理,这对每幅像素层进行像素填充pad=2),分为2组像素数据,两组像素数据分别在两个不同的GPU中进行运算。每组像素数据被5x5x48的卷积核进行卷积运算,同理按照第一层的方式进行:(27-5+2x2)/1+1=27个像素,一共有256个卷积核,这样也就有了27x27x128两组像素层。
重叠pool池化层:同样经过池化运算,池化后的图像尺寸为 (27-3)/2+1=13,即池化后像素的规模为2组13x13x128的像素层。
局部响应归一化层(LRN):最后经归一化处理,分别对应2组128个卷积核所运算形成。每组在一个GPU上进行运算。即共256个卷积核,共2个GPU进行运算。

Conv3

第三层输入数据为第二层输出的两组13x13x128的像素层(为方便后续处理,这对每幅像素层进行像素填充pad=1),分为2组像素数据,两组像素数据分别在两个不同的GPU中进行运算。每组像素数据被3x3x128的卷积核(两组,一共也就有3x3x256)进行卷积运算,同理按照第一层的方式进行:(13-3+1x2)/1+1=13个像素,一共有384个卷积核,这样也就有了13x13x192两组像素层。

Conv4

第四层输入数据为第三层输出的两组13x13x192的像素层(为方便后续处理,这对每幅像素层进行像素填充pad=1),分为2组像素数据,两组像素数据分别在两个不同的GPU中进行运算。每组像素数据被3x3x192的卷积核进行卷积运算,同理按照第一层的方式进行:(13-3+1x2)/1+1=13个像素,一共有384个卷积核,这样也就有了13x13x192两组像素层。

Conv5

第五层输入数据为第四层输出的两组13x13x192的像素层(为方便后续处理,这对每幅像素层进行像素填充pad=1),分为2组像素数据,两组像素数据分别在两个不同的GPU中进行运算。每组像素数据被3x3x192的卷积核进行卷积运算,同理按照第一层的方式进行:(13-3+1x2)/1+1=13个像素,一共有256个卷积核,这样也就有了13x13x128两组像素层。
重叠pool池化层:进过池化运算,池化后像素的尺寸为(13-3)/2+1=6,即池化后像素的规模变成了两组6x6x128的像素层,共6x6x256规模的像素层。

FC6

第6层输入数据的尺寸是6x6x256,采用6x6x256尺寸的滤波器对第六层的输入数据进行卷积运算;每个6x6x256尺寸的滤波器对第六层的输入数据进行卷积运算生成一个运算结果,通过一个神经元输出这个运算结果;共有4096个6x6x256尺寸的滤波器对输入数据进行卷积,通过4096个神经元的输出运算结果;然后通过ReLU激活函数以及dropout运算输出4096个本层的输出结果值。

FC7

第6层输出的4096个数据与第7层的4096个神经元进行全连接,然后经由ReLUDropout进行处理后生成4096个数据。

FC8

第7层输入的4096个数据与第8层的1000个神经元进行全连接,经过训练后输出被训练的数值。

各层训练参数

前五层:卷积层

后三层:全连接层

新技术点

  1. ReLU作为激活函数
    ReLU为非饱和函数,论文中验证其效果在较深的网络超过了Sigmoid,成功解决了Sigmoid在网络较深时的梯度弥散问题
  2. Dropout避免模型过拟合
    在训练时使用Dropout随机忽略一部分神经元,以避免模型过拟合。在Alexnet的最后几个全连接层中使用了Dropout。
  3. 重叠的最大池化
    之前的CNN中普遍使用平均池化,而Alexnet全部使用最大池化避免平均池化的模糊化效果。并且,池化的步长小于核尺寸,这样使得池化层的输出之间会有重叠和覆盖,提升了特征的丰富性
  4. 提出LRN层
    对局部神经元的活动创建竞争机制,使得其中响应比较大的值变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力。
  5. GPU加速
  6. 数据增强
    随机从256*256的原始图像中截取224x224大小的区域(以及水平翻转的镜像),相当于增强了(256-224)x(256-224)x2=2048倍的数据量。使用了数据增强后,减轻过拟合,提升泛化能力。避免因为原始数据量的大小使得参数众多的CNN陷入过拟合中。

ZF-Net

ZFNet是2013 ImageNet分类任务的冠军,其网络结构没什么改进,只是调了调参,性能较Alex提升了不少。差异表现在

  1. AlexNet用了两块GPU的稀疏连接结构,而ZFNet只用了一块 GPU 的稠密连接结构。
  2. 除此之外,ZF-Net将AlexNet第一层卷积核由11变成7,步长由4变为2

模型结构

VGG-Nets

VGG-Nets是由牛津大学VGG(Visual Geometry Group)提出,是2014年ImageNet竞赛定位任务的第一名和分类任务的第二名的中的基础网络。VGG可以看成是加深版本的AlexNet. 都是conv layer + FC layer,在当时看来这是一个非常深的网络了,因为层数高达十多层,当然以现在的目光看来VGG真的称不上是一个very deep的网络。

模型结构

上面表格是描述的是VGG-Net的网络结构以及诞生过程。为了解决初始化(权重初始化)等问题,VGG采用的是一种Pre-training的方式,这种方式在经典的神经网络中经常见得到,就是先训练一部分小网络,然后再确保这部分网络稳定之后,再在这基础上逐渐加深。表1从左到右体现的就是这个过程,并且当网络处于D阶段的时候,效果是最优的,即VGG-16。E阶段得到的网络就是VGG-19。VGG-16的16指的是conv+fc的总层数是16,是不包括max pool的层数

VGG16

网络使用的统一的卷积核大小:3x3,stride=1,padding=same,统一的Max-Pool: 2x2,stride=2

由上图看出,VGG-16的结构非常整洁,深度较AlexNet深得多,里面包含多个conv->conv->max_pool这类的结构,VGG的卷积层都是same的卷积,即卷积过后的输出图像的尺寸与输入是一致的,它的下采样完全是由max pooling来实现。
VGG网络后接3个全连接层,filter的个数(卷积后的输出通道数)从64开始,然后每进行一个pooling后其成倍的增加,128、512,VGG的主要贡献是使用小尺寸的filter,及有规则的卷积-池化操作

闪光点

  1. 卷积层使用更小的filter尺寸和间隔
    3×3卷积核的优点:
  2. 多个3×3的卷基层比一个大尺寸filter卷基层有更多的非线性,使得判决函数更加具有判决性
  3. 多个3×3的卷积层比一个大尺寸的filter有更少的参数,假设卷基层的输入和输出的特征图大小相同为C,那么三个3×3的卷积层参数个数3×(3×3×C×C)=27CC;一个7×7的卷积层参数为49CC;所以可以把三个3×3的filter看成是一个7×7filter的分解(中间层有非线性的分解)
  4. C中1*1卷积核的优点:在不影响输入输出维数的情况下,对输入进行线性形变,然后通过Relu进行非线性处理,增加网络的非线性表达能力。
  5. 使用了Multi-Scale的方法做数据增强,将原始图像缩放到不同尺寸S,然后再随机裁切224x224的图片,这样能增加很多数据量,对于防止模型过拟合有很不错的效果。

Network in Network

NIN的结构和传统的神经网络中多层的结构有些类似,后者的多层是跨越了不同尺寸的感受野(通过层与层中间加pool层),从而在更高尺度上提取出特征;NIN结构是在同一个尺度上的多层(中间没有pool层),从而在相同的感受野范围能提取更强的非线性使用 1×1 卷积为卷积层的特征提供更组合性的能力,这个想法之后被用到一些最近的架构中,例如 ResNet、Inception 及其衍生技术。

模型结构

创新点

  1. 提出了抽象能力更高的 Mlpconv 层
    一般用 CNN 进行特征提取时,其实就隐含地假设了特征是线性可分的, 可实际问题往往是难以线性可分的。一般来说我们所要提取的特征一般是高度非线性的。NIN提出在每个局部感受野中进行更加复杂的运算,提出了对卷积层的改进算法:MLP 卷积层。
  2. 提出了 Global Average Pooling(全局平均池化)层
    另一方面,传统的 CNN 最后一层都是全连接层,参数个数非常之多,容易引起过拟合。NIN提出采用了全局均值池化替代全连接层。与传统的全连接层不同,NIN对每个特征图一整张图片进行全局均值池化,这样每张特征图都可以得到一个输出。这样采用均值池化,连参数都省了,可以大大减小网络参数,避免过拟合

上图左侧是是传统的卷积层结构(线性卷积),在一个尺度上只有一次卷积;右图是Network in Network结构(NIN结构),先进行一次普通的卷积(比如3x3),紧跟再进行一次1x1的卷积,对于某个像素点来说1x1卷积等效于该像素点在所有特征上进行一次全连接的计算,所以右侧图的1x1卷积画成了全连接层的形式,需要注意的是NIN结构中无论是第一个3x3卷积还是新增的1x1卷积,后面都紧跟着激活函数(比如relu)。将两个卷积串联,就能组合出更多的非线性特征

GoogLeNet(从Inception v1到v4的演进)

GoogLeNet在2014的ImageNet分类任务上击败了VGG-Nets夺得冠军。跟AlexNet,VGG-Nets这种单纯依靠加深网络结构进而改进网络性能的思路不一样,它另辟幽径,在加深网络的同时(22层),也在网络结构上做了创新,引入Inception结构代替了单纯的卷积+激活的传统操作

获得高质量模型最保险的做法就是增加模型的深度(层数)或者是其宽度(层核或者神经元数),但是这里一般设计思路的情况下会出现如下的缺陷:

  1. 参数太多,若训练数据集有限,容易过拟合
  2. 网络越大计算复杂度越大,难以应用
  3. 网络越深,梯度越往后穿越容易消失,难以优化模型

解决上述问题的根本方法是将全连接甚至一般的卷积都转化为稀疏连接。为了打破网络对称性和提高学习能力,传统的网络都使用了随机稀疏连接。但是,计算机软硬件对非均匀稀疏数据的计算效率很差,所以在AlexNet中又重新启用了全连接层,目的是为了更好地优化并行运算。现在的问题是有没有一种方法:既能保持网络结构的稀疏性,又能利用密集矩阵的高计算性能。

Inception模块(V1)

Inception架构的主要思想是设计一个稀疏网络结构,但是能够产生稠密的数据,既能增加神经网络表现,又能保证计算资源的使用效率

  1. 采用不同大小的卷积核意味着不同大小的感受野,最后拼接意味着不同尺度特征的融合;
  2. 之所以卷积核大小采用1x1、3x3和5x5,主要是为了方便对齐。设定卷积步长stride=1之后,只要分别设定padding =0、1、2,采用same卷积可以得到相同维度的特征,然后这些特征直接拼接在一起;
  3. 该结构将CNN中常用的卷积(1x1,3x3,5x5)、池化操作(3x3)堆叠在一起(卷积、池化后的尺寸相同,将通道相加),一方面增加了网络的宽度,另一方面也增加了网络对尺度的适应性

然而上面这个Inception原始版本,所有的卷积核都在上一层的所有输出上来做,而那个5x5的卷积核所需的计算量就太大了,约需要1.2亿次的计算量(下图左边所示),造成了特征图的厚度很大。如果在5x5卷积前加一个1x1卷积,计算量将大大减少,如下图所示:

1x1卷积的主要目的是为了减少维度,还用于修正线性激活(ReLU)
比如,上一层的输出为100x100x128,经过具有256个通道的5x5卷积层之后(stride=1,pad=2),输出数据为100x100x256,其中,卷积层的参数为128x5x5x256= 819200。而假如上一层输出先经过具有32个通道的1x1卷积层,再经过具有256个输出的5x5卷积层,那么输出数据仍为为100x100x256,但卷积参数量已经减少为128x1x1x32 + 32x5x5x256= 204800,大约减少了4倍。
为了避免这种情况,在3x3前、5x5前、max pooling后分别加上了1x1的卷积核,以起到了降低特征图厚度的作用,这也就形成了Inception v1的网络结构,如下图所示:

InceptionV1创新点

  1. GoogLeNet采用了Inception(V1)模块化(9个)的结构,共22层,方便增添和修改;
  2. 网络最后采用了average pooling来代替全连接层,想法来自NIN,参数量仅为AlexNet的1/12,性能优于AlexNet,。但是,实际在最后还是加了一个全连接层,主要是为了方便finetune
  3. 虽然移除了全连接,但是网络中依然使用了Dropout ;
  4. 为了避免梯度消失,网络额外增加了2个辅助的softmax用于向前传导梯度。辅助分类器是将中间某一层的输出用作分类,并按一个较小的权重(0.3)加到最终分类结果中,这样相当于做了模型融合,同时给网络增加了反向传播的梯度信号,也提供了额外的正则化,对于整个网络的训练很有裨益。而在实际测试的时候,这两个额外的softmax会被去掉

InceptionV2

大尺寸的卷积核可以带来更大的感受野,也意味着更多的参数,比如5x5卷积核参数是3x3卷积核的25/9=2.78倍。
为此,作者提出可以用2个连续的3x3卷积层(stride=1)组成的小网络来代替单个的5x5卷积层,这便是Inception V2结构,保持感受野范围的同时又减少了参数量,如下图:

InceptionV2创新点

  1. 卷积分解,将单个的5x5卷积层用2个连续的3x3卷积层组成的小网络来代替,在保持感受野范围的同时又减少了参数量,也加深了网络。
  2. 提出了著名的Batch Normalization (BN)方法。BN会对每一个mini-batch数据的内部进行标准化(normalization),使输出规范到N(0,1)的正态分布,加快了网络的训练速度,还可以增大学习率。
  3. BN某种意义上起到了正则化的作用,所以可以减少或者取消dropout,简化网络结构。V2在训练达到V1准确率时快了14倍,最后收敛的准确率也比V1高。

InceptionV3

大卷积核完全可以由一系列的3x3卷积核来替代,那能不能分解的更小一点呢。
文章考虑了nx1卷积核,如下图所示的取代3x3卷积:
于是,任意nxn的卷积都可以通过1xn卷积后接nx1卷积来替代。实际上,作者发现在网络的前期使用这种分解效果并不好,还有在中度大小的feature map上使用效果才会更好,对于mxm大小的feature map,建议m在12到20之间
用nx1卷积来代替大卷积核,这里设定n=7来应对17x17大小的feature map。该结构被正式用在GoogLeNet V2中。

InceptionV3创新点

  1. 将一个较大的二维卷积拆成两个较小的一维卷积(7x7拆成了7x1和1x7,3x3拆成了1x3和3x1),一方面节约了大量参数,加速运算并减轻了过拟合),同时网络深度进一步增加,增加了网络的非线性(每增加一层都要进行ReLU)。
  2. 网络输入从224x224变为了299x299
  3. 优化了Inception Module的结构,主要有三种不同的结构(35x35、17x17、8x8),见下图。

InceptionV4

Inception V4研究了Inception模块与残差连接的结合。Inception V4主要利用残差连接(Residual Connection)来改进V3结构,得到Inception-ResNet-v1,Inception-ResNet-v2,Inception-v4网络。

ResNet的残差结构 -------------------------------> 与Inception相结合

ResNet

理论上,深的网络一般会比浅的网络效果好,如果要进一步地提升模型的准确率,最直接的方法就是把网络设计得越深越好,这样模型的准确率也就会越来越准确。
事实上呢?看下面一个例子,对常规的网络(plain network,也称平原网络)直接堆叠很多层次,经对图像识别结果进行检验,训练集、测试集的误差结果如下图:

从上面两个图可以看出,在网络很深的时候(56层相比20层),模型效果却越来越差了(误差率越高),并不是网络越深越好。
通过实验可以发现:随着网络层级的不断增加,模型精度不断得到提升,而当网络层级增加到一定的数目以后,训练精度和测试精度迅速下降,这说明当网络变得很深以后,深度网络就变得更加难以训练了

残差模块——Residual bloack

前面描述了一个实验结果现象,在不断加神经网络的深度时,模型准确率会先上升然后达到饱和,再持续增加深度时则会导致准确率下降。那么我们作这样一个假设:假设现有一个比较浅的网络(Shallow Net)已达到了饱和的准确率,这时在它后面再加上几个恒等映射层(Identity mapping,也即y=x,输出等于输入),这样就增加了网络的深度,并且起码误差不会增加,也即更深的网络不应该带来训练集上误差的上升。而这里提到的使用恒等映射直接将前一层输出传到后面的思想,便是著名深度残差网络ResNet的灵感来源。

a[l+2] 加上了 a[l]的残差块,即:残差网络中,直接将a[l]向后拷贝到神经网络的更深层,在ReLU非线性激活前面加上a[l],a[l]的信息直接达到网络深层。使用残差块能够训练更深层的网络,构建一个ResNet网络就是通过将很多这样的残差块堆积在一起,形成一个深度神经网络。

上图中是用5个残差块连接在一起构成的残差网络,用梯度下降算法训练一个神经网络,若没有残差,会发现随着网络加深,训练误差先减少后增加,理论上训练误差越来越小比较好。而对于残差网络来讲,随着层数增加,训练误差越来越减小,这种方式能够到达网络更深层,有助于解决梯度消失和梯度爆炸的问题,让我们训练更深网络同时又能保证良好的性能。

ResNet引入了残差网络结构(residual network),通过这种残差网络结构,可以把网络层弄的很深(据说目前可以达到1000多层),并且最终的分类效果也非常好,残差网络的基本结构如下图所示,很明显,该图是带有跳跃结构的

假定某段神经网络的输入是x,期望输出是H(x),即H(x)是期望的复杂潜在映射,如果是要学习这样的模型,则训练难度会比较大;
在上图的残差网络结构图中,通过 “shortcut connections(捷径连接)”的方式,直接把输入x传到输出作为初始结果,输出结果为H(x)=F(x)+x,当F(x)=0时,那么H(x)=x,也就是上面所提到的恒等映射。于是,ResNet相当于将学习目标改变了,不再是学习一个完整的输出,而是目标值H(X)和x的差值,也就是所谓的残差F(x) := H(x)-x,因此,后面的训练目标就是要将残差结果逼近于0,使到随着网络加深,准确率不下降。

残差网络

“shortcut connections(捷径连接)”是实线,有一些是虚线,有什么区别呢?

因为经过“shortcut connections(捷径连接)”后,H(x)=F(x)+x,如果F(x)和x的通道相同,则可直接相加,那么通道不同怎么相加呢。上图中的实线、虚线就是为了区分这两种情况的:

  1. 实线的Connection部分,表示通道相同,如左图的第一个粉色矩形和第三个粉色矩形,都是3364的特征图,由于通道相同,所以采用计算方式为H(x)=F(x)+x
  2. 虚线的Connection部分,表示通道不同,如左图的第一个绿色矩形和第三个绿色矩形,分别是3364和33128的特征图,通道不同,采用的计算方式为H(x)=F(x)+Wx,其中W是卷积操作,用来调整 x维度的。

残差学习单元

除了上面提到的两层残差学习单元,还有三层的残差学习单元,如下图所示:

实际中,考虑计算的成本,对残差块做了计算优化,即将两个3x3的卷积层替换为1x1 + 3x3 + 1x1, 如上图。新结构中的中间3x3的卷积层首先在一个降维1x1卷积层下减少了计算,然后在另一个1x1的卷积层下做了还原,既保持了精度又减少了计算量

创新点

  1. 引入残差单元,简化学习目标和难度,加快训练速度,模型加深时,不会产生退化问题
  2. 引入残差单元,能够有效解决训练过程中梯度消失和梯度爆炸问题

DenseNet

模型结构

创新点

  1. 密集连接,具体来说就是每个层都会接受其前面所有层作为其额外的输入,缓解梯度消失问题
  2. 加强特征传播,鼓励特征复用,极大的减少了参数量。

CNN发展总结

CNN 的演化路径可以总结为以下几个方向:

  1. 进化之路一: 网络结构加深
  2. 进化之路二: 加强卷积功能
  3. 进化之路三: 从分类到检测
  4. 进化之路四: 新增功能模块

本文主要介绍的CNN用在分类上的经典网络,关于CNN在目标检测(R-CNN系列)和图像分割(FCN)的应用,以后会有所介绍。

全部评论

相关推荐

头像
11-09 17:30
门头沟学院 Java
TYUT太摆金星:我也是,好几个华为的社招找我了
点赞 评论 收藏
分享
11-01 08:48
门头沟学院 C++
伤心的候选人在吵架:佬你不要的,能不能拿户口本证明过户给我。。球球了
点赞 评论 收藏
分享
点赞 1 评论
分享
牛客网
牛客企业服务