GoogLeNet详细介绍与TensorFlow实现


GoogLeNet,是2014年ILSVRC比赛分类任务的第一名(同年分类任务的第二名是VGG),该名字是为了向 LeNet 5致敬。该网络为了提高计算效率,使用了一种叫做 Inception的网络模块,该模块可以保证在增加网络深度和宽度的前提下,计算效率不会发生明显衰减。该网络发表在论文《Going deeper with convolutions》,文中给出了关于Inception模块的提出思路,读起来相对比较晦涩难懂一点,这里我简单介绍一下其设计思路,如果理解有偏差,欢迎留言讨论!

1. Inception 模块提出

L e n e t 5 Lenet 5 Lenet5 开始,传统的卷积网络都是堆叠卷积层(也会接归一化层)、池化层然后接一个或者多个全连接层,研究人员基于这种模式设计了很多网络,它们在MNIST、CIFAR、ImageNet 数据集上都取得了很好的结果,对于较大较深的网络,则使用Dropout来缓解过拟合问题。

一般来说,提高网络的深度(增加网络层数)和宽度(增加每一层神经元的个数),是提高网络性能的最直接方式,但是由此带来的问题是:

  1. 参数增多,特别是带标签的数据有限的情况下,很容易造成过拟合。而一些数据的标记工作可能需要“专家”来完成,代价也会很昂贵,论文中举了一个ImageNet的数据标注例子,如下图

    在这里插入图片描述

    没有专业的知识,你可能很难分清两条狗的详细类型

  2. 计算量急剧增大

    增加网络深度,会造成计算量剧增,例如将两个卷积层连接在一起,第一层卷积使用 128 128 128个大小为 3 ∗ 3 ∗ 64 3*3*64 3364的卷积核,下一层卷积使用 256 256 256个大小为 3 ∗ 3 ∗ 128 3*3*128 33128的卷积核,此时网络性能可能有所提升,但是参数量增加了 4 4 4倍,如果这些新增的网络层利用率较低(大部分权重值趋于 0 0 0),则会造成计算资源的严重浪费。因此在计算资源有限的情况下,不能为了提高网络性能而一味地增加网络深度或者宽度。

为了解决这两个问题,作者认为需要引入稀疏性,包括将全连接转换成稀疏连接,以及在卷积内部也实现稀疏连接。引入稀疏性的原因是:

  1. 生物学的神经网络本身就具有稀疏性

  2. Arora等人的研究表明,如果数据集的概率分布可以用一个大的、稀疏的深度神经网络来表示,那么可以通过分析激活值的相关性统计以及将输出值相关性高的神经元聚合在一起的方式,逐层分析出该最优的网络结构

  3. Hebbian 理论:neurons that fire together, wire together,即如果两个神经元经常被同时激活,则这两个神经元的相关性极高。反之,如果两个神经元经常被分开激活,则两个神经元的相关性极低。

    关于该理论在人工神经网络引入稀疏性的理论支撑在于:

    • 如果两个神经元经常被同时激活,则这两个神经元的相关性极高,在人工神经网络中则表现为两个神经元的连接权重增大

    • 如果两个神经元经常被分开激活,则两个神经元的相关性极低,在人工神经网络中则表现为两个神经元的连接权重很小,甚至两个神经元的连接毫无意义(即权重为 0 0 0),这种无意义的神经元连接,不需要连接两个神经元,也就由此引入了稀疏性

然而,引入稀疏性带来的问题是,当前的计算设备在处理非均匀的稀疏数据结构时非常低效,即使运算量减少100倍,查询和cache misses的开销很大,计算效率依然低下。 另外,由于数值库利用CPU和GPU的硬件特性对稠密矩阵计算进行的很多优化,使得计算设备处理稠密数据和稀疏数据的性能差距更大。

现在矛盾的点是,我们既想引入稀疏性,又想使用稠密矩阵的高速计算性能。

如何引入稀疏性?

我们一般通过卷积来实现稀疏性。然而卷积是对前一层patches(每次进行卷积计算时,卷积核覆盖输入图的区域大小)的稠密连接,这句话我的理解是:

  1. 卷积既有稀疏性,也能利用稠密计算的性能

  2. 卷积过程有稠密连接,言外之意,应该考虑将这个稠密连接也转换成稀疏连接

