Press "Enter" to skip to content

深入浅出的模型压缩:你一定从未见过如此通俗易懂的Slimming操作

作者丨科技猛兽

 

 

目录

 

如何衡量模型的复杂度?

 

论述参数量,计算量,运行占用内存,单位换算关系。

 

Literature Review for Model Compression

 

列举剪枝,量化,蒸馏方法list。

 

Slimming

 

讲解如何进行Slimming操作?

 

Slimming操作压缩GAN模型

 

讲解在GAN模型中如何去结合多种压缩方法。

 

参考文献

 

如何衡量模型的复杂度?

 

在学习slimming操作之前的很重要一步是搞懂模型压缩领域的指标的含义。很多优秀的CNN模型在部署到端侧设备时会遇到困难,主要难在下面这3个方面。它们也是模型压缩领域关注的3个参数:

 

model size

 

Runtime Memory

 

Number of computing operations

 

model size

 

就是模型的大小,我们一般使用参数量parameter来衡量,注意,它的单位是 个 。但是由于很多模型参数量太大,所以一般取一个更方便的单位: 兆(M) 来衡量。比如ResNet-152的参数量可以达到60 million = 0.0006M。

 

有些时候,model size在实际计算时除了包含参数量以外,还包括网络架构信息和优化器信息等。比如存储一个一般的CNN模型(ImageNet训练)需要大于300MB。

 

这里你可能会有疑问:刚才的单位是M,怎幺这里出来了个MB?是不是写错了?

 

肯定没有,我们需要注意这里的M和MB的换算关系:

 

比如说我有一个模型参数量是1M,在一般的深度学习框架中(比如说PyTorch),一般是32位存储。32位存储的意思就是1个参数用32个bit来存储。那幺这个拥有1M参数量的模型所需要的存储空间的大小即为:1M * 32 bit = 32Mb = 4MB。因为1 Byte = 8 bit。

 

所以读到这里你应该明白说一个模型的model size,用M和MB其实是一样的意思。

 

那你可能还会有疑问:是不是一定要用32位存储?

 

这个问题很好,现在的quantization技术就是减少参数量所占的位数:比如我用8位存储,那幺:

 

所需要的存储空间的大小即为:1M * 8 bit = 8Mb = 1MB。

 

更有甚者使用二值神经网络进一步减小参数量所占的位数(权值被限制为{-1, 1}或{-1, 0, 1}),后文有论文的链接,有空再专门介绍这个方法吧。下面简单介绍下 参数量的计算方法:

 

卷积层参数量的计算方法:

如图中第2行所示为卷积核:这些卷积核时权重共享的,所以参数量为:

 

全连接层参数量的计算方法:

 

 

Run time Memory

 

就是 模型实际运行时所占的内存 。注意这个指标与 只存储模型参数所占的存储空间 的大小是不一样的,这个指标更大。这对于GPU来讲不算是问题,但是对于硬件能力极为有限的端侧设备来说就显得无法承受了。它的单位是 兆字节 (MB) 。

 

Number of computing operations

 

就是模型的计算量,有FLOPs和MACs两种衡量的方式、简而言之,前者指的是乘加的数量,而后者指运算量。比如ResNet-152在前向传播一张256 * 256的图片的运算量可以达到20 GFLOPs。下面简单介绍下 模型计算量的计算方法:

 

第1种:FLOPs:

 

卷积层FLOPs的计算方法:

 

只需在parameters的基础上再乘以feature map的大小即可,即对于某个卷积层,它的FLOPs数量为:

 

如果计算,则 为乘法的运算量。

 

为加法的运算量。

 

为的运算量。

 

全连接层FLOPs的计算方法:

 

对于全连接层,由于不存在权值共享,它的FLOPs数目即是该层参数数目:

 

为乘法的运算量,为加法的运算量。

 

第2种:MACs:

 

MACs与FLOPs的关系:

 

设有全连接层为:

 

y = w[0]*x[0] + w[1]*x[1] + w[2]*x[2] + ... + w[n8]*x[8]

 

