深度学习_吴恩达_Part_1

第一章 绪论

1.0 深度学习发展历史

本小节参考链接:
参考链接1.1:深度学习(deep learning)发展史 - 极海·GeoHey的文章 - 知乎
参考链接1.2:深度学习发展历程(MindSpore)-哔哩哔哩, https://b23.tv/t4osJ6I

1.1 欢迎

从本节课我们将学到:

  • 学习神经网络的基础——神经网络与深度学习;
  • 深度学习方面的实践;
  • 如何结构化机器学习工程;
  • 卷积神经网络(经常用于图像);
  • 序列模型以及如何应用到自然语言处理(NLP),常见的序列模型有:循环神经网络(RNN),长短期记忆网络(LSTM)模型。

1.2 什么是神经网络

深度学习指的是“训练神经网络”

下面以房屋价格预测为例。首先,将已知的六间房子的价格和面积的关系绘制在二维平面上,如下图所示:

一般地,会用一条直线来拟合图中这些离散点,即建立房价与面积的线性模型。但是从实际考虑,价格永远不会是负数。所以对该直线做一点点修正,让它变成折线的形状,当面积小于某个值时,价格始终为零。如下图蓝色折线所示,就是建立的房价预测模型。

其实这个简单的模型(蓝色折线)就可以看成是一个神经网络,而且几乎是一个最简单的神经网络。我们把该房价预测用一个最简单的神经网络模型来表示,如下图所示:

上图中的小圆圈就可以视为一个独立的神经元,这个简单网络实现了左边函数的功能值得一提的是,上图神经元的预测函数(蓝色折线)在神经网络应用中比较常见。把这个函数称为线性整流函数(Rectified Linear Unit, ReLU),形如下图所示:

上面是一个最为简单的神经网络,更深的神经网络可以视为:将这些单个的神经元看作乐高积木,通过搭建积木来构建更大更深的网络。把上面举的房价预测的例子变得复杂一些,而不是仅仅使用房屋面积一个判断因素。

在给定这四个输入后,神经网络所做的就是输出房屋的预测价格y。上图中三个神经元所在的位置称之为中间层或者隐藏层(x所在的称之为输入层,y所在的称之为输出层),每个神经元与所有的输入x都有关联(直线相连)。

1.3 使用神经网络进行监督学习

由神经网络模型创造的价值基本上都是基于监督式学习(Supervised Learning)的。监督式学习与非监督式学习本质区别就是是否已知训练样本的输出y。在实际应用中,机器学习解决的大部分问题都属于监督式学习,神经网络模型也大都属于监督式学习。下面我们来看几个监督式学习在神经网络中应用的例子。

  • 房屋价格预测。根据训练样本的输入x和输出y,训练神经网络模型,预测房价。
  • 线上广告。输入x是广告和用户个人信息,输出y是用户是否对广告进行点击。神经网络模型经过训练,能够根据广告类型和用户信息对用户的点击行为进行预测,从而向用户提供用户自己可能感兴趣的广告。
  • 电脑视觉(computer vision)。电脑视觉是近些年来越来越火的课题,而电脑视觉发展迅速的原因很大程度上是得益于深度学习。其中,输入x是图片像素值,输出是图片所属的不同类别。
  • 语音识别(speech recognition)。深度学习可以将一段语音信号辨识为相应的文字信息。
  • 智能翻译。例如通过神经网络输入英文,然后直接输出中文。
  • 自动驾驶。通过输入一张图片或者汽车雷达信息,神经网络通过训练来告诉你相应的路况信息并作出相应的决策。

根据不同的问题和应用场合,应该使用不同类型的神经网络模型。CNN和RNN是比较常用的神经网络模型。下图给出了Standard NN,Convolutional NN和Recurrent NN的神经网络结构图。

数据类型一般分为两种:结构化数据(Structured Data)非结构化数据(Unstructured Data)

1.4 为什么深度学习流行起来了

第二章 神经网络基础之逻辑回归

下面开始介绍神经网络的基础:逻辑回归(Logistic Regression)。通过对逻辑回归模型结构的分析,为后面学习神经网络模型打下基础。

2.1 二分类(Binary Classification)

逻辑回归模型一般用来解决二分类(Binary Classification)问题。二分类就是输出只有{0,1\}两个离散值(也有{-1,1}的情况)。以一个图像识别问题为例,判断图片中是否有猫存在,0代表not cat,1代表cat。

