- 在线时间
- 1630 小时
- 最后登录
- 2024-1-29
- 注册时间
- 2017-5-16
- 听众数
- 82
- 收听数
- 1
- 能力
- 120 分
- 体力
- 564706 点
- 威望
- 12 点
- 阅读权限
- 255
- 积分
- 174635
- 相册
- 1
- 日志
- 0
- 记录
- 0
- 帖子
- 5313
- 主题
- 5273
- 精华
- 3
- 分享
- 0
- 好友
- 163
TA的每日心情 | 开心 2021-8-11 17:59 |
|---|
签到天数: 17 天 [LV.4]偶尔看看III 网络挑战赛参赛者 网络挑战赛参赛者 - 自我介绍
- 本人女,毕业于内蒙古科技大学,担任文职专业,毕业专业英语。
 群组: 2018美赛大象算法课程 群组: 2018美赛护航培训课程 群组: 2019年 数学中国站长建 群组: 2019年数据分析师课程 群组: 2018年大象老师国赛优 |
深度卷积神经网络(AlexNet)
4 R* u5 f1 p2 p$ I9 t
/ U4 h& T' h$ ]8 h& J在LeNet提出后,卷积神经网络在计算机视觉和机器学习领域中很有名气。但卷积神经网络并没有主导这些领域。这是因为虽然LeNet在小数据集上取得了很好的效果,但是在更大、更真实的数据集上训练卷积神经网络的性能和可行性还有待研究。事实上,在上世纪90年代初到2012年之间的大部分时间里,神经网络往往被其他机器学习方法超越,如支持向量机(support vector machines)。
% m( u' f8 h X+ ]8 Z0 a* w
1 W N# A( R$ b, O# G9 O, K+ K- I" Q在计算机视觉中,直接将神经网络与其他机器学习方法进行比较也许不公平。这是因为,卷积神经网络的输入是由原始像素值或是经过简单预处理(例如居中、缩放)的像素值组成的。但在使用传统机器学习方法时,从业者永远不会将原始像素作为输入。在传统机器学习方法中,计算机视觉流水线是由经过人的手工精心设计的特征流水线组成的。对于这些传统方法,大部分的进展都来自于对特征有了更聪明的想法,并且学习到的算法往往归于事后的解释。3 P7 C& L& h" C4 l% p
/ C+ N( n$ i& Y, u
虽然上世纪90年代就有了一些神经网络加速卡,但仅靠它们还不足以开发出有大量参数的深层多通道多层卷积神经网络。此外,当时的数据集仍然相对较小。除了这些障碍,训练神经网络的一些关键技巧仍然缺失,包括启发式参数初始化、随机梯度下降的变体、非挤压激活函数和有效的正则化技术。- @2 a, B! X4 ~9 U5 n
& O' e1 P9 u/ Q6 l# ]因此,与训练端到端(从像素到分类结果)系统不同,经典机器学习的流水线看起来更像下面这样:) a0 r: w5 W1 r2 U6 g; Q6 D9 X7 I- _3 M
& t! B! m+ M- O* X% I* l
获取一个有趣的数据集。在早期,收集这些数据集需要昂贵的传感器(在当时最先进的图像也就100万像素)。
" t2 V/ ^9 P5 T! ^, D
% e N/ b! o( L0 ^- o根据光学、几何学、其他知识以及偶然的发现,手工对特征数据集进行预处理。
- _2 y( e( H8 \5 e! p$ C3 V4 V# ]) f* T. @8 j
通过标准的特征提取算法,如SIFT(尺度不变特征变换) [Lowe, 2004]和SURF(加速鲁棒特征) [Bay et al., 2006]或其他手动调整的流水线来输入数据。
, a* U- M8 B, i# I" L( P0 D
% i3 w5 I' R1 x+ n9 i+ p将提取的特征送入最喜欢的分类器中(例如线性模型或其它核方法),以训练分类器。
, F" O* }4 k* n& p/ k+ D2 v: s: `
; Z# G/ Q) K$ w1 X, f如果你和机器学习研究人员交谈,你会发现他们相信机器学习既重要又美丽:优雅的理论去证明各种模型的性质。机器学习是一个正在蓬勃发展、严谨且非常有用的领域。然而,如果你和计算机视觉研究人员交谈,你会听到一个完全不同的故事。他们会告诉你图像识别的诡异事实————推动领域进步的是数据特征,而不是学习算法。计算机视觉研究人员相信,从对最终模型精度的影响来说,更大或更干净的数据集、或是稍微改进的特征提取,比任何学习算法带来的进步要大得多。
! [$ } ?/ _: r1 |; [3 ^; u
" {6 y: k; y8 I: p9 e2 f1.1. 学习表征
. X' O( B& u0 w9 \0 w+ M另一种预测这个领域发展的方法————观察图像特征的提取方法。在2012年前,图像特征都是机械地计算出来的。事实上,设计一套新的特征函数、改进结果,并撰写论文是盛极一时的潮流。SIFT [Lowe, 2004]、SURF [Bay et al., 2006]、HOG(定向梯度直方图) [Dalal & Triggs, 2005]、bags of visual words和类似的特征提取方法占据了主导地位。; I+ ]/ ^# _+ O" d% {6 H
% E- j, N. {. y4 J* A% L
另一组研究人员,包括Yann LeCun、Geoff Hinton、Yoshua Bengio、Andrew Ng、Shun ichi Amari和Juergen Schmidhuber,想法则与众不同:他们认为特征本身应该被学习。此外,他们还认为,在合理地复杂性前提下,特征应该由多个共同学习的神经网络层组成,每个层都有可学习的参数。在机器视觉中,最底层可能检测边缘、颜色和纹理。事实上,Alex Krizhevsky、Ilya Sutskever和Geoff Hinton提出了一种新的卷积神经网络变体AlexNet。在2012年ImageNet挑战赛中取得了轰动一时的成绩。AlexNet以Alex Krizhevsky的名字命名,他是论文 [Krizhevsky et al., 2012]的第一作者。/ A/ U; e- Q4 k) _
1 @, r: l7 h5 n1 F9 ^' \6 i$ j
有趣的是,在网络的最底层,模型学习到了一些类似于传统滤波器的特征抽取器。 图7.1.1是从AlexNet论文 [Krizhevsky et al., 2012]复制的,描述了底层图像特征。0 F* P5 F! I! }) U, G& [3 B
# s1 H& K* A+ J7 U9 a; M3 p
, Z4 w( Y6 P7 ]1 ~8 x# y
1 b- b) M8 r: ^4 i
1.1 AlexNet第一层学习到的特征抽取器。4 X `1 A8 A1 c+ e, [
+ U1 V" j$ I. J# n9 N7 c. Q( pAlexNet的更高层建立在这些底层表示的基础上,以表示更大的特征,如眼睛、鼻子、草叶等等。而更高的层可以检测整个物体,如人、飞机、狗或飞盘。最终的隐藏神经元可以学习图像的综合表示,从而使属于不同类别的数据易于区分。尽管一直有一群执着的研究者不断钻研,试图学习视觉数据的逐级表征,然而很长一段时间里这些尝试都未有突破。深度卷积神经网络的突破出现在2012年。突破可归因于两个关键因素。
0 D/ D( h, j) h. n( i: k6 m$ v- e
. `" u4 i( z. k% L0 Z+ | R+ F1.1.1. 缺少的成分:数据
$ Z& M, [) z+ E( i) o" V包含许多特征的深度模型需要大量的有标签数据,才能显著优于基于凸优化的传统方法(如线性方法和核方法)。 然而,限于早期计算机有限的存储和90年代有限的研究预算,大部分研究只基于小的公开数据集。例如,不少研究论文基于加州大学欧文分校(UCI)提供的若干个公开数据集,其中许多数据集只有几百至几千张在非自然环境下以低分辨率拍摄的图像。这一状况在2010年前后兴起的大数据浪潮中得到改善。2009年,ImageNet数据集发布,并发起ImageNet挑战赛:要求研究人员从100万个样本中训练模型,以区分1000个不同类别的对象。ImageNet数据集由斯坦福教授李飞飞小组的研究人员开发,利用谷歌图像搜索(Google Image Search)对每一类图像进行预筛选,并利用亚马逊众包(Amazon Mechanical Turk)来标注每张图片的相关类别。这种规模是前所未有的。这项被称为ImageNet的挑战赛推动了计算机视觉和机器学习研究的发展,挑战研究人员确定哪些模型能够在更大的数据规模下表现最好。
) x/ m2 x. |% ]9 m: I: R+ P& m# G O3 r% U" a
1.1.2. 缺少的成分:硬件
# N+ u6 d' I) F$ ^, d8 q深度学习对计算资源要求很高,训练可能需要数百个迭代轮数,每次迭代都需要通过代价高昂的许多线性代数层传递数据。这也是为什么在20世纪90年代至21世纪初,优化凸目标的简单算法是研究人员的首选。然而,用GPU训练神经网络改变了这一格局。图形处理器(Graphics Processing Unit,GPU)早年用来加速图形处理,使电脑游戏玩家受益。GPU可优化高吞吐量的4×4矩阵和向量乘法,从而服务于基本的图形任务。幸运的是,这些数学运算与卷积层的计算惊人地相似。由此,英伟达(NVIDIA)和ATI已经开始为通用计算操作优化gpu,甚至把它们作为通用GPU(general-purpose GPUs,GPGPU)来销售。/ v: \! U; o3 y- u- f
! F7 h/ L9 Z' k% t- d6 ~
那么GPU比CPU强在哪里呢?$ W; W2 s T ?5 }; Z
u! s' Y8 A, `" C
首先,我们深度理解一下中央处理器(Central Processing Unit,CPU)的核心。 CPU的每个核心都拥有高时钟频率的运行能力,和高达数MB的三级缓存(L3Cache)。 它们非常适合执行各种指令,具有分支预测器、深层流水线和其他使CPU能够运行各种程序的功能。 然而,这种明显的优势也是它的致命弱点:通用核心的制造成本非常高。 它们需要大量的芯片面积、复杂的支持结构(内存接口、内核之间的缓存逻辑、高速互连等等),而且它们在任何单个任务上的性能都相对较差。 现代笔记本电脑最多有4核,即使是高端服务器也很少超过64核,因为它们的性价比不高。
7 B4 D" X2 D: @2 a* y# x2 W8 u/ `4 M8 U+ Y. S2 b- s
相比于CPU,GPU由100~1000个小的处理单元组成(NVIDIA、ATI、ARM和其他芯片供应商之间的细节稍有不同),通常被分成更大的组(NVIDIA称之为warps)。 虽然每个GPU核心都相对较弱,有时甚至以低于1GHz的时钟频率运行,但庞大的核心数量使GPU比CPU快几个数量级。 例如,NVIDIA最近一代的Ampere GPU架构为每个芯片提供了高达312 TFlops的浮点性能,而CPU的浮点性能到目前为止还没有超过1 TFlops。 之所以有如此大的差距,原因其实很简单:首先,功耗往往会随时钟频率呈二次方增长。 对于一个CPU核心,假设它的运行速度比GPU快4倍,你可以使用16个GPU内核取代,那么GPU的综合性能就是CPU的16×1/4=4倍。 其次,GPU内核要简单得多,这使得它们更节能。 此外,深度学习中的许多操作需要相对较高的内存带宽,而GPU拥有10倍于CPU的带宽。
" s! U* Y% Z0 M$ ~, Q! y1 L. v. K4 Q7 |+ @' t& S+ \+ G/ j: `: d
回到2012年的重大突破,当Alex Krizhevsky和Ilya Sutskever实现了可以在GPU硬件上运行的深度卷积神经网络时,一个重大突破出现了。他们意识到卷积神经网络中的计算瓶颈:卷积和矩阵乘法,都是可以在硬件上并行化的操作。 于是,他们使用两个显存为3GB的NVIDIA GTX580 GPU实现了快速卷积运算。他们的创新cuda-convnet几年来它一直是行业标准,并推动了深度学习热潮。
! t, U& J# q3 h" y: ^8 I' y) F6 c# v! k) _& ^% o; X3 ~3 W
1.2. AlexNet
1 r; b5 U) i6 A! U3 l+ R( V% B2012年,AlexNet横空出世。它首次证明了学习到的特征可以超越手工设计的特征。它一举打破了计算机视觉研究的现状。 AlexNet使用了8层卷积神经网络,并以很大的优势赢得了2012年ImageNet图像识别挑战赛。/ {8 ?6 b' R1 _4 q
: r* v3 I" i ^! K. C3 M. HAlexNet和LeNet的架构非常相似,如 图所示。 注意,这里我们提供了一个稍微精简版本的AlexNet,去除了当年需要两个小型GPU同时运算的设计特点。
, A: R" ?( s, A0 K% c; ]
! r% i5 j- ?: o4 b: I- e1 T4 p( m+ P$ D; A7 L A
2 E8 ]( O0 Q, @ A1 [6 N
图1.2 从LeNet(左)到AlexNet(右)
' w! v4 Y, U9 _, R5 y, ^' a, X% |) ]9 v2 E2 Y. O
AlexNet和LeNet的设计理念非常相似,但也存在显著差异。 首先,AlexNet比相对较小的LeNet5要深得多。 AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全连接输出层。 其次,AlexNet使用ReLU而不是sigmoid作为其激活函数。 下面,让我们深入研究AlexNet的细节。
8 V3 O! b! {. g/ \7 O5 L9 V" n) ~) X- i
1.2.1. 模型设计
- e% q" m k( v- R# T1 q在AlexNet的第一层,卷积窗口的形状是11×11。 由于ImageNet中大多数图像的宽和高比MNIST图像的多10倍以上,因此,需要一个更大的卷积窗口来捕获目标。 第二层中的卷积窗口形状被缩减为5×5,然后是3×3。 此外,在第一层、第二层和第五层卷积层之后,加入窗口形状为3×3、步幅为2的最大汇聚层。 而且,AlexNet的卷积通道数目是LeNet的10倍。
: L# z5 e5 b0 r( n x* z8 R. C3 M5 U- q: h
在最后一个卷积层后有两个全连接层,分别有4096个输出。 这两个巨大的全连接层拥有将近1GB的模型参数。 由于早期GPU显存有限,原版的AlexNet采用了双数据流设计,使得每个GPU只负责存储和计算模型的一半参数。 幸运的是,现在GPU显存相对充裕,所以我们现在很少需要跨GPU分解模型(因此,我们的AlexNet模型在这方面与原始论文稍有不同)。
" H6 L5 f. T; Y: W, A- {, G
; _9 ]) S4 Z2 O+ c1.2.2. 激活函数1 S0 y5 M2 p% b, |, G: U
此外,AlexNet将sigmoid激活函数改为更简单的ReLU激活函数。 一方面,ReLU激活函数的计算更简单,它不需要如sigmoid激活函数那般复杂的求幂运算。 另一方面,当使用不同的参数初始化方法时,ReLU激活函数使训练模型更加容易。 当sigmoid激活函数的输出非常接近于0或1时,这些区域的梯度几乎为0,因此反向传播无法继续更新一些模型参数。 相反,ReLU激活函数在正区间的梯度总是1。 因此,如果模型参数没有正确初始化,sigmoid函数可能在正区间内得到几乎为0的梯度,从而使模型无法得到有效的训练。/ s9 N& ?/ N9 Q1 \; c m' C$ ]
+ ?' z/ ^) S1 S( t' y
1.2.3. 容量控制和预处理: y, H! _9 K; D- ^6 ?4 I
AlexNet通过暂退法控制全连接层的模型复杂度,而LeNet只使用了权重衰减。 为了进一步扩充数据,AlexNet在训练时增加了大量的图像增强数据,如翻转、裁切和变色。 这使得模型更健壮,更大的样本量有效地减少了过拟合。
2 \. A. R5 ?2 w, X/ r6 H! y/ U# E3 [; i' D
& Y# A8 i' |8 F
. _& K; x' x4 ~" dimport torch
: u V0 v8 n6 ?" `3 h4 Z* N3 o$ E1 bfrom torch import nn5 _& T( W* R% R. L
from d2l import torch as d2l& A; L+ o; S. M7 k1 N" R
! o% R0 j3 Z: r$ |# W! Z
net = nn.Sequential(
7 m1 R$ b; L0 A! Y6 E # 这里,我们使用一个11*11的更大窗口来捕捉对象。
* ^9 E; O& V, H) N4 H # 同时,步幅为4,以减少输出的高度和宽度。
) C: B# s7 p- u6 K6 t+ s/ } # 另外,输出通道的数目远大于LeNet5 t+ c0 g! I; v s7 \
nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
* w% M( ~. b9 z% z6 d8 { nn.MaxPool2d(kernel_size=3, stride=2),
2 ^% S7 f. v: U4 U5 ] # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
8 }' ^& `0 ^5 l4 Q$ h6 ^# @. m" A nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),. k0 i# e' A9 b9 P. R3 G
nn.MaxPool2d(kernel_size=3, stride=2),
, V. f- e* W5 ?5 ?# R, o # 使用三个连续的卷积层和较小的卷积窗口。
; K2 S4 |' ?6 S5 Y5 l& R1 B # 除了最后的卷积层,输出通道的数量进一步增加。. B* z5 k; L! {0 |& K
# 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度- ~3 }" p# h2 q! w9 v w7 ^
nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
* e2 t" j% o* Y9 \0 }& k+ n x nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
* @7 D9 \8 ]3 O' m1 r nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
+ o3 l5 @1 K. X nn.MaxPool2d(kernel_size=3, stride=2),
& j4 I A7 X8 M n& ^ nn.Flatten(),+ G+ Q; b* X+ w8 Z. t8 V+ [
# 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
7 Y: t2 m( s' i% @. N nn.Linear(6400, 4096), nn.ReLU(),3 a$ z2 B. o* m( Z2 W2 O8 o
nn.Dropout(p=0.5),* H. B) G' f6 Y
nn.Linear(4096, 4096), nn.ReLU(),# w- S: K& u6 o3 p" T8 ^/ _* i8 ]) ]
nn.Dropout(p=0.5),( f/ U6 L0 O, g0 j& Y- V, R9 C
# 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的10004 z9 O, L: b( p W( I* e" _
nn.Linear(4096, 10))( _' G+ q! C% E
9 ?, ?9 R% ]) T: Q( k9 m& L- H; O我们构造一个高度和宽度都为224的单通道数据,来观察每一层输出的形状。
2 Y* _% @4 R' e6 B; m/ {+ |1 d3 Y4 r1 C. D f% j: S
X = torch.randn(1, 1, 224, 224) D& i, P- }9 x4 @2 t% \% F
for layer in net:
' A0 b! B+ m _: D$ g X=layer(X)
/ W ]2 Q) C% d) v0 V print(layer.__class__.__name__,'output shape:\t',X.shape)1 a* ~. E& H4 d+ C! Q& V7 D% w7 y
Conv2d output shape: torch.Size([1, 96, 54, 54])
5 t% B7 V) u' |# ]; i) _1 ]ReLU output shape: torch.Size([1, 96, 54, 54])" E- y7 A5 @6 Z! p
MaxPool2d output shape: torch.Size([1, 96, 26, 26])5 H* H) e, X5 l2 t {3 e
Conv2d output shape: torch.Size([1, 256, 26, 26])
# P' V- `+ ~ \4 hReLU output shape: torch.Size([1, 256, 26, 26])
2 v( J" d& p& vMaxPool2d output shape: torch.Size([1, 256, 12, 12]), } d& {- i+ @9 y
Conv2d output shape: torch.Size([1, 384, 12, 12])
* K' U h) @2 w" N$ `ReLU output shape: torch.Size([1, 384, 12, 12])
7 {; u# u6 V# o) ]Conv2d output shape: torch.Size([1, 384, 12, 12])2 D4 O9 Z1 g# G+ ?
ReLU output shape: torch.Size([1, 384, 12, 12])
2 h) N) j; U9 O9 q. @4 j/ kConv2d output shape: torch.Size([1, 256, 12, 12])
5 J( D4 l3 {9 ?. IReLU output shape: torch.Size([1, 256, 12, 12])
, x, L/ e6 M( h5 e5 v. r- oMaxPool2d output shape: torch.Size([1, 256, 5, 5])$ [/ ]9 N8 L/ h }. T' ?* ]- C! x. I
Flatten output shape: torch.Size([1, 6400])
! a0 s i2 T3 k4 [Linear output shape: torch.Size([1, 4096])
' c- s. \3 n5 h" S/ gReLU output shape: torch.Size([1, 4096])
0 i; Y% ?" `# {9 f O: k" YDropout output shape: torch.Size([1, 4096]) m+ j6 @0 _8 [+ `% u5 ?5 D
Linear output shape: torch.Size([1, 4096])
8 z) v, t& r) _2 HReLU output shape: torch.Size([1, 4096])8 J1 [8 V3 {; K/ V0 ~" ~8 s+ U
Dropout output shape: torch.Size([1, 4096])9 S; V" x( d: ?: s* g- Q
Linear output shape: torch.Size([1, 10])
( o! J$ E" u1 G( O0 g
; I' P l- x: W5 `6 M1.3. 读取数据集
( N8 u2 D/ ^! |# A- u. v) ?2 m尽管本文中AlexNet是在ImageNet上进行训练的,但我们在这里使用的是Fashion-MNIST数据集。因为即使在现代GPU上,训练ImageNet模型,同时使其收敛可能需要数小时或数天的时间。 将AlexNet直接应用于Fashion-MNIST的一个问题是,Fashion-MNIST图像的分辨率(28×28像素)低于ImageNet图像。 为了解决这个问题,我们将它们增加到224×224(通常来讲这不是一个明智的做法,但我们在这里这样做是为了有效使用AlexNet架构)。 我们使用d2l.load_data_fashion_mnist函数中的resize参数执行此调整。2 c5 Z U1 f0 k4 Z
4 O# J2 E3 e& W" a: J* ^
batch_size = 128
# A$ R, F+ E$ g3 d* g. ltrain_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
* ~- e4 Z' @' U2 D; O8 Z1.4. 训练AlexNet
" \0 N7 A6 j ]5 G( x5 ~8 Y0 H现在,我们可以开始训练AlexNet了。与LeNet相比,这里的主要变化是使用更小的学习速率训练,这是因为网络更深更广、图像分辨率更高,训练卷积神经网络就更昂贵。
' L/ _1 y- Q% q& S- e1 i% o, U% f0 \ f; C. ^; ?
lr, num_epochs = 0.01, 10- @% d) V& S6 b' I" n1 V
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())6 |7 m7 y' P. W
loss 0.327, train acc 0.881, test acc 0.885' _7 u4 ?0 t* O( I$ O- Q& ~
4149.6 examples/sec on cuda:0
$ Y) d. b( D: Q; L0 |# ?. a4 g s4 y3 Z, l
( O6 P, g& {! T) ]! d' u; h* y/ @3 b! i& g2 i$ b
1 y. `# F4 b) w2 M
1.5. 小结
9 C ^* M7 t5 ?# P/ b$ TAlexNet的架构与LeNet相似,但使用了更多的卷积层和更多的参数来拟合大规模的ImageNet数据集。6 }$ }( y. b" ]# |) y. \ F9 G5 B
1 v5 g; p: z, h& J( m今天,AlexNet已经被更有效的架构所超越,但它是从浅层网络到深层网络的关键一步。3 c0 u7 u# w k
. P- W9 L- ?, n( _2 r
尽管AlexNet的代码只比LeNet多出几行,但学术界花了很多年才接受深度学习这一概念,并应用其出色的实验结果。这也是由于缺乏有效的计算工具。# Y V- `5 E/ [) Z
7 ~# K0 K( A8 H4 H7 {3 r4 YDropout、ReLU和预处理是提升计算机视觉任务性能的其他关键步骤。
5 Y- m' b. d6 Y1 p————————————————
; S" H) u0 W7 z1 M4 [- Q# L/ ]: X' }0 \版权声明:本文为CSDN博主「Soloy_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
9 v6 y% d* M. o7 ^1 P8 s) H. B8 ?原文链接:https://blog.csdn.net/sikh_0529/article/details/126805057
" Q W' O: [0 H' c6 r* N+ L5 @: c/ A0 a* t2 Q3 q8 J E
1 `& b7 C! l9 o8 {. e4 p |
zan
|