为了让卷积过程也引入稀疏性,传统的卷积网络在特征空间中使用随机稀疏连接表,比如 L e N e t 5 LeNet5 LeNet5的结构中就采用了这种方式,其论文(Gradient-Based Learning Applied to Do cument Recognition) 提到,在 L e N e t 5 LeNet5 LeNet5网络结构中, S 2 S2 S2层(大小为 14 ∗ 14 ∗ 6 14*14*6 14146)经过卷积得到 C 3 C3 C3层(大小为 10 ∗ 10 ∗ 16 10*10*16 101016),采用了如下图所示的稀疏连接表(如 C 3 C3 C3的前 4 4 4个特征(通道)图来自 S 2 S2 S2层不同但连续的三个特征卷积得来)

在这里插入图片描述

为了能使得卷积使用稠密矩阵的计算特性, A l e x N e t AlexNet AlexNet 又移除了特征空间的稀疏连接。那么问题来了,我们能否在滤波器层面上也使用稀疏模型,并使用稠密矩阵进行计算?答案是肯定的,正是我们等下要介绍的 I n c e p t i o n Inception Inception模块。另外作者在这里还提到,大量关于稀疏矩阵运算的文献(如On two-dimensional sparse matrix par-titioning: Models, methods, and a recipe)认为可以将稀疏矩阵聚类为相对密集的子矩阵来提高计算稀疏矩阵的运行效率。

总之,我们最终的目的,就是找到一个基于卷积网络的最优稀疏模块,且该模块可以通过密集组件进行构建。

2. Inception 模块结构

最终,提出了 I n c e p t i o n Inception Inception模块的简单版本,如下图所示:

在这里插入图片描述

模型有以下特点:

  1. 使用卷积实现稀疏性
  2. 使用不同尺寸的卷积核,相当于不同的稀疏连接方式,同时还能提取多尺度特征。这里使用大小为 1 ∗ 1 、 3 ∗ 3 、 5 ∗ 5 1*1、3*3、5*5 113355的卷积核,是为了卷积时方便padding
  3. 最大池化层,由于很多优秀的模型都使用最大池化,且能取得很好的效果,因此这里也添加了最大池化模块。池化大小为 3 ∗ 3 3*3 33,为了保证和输入的宽高保持一致,也使用padding,并设置步长为 1 1 1
  4. 将卷积和池化之后的特征图连接在一起,近似最优的稀疏模型

这种模型的存在的问题是:

  1. 运算量巨大

    假设输入图的大小为 28 ∗ 28 ∗ 192 28*28*192 2828192,Inception模块的几个卷积配置为: 64 64 64 1 ∗ 1 ∗ 192 1*1*192 11192的卷积核、 128 128 128 3 ∗ 3 ∗ 192 3*3*192 33192的卷积核、 32 32 32 5 ∗ 5 ∗ 192 5*5*192 55192的卷积核进行卷积。 大卷积核的计算量也会很大,我们以 5 ∗ 5 5*5 55卷积为例,计算其运算次数,使用了 32 32 32 5 ∗ 5 ∗ 192 5*5*192 55192的卷积核进行卷积,将得到 28 ∗ 28 ∗ 32 28*28*32 282832的特征图,共有 28 ∗ 28 ∗ 32 = 25088 28*28*32=25088 282832=25088个特征值,每个特征值需要执行 5 ∗ 5 ∗ 192 = 4800 5*5*192=4800 55192=4800次乘法,总共需要 计算 25088 ∗ 4800 = 120422400 25088*4800=120422400 250884800=120422400次乘法。 这里我们值计算乘法运算次数,不计算加法次数。

  2. 由于最大池化不改变输入图的通道大小,执行完卷积核池化之后需要将特征图沿通道方向拼接在一起,随着Inception模块的堆叠,模块输出的特征图通道数越来越大,导致计算崩溃。

因此对该模块的结构进行了优化,如下图所示

在这里插入图片描述