如上图所示,这是一个典型的二分类问题。一般来说,彩色图片包含RGB三个通道。例如该cat图片的尺寸为。在神经网络模型中,我们首先要将图片输入(维度是)转化为一维的特征向量(feature vector)。方法是每个通道一行一行取,再连接起来。由于,则转化后的输入特征向量维度为。此特征向量是列向量,维度一般记为

如果训练样本共有张图片,那么整个训练样本组成了矩阵,维度是。注意,这里矩阵的行代表了每个样本特征个数,列代表了样本个数。这里,Andrew解释了的维度之所以是而不是的原因是为了之后矩阵运算的方便。算是Andrew给我们的一个小小的经验吧。而所有训练样本的输出也组成了一维的行向量,写成矩阵的形式后,它的维度就是

后面课程会用到的一些符号
用一对来表示一个单独的样本,其中维特征向量(可记为),,训练集由个训练样本组成,表示样本的输入输出。为了便于表示和区分,有时训练集表示为,测试集表示为,可进一步将训练集表示为更紧凑的形式,用矩阵表示:的大小为,输出用表示:的大小为

2.2 logistic回归

这是一个学习算法,用于监督学习中输出的二元分类问题。

逻辑回归中,预测值表示为输入输出为的概率,取值范围在之间,这是其与二分类模型不同的地方。使用线性模型,引入参数。权重的维度是是一个常数项,即。这样,逻辑回归的线性预测输出可以写成:

值得注意的是,很多其它机器学习资料中,可能把常数当做处理,并引入。这样从维度上来看,都会增加一维。但在本课程中,为了简化计算和便于理解,Andrew建议还是使用上式这种形式将分开比较好。

上式的线性输出区间为整个实数范围,而逻辑回归要求输出范围在之间,所以还需要对上式的线性函数输出进行处理。方法是引入sigmoid函数,让输出限定在之间。这样,逻辑回归的预测输出就可以完整写成:

sigmoid函数是一种非线性的S型函数,输出被限定在之间,通常被用在神经网络中当作激活函数(Activation function)使用。Sigmoid函数的表达式:

通过Sigmoid函数,就能够将逻辑回归的输出限定在之间了。

2.3 logistic回归损失函数

逻辑回归中,都是未知参数,需要反复训练优化得到。因此,我们需要定义一个成本函数(cost function),包含了参数。通过优化cost function,当cost function取值最小时,得到对应的

如何定义所有个样本的cost function呢?先从单个样本出发,我们希望该样本的预测值与真实值越相似越好。我们把单个样本的cost function用Loss function来表示,根据以往经验,如果使用平方错误(squared error)来衡量,如下所示:

但是,对于逻辑回归,我们一般不使用平方错误来作为Loss function。原因是这种Loss function一般是非凸(non-convex)的。non-convex函数在使用梯度下降算法时,容易得到局部最小值(local minumum),即局部最优化。而我们最优化的目标是计算得到全局最优化(Global optimization)。因此,我们一般选择的Loss function应该是convex的。因此,我们可以构建另外一种Loss function,且是convex的,如下所示:

我们来分析一下这个Loss function,它是衡量错误大小的,Loss function越小越好。

时,我们带入上式容易得知,预测效果越好;同理,当时,,预测效果越好。后续将会提到这个损失函数是如何推导出来的。

上面介绍的Loss function是针对单个样本的。那对于个样本,我们定义Cost function,Cost function是个样本的Loss function的平均值,反映了个样本的预测输出与真实样本输出的平均接近程度。Cost function可表示为:

Cost function已经推导出来了,Cost function是关于待求系数w和b的函数。我们的目标就是迭代计算出最佳的w和b值,最小化Cost function,让Cost function尽可能地接近于零。

其实逻辑回归问题可以看成是一个简单的神经网络,只包含一个神经元。这也是我们这里先介绍逻辑回归的原因。

2.4 梯度下降法

使用梯度下降(Gradient Descent)算法来计算出合适的值,从而最小化个训练样本的Cost function,即

由于是convex function,梯度下降算法是先随机选择一组参数值,然后每次迭代的过程中分别沿着的梯度(偏导数)的反方向前进一小步,不断修正。每次迭代更新后,都能让更接近全局最小值。梯度下降的过程如下图所示。