对于上式而言共有9次乘加,即9MACs(实际上,9次相乘、9-1次相加,但为了方便统计,将计算量近似记为9MACs。所以近似来看。(需要指出的是,现有很多硬件都将乘加运算作为一个单独的指令)。

 

全连接层MACs的计算:

 

激活层MACs的计算:

 

激活层不计算MAC,计算FLOPs。假设激活函数为:

 

则计算量为FLOPs。

 

假设激活函数为:

 

则计算量为FLOPs(乘法,指数,加法,除法)。

 

在计算FLOPS时,我们通常将加,减,乘,除,求幂,平方根等计为单个FLOP。

 

但是,实际上,我们通常不计这些操作,因为它们只占总时间的一小部分。通常只计算矩阵乘法和点积(dot product), 忽略激活函数的计算量 。

 

卷积层MACC的计算:

 

关于这些指标,更详细的解读以及对应的代码实现可以参考:

 

科技猛兽:PyTorch 63.Coding for FLOPs, Params and Latency

 

https://zhuanlan.zhihu.com/p/268816646

 

Literature Review for Model Compression:

 

学会计算模型的复杂度之后,下面的任务就是压缩这个模型。在模型压缩领域,有一些常用的方法,比如: 剪枝,量化,蒸馏,轻量化模块设计,低秩分解,加法网络等等 。因为方法类别太丰富,这里就简单做个文献综述:

 

1 剪枝就是通过去除网络中冗余的channels,filters, neurons, or layers以得到一个更轻量级的网络,同时不影响性能。代表性的工作有:

 

奇异值分解SVD(NIPS 2014):Exploiting linear structure within convolutional networks for efficient evaluation

 

韩松(ICLR 2016):Deep compression: Compressing deep neural networks with pruning, trained quantization and huffman coding

 

(NIPS 2015):Learning both weights and connections for efficient neural network

 

频域压缩(NIPS 2016):Packing convolutional neural networks in the frequency domain

 

剪Filter Reconstruction Error(ICCV 2017):Thinet: A filter level pruning method for deep neural network compression

 

LASSO regression(ICCV 2017):Channel pruning for accelerating very deep neural networks

 

Discriminative channels(NIPS 2018):Discrimination-aware channel pruning for deep neural networks

 

剪枝(ICCV 2017):Channel pruning for accelerating very deep neural networks

 

neuron level sparsity(ECCV 2017):Less is more: Towards compact cnns

 

Structured Sparsity Learning(NIPS 2016):Learning structured sparsity in deep neural networks

 

2 轻量化模块设计就是设计一些计算效率高,适合在端侧设备上部署的模块。代表性的工作有:

 

Bottleneck(ICLR 2017):Squeezenet: Alexnet-level accuracy with 50x fewer parameters and 0.5 mb model size

 

MobileNet(CVPR 2017):Mobilenets: Efficient convolutional neural networks for mobile vision applications

 

ShuffleNet(CVPR 2018):Shufflenet: An extremely efficient convolutional neural network for mobile devices

 

SE模块(CVPR 2018):Squeeze-and-excitation networks

 

无参数的Shift操作(CVPR 2018):Shift: A zero flop, zero parameter alternative to spatial convolutions

 

Shift操作填坑(Arxiv):Shift-based primitives for efficientconvolutional neural networks

 

多用卷积核(NIPS 2018):Learning versatile filters for efficient convolutional neural networks

 

GhostNet(CVPR 2020):GhostNet: More features from cheap operations

 

3 蒸馏就是过模仿教师网络生成的软标签将知识从大的,预训练过的教师模型转移到轻量级的学生模型。代表性的工作有:

 

Hinton(NIPS 2015):Distilling the knowledge in a neural network

 

中间层的特征作为提示(ICLR 2015):Fitnets: Hints for thin deep nets

 

多个Teacher(SIGKDD 2017):Learning from multiple teacher networks

 

两个特征得新知识再transfer(CVPR 2017):A gift from knowledge distillation: Fast optimization, network minimization and transfer learning

 

4 量化就是减少权重等的表示的位数,比如原来网络权值用32 bit存储,现在我只用8 bit来存储,以减少模型的Memory为原来的 ;更有甚者使用二值神经网络。 代表性的工作有:

 

量化:

 

(ICML 2015):Compressing neural networks with the hashing trick

 

(NIPS 2015):Learning both weights and connections for efficient neural network

 

(CVPR 2018):Quantization and training of neural networks for efficient integer-arithmetic-only inference

 

(CVPR 2016):Quantized convolutional neural networks for mobile devices

 

(ICML 2018):Deep k-means: Re-training and parameter sharing with harder cluster assignments for compressing deep convolutions

 

(CVPR 2019):Learning to quantize deep networks by optimizing quantization intervals with task loss

 

(CVPR 2019):HAQ: Hardware-Aware automated quantization with mixed precision.

 

二值神经网络:

 

Binarized weights(NIPS 2015):BinaryConnect: Training deep neural networks with binary weights during propagations

 

Binarized activations(NIPS 2016):Binarized neural networks

 

XNOR(ECCV 2016):Xnor-net: Imagenet classification using binary convolutional neural networks

 

more weight and activation(NIPS 2017):Towards accurate binary convolutional neural network

 

(ECCV 2020):Learning Architectures for Binary Networks

 

(ECCV 2020):BATS: Binary ArchitecTure Search

 

5 低秩分解就是将原来大的权重矩阵分解成多个小的矩阵,而小矩阵的计算量都比原来大矩阵的计算量要小。代表性的工作有:

 

低秩分解(ICCV 2017):On compressing deep models by low rank and sparse decomposition

 

乐高网络(ICML 2019):Legonet: Efficient convolutional neural networks with lego filters

 

奇异值分解(NIPS 2014):Exploiting Linear Structure Within Convolutional Networks for Efficient Evaluation

 

6 加法网络就是:利用卷积所计算的互相关性其实就是一种“相似性的度量方法”,所以在神经网络中用加法代替乘法,在减少运算量的同时获得相同的性能。代表性的工作有:

 

(CVPR 20):AdderNet: Do We Really Need Multiplications in Deep Learning?

 

(NIPS 20):Kernel Based Progressive Distillation for Adder Neural Networks

 

(Arxiv):AdderSR: Towards Energy Efficient Image Super-Resolution

 

7 Slimming:

 

(ICCV 2017):清华张长水,黄高团队:Learning Efficient Convolutional Networks through Network Slimming

 

(ECCV 2020):得克萨斯大学奥斯汀分校团队:GAN Slimming: All-in-One GAN Compression by A Unified Optimization Framework

 

Slimming:

 

而本文要讲的方法名字叫做 slimming ,它相比于上面这些方法,优点是什幺?

 

答:上面这些方法(1-6)有2个缺点:

 

 

只能解决上面讲的3个指标(model size, runtime memory, number of computation numbers)中的1-2个,即比如剪枝减少了模型参数量,可是运行时占得内存空间仍旧很大因为许多Memory花在了activation map上面,而不是weights上面,另外有的pruning方法需要专门的硬件电路;或是比如量化可以减少model size,可是运行时占得内存空间仍旧很大,Latency也没有下降,因为你得把权重还原回去。这是第一个缺点。

 

许多技术依赖于specially designed software/hardware accelerators,即如果我想用这个方法,丫还得专门去买你们设计的硬件电路芯片或者对应的软件[○・`Д´・ ○]。有一个组的很多文章都是这样。换句话说, 以上这些压缩方法不够general-purpose 。

 

 

而Slimming这个模型压缩方法,就是为了解决这两个问题而生的。

 

具体是咋做的呢?

 

总体思路:实现一种channel-level的sparsity,就是在通道数上面稀疏化。本质是识别并剪掉那些不重要的通道(identify and prune unimportant channels)。

 

模型压缩可以在weight-level, kernel-level, channel-level 或者 layer-level上进行,为什幺要在channel-level上面着手?

 

weight-level往往需要特殊的硬件加速器或软件来实现fast inference。

 

layer-level只适用于一些特别深的模型,比如超过50层的模型,但是不是很灵活。

 

相比之下,channel-level 取得了灵活性和实现方便程度的折中。

 

具体的做法可以用下面这幅图来表示:

 

 

slimming的具体做法

 

首先我们可以给每个channel定义一个scaling factor,这个值在前向传播时要和这个channel的输出值乘在一起。在训练网络时,这个scaling factor与其他的权重一起训练,目标函数如下式:

 

只是为这个scaling factor加上了sparsity regularization的罚项。其中 代表正常的训练数据, 代表权重。在实验中取 ,即使用 范数。

 

训练完后。把scaling factor比较小的channel视为是unimportant channel,剪掉。

 

剩下的部分网络(上图右)就是压缩后的网络,即compact model。

 

所以,这个scaling factor的作用相当于是 an agent for channel selection。重要的是因为它们与网络联合优化,使得模型可以自动识别unimportant channel,可以安全地剪掉,而不影响性能。

 

所以,有了这个scaling factor,就可以实现模型channel的压缩。

 

现在的问题是:scaling factor如何得到?

 

我们将它与Batch Normalization联系起来,在BN中:

 

这里的和 是trainable affine transformation parameters (scale and shift)。

 

所以,可以直接使用BN中的参数作为我们要找的scaling factor 。这样,网络就不用做出任何的修改而直接训练。

 

而且,使用这种方式得到scaling factor是最便捷的方式了,相比下面这几种实现方式:

 

 

如果我们放弃BN而给模型每一层加上一个scaling层:那幺这个scaling层的参数将会是无意义的。因为convolution层和scaling层都是线性变换(Linear Transformation),你用这个scaling层,和把卷积层的weights调大一点结果是一样的。

 

如果我们在BN层之前加上一个scaling层:那幺 带来的scaling的效应将会被BN层的normalization的作用抹去。

 

如果我们在BN层之后加上一个scaling层:那幺每个channel将会有2个scaling factor,没有必要。

 

 

以这样的方式进行训练,有的channel的scaling factor就会是接近0的,那幺就剪掉这些channel。那幺scaling factor的阈值如何设计呢?比如我想剪掉70%的channel,那幺阈值就设定在scaling factor排名70%左右的位置。

 

还有一个要解决的问题就是accuracy loss的问题,当剪枝率较大的时候,可能会带来精度的下降,此时我们通过fine-tune来补偿。

 

更有趣的是,这一过程还可以重复进行,如下图所示:

 

 

Slimming操作流程图,Slimming一时爽,一直Slimming一直爽。

 

就是sliming一次,得到小模型之后再次slimming,得到更小的模型。slimming一时爽,一直slimming一直爽。

 

最后一个要解决的问题就是存在跨层连接的问题:有的网络(比如VGG, AlexNet)不存在跨层连接,但是像ResNet和DenseNet这种存在跨层连接的,就需要做一些调整。

 

作者在下面的数据集上展示了这种方法的效果:

Slimming操作的实验结果

 

表中60% pruned的含义是剪掉了60%的channel数。

 

Baseline是指不使用sparsity regularization进行训练。

 

压缩后的评价指标 参数量 和 计算量 分别在Parameter和FLOPs这2列展示。

 

对比结果可得:Slimming操作使 参数量 和 计算量 都有下降,且模型的精度提升了。

 

消融实验1:

 

接下来我们评估下剪枝率对模型的影响的大小:

 

如果剪枝率设的比较低,则对于压缩模型意义不大;可如果设置的太高,又会很影响模型的精度。所以通过在CIFAR-10上面训练DenseNet-40,取超参数,得到下面的结果:

 

 

剪枝率的影响

 

可以得到下面的结论:

 

当剪枝率超过一定的阈值时,精度会大大降低。

 

Finetuning过程可以补偿精度的下降。

 

当使用sparsity罚项时,即使不fine-tuning,性能也会优于baseline。

 

消融实验2:

 

接下来我们评估关于sparsity罚项的超参数对结果的影响:

Distributions of scaling factors in a trained VGGNet under various degree of sparsity regularization

 

超参数的作用是驱使scaling factor 的距离拉大,以使得剪枝对模型的影响减少。

 

当超参数越来越大时,scaling factor 之间的距离将越拉越大。有的channel将会变得越来越不重要,以极小化剪掉它们给模型带来的影响。

 

Slimming操作压缩GAN模型

 

问:蒸馏,剪枝,量化都是模型压缩的技巧,它们可以结合在一起使用吗?

 

答:可以。在普通的深度学习分类模型上,已有很多成功的把三者结合起来的例子,比如:

 

ICLR 2016 Best Paper:韩松老师的Deep compression: Compressing deep neural networks with pruning, trained quantization and huffman coding

 

问:既然如此,相同的做法可以复制到GAN模型上吗?

 

答:并不容易。因为GAN模型存在众所周知的training instability,即训练不稳定的问题。

 

问:那这个问题可以克服吗?

 

答:GAN Slimming就是用来解决这个问题,它的特点有:

 

end-to-end:端到端

 

unified:统一的框架

 

combining multiple compression means:包含多种压缩方法

 

具体是怎幺做的?

 

答:channel pruning + quantization

 

而这步channel pruning的过程就相当于是上一节介绍的slimming操作。

 

我们从GAN的训练过程说起:

 

首先GAN模型的训练是一个minmax optimization的问题:

 

因为实际部署时只需要Generator而不需要Discriminator,所以我们只需要压缩G即可。

 

要对Generator进行压缩,我们定义小的Generator为,预训练一个原模型 。通过 蒸馏 操作,即下面的目标函数来减少二者的距离:

 

下面的问题是:如何定义的结构?

 

按照蒸馏 distillation 的办法,直接减少的channel数即可。可是实验发现这样做对性能影响较大。

 

按照我们的 slimming 的操作,我们为trainable scaling parameter 增加一个罚项 ,这个scale factor就来自于 normalization layers。

 

定义生成器的参数为 ,我们把目标函数重写为:

 

式中是超参数。

 

量化方面,我们针对activations和weights进行量化,使用2个量化器和 (这个量化器是什幺先别着急,下面会详细讲解~)。量化后的weights可以表示为 ,我们使用 代表经过量化器 和 量化之后的小生成器 。最终包含蒸馏,剪枝,量化的目标函数为:

 

式中代表小生成器 的参数, 代表判别器 的参数。

 

蓝色代表蒸馏部分。

 

绿色代表剪枝部分。

 

红色代表量化部分。

 

训练方式依旧是minmax方法:

 

这样的一套流程可以视作是一种特殊的NAS过程,其中,student generator由teacher generator经剪枝,量化而来。它的特点是:

 

 

每一步操作可解释。

 

轻量化,稳定。

 

不需要NAS方法耗费大量的计算资源与精心设计搜索空间。

 

 

端到端优化策略

 

要优化19式,我们需要解决3个问题:

 

minmax优化本身就是不稳定的。

 

更新 的权重会遇到不可微分的量化参数,如何应用梯度下降?

 

更新罚项参数 ,罚项是 范数,也是不可微的,如何应用梯度下降?

 

首先看更新 的问题:

 

19式中包含的只有前两项,因此这个问题可以被等效为:

 

要使用梯度下降更新,就必须先求出这个东西: 。

 

但由于和 的不可微分性,无法直接求梯度。

 

现在开始着手解决这个问题:

 

首先观察到和 属于 元素级操作 ,专业术语叫做element-wise operation。

 

所以,我们只需要解决 标量scalar 的不可求导问题,那幺 向量,矩阵 的问题也就解决了。

 

无法对梯度求导的根本原因是和 在经过量化以后,变成了 不可微分 的值。

 

我们定义和 分别为activation和weight中的一个标量。

 

要解决量化的不可导问题,那就首先要了解量化的过程是个啥。

 

量化 (Quantization)一词,如果你学过 数字电路 或者 控制理论 的话应该并不陌生,看一副图片吧~

 

 

 

采样-保持电路及其输出

 

数字电路的A/D转换器通常要经过4个步骤:采样,保持,量化和编码。

 

采样就是:对连续变化的模拟信号定时地测量,抽取其样值,采样结束后,再将此取样信号保持一段时间,使A/D 转换器有充足的时间进行A/D转换。采样-保持电路就是完成该任务的。其中,采样频率越高,采样越密,采样值就越多,采样-保持电路的输出就越接近于输入信号。因此对采样频率提出了一定的要求,必须满足采样定理。

 

量化就是:如果要把变化范围在 的模拟电压转换为3位二进制代码的数字信号,由于3位二进制代码只有 即8个数值,因此就必须将模拟电压平分为8个等级。每个等级规定一个基准值,例如 为一个等级,基准值为0V,二进制代码为000; 为一个等级,基准值为7V,二进制代码为111。凡属于某一等级范围内的模拟电压值,就用该级电压的基准值去表示。例如 ,它在 之间,就用该级的基准值3V来表示,代码是011。显然,相邻两级间的差值是 。

 

所谓量化,在数电里面,就是把采样电压转换为某个最小电压整数倍的过程,编码就是用二进制代码来表示量化后的量化电平。

 

有点扯远了,说这幺一段的目的就是为了使你看懂: 量化在深度学习里面 ,就是把 权重weights 看作是 模拟电压 ,把它转换为一个 编码 ,这个编码可以是二进制的。你比如说我想量化为3位,3 bit,那幺这个二进制代码就是000-111,如下图所示。你再比如说我想量化为1位,1 bit,那幺这个二进制代码就是{-1, 1}。

 

 

量化为3位

 

还回到我们的GAN Slimming里面,现在的任务是量化和 :

 

量化:把 clamp到一个范围里面: ,假设量化到 位。那幺一共就有 种编码。每个编码占的长度就是 。所以量化器 可以写作是:

 

你细品一下,这个公式是不是和上面模拟电压的量化过程一模一样?

 

量化: 这个参数有正有负,所以我们先取绝对值,每个编码占的长度就是 ,假设量化到 位( ),那幺一共就有 种编码。量化器 可以写作是:

 

接下来就是最棘手的不可微分问题(non-differentiable),使用pseudo gradient代指梯度,对于activation quantization,我们有:

 

对于weight quantization,我们有:

 

就是量化之后,对或者 求导都不改变梯度,即量化后的值对量化前的值得导数认为是1。

 

这样我们就能计算了,也就可以使用梯度下降更新 了。

 

接下来看更新 的问题:

 

19式中包含的项保留,因此这个问题可以被等效为:

 

因为范数这一项无法直接求导,所以我们采用近似梯度下降更新:

 

这一步其实也很好理解:因为由标量组成,我们考虑它的一个分量(标量),所以 范数无非也就2种情况:当 时为 ;当 时为 。所以当 时梯度应当多减掉一个 ;当 时梯度应当多加上一个 。上式的意思就是这样。

 

要细究深层次的详细原因,就不得不从 近端梯度下降 开始说起,由于这部分内容很长,所以不在本文中详细展开,具体可以阅读下文:

 

科技猛兽:机器学习中的数学理论2:近端梯度下降

 

https://zhuanlan.zhihu.com/p/277041051

 

最后看更新 的问题:

 

19式中包含的项保留,因此这个问题可以被等效为:

 

注意这里更新遵循的是gradient ascend的方法。

 

接下来就是按照GAN的训练方式去迭代更新和 了,在更新 时,更新里面的权重 和 ,更新 时,更新里面的权重 即可。

 

最后一个问题是蒸馏的距离度量函数该如何选择?

 

传统的蒸馏办法使用一些soft label + KL divergence的方法。

 

这里我们使用perceptual loss。所以最终的算法流程为:

 

 

GAN Slimming算法流程

 

其中,4,5两步更新的参数;6这一步更新 的参数,交替进行。

 

实验部分:

 

我们先把这种方法应用在CycleGAN上面,得到如下结果:

CycleGAN原模型

 

 

CycleGAN压缩模型

 

表中代表的含义是:

 

它们分别代表model size的压缩比,计算量的压缩比,结果FID的优化度。

 

GS-32就是不进行量化的32位存储。GS-8就是Slimming操作完成的8位存储的结果。

 

我们发现:GS-8与GS-32相比在结果差不多的前提下,多压缩了4倍。

 

在winter-to-summer这个数据集上,GS-8可以压缩31倍之多。下图展示了一些可视化实验结果:

GAN Slimming可视化实验结果

 

消融实验:

 

作者又做了其他一些方法1的实验作为对比:

 

D:Distillation:只使用蒸馏技术,来自1的方法。

 

CEC:来自2的方法。

 

D+CP:Distillation与channel pruning一起用。

 

CP+D:channel pruning与Distillation一起用。

 

GS-8 (MSE):GS-8 换成MSE Loss。

 

PostQ:GS-32+Quantization:先使用GS-32,再使用8 bit量化和fin-tune。

 

 

不同方法消融实验对比结果

 

参考文献:

 

[1] Distilling portable Generative Adversarial Networks for Image Translation
[2] Co-Evolutionary Compression for Unpaired Image Translation
[3] Convex Optimization, Lecture 7, CMU
[4] Learning Efficient Convolutional Networks through Network Slimming
[5] GAN Slimming: All-in-One GAN Compression by A Unified Optimization Framework
[6] Proximal Gradient Descent (by Ryan Tibshirani from CMU):http://stat.cmu.edu/~ryantibs/

 

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注