改进后的结构在每次卷积之前,最大池化之后加入 1 ∗ 1 1*1 11的卷积。还是以假设输入图的大小为 28 ∗ 28 ∗ 192 28*28*192 2828192,Inception模块的几个卷积配置为: 64 64 64 1 ∗ 1 1*1 11的卷积核、 128 128 128 3 ∗ 3 3*3 33的卷积核、 32 32 32 5 ∗ 5 5*5 55的卷积核进行卷积为例。在 5 ∗ 5 5*5 55的卷积之前,添加 16 16 16 1 ∗ 1 1*1 11的卷积核,将得到 28 ∗ 28 ∗ 16 28*28*16 282816的特征图,此时的的乘法计算次数为 28 ∗ 28 ∗ 16 ∗ 1 ∗ 1 ∗ 192 = 2408448 28*28*16*1*1*192=2408448 28281611192=2408448, 之后需要使用 32 32 32 5 ∗ 5 5*5 55的卷积核对 28 ∗ 28 ∗ 16 28*28*16 282816的特征图进行卷积,将得到 28 ∗ 28 ∗ 32 28*28*32 282832的特征图,此时需要的乘法计算量为 28 ∗ 28 ∗ 32 ∗ 5 ∗ 5 ∗ 16 = 10035200 28*28*32*5*5*16=10035200 2828325516=10035200,总共的乘法计算量为 10035200 + 2408448 = 12443648 10035200+2408448 = 12443648 10035200+2408448=12443648,如果不添加 1 ∗ 1 1*1 11卷积其乘法计算量为 120422400 120422400 120422400, 添加之后计算量仅为之前的 1 10 \frac{1}{10} 101

3. GoogLeNet网络结构

利用Inception模块构建GoogLeNet,参数设置如下表所示

  1. 网络中所使用的卷积,包括Inception模块内的卷积,均使用Relu激活函数
  2. 输入图像大小为 224 ∗ 224 ∗ 3 224*224*3 2242243, 预处理为减去训练集平均值
  3. “#3×3 reduce” 和“#5×5 reduce” 表示在执行 3 × 3 3\times3 3×3 5 × 5 5\times5 5×5卷积之前,使用 1 × 1 1\times1 1×1卷积核的个数
  4. “pool proj”表示 m a x − p o o l i n g max-pooling maxpooling之后使用的 1 × 1 1\times1 1×1卷积核的个数
  5. ‘’deep‘’表示网络深度,所有的值加起来刚好是 22 22 22层,这里只计算权重层,pooling层无可训练参数,不计入网络深度
  6. ‘’params‘’表示某一层的待训练参数量
  7. ‘’ops‘’表示某一层的计算量
  8. 使用平均池化代替最后的全连接层,结果发现替换全连接层之后,top1的分类精度提高了 0.6 % 0.6\% 0.6%
  9. 为了方便迁移学习,在平均池化之后加了一层具有 1000 1000 1000个神经元的全连接层,使用 s o f t m a x softmax softmax激活函数
  10. 在全局池化层和全连接层之间,使用了Dropout
  11. 网络太深可能导致梯度消失,且作者发现该网络结构的浅层也能有很好的判决能力,因此在网络中添加了两个辅助分类器,防止梯度消失并增加正则化。辅助分类器的损失以 0.3 0.3 0.3的权重加入主分类器的损失中。 辅助分类器的配置如下:
    • 在inception(4a)和inception(4d)的输出位置添加两个辅助分类器,首先是使用大小为 5 ∗ 5 5*5 55,步长为 3 3 3的平均池化
    • 使用128个 1 ∗ 1 1*1 11的卷积进行降维,使用 r e l u relu relu激活函数
    • 使用具有 1024 1024 1024个神经元的全连接层,使用 r e l u relu relu激活函数
    • 使用Dropout,丢失率 70 % 70\% 70%
    • 使用具有 1000 1000 1000个神经元的全连接层,使用 s o f t m a x softmax softmax激活函数

其完整的网络结构如下图所示:

在这里插入图片描述

4. Tensorflow实现GoogLeNet结构

#!/usr/bin/python3

# @Time    : 2021/04/01 10:07
# @Author  : 
# @File    : GoogLeNet
# @Software: PyCharm
# @Description : 使用TensorFlow实现GoogLeNet网络结构

import tensorflow as tf
from tensorflow.keras.layers import Layer,Conv2D,MaxPool2D,Dropout,Activation,Flatten,Dense,AvgPool2D,concatenate,Softmax,Input