梯度下降算法每次迭代更新,的修正表达式为:

上式中,是学习因子(learning rate),表示梯度下降的不仅长度。梯度下降算法能够保证每次迭代w和b都能向着J(w,b)全局最小化的方向进行。其数学原理主要是运用泰勒一阶展开来证明的。

2.5-2.6 导数复习

这一部分的内容相对简单,Andrew主要是给对微积分、求导数不太清楚的同学介绍的。梯度或者导数一定程度上可以看成是斜率。关于求导数的方法这里就不再赘述了。

2.7 计算图

整个神经网络的训练过程实际上包含了两个过程:正向传播(Forward Propagation)反向传播(Back Propagation)。正向传播是从输入到输出,由神经网络计算得到预测输出的过程;反向传播是从输出到输入,对参数w和b计算梯度的过程。下面,我们用计算图(Computation graph)的形式来理解这两个过程。

举个简单的例子,假如Cost function为,包含三个变量。我们用表示表示,则。它的计算图可以写成如下图所示:

,则。计算图中,这种从左到右,从输入到输出的过程就对应着神经网络或者逻辑回归中输入与权重经过运算计算得到Cost function的正向过程。

2.8 使用计算图求导

下面我们来介绍反向传播(Back Propagation),即计算输出对输入的偏导数。

2.9 logistic回归中的梯度下降法

对逻辑回归进行梯度计算。对单个样本而言,逻辑回归Loss function表达式如下:

该逻辑回归的正向传播过程非常简单。据上述公式,例如输入样本有两个特征,相应的权重也有两个,则

然后,计算该逻辑回归的反向传播过程,即由Loss function计算参数的偏导数:

知道了之后,就可以直接对进行求导了:

则梯度下降算法可表示为:

2.10 m 个样本的梯度下降

上一部分讲的是对单个样本求偏导和梯度下降。如果有m个样本,其Cost function表达式如下:

Cost function关于的偏导数可以写成和平均的形式:

