网络搭建目录:
- Lenet 学习笔记 pytorch官方demo代码复现_放风筝的猪的博客-CSDN博客
- AlexNet网络结构详解与代码复现_放风筝的猪的博客-CSDN博客
- VGG网络结构详解与代码复现,感受野计算_放风筝的猪的博客-CSDN博客
- GoogLeNet网络结构详解与代码复现_放风筝的猪的博客-CSDN博客
- ResNet网络结构详解,网络搭建,迁移学习_放风筝的猪的博客-CSDN博客
- Network in Network(NIN)网络结构详解,网络搭建_放风筝的猪的博客-CSDN博客
一、简述
今年读研开始转入深度学习方向,而CNN是深度学习中的核心算法之一,也是2012年以来将人工智能推向风口浪尖的推手。所以决定从CNN入手学习,下面将介绍一下CNN的发展历史以及PyTorch的实现代码,文章内容讲从以下几个网络进行介绍。
1、卷积神经网络
首先来简单说一下,什么是CNN,卷积神经网络,它是一种人工神经网络的结构,是从猫的视觉神经结构中得到了灵感,进而模拟其结构设计出来的一种人工神经网络结构。
简单来说:只要包含了卷积层的网络都可以理解成卷积神经网络
这种结构通过卷积核来获取“感受野”范围内数据之间的关系特征。一张图片里,相邻的像素显然是有更强的相关性,相比于全连接神经网络,CNN突出了这种相邻的关系特征,因而更加准确的获取了图片内的有用信息。
具体卷积核工作图如下:
神经元感受野的值越大表示其能接触到的原始图像范围就越大,也意味着它可能蕴含更为全局,语义层次更高的特征;相反,值越小则表示其所包含的特征越趋向局部和细节。因此感受野的值可以用来大致判断每一层的抽象层次
2、ILSVRC竞赛
讲历史就必须有一个主线,而这个历史主线就是ILSVRC,一个想了解CNN必须要了解的计算机视觉领域最权威的学术竞赛。
ILSVRC(ImageNet Large Scale Visual Recognition Challenge)竞赛(2010-2017),这个竞赛所使用的数据集,是由斯坦福大学李飞飞教授主导,包含了超过1400万张全尺寸的有标记图片。ILSVRC比赛会每年从ImageNet数据集中抽出部分样本用于比赛,以2012年为例,比赛的训练集包含1281167张图片,验证集包含50000张图片,测试集为100000张图片。竞赛的项目主要包括图像的分类和定位(CLS-LOC)、目标检测(DET)、视频目标检测(VID)和场景分类(Scene)。
我下面讲的,主要是在最基本的分类问题上,历届表现优异的模型。不过ILSVRC从2017之后就停办了,另外一个计算机视觉领域的赛事,就成为了该领域权威的竞赛,它就是从2014年开始由微软举办的MS COCO竞赛。
二、CNN发展史
下图是一系列里程碑式的模型,分别在ImageNet数据集图像分类的准确率表现:
偷张图介绍一下各个模型之间的关系:
2012年AlexNet的发布使人工智能算法迎来了历史性的突破,才造就了之后的舆论关注和CNN的研究爆发式的增长。之后一系列著名的CNN模型都是在AlexNet的基础上演变出来的,它门主要分为两个流派,一个是以加深网络为主的VGG流派,另一个是以增强卷积模块功能为主的NIN流派。之后两个流派相结合产生了Inception ResNet。
而AlexNet又是在LeNet的基础上发展而来的。下面我们就从LeNet开始一一介绍各个经典模型都做了哪些工作,解决了什么问题。
下面的介绍中会提到一些经典的论文,这些论文可以从这里获取:
链接:https://pan.baidu.com/s/1yCCcCvCp6ResNbPYrbxang?pwd=kdlf
提取码:kdlf
1、反向传播算法(Backpropagation algorithm,BP)算法的提出与重新描述
在1986年,反向传播算法由Rumelhart,Hinton和Williams重新描述,展示了这个方法可以根据输入数据适用隐含层来表示内在的联系。
2、LeNet(1998)– 开山鼻祖
首先是LeNet,它是由CNN之父Lecun在1998年提出,用于解决手写数字识别任务的,著名的Tensorflow入门学习数据集MNIST,就是那时候做出来的。LeNet对应的论文是《Gradient-Based Learning Applied to Document Recognition》。
LeNet
展示了通过梯度下降训练卷积神经网络可以达到手写数字识别在当时最先进的结果。这个奠基性的工作第一次将卷积神经网络推上舞台,为世人所知。
LeNet又叫LeNet-5,这个5是指它的网络结构中有5个表示层,具体结构如下图所示:
LeNet5包含Input、卷积层1、池化层1、卷积层2、池化层2、全连接层、输出层。
1998年的LeNet5模型的发布标注着CNN的真正面世,但是这个模型在后来的一段时间并未能火起来,主要原因是当时的计算力跟不上,不过LeNet最大的贡献是:定义了CNN卷积层、池化层、全连接层的基本结构,是CNN的鼻祖。
(1)卷积层块
卷积层块里的基本单位是卷积层后接最大池化层:卷积层用来识别图像里的空间模式,如线条和物体局部,之后的最大池化层则用来降低卷积层对位置的敏感性。
卷积层块由两个这样的基本单位重复堆叠构成。
- 在卷积层块中,每个卷积层都使用5×5的窗口,并在输出上使用
sigmoid
激活函数。
第一个卷积层输出通道数为 6 ,第二个卷积层输出通道数则增加到 16 。这是因为第二个卷积层比第一个卷积层的输入的高和宽要小,所以增加输出通道使两个卷积层的参数尺寸类似。
- 卷积层块的两个最大池化层的窗口形状均为2×2,且步幅为 2 。由于池化窗口与步幅形状相同,池化窗口在输入上每次滑动所覆盖的区域互不重叠。
卷积层块的输出形状为(批量大小, 通道, 高, 宽)。
(2)全连接层块
当卷积层块的输出传入全连接层块时,全连接层块会将小批量中每个样本变平flatten
。
也就是说,全连接层的输入形状将变成二维,其中第一维是小批量中的样本大小,第二维是每个样本变平后的向量表示,且向量长度为通道、高和宽的乘积。
全连接层块含 3 个全连接层。它们的输出个数分别是 120 、84 和 10 ,其中 10 为输出的类别个数。
(3)网络实现(PyTorch)
class Net(nn.Module):def __init__(self):super().__init__()self.Features = nn.Sequential(nn.Conv2d(1, 6, 5),nn.Sigmoid(),nn.MaxPool2d(2, 2),nn.Conv2d(6, 16, 5),nn.Sigmoid(),nn.MaxPool2d(2, 2))self.Classifier = nn.Sequential(nn.Linear(16 * 4 * 4, 120),nn.Sigmoid(),nn.Linear(120, 84),nn.Sigmoid(),nn.Linear(84, 10))def forward(self, x):x = self.Features(x)return self.Classifier(x.view(-1, 16 * 4 * 4))
最后得到的网络结构如下:
Net((Features): Sequential((0): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))(1): Sigmoid()(2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)(3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))(4): Sigmoid()(5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False))(Classifier): Sequential((0): Linear(in_features=256, out_features=120, bias=True)(1): Sigmoid()(2): Linear(in_features=120, out_features=84, bias=True)(3): Sigmoid()(4): Linear(in_features=84, out_features=10, bias=True)))
3、2006年Hinton在他们的Science Paper中首次提出Deep Learning的概念
2006年,加拿大多伦多大学教授、机器学习领域泰斗、神经网络之父—— Geoffrey Hinton 和他的学生 Ruslan Salakhutdinov 在顶尖学术刊物《科学》上发表了一篇文章,该文章提出了深层网络训练中梯度消失问题的解决方案:无监督预训练对权值进行初始化+有监督训练微调。斯坦福大学、纽约大学、加拿大蒙特利尔大学等成为研究深度学习的重镇,至此开启了深度学习在学术界和工业界的浪潮。
4、AlexNet(2012)–王者归来
AlexNet在2012年ImageNet竞赛中以超过第二名10.9个百分点的绝对优势一举夺冠,这一突破是具有重大历史意义,因为它使计算机视觉模型跨过了从学术demo到商业化产品的门槛,从而将深度学习和CNN的名声突破学术界,在产业界一鸣惊人。
(1)背景
LeNet
可以在早期的小数据集上取得好的成绩,但是在更大的真实数据集上的表现并不尽如人意。
一方面,神经网络计算复杂。虽然 20 世纪 90 年代也有过一些针对神经网络的加速硬件,但并没有像之后 GPU 那样大量普及。因此,训练一个多通道、多层和有大量参数的卷积神经网络在当年很难完成。
另一方面,当年研究者还没有大量深入研究参数初始化和非凸优化算法等诸多领域,导致复杂的神经网络的训练通常较困难。
(2)AlexNet 网络结构
2012 年,AlexNet
横空出世。这个模型的名字来源于论文第一作者的姓名 Alex Krizhevsky 。
AlexNet
使用了 8 层卷积神经网络,并以很大的优势赢得了ImageNet 2012
图像识别挑战赛。
它首次证明了学习到的特征可以超越手工设计的特征,从而一举打破计算机视觉研究的前状。
其结构如下:
AlexNet
包含 8 层变换,其中有 5 层卷积和 2 层全连接隐藏层,以及 1 个全连接输出层。
AlexNet
第一层中的卷积窗口形状是11×11。因为ImageNet
中绝大多数图像的高和宽均比MNIST
图像的高和宽大 10 倍以上,ImageNet
图像的物体占用更多的像素,所以需要更大的卷积窗口来捕获物体。
第二层中的卷积窗口形状减小到5×5,之后全采用3×3。
此外,第一、第二和第五个卷积层之后都使用了窗口形状为3×3、步幅为 2 的最大池化层。
而且,AlexNet
使用的卷积通道数也大于LeNet
中的卷积通道数数十倍。
紧接着最后一个卷积层的是两个输出个数为 4096 的全连接层。这两个巨大的全连接层带来将近 1GB 的模型参数。
(3)激活函数
AlexNet
将sigmoid
激活函数改成了更加简单的ReLU
激活函数。
- 一方面,
ReLU
激活函数的计算更简单,例如它并没有sigmoid
激活函数中的求幂运算。 - 另一方面,
ReLU
激活函数在不同的参数初始化方法下使模型更容易训练。
这是由于当sigmoid
激活函数输出极接近 0 或 1时,这些区域的梯度几乎为 0 ,从而造成反向传播无法继续更新部分模型参数;
而ReLU
激活函数在正区间的梯度恒为 1 。因此,若模型参数初始化不当,sigmoid
函数可能在正区间得到几乎为 0 的梯度,从而令模型无法得到有效训练。
(4)Dropout
该方法通过让全连接层的神经元(该模型在前两个全连接层引入Dropout)以一定的概率失去活性(比如0.5),失活的神经元不再参与前向和反向传播,相当于约有一半的神经元不再起作用。在预测的时候,让所有神经元的输出乘Dropout值(比如0.5)。这一机制有效缓解了模型的过拟合。
(5)图像增广
AlexNet
引入了大量的图像增广,如翻转、裁剪和颜色变化,从而进一步扩大数据集来缓解过拟合。
同时AlexNet
网络中还使用了 局部响应归一化层LRN
提高精度等操作,但现在用的不多了,一般使用BN
来完成。
(6)网络实现(PyTorch)
class AlexNet(nn.Module):def __init__(self):super().__init__()self.Features = nn.Sequential(nn.Conv2d(1, 96, 11, 4),nn.ReLU(),nn.MaxPool2d(3, 2),nn.Conv2d(96, 256, 5, 1),nn.ReLU(),nn.MaxPool2d(3, 2),nn.Conv2d(256, 384, 3, 1, 1),nn.ReLU(),nn.Conv2d(384, 384, 3, 1, 1),nn.ReLU(),nn.Conv2d(384, 256, 3, 1, 1),nn.ReLU(),nn.MaxPool2d(3, 2))self.Classifier = nn.Sequential(nn.Dropout(0.5),nn.Linear(256 * 5 * 5, 4096),nn.ReLU(),nn.Dropout(0.5),nn.Linear(4096, 4096),nn.ReLU(),nn.Dropout(0.5),nn.Linear(4096, 10))def forward(self, x):x = self.Features(x)return self.Classifier(x.view(-1, 256 * 5 * 5))
5、VGG(2014)–越走越深
VGG-Nets是由牛津大学VGG(Visual Geometry Group)提出,是2014年ImageNet竞赛定位任务的第一名和分类任务的第二名的中的基础网络。VGG可以看成是加深版本的AlexNet. 都是卷积层+全连接层,在当时看来这是一个非常深的网络了,因为层数高达十多层,我们从其论文名字就知道了(《Very Deep Convolutional Networks for Large-Scale Visual Recognition》),当然以现在的目光看来VGG真的称不上是一个very deep的网络。
(1)设计思路
VGG
,它的名字来源于论文作者所在的实验室 Visual Geometry Group, 其提出了可以通过重复使用简单的基础块来构建深度模型的思路。
VGG
块的组成规律是:连续使用若干个相同的填充为 1 、窗口形状为3×3的卷积层后接上一个步幅为 2 、窗口形状为2×2的最大池化层。
卷积层保持输入的高和宽不变,而池化层则对其减半。我们使用vgg_block
函数来实现这个基础的VGG块,它可以指定卷积层的数量和输入输出通道数。
对于给定的感受野(与输出有关的输入图片的局部大小),采用堆积的小卷积核优于采用大的卷积核,因为可以增加网络深度来保证学习更复杂的模式,而且代价还比较小(参数更少)。
常见的VGG
网络有:VGG-11
、VGG-13
、VGG-16
、VGG-19
(2)网络实现(PyTorch)
cfg = {'VGG11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],'VGG13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],'VGG16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],'VGG19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M']}class VGG(nn.Module):def __init__(self, vgg_name, fc_features, fc_hidden_units):super().__init__()self.Features = self.GetLayer(vgg_name)self.Classifier = nn.Sequential(nn.Linear(fc_features, fc_hidden_units),nn.ReLU(),nn.Dropout(0.5),nn.Linear(fc_hidden_units, fc_hidden_units),nn.ReLU(),nn.Dropout(0.5),nn.Linear(fc_hidden_units, 10))def forward(self, x, fc_features):x = self.Features(x)return self.Classifier(x.view(-1, fc_features))def GetLayer(self, vgg_name):Layer_List = cfg[vgg_name]Layer = []in_channel = 1for x in Layer_List:if x == 'M':Layer.append(nn.MaxPool2d(2, 2))else:Layer += [nn.Conv2d(in_channel, x, 3, padding=1), nn.ReLU()]in_channel = xLayer += [nn.Dropout2d(0.5)]return nn.Sequential(*Layer)net = VGG('VGG11', 512*7*7, 512)print(net)
输出的结构如下:
VGG((Features): Sequential((0): Conv2d(1, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))(1): ReLU()(2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)(3): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))(4): ReLU()(5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)(6): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))(7): ReLU()(8): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))(9): ReLU()(10): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)(11): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))(12): ReLU()(13): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))(14): ReLU()(15): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)(16): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))(17): ReLU()(18): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))(19): ReLU()(20): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)(21): Dropout2d(p=0.5))(Classifier): Sequential((0): Linear(in_features=25088, out_features=512, bias=True)(1): ReLU()(2): Dropout(p=0.5)(3): Linear(in_features=512, out_features=512, bias=True)(4): ReLU()(5): Dropout(p=0.5)(6): Linear(in_features=512, out_features=10, bias=True)))
六、GoogLeNet(2014)
Google Inception Net首次出现在ILSVRC2014的比赛中(和VGGNet同年),以较大的优势获得冠军。那一届的GoogLeNet通常被称为Inception V1,Inception V1的特点是控制了计算量的参数量的同时,获得了非常好的性能-top5错误率6.67%, 这主要归功于GoogLeNet中引入一个新的网络结构Inception模块(该设计灵感来源于《Network in Network》,也就是NIN),所以GoogLeNet又被称为Inception V1(后面还有改进版V2、V3、V4)架构中有22层深,V1比VGGNet和AlexNet都深,但是它只有500万的参数量,计算量也只有15亿次浮点运算。
(1)Inception 块
Inception
块里有 4 条并行的线路,它通过不同窗口形状的卷积层和最大池化层来并行抽取信息,并使用1×1卷积层减少通道数从而降低模型复杂度。
前3条线路使用窗口大小分别是1×1,3×3,5×5的卷积层来抽取不同空间尺寸下的信息,其中中间2个线路会对输入先做1×1卷积来减少输入通道数,以降低模型复杂度。
第四条线路则使用3×3最大池化层,后接1×1卷积层来改变通道数。
4 条线路都使用了合适的填充来使输入与输出的高和宽一致。
最后我们将每条线路的输出在通道维上连结,并输入接下来的层中去。
(2)GoogLeNet 模型
七、ResNet(2015)–里程碑式创新
2015年何恺明推出的ResNet在ISLVRC和COCO上横扫所有选手,获得冠军。ResNet在网络结构上做了大创新,而不再是简单的堆积层数,ResNet在卷积神经网络的新思路,绝对是深度学习发展历程上里程碑式的事件。它在VGGNet和MSRANet基础上进一步加深网络,并通过引入残差单元来解决网络过深引起的退化问题。而ResNet的主要创新就是残差模块,如下图所示:
残差模块借鉴了Highway Network思想的网络相当于旁边专门开个通道使得输入可以直达输出,而优化的目标由原来的拟合输出F(x)变成输出和输入的差F(x)-x,其中F(x)是某一层原始的期望映射输出,x是输入。
随着网络进一步加深,这种残差结构在实践中并不是十分有效。针对这问题,上图的“瓶颈残差模块”(bottleneck residual block)可以有更好的效果,它依次由1×1、3×3、1×1这三个卷积层堆积而成,这里的1×1的卷积能够起降维或升维的作用,从而令3×3的卷积可以在相对较低维度的输入上进行,以达到提高计算效率的目的。
18和34层模块代码:
class BasicBlock(nn.Module):expansion = 1def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):super(BasicBlock, self).__init__()self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel, kernel_size=3, stride=stride, padding=1, bias=False)self.bn1 = nn.BatchNorm2d(out_channel)self.relu = nn.ReLU()self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel, kernel_size=3, stride=1, padding=1, bias=False)self.bn2 = nn.BatchNorm2d(out_channel)self.downsample = downsampledef forward(self, x):identity = xif self.downsample is not None:identity = self.downsample(x)out = self.conv1(x)out = self.bn1(out)out = self.relu(out)out = self.conv2(out)out = self.bn2(out)out += identityout = self.relu(out)return out
50,101,152层模块代码:
class Bottleneck(nn.Module):"""注意:原论文中,在虚线残差结构的主分支上,第一个1x1卷积层的步距是2,第二个3x3卷积层步距是1。但在pytorch官方实现过程中是第一个1x1卷积层的步距是1,第二个3x3卷积层步距是2,这么做的好处是能够在top1上提升大概0.5%的准确率。可参考Resnet v1.5 https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch"""expansion = 4def __init__(self, in_channel, out_channel, stride=1, downsample=None, groups=1, width_per_group=64):super(Bottleneck, self).__init__()width = int(out_channel * (width_per_group / 64.)) * groupsself.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=width, kernel_size=1, stride=1, bias=False)# squeeze channelsself.bn1 = nn.BatchNorm2d(width)# -----------------------------------------self.conv2 = nn.Conv2d(in_channels=width, out_channels=width, groups=groups, kernel_size=3, stride=stride, bias=False, padding=1)self.bn2 = nn.BatchNorm2d(width)# -----------------------------------------self.conv3 = nn.Conv2d(in_channels=width, out_channels=out_channel*self.expansion, kernel_size=1, stride=1, bias=False)# unsqueeze channelsself.bn3 = nn.BatchNorm2d(out_channel*self.expansion)self.relu = nn.ReLU(inplace=True)self.downsample = downsampledef forward(self, x):identity = xif self.downsample is not None:identity = self.downsample(x)out = self.conv1(x)out = self.bn1(out)out = self.relu(out)out = self.conv2(out)out = self.bn2(out)out = self.relu(out)out = self.conv3(out)out = self.bn3(out)out += identityout = self.relu(out)return out
resnet模块代码:
class ResNet(nn.Module):def __init__(self, block, blocks_num, num_classes=1000, include_top=True, groups=1, width_per_group=64):super(ResNet, self).__init__()self.include_top = include_topself.in_channel = 64self.groups = groupsself.width_per_group = width_per_groupself.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2, padding=3, bias=False)self.bn1 = nn.BatchNorm2d(self.in_channel)self.relu = nn.ReLU(inplace=True)self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)self.layer1 = self._make_layer(block, 64, blocks_num[0])self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)if self.include_top:self.avgpool = nn.AdaptiveAvgPool2d((1, 1))# output size = (1, 1)self.fc = nn.Linear(512 * block.expansion, num_classes)for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')def _make_layer(self, block, channel, block_num, stride=1):downsample = Noneif stride != 1 or self.in_channel != channel * block.expansion:downsample = nn.Sequential(nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),nn.BatchNorm2d(channel * block.expansion))layers = []layers.append(block(self.in_channel,channel,downsample=downsample,stride=stride,groups=self.groups,width_per_group=self.width_per_group))self.in_channel = channel * block.expansionfor _ in range(1, block_num):layers.append(block(self.in_channel,channel,groups=self.groups,width_per_group=self.width_per_group))return nn.Sequential(*layers)def forward(self, x):x = self.conv1(x)x = self.bn1(x)x = self.relu(x)x = self.maxpool(x)x = self.layer1(x)x = self.layer2(x)x = self.layer3(x)x = self.layer4(x)if self.include_top:x = self.avgpool(x)x = torch.flatten(x, 1)x = self.fc(x)return x
学习视频来自b站up:霹雳吧啦Wz
霹雳吧啦Wz的个人空间_哔哩哔哩_bilibili