# 定义Inception 模块类
class Inception(Layer,):
    def __init__(self, cov1_1,cov_reduce3_3,cov3_3,cov_reduce5_5,cov5_5,pool_proj, **kwargs):
        super(Inception, self).__init__(**kwargs)
        self.branch1 = Conv2D(filters=cov1_1,kernel_size=1,activation='relu') 
        
        self.branch2 = tf.keras.Sequential([
            Conv2D(filters=cov_reduce3_3,kernel_size=1,activation='relu'),
            Conv2D(filters=cov3_3,kernel_size=3,padding='same',activation='relu')
        ])
        
        self.branch3 = tf.keras.Sequential([
            Conv2D(filters=cov_reduce5_5,kernel_size=1,activation='relu'),
            Conv2D(filters=cov5_5,kernel_size=5,padding='same',activation='relu')
        ])
        
        self.branch4 = tf.keras.Sequential([
            MaxPool2D(pool_size=3,strides=1,padding='same'),
            Conv2D(filters=pool_proj,kernel_size=1,activation='relu')
        ])
        
    def call(self,inputs):
        branch1 = self.branch1(inputs)
        branch2 = self.branch2(inputs)
        branch3 = self.branch3(inputs)
        branch4 = self.branch4(inputs)
        outputs = concatenate([branch1,branch2,branch3,branch4])
        return outputs
        
# 定义辅助分类器
class AuxiliaryClassifier(Layer,):
    def __init__(self, **kwargs):
        super(AuxiliaryClassifier, self).__init__(**kwargs)
        self.avgpool = AvgPool2D(pool_size=5,strides=3)
        self.conv = Conv2D(filters=128,kernel_size=1,activation='relu')
        self.flatten = Flatten()
        self.fc1 = Dense(units=1024,activation="relu")
        self.dropout = Dropout(rate=0.7)
        self.fc2 = Dense(units=1000,activation="softmax")
        
    def call(self,inputs):
        x = self.avgpool(inputs)
        x = self.conv(x)
        x = self.flatten(x)
        x = self.fc1(x)
        x = self.dropout(x)
        outputs = self.fc2(x)
        return outputs     

def GoogLeNet(model_name='GoogLeNet',im_height=224,im_width=224,class_num=1000):
#     inputs = tf.keras.Input(shape=(im_height,im_width,3),name='Inputs')
    inputs = Input(shape=(im_height,im_width,3),name='Inputs')
    
    # input [224,224,3]  -->  [112,112,64]
    x = Conv2D(filters=64,kernel_size=7,strides=2,padding='same',activation='relu',name='convolution-1')(inputs)
    # [112,112,64]  -->  [56,56,64]
    x = MaxPool2D(pool_size=3,strides=2,padding='same',name='max-pool-1')(x)
    # [56,56,64]  -->  [56,56,192]
    x = Conv2D(filters=192,kernel_size=3,strides=1,padding='same',activation='relu',name='convolution-2')(x)
    # [56,56,192]  -->  [28,28,192]
    x = MaxPool2D(pool_size=3,strides=2,padding='same',name='max-pool-2')(x)
    # [28,28,192] --> [28, 28, 256]
    x = Inception(64,96,128,16,32,32,name='inception-3a')(x)
    # [28, 28, 256] --> [28, 28, 480]
    x = Inception(128,128,192,32,96,64,name='inception-3b')(x)
    # [28, 28, 480] --> [14, 14, 480]
    x = MaxPool2D(pool_size=3,strides=2,padding='same',name='max-pool-3')(x)
    # [14, 14, 480] --> [14, 14, 512]
    x = Inception(192,96,208,16,48,64,name='inception-4a')(x)
    # [14, 14, 512] --> [1000]
    outputs1 = AuxiliaryClassifier(name='AuxiliaryClassifier-1')(x)
    # [14, 14, 512] --> [14, 14, 512]
    x = Inception(160,112,224,24,64,64,name='inception-4b')(x)
    # [14, 14, 512] --> [14, 14, 512]
    x = Inception(128,128,256,24,64,64,name='inception-4c')(x)
    # [14, 14, 512] --> [14, 14, 528]
    x = Inception(112,144,288,32,64,64,name='inception-4d')(x)
    # [14, 14, 528] --> [1000,]
    outputs2 = AuxiliaryClassifier(name='AuxiliaryClassifier-2')(x)
    # [14, 14, 528] --> [14, 14, 832]
    x = Inception(256,160,320,32,128,128,name='inception-4e')(x)
    # [14, 14, 832] --> [7, 7, 832]
    x = MaxPool2D(pool_size=3,strides=2,padding='same',name='max-pool-4')(x)
    # [7, 7, 832] --> [7, 7, 832]
    x = Inception(256,160,320,32,128,128,name='inception-5a')(x)
    # [7, 7, 832] --> [7, 7, 1024]
    x = Inception(384,192,384,48,128,128,name='inception-5b')(x)
    # [7, 7, 1024] --> [1, 1, 1024]
    x = AvgPool2D(pool_size=7,strides=1,name='avg-pool')(x)
    # [1, 1, 1024] --> [1024,]
    x = Flatten(name='flatten')(x)
    x = Dropout(rate=0.4,name='dropout')(x)
    x = Dense(units=1000,name='linear')(x)
    outputs = Softmax(name='softmax')(x)
    model = tf.keras.models.Model(inputs=inputs,outputs=[outputs1,outputs2,outputs],name='GoogLeNet')
    return model