这样,每次迭代中的梯度有$m$个训练样本计算平均值得到。其算法伪代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
J=0; dw1=0; dw2=0; db=0;
for i = 1 to m
    z(i) = wx(i)+b;
    a(i) = sigmoid(z(i));
    J += -[y(i)log(a(i))+(1-y(i))log(1-a(i));
    dz(i) = a(i)-y(i);
    dw1 += x1(i)dz(i);
    dw2 += x2(i)dz(i);
    db += dz(i);
J /= m;
dw1 /= m;
dw2 /= m;
db /= m;

经过每次迭代后,根据梯度下降算法,都进行更新:

这样经过次迭代后,整个梯度下降算法就完成了。

值得一提的是,在上述的梯度下降算法中,是利用for循环对每个样本进行dw1,dw2和db的累加计算最后再求平均数的。在深度学习中,样本数量$m$通常很大,使用for循环会让神经网络程序运行得很慢。所以,我们应该尽量避免使用for循环操作,而使用矩阵运算,能够大大提高程序运行速度。关于向量化(vectorization)的内容我们放在下次笔记中再说。

2.11-2.12 向量化

深度学习算法中,数据量很大,在程序中应该尽量减少使用loop循环语句,而可以使用向量运算来提高程序运行速度。

向量化(Vectorization)就是利用矩阵运算的思想,大大提高运算速度。

上一部分我们讲了应该尽量避免使用for循环而使用向量化矩阵运算。在python的numpy库中,我们通常使用np.dot()函数来进行矩阵运算。

我们将向量化的思想使用在逻辑回归算法上,尽可能减少for循环,而只使用矩阵运算。值得注意的是,算法最顶层的迭代训练的for循环是不能替换的。而每次迭代过程对J,dw,b的计算是可以直接使用矩阵运算。

2.13 向量化logistic回归

在前面的笔记中我们提到过,整个训练样本构成的输入矩阵的维度是,权重矩阵的维度是是一个常数值,而整个训练样本构成的输出矩阵的维度为。利用向量化的思想,所有个样本的线性输出可以用矩阵表示:

在python的numpy库中可以表示为:

1
2
Z = np.dot(w.T,X) + b      # w.T表示w的转置
A = sigmoid(Z)

这样,我们就能够使用向量化矩阵运算代替for循环,对所有个样本同时运算,大大提高了运算速度。

2.14 向量化logistic回归的梯度输出

逻辑回归中的梯度下降算法如何转化为向量化的矩阵形式。对于所有个样本,的维度是,可表示为:

$\mathrm d b$可以表示为:

对应的程序可以写成:

1
dw = 1/m*np.dot(X,dZ.T)

这样,我们把整个逻辑回归中的for循环尽可能用矩阵运算代替,对于单次迭代,梯度下降算法流程如下所示:

1
2
3
4
5
6
7
Z = np.dot(w.T,X) + b
A = sigmoid(Z)
dZ = A-Y
dw = 1/m*np.dot(X,dZ.T)
db = 1/m*np.sum(dZ)
w = w - alpha*dw
b = b - alpha*db

其中,alpha是学习因子,决定的更新速度。上述代码只是对单次训练更新而言的,外层还需要一个for循环,表示迭代次数。

第三章 浅层神经网络

3.1 神经网络概览

首先,我们从整体结构上来大致看一下神经网络模型。

前面的课程中,我们已经使用计算图的方式介绍了逻辑回归梯度下降算法正向传播反向传播两个过程。如下图所示。神经网络的结构与逻辑回归类似,只是神经网络的层数比逻辑回归多一层,多出来的中间那层称为隐藏层或中间层。这样从计算上来说,神经网络的正向传播和反向传播过程只是比逻辑回归多了一次重复的计算。

正向传播过程分成两层,第一层是输入层到隐藏层,用上标[1]来表示:

第二层是隐藏层到输出层,用上标[2]来表示:

在写法上值得注意的是,方括号上标[i]表示当前所处的层数;圆括号上标(i)表示第i个样本。

同样,反向传播过程也分成两层。第一层是输出层到隐藏层,第二层是隐藏层到输入层。其细节部分我们之后再来讨论。

3.2 神经网络的表示

下面我们以图示的方式来介绍单隐藏层的神经网络结构。如下图所示,单隐藏层神经网络就是典型的浅层(shallow)神经网络。

结构上,从左到右,可以分成三层:输入层(Input layer)隐藏层(Hidden layer)输出层(Output layer)。输入层和输出层,顾名思义,对应着训练样本的输入和输出,很好理解。隐藏层是抽象的非线性的中间层,这也是其被命名为隐藏层的原因。

在写法上,我们通常把输入矩阵记为,把隐藏层输出记为,上标从开始。用下标表示第几个神经元,注意下标从开始。例如表示隐藏层第个神经元,表示隐藏层第个神经元等等。这样隐藏层有个神经元就可以将其输出写成矩阵的形式:

相应的输出层记为,即。这种单隐藏层神经网络也称为两层神经网络(2 layer NN)。

之所以叫两层神经网络是因为,通常我们只会计算隐藏层输出和输出层的输出,输入层是不用计算的。这也是我们把输入层层数上标记为的原因()。
关于隐藏层对应的权重和常数项的维度是。这里的对应着隐藏层神经元个数,对应着输入层特征向量包含元素个数。常数项的维度是,这里的同样对应着隐藏层神经元个数。关于输出层对应的权重和常数项的维度是,这里的对应着输出层神经元个数,对应着隐藏层神经元个数。常数项的维度是,因为输出只有一个神经元。

总结一下,第层的权重维度的行等于层神经元的个数,列等于层神经元的个数;第层常数项维度的行等于层神经元的个数,列始终为

3.3 计算神经网络的输出

本节详细推导神经网络的计算过程。前面讲过两层神经网络可以看成是逻辑回归再重复计算一次。如下图所示,逻辑回归的正向计算可以分解成计算的两部分:

对于两层神经网络,从输入层到隐藏层对应一次逻辑回归运算;从隐藏层到输出层对应一次逻辑回归运算。每层计算时,要注意对应的上标和下标,一般我们记上标方括号表示layer,下标表示第几个神经元。例如表示第层的第个神经元。注意,开始,开始

将从输入层到输出层的计算公式列出来:

然后,从隐藏层到输出层的计算公式为:

其中

上述每个节点的计算都对应着一次逻辑运算的过程,分别由计算两部分组成。

为了提高程序运算速度,我们引入向量化和矩阵运算的思想,将上述表达式转换成矩阵运算的形式:

之前也介绍过,这里顺便提一下,的维度是的维度是的维度是的维度是。这点需要特别注意。

3.4 多个样本的向量化

上一部分我们只是介绍了单个样本的神经网络正向传播矩阵运算过程。而对于个训练样本,我们也可以使用矩阵相乘的形式来提高计算效率。而且它的形式与上一部分单个样本的矩阵运算十分相似,比较简单。

之前我们也介绍过,在书写标记上用上标表示第个样本,例如。对于每个样本,可以使用for循环来求解其正向输出:

不使用for循环,利用矩阵运算的思想,输入矩阵的维度为。这样,我们可以把上面的for循环写成矩阵运算的形式:

其中,的维度是是隐藏层神经元的个数;的维度与相同;的维度均为。对上面这四个矩阵来说,均可以这样来理解:行表示神经元个数,列表示样本数目

3.5向量化实现的解释

这部分Andrew用图示的方式解释了个样本的神经网络矩阵运算过程。其实内容比较简单,只要记住上述四个矩阵的行表示神经元个数,列表示样本数目就行了。

值得注意的是输入矩阵也可以写成

3.6 激活函数

神经网络隐藏层和输出层都需要激活函数(activation function),在之前的课程中我们都默认使用Sigmoid函数作为激活函数。其实,还有其它激活函数可供使用,不同的激活函数有各自的优点。下面我们就来介绍几个不同的激活函数

(1) sigmoid函数

(2) tanh函数

(3) ReLU函数

(4) Leaky ReLU函数

如上图所示,不同激活函数形状不同,的取值范围也有差异。

如何选择合适的激活函数呢?首先我们来比较sigmoid函数和tanh函数。对于隐藏层的激活函数,一般来说,tanh函数要比sigmoid函数表现更好一些。因为tanh函数的取值范围在[-1,+1]之间,隐藏层的输出被限定在[-1,+1]之间,可以看成是在0值附近分布,均值为0。这样从隐藏层到输出层,数据起到了归一化(均值为0)的效果。因此,隐藏层的激活函数,tanh比sigmoid更好一些。而对于输出层的激活函数,因为二分类问题的输出取值为{0,+1},所以一般会选择sigmoid作为激活函数。

观察sigmoid函数和tanh函数,我们发现有这样一个问题,就是当很大的时候,激活函数的斜率(梯度)很小。因此,在这个区域内,梯度下降算法会运行得比较慢。在实际应用中,应尽量避免使z落在这个区域,使尽可能限定在零值附近,从而提高梯度下降算法运算速度。

为弥补sigmoid函数和tanh函数的这个缺陷,就出现了ReLU激活函数。ReLU激活函数在大于零时梯度始终为1;在小于零时梯度始终为0;等于零时的梯度可以当成1也可以当成0,实际应用中并不影响。对于隐藏层,选择ReLU作为激活函数能够保证z大于零时梯度始终为1,从而提高神经网络梯度下降算法运算速度。但当z小于零时,存在梯度为0的缺点,实际应用中,这个缺点影响不是很大。为了弥补这个缺点,出现了Leaky ReLU激活函数,能够保证z小于零是梯度不为0。

最后总结一下,如果是分类问题,输出层的激活函数一般会选择sigmoid函数。但是隐藏层的激活函数通常不会选择sigmoid函数,tanh函数的表现会比sigmoid函数好一些。实际应用中,通常会会选择使用ReLU或者Leaky ReLU函数,保证梯度下降速度不会太小。其实具体选择哪个函数作为激活函数没有一个固定的准确的答案,应该要根据具体实际问题进行验证(validation)。

3.7 为什么需要非线性激活函数

我们知道上一部分讲的四种激活函数都是非线性(non-linear)的。那是否可以使用线性激活函数呢?答案是不行!下面我们就来进行简要的解释和说明。

假设所有的激活函数都是线性的,为了简化计算,我们直接令激活函数,即。那么,浅层神经网络的各层输出为:

我们对上式中进行化简计算:

经过推导我们发现仍是输入变量线性组合。这表明,使用神经网络与直接使用线性模型的效果并没有什么两样。即便是包含多层隐藏层的神经网络,如果使用线性函数作为激活函数,最终的输出仍然是输入的线性模型。这样的话神经网络就没有任何作用了。因此,隐藏层的激活函数必须要是非线性的

另外,如果所有的隐藏层全部使用线性激活函数,只有输出层使用非线性激活函数,那么整个神经网络的结构就类似于一个简单的逻辑回归模型,而失去了神经网络模型本身的优势和价值。

值得一提的是,如果是预测问题而不是分类问题,输出是连续的情况下,输出层的激活函数可以使用线性函数。如果输出恒为正值,则也可以使用ReLU激活函数,具体情况,具体分析。

3.8 激活函数的导数

在梯度下降反向计算过程中少不了计算激活函数的导数即梯度。

(1) sigmoid函数的导数

(2) tanh函数的导数

(3) ReLU函数的导数

(4) Leaky ReLU函数的导数

3.9 神经网络的梯度下降法

你的单隐层神经网络会有这些参数,还有个表示输入特征的个数,表示隐藏单元个数, 表示输出单元个数。只介绍这种情况,那么参数:

矩阵的维度就是就是维向量,可以写成,就是一个的列向量。矩阵的维度就是, 的维度就是

你还有一个神经网络的成本函数,假设你在做二分类任务,那么你的成本函数等于

训练参数需要做梯度下降,在训练神经网络的时候,随机初始化参数很重要,而不是初始化成全零。当你参数初始化成某些值后,每次梯度下降都会循环计算以下预测值

其中,

使用计算图的方式来推导神经网络反向传播过程。记得之前介绍逻辑回归时,我们就引入了计算图来推导正向传播和反向传播,其过程如下图所示:

由于多了一个隐藏层,神经网络的计算图要比逻辑回归的复杂一些,如下图所示。对于单个训练样本,正向过程很容易,反向过程可以根据梯度计算方法逐一推导。

上述是反向传播的步骤,注:这些都是针对所有样本进行过向量化,的矩阵;这里np.sum是python的numpy命令,axis=1表示水平相加求和keepdims防止python输出那些古怪的秩数,加上这个确保矩阵这个向量输出维度为这样标准的形式。

总结一下,浅层神经网络(包含一个隐藏层),个训练样本的正向传播过程和反向传播过程分别包含了6个表达式,其向量化矩阵形式如下图所示:

3.10 (选修)直观理解反向传播

本节记录一下我的两个疑问:

(1) 矩阵微积分问题

(2) 的式子中没有

本小节笔记参考链接:3.10 直观理解反向传播-深度学习-Stanford吴恩达教授_赵继超的笔记-CSDN博客

3.11 随机初始化

神经网络模型中的参数权重W是不能全部初始化为零的,接下来我们分析一下原因。

举个简单的例子,一个浅层神经网络包含两个输入,隐藏层包含两个神经元。如果权重都初始化为零,即:

这样使得隐藏层第一个神经元的输出等于第二个神经元的输出,即。经过推导得到,以及。因此,这样的结果是隐藏层两个神经元对应的权重行向量,每次迭代更新都会得到完全相同的结果,始终等于,完全对称。这样隐藏层设置多个神经元就没有任何意义了。值得一提的是,参数可以全部初始化为零,并不会影响神经网络训练效果;此外权重不能全初始化为零,也不能全部初始化为一样的值。

我们把这种权重W全部初始化为零带来的问题称为symmetry breaking problem。解决方法也很简单,就是将W进行随机初始化(b可初始化为零)。python里可以使用如下语句进行W和b的初始化:

1
2
3
4
W_1 = np.random.randn((2,2))*0.01
b_1 = np.zero((2,1))
W_2 = np.random.randn((1,2))*0.01
b_2 = 0

这里我们将乘以的目的是尽量使得权重初始化比较小的值。之所以让比较小,是因为如果使用sigmoid函数或者tanh函数作为激活函数的话,比较小,得到的也比较小(靠近零点),而零点区域的梯度比较大,这样能大大提高梯度下降算法的更新速度,尽快找到全局最优解。如果较大,得到的也比较大,附近曲线平缓,梯度较小,训练过程会慢很多。

当然,如果激活函数是ReLU或者Leaky ReLU函数,则不需要考虑这个问题。但是,如果输出层是sigmoid函数,则对应的权重最好初始化到比较小的值。

  • Copyrights © 2015-2024 wjh
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信