model = GoogLeNet()
model.summary()

模型摘要如下

Model: "GoogLeNet"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
Inputs (InputLayer)             [(None, 224, 224, 3) 0                                            
__________________________________________________________________________________________________
convolution-1 (Conv2D)          (None, 112, 112, 64) 9472        Inputs[0][0]                     
__________________________________________________________________________________________________
max-pool-1 (MaxPooling2D)       (None, 56, 56, 64)   0           convolution-1[0][0]              
__________________________________________________________________________________________________
convolution-2 (Conv2D)          (None, 56, 56, 192)  110784      max-pool-1[0][0]                 
__________________________________________________________________________________________________
max-pool-2 (MaxPooling2D)       (None, 28, 28, 192)  0           convolution-2[0][0]              
__________________________________________________________________________________________________
inception-3a (Inception)        (None, 28, 28, 256)  163696      max-pool-2[0][0]                 
__________________________________________________________________________________________________
inception-3b (Inception)        (None, 28, 28, 480)  388736      inception-3a[0][0]               
__________________________________________________________________________________________________
max-pool-3 (MaxPooling2D)       (None, 14, 14, 480)  0           inception-3b[0][0]               
__________________________________________________________________________________________________
inception-4a (Inception)        (None, 14, 14, 512)  376176      max-pool-3[0][0]                 
__________________________________________________________________________________________________
inception-4b (Inception)        (None, 14, 14, 512)  449160      inception-4a[0][0]               
__________________________________________________________________________________________________
inception-4c (Inception)        (None, 14, 14, 512)  510104      inception-4b[0][0]               
__________________________________________________________________________________________________
inception-4d (Inception)        (None, 14, 14, 528)  605376      inception-4c[0][0]               
__________________________________________________________________________________________________
inception-4e (Inception)        (None, 14, 14, 832)  868352      inception-4d[0][0]               
__________________________________________________________________________________________________
max-pool-4 (MaxPooling2D)       (None, 7, 7, 832)    0           inception-4e[0][0]               
__________________________________________________________________________________________________
inception-5a (Inception)        (None, 7, 7, 832)    1043456     max-pool-4[0][0]                 
__________________________________________________________________________________________________
inception-5b (Inception)        (None, 7, 7, 1024)   1444080     inception-5a[0][0]               
__________________________________________________________________________________________________
avg-pool (AveragePooling2D)     (None, 1, 1, 1024)   0           inception-5b[0][0]               
__________________________________________________________________________________________________
flatten (Flatten)               (None, 1024)         0           avg-pool[0][0]                   
__________________________________________________________________________________________________
dropout (Dropout)               (None, 1024)         0           flatten[0][0]                    
__________________________________________________________________________________________________
linear (Dense)                  (None, 1000)         1025000     dropout[0][0]                    
__________________________________________________________________________________________________
AuxiliaryClassifier-1 (Auxiliar (None, 1000)         3188840     inception-4a[0][0]               
__________________________________________________________________________________________________
AuxiliaryClassifier-2 (Auxiliar (None, 1000)         3190888     inception-4d[0][0]               
__________________________________________________________________________________________________
softmax (Softmax)               (None, 1000)         0           linear[0][0]                     
==================================================================================================
Total params: 13,374,120
Trainable params: 13,374,120
Non-trainable params: 0
  • 4
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
GoogleNet是2014年谷歌公司提出的一个深度神经网络模型,也被称为Inception v1。它是一个非常复杂的模型,但是可以使用TensorFlow相对容易地实现GoogleNet的主要创新是使用了“Inception模块”,该模块可以在不增加参数数量的情况下提高模型的性能。下面是GoogleNet的基本结构: ![GoogleNet](https://miro.medium.com/max/1838/1*ZFPOSAted10TPd3hBQU8iQ.png) GoogleNet由多个Inception模块组成,每个模块包含多个卷积层和池化层,以及一些1x1卷积核的处理。使用1x1卷积核可以减少模型的计算量,同时保持模型的表现力。 下面是使用TensorFlow实现GoogleNet的代码: ```python import tensorflow as tf from tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, Dropout, Dense, concatenate, AveragePooling2D, Flatten def inception_module(x, filters): # 1x1 Convolution path1 = Conv2D(filters=filters[0], kernel_size=1, activation='relu')(x) # 1x1 Convolution + 3x3 Convolution path2 = Conv2D(filters=filters[1], kernel_size=1, activation='relu')(x) path2 = Conv2D(filters=filters[2], kernel_size=3, padding='same', activation='relu')(path2) # 1x1 Convolution + 5x5 Convolution path3 = Conv2D(filters=filters[3], kernel_size=1, activation='relu')(x) path3 = Conv2D(filters=filters[4], kernel_size=5, padding='same', activation='relu')(path3) # 3x3 Max Pooling + 1x1 Convolution path4 = MaxPooling2D(pool_size=3, strides=1, padding='same')(x) path4 = Conv2D(filters=filters[5], kernel_size=1, activation='relu')(path4) # Concatenate all paths output = concatenate([path1, path2, path3, path4]) return output def create_model(input_shape, num_classes): inputs = Input(shape=input_shape) # First Convolution and Pooling Layers x = Conv2D(filters=64, kernel_size=7, strides=2, padding='same', activation='relu')(inputs) x = MaxPooling2D(pool_size=3, strides=2, padding='same')(x) # Second Convolution and Pooling Layers x = Conv2D(filters=64, kernel_size=1, strides=1, padding='same', activation='relu')(x) x = Conv2D(filters=192, kernel_size=3, strides=1, padding='same', activation='relu')(x) x = MaxPooling2D(pool_size=3, strides=2, padding='same')(x) # Inception Modules x = inception_module(x, [64, 96, 128, 16, 32, 32]) x = inception_module(x, [128, 128, 192, 32, 96, 64]) x = MaxPooling2D(pool_size=3, strides=2, padding='same')(x) x = inception_module(x, [192, 96, 208, 16, 48, 64]) x = inception_module(x, [160, 112, 224, 24, 64, 64]) x = inception_module(x, [128, 128, 256, 24, 64, 64]) x = inception_module(x, [112, 144, 288, 32, 64, 64]) x = inception_module(x, [256, 160, 320, 32, 128, 128]) x = MaxPooling2D(pool_size=3, strides=2, padding='same')(x) x = inception_module(x, [256, 160, 320, 32, 128, 128]) x = inception_module(x, [384, 192, 384, 48, 128, 128]) # Final Pooling and Output Layers x = AveragePooling2D(pool_size=7, strides=1, padding='valid')(x) x = Flatten()(x) outputs = Dense(units=num_classes, activation='softmax')(x) # Create Model model = tf.keras.Model(inputs=inputs, outputs=outputs) return model ``` 这个代码实现GoogleNet的基本结构。需要注意的是,这里的实现使用了TensorFlow 2.0的Keras API,因此可以很方便地创建模型,并且使用各种优化器和损失函数进行训练。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

积跬步以至千里。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值