实现CNN图像的识别和训练通过tensorflow框架对cifar10数据集等方法的处理

VGGNet是牛津大学计算机视觉组(Visual Geometry Group)和Google DeepMind公司的研究员一起研发的深度卷积神经网络
VGG探索了卷积神经网络的深度与其性能之间的关系,通过反复堆叠33的小型卷积核和22的最大池化层,VGG成功构筑了16-19层深的卷积神经网络
VGG取得了2014年比赛分类项目第二名和定位项目第一名。同时,VGG拓展性很强,迁移到其他图片数据上的泛化性非常好。VGG的结构简洁, 整个网络都是使用了同样大小的卷积核尺寸33和池化层22.
VGG现在也还经常被用来提取图像特征,VGGNet训练后的模型参数在其官网上开源了,可用来在图像分类任务上进行再训练,相当于提供了非常好的 初始化权重

VGG通过加深层次来提升性能,拥有5段卷积,每一段内有2-3个卷积层,同时每段尾部都会连接一个最大池化层来缩小图片尺寸 每段内的卷积核数量一样,越靠后段的卷积核数量越多,64-128-258-512-512 其中经常出现多个完全一样的33的卷积层堆叠在一起的情况,这其实是非常有用的设计,两个33的卷积层串联相当于一个55的卷积层
即一个像素会和周边5
5的像素产生关联,可以说感受野大小是55,而三个33的卷积层串联效果相当于一个77的卷积层 另外,3个串联的卷积层拥有比一个77的卷积层更少的参数量,只有后者的 333/(7*7)= 55%,更重要的是,3个串联的层拥有比1个层 更多的非线性变换,前者可以使用三次ReLU激活函数,而后者只有一次,使得CNN对特征的学习能力更强

def conv_op(input_op, name, kernel_height, kernel_width, n_out, stride_height, stride_width, param_list):
    # 获取输入的通道数
    n_in = input_op.get_shape()[-1].value

    with tf.name_scope(name=name) as scope:
        kernel = tf.get_variable(scope+'w',
                                 shape=[kernel_height, kernel_width, n_in, n_out], dtype=tf.float32,
                                 initializer=tf.contrib.layers.xavier_initializer_conv2d())

追本溯源:
ILSVRC(ImageNet Large Scale Visual Recognition Challenge) ImageNet项目于2007年由斯坦福大学华人教授李飞飞创办,目标是收集大量带有标注信息的图片数据供计算机视觉模型训练 ImageNet拥有1500万张标注过的高清图片,总共拥有22000类,其中约有100万张标注了图片中主要物体的定位边框。
对于Alexnet网络:

ImageNet项目最早的灵感来自于人类通过视觉学习世界的方式,如果假定儿童的眼睛是生物照相机,它们平均每200ms就拍照一次眼球转动一次的平均时间200ms,那么3岁大的孩子就已经看过了上亿张真实时间的照片,使用亚马逊的土耳其机器人平台实现标注过程 来自世界上167个国家的5万名工作者帮忙一起筛选、标注

每年ILSVRC比赛数据集中大概有120万张图片,以及1000类的标注,是ImageNet全部数据的一个子集。比赛采用top-5和top-1分类错误率 作为模型性能的评测指标
AlexNet比赛分类项目的2012年冠军,top5错误率16.4%,8层神经网络 神经网络模型AlexNet可以算是LeNet的一种更深更宽的版本!AlexNet中包含了几个比较新的技术点,首次在CNN中成功应用了ReLU、Dropout、 LRN等Trick,AlexNet包含了6亿3000万个连接,6000多万个参数!和65万个神经元,拥有5个卷积层,3个全连接层,其中3个卷积层后面 连接了最大池化层,输出层是1000类的softmax层做分类,LRN应用在了第一卷积层和第二卷积层后面,ReLU激活函数应用在8层每一层后面

Alexnet网络模型性能的评测

1,运用ReLU,解决Sigmoid在网络层次较深时的梯度弥散
2,训练Dropout,随机忽略一些神经元,避免过拟合
3,使用重叠的最大池化,此前CNN普遍平均池化,最大池化避免平均池化的模糊化效果
4,提出了LRN层,局部神经元活动创建竞争机制,响应比较大的值变得更大,抑制其他反馈小的神经元,增强泛化能力
5,使用CUDA加速深度卷积网络的训练
6,数据增强,随机地从256256的原始图像中截取224224大小的区域,以及水平翻转的镜像,相当于增加了【(256-224)^2】*2=2048
倍的数据量。没有数据增强,紧靠原始的数据量,参数众多的CNN会陷入过拟合中

代码如下:

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data


mnist = input_data.read_data_sets('MNIST_data_bak/', one_hot=True)
sess = tf.InteractiveSession()


# 截断的正太分布噪声,标准差设为0.1
# 同时因为我们使用ReLU,也给偏置项增加一些小的正值0.1用来避免死亡节点(dead neurons)
def weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initial)


def bias_variable(shape):
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)


# 卷积层和池化层也是接下来要重复使用的,因此也为它们定义创建函数

# tf.nn.conv2d是TensorFlow中的2维卷积函数,参数中x是输入,W是卷积的参数,比如[5, 5, 1, 32]
# 前面两个数字代表卷积核的尺寸,第三个数字代表有多少个channel,因为我们只有灰度单色,所以是1,如果是彩色的RGB图片,这里是3
# 最后代表核的数量,也就是这个卷积层会提取多少类的特征

# Strides代表卷积模板移动的步长,都是1代表会不遗漏地划过图片的每一个点!Padding代表边界的处理方式,这里的SAME代表给
# 边界加上Padding让卷积的输出和输入保持同样SAME的尺寸
def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')


# tf.nn.max_pool是TensorFlow中的最大池化函数,我们这里使用2*2的最大池化,即将2*2的像素块降为1*1的像素
# 最大池化会保留原始像素块中灰度值最高的那一个像素,即保留最显著的特征,因为希望整体上缩小图片尺寸,因此池化层
# strides也设为横竖两个方向以2为步长。如果步长还是1,那么我们会得到一个尺寸不变的图片
def max_pool_2x2(x):
    return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')


# 因为卷积神经网络会利用到空间结构信息,因此需要将1D的输入向量转为2D的图片结构,即从1*784的形式转为原始的28*28的结构
# 同时因为只有一个颜色通道,故最终尺寸为[-1, 28, 28, 1],前面的-1代表样本数量不固定,最后的1代表颜色通道数量
x = tf.placeholder(tf.float32, [None, 784])
y_ = tf.placeholder(tf.float32, [None, 10])
x_image = tf.reshape(x, [-1, 28, 28, 1])

# 定义我的第一个卷积层,我们先使用前面写好的函数进行参数初始化,包括weights和bias,这里的[5, 5, 1, 32]代表卷积
# 核尺寸为5*51个颜色通道,32个不同的卷积核,然后使用conv2d函数进行卷积操作,并加上偏置项,接着再使用ReLU激活函数进行
# 非线性处理,最后,使用最大池化函数max_pool_2*2对卷积的输出结果进行池化操作
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

# 第二层和第一个一样,但是卷积核变成了64
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

# 因为前面经历了两次步长为2*2的最大池化,所以边长已经只有1/4了,图片尺寸由28*28变成了7*7
# 而第二个卷积层的卷积核数量为64,其输出的tensor尺寸即为7*7*64
# 我们使用tf.reshape函数对第二个卷积层的输出tensor进行变形,将其转成1D的向量
# 然后连接一个全连接层,隐含节点为1024,并使用ReLU激活函数
W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

# 防止过拟合,使用Dropout层
keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

# 接 Softmax分类
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

# 定义损失函数
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y_conv),
                                              reduction_indices=[1]))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# 训练
tf.global_variables_initializer().run()
for i in range(20000):
    batch = mnist.train.next_batch(50)
    if i % 100 == 0:
        train_accuracy = accuracy.eval(feed_dict={x: batch[0], y_: batch[1],
                                                  keep_prob: 1.0})
        print("step %d, training accuracy %g" % (i, train_accuracy))
    train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

print("test accuracy %g" % accuracy.eval(feed_dict={
    x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0
}))

最后,这个CNN模型可以得到的准确率约为99.2%,基本可以满足对手写数字识别准确率的要求
相比之前的MLP的2%的错误率,CNN的错误率下降了大约60%,这里主要的性能提升都来自于更优秀的网络设计
即卷积网络对图像特征的提取和抽象能力,依靠卷积核的权值共享,CNN的参数数量并没有爆炸,降低计算量的同时
也减轻了过拟合,因此整个模型的性能有较大的提升,这里我们只是实现了一个简单的卷积神经网络,没有复杂的Trick
接下来我们实现复杂一点的卷积网络,MNIST数据集已经不适合用来评测其性能
我们将使用CIFAR-10数据集进行训练,这也是深度学习可以大幅领先其它模型的一个数据集

本节使用的数据集是CIFAR-10,这是一个经典的数据集,包含60000张32*32的彩色图像,其中训练集50000张,测试集10000张
一共标注为10类,每一类图片6000张。10类分别是 airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck

从头开始:
载入相关库

import tensorflow as tf
import numpy as np
import math
import time
from tutorials.image.cifar10 import cifar10
from tutorials.image.cifar10 import cifar10_input

初始化参数

max_steps = 3000
batch_size = 128
# 下载cifar10数据集的默认路径
data_dir = 'D:/cifar10_data/cifar-10-batches-bin'

定义初始化weights的函数,和之前一样依然使用tf.truncated_normal截断的正太分布来初始化权值

def variable_with_weight_losses(shape, stddev, wl):
    # 定义初始化weights的函数,和之前一样依然使用tf.truncated_normal截断的正太分布来初始化权值
    var = tf.Variable(tf.truncated_normal(shape, stddev=stddev))
    if wl is not None:
        # 给weight加一个L2的loss,相当于做了一个L2的正则化处理
        # 在机器学习中,不管是分类还是回归任务,都可能因为特征过多而导致过拟合,一般可以通过减少特征或者惩罚不重要特征的权重来缓解这个问题
        # 但是通常我们并不知道该惩罚哪些特征的权重,而正则化就是帮助我们惩罚特征权重的,即特征的权重也会成为模型的损失函数的一部分
        # 我们使用w1来控制L2 loss的大小
        weight_loss = tf.multiply(tf.nn.l2_loss(var), wl, name='weight_loss')
        # 我们使用tf.add_to_collection把weight loss统一存到一个collection,这个collection名为"losses",它会在后面计算神经网络
        # 总体loss时被用上
        tf.add_to_collection("losses", weight_loss)
    return var

下载cifar10类下载数据集,并解压,展开到其默认位置


cifar10.maybe_download_and_extract()
# 使用cifar10_input类中的distorted_inputs函数产生训练需要使用的数据,包括特征及其对应的label,这里是封装好的tensor,
# 每次执行都会生成一个batch_size的数量的样本。需要注意的是这里对数据进行了Data Augmentation数据增强
# 具体实现细节查看函数,其中数据增强操作包括随机水平翻转tf.image.random_flip_left_right()
# 随机剪切一块24*24大小的图片tf.random_crop,随机设置亮度和对比度,tf.image.random_brightness、tf.image.random_contrast
# 以及对数据进行标准化,白化 tf.image.per_image_standardization() 减去均值、除以方差,保证数据零均值,方差为1
images_train, labels_train = cifar10_input.distorted_inputs(
    data_dir=data_dir, batch_size=batch_size
)

生成测试数据


# 生成测试数据,不过这里不需要进行太多处理,不需要对图片进行翻转或修改亮度、对比度,不过需要裁剪图片正中间的24*24大小的区块,
# 并进行数据标准化操作
images_test, labels_test = cifar10_input.inputs(eval_data=True, data_dir=data_dir, batch_size=batch_size)

# 因为batch_size在之后定义网络结构时被用到了,所以数据尺寸中的第一个值即样本条数需要被预先设定,而不能像以前那样设置为None
# 而数据尺寸中的图片尺寸为24*24即是剪裁后的大小,颜色通道数则设为3
image_holder = tf.placeholder(tf.float32, [batch_size, 24, 24, 3])
label_holder = tf.placeholder(tf.int32, [batch_size])

设置卷积初始参数


# 初始设置第一个卷积层,64个卷积核,卷积核大小是5*53通道
weight1 = variable_with_weight_losses(shape=[5, 5, 3, 64], stddev=5e-2, wl=0.0)
kernel1 = tf.nn.conv2d(image_holder, filter=weight1, strides=[1, 1, 1, 1], padding='SAME')
bias1 = tf.Variable(tf.constant(0.0, shape=[64]))
conv1 = tf.nn.relu(tf.nn.bias_add(kernel1, bias1))
# 使用尺寸3*3步长2*2的最大池化层处理数据,这里最大池化的尺寸和步长不一样,可以增加数据的丰富性
pool1 = tf.nn.max_pool(conv1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')
norm1 = tf.nn.lrn(pool1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75)

创建第二个卷积层

# 创建第二个卷积层
# 上面64个卷积核,即输出64个通道,所以本层卷积核尺寸的第三个维度即输入的通道数也需要调整为64
weight2 = variable_with_weight_losses(shape=[5, 5, 64, 64], stddev=5e-2, wl=0.0)
kernel2 = tf.nn.conv2d(norm1, weight2, [1, 1, 1, 1], padding='SAME')
# 还有这里的bias值全部初始化为0.1,而不是0.最后,调换了最大池化层和LRN层的顺序,先进行LRN层处理,再使用最大池化层
bias2 = tf.Variable(tf.constant(0.1, shape=[64]))
conv2 = tf.nn.relu(tf.nn.bias_add(kernel2, bias2))
norm2 = tf.nn.lrn(conv2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75)
pool2 = tf.nn.max_pool(norm2, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')

然后是全连接层


# 两个卷积层之后,是全连接层
# 先把第二个卷积层之后的输出结果flatten,使用tf.reshape函数将每个样本都变成一维向量,使用get_shape函数获取数据扁平化之后的长度
reshape = tf.reshape(pool2, [batch_size, -1])
dim = reshape.get_shape()[1].value
# 接着初始化权值,隐含节点384个,正太分布的标准差设为0.04,bias的值也初始化为0.1
# 注意这里我们希望这个全连接层不要过拟合,因此设了一个非零的weight loss值0.04,让这一层具有L2正则所约束。
weight3 = variable_with_weight_losses(shape=[dim, 384], stddev=0.04, wl=0.004)
bias3 = tf.Variable(tf.constant(0.1, shape=[384]))
# 最后我们依然使用ReLU激活函数进行非线性化
local3 = tf.nn.relu(tf.matmul(reshape, weight3) + bias3)

# 接下来还是全连接层,只是隐含节点只有一半,其他一样
weight4 = variable_with_weight_losses(shape=[384, 192], stddev=0.04, wl=0.004)
bias4 = tf.Variable(tf.constant(0.1, shape=[192]))
local4 = tf.nn.relu(tf.matmul(local3, weight4) + bias4)

最后一层处理

# 最后一层,依然先创建一层weight,其正太分布标准差设为一个隐含层节点数的倒数,并且不用L2正则
# 这里没有用之前的softmax输出最后结果,这里把softmax操作放在了计算loss部分,其实我们不需要对inference的输出进行softmax
# 处理就可以获得最终分类结果(直接比较inference输出的各类的数值大小即可),计算softmax主要是为了计算loss,因此softmax操作整合到后面合理
weight5 = variable_with_weight_losses(shape=[192, 10], stddev=1/192.0, wl=0.0)
bias5 = tf.Variable(tf.constant(0.0, shape=[10]))
logits = tf.add(tf.matmul(local4, weight5), bias5)

到这里就完成了整个网络inference的部分,梳理整个网络结构,设计性能良好的CNN是有一定规律可循的,但是想要针对某个问题设计最合适的
网络结构,是需要大量实际摸索的
完成模型inference的构建,接下来是计算CNN的loss,这里依然是用cross_entropy,这里我们把softmax的计算和cross_entropy的计算
合在了一起,即 tf.nn.sparse_softmax_cross_entropy_with_logits()
这里使用 tf.reduce_mean() 对 cross entropy计算均值,再使用 tf.add_to_collection()把cross entropy的loss添加到整体
losses的collection中,最后,使用tf.add_n将整体losses的collection集合中的全部loss求和,得到最终的loss,其中包括
cross entropy loss, 还有后两个全连接层中weight的L2 loss

定义损失函数


def loss(logits, labels):
    labels = tf.cast(labels, tf.int64)
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
        logits=logits, labels=labels, name='cross_entropy_per_example'
    )
    cross_entropy_mean = tf.reduce_mean(cross_entropy,
                                        name='cross_entropy')
    tf.add_to_collection('losses', cross_entropy_mean)

    return tf.add_n(tf.get_collection('losses'), name='total_loss')

模型评估与结果输出

num_examples = 10000
# 先计算一共要多少个batch才能将全部样本评测完
num_iter = int(math.ceil(num_examples / batch_size))
true_count = 0
total_sample_count = num_iter * batch_size
step = 0
while step < num_iter:
    image_batch, label_batch = sess.run([images_test, labels_test])
    predictions = sess.run([top_k_op], feed_dict={image_holder: image_batch,
                                                  label_holder: label_batch})
    true_count += np.sum(predictions)
    step += 1

precision = true_count / total_sample_count
print('precision @ 1 = %.3f' % precision)

最终,在cifar-10数据集上,通过一个短时间小迭代的训练,可以达到大致73%的准确率,持续增加max_steps,可以期望准确率逐渐增加
如果max_steps比较大,则推荐使用学习速率衰减decay的SGD进行训练,这样训练过程中能达到的准确率峰值会比较高,大致有86%
其中L2正则以及LRN层的使用都对模型准确率有提升作用,它们都可以提升模型的泛化能力

数据增强Data Augmentation在我们的训练中作用很大,它可以给单幅图增加多个副本,提高图片的利用率,防止对某一张图片结构的学习过拟合
这刚好是利用了图片数据本身的性质,图片的冗余信息量比较大,因此可以制造不同的噪声并让图片依然可以被识别出来。如果神经网络可以克服这些
噪声并准确识别,那么他的泛化能力必然很好。数据增强大大增加了样本量,而数据量的大小恰恰是深度学习最看重的,深度学习可以在图像识别上领先
其他算法的一大因素就是它对海量数据的利用效率非常高。其他算法,可能在数据量大到一定程度时,准确率就不再上升了,而深度学习只要提供足够
多的样本,准确率基本持续提升,所以说它是最适合大数据的算法。
本文相关的代码:
部分 代码1
CNN-VGG

from datetime import datetime
import math
import time
import tensorflow as tf
def conv_op(input_op, name, kernel_height, kernel_width, n_out, stride_height, stride_width, param_list):
    # 获取输入的通道数
    n_in = input_op.get_shape()[-1].value

    with tf.name_scope(name=name) as scope:
        kernel = tf.get_variable(scope+'w',
                                 shape=[kernel_height, kernel_width, n_in, n_out], dtype=tf.float32,
                                 initializer=tf.contrib.layers.xavier_initializer_conv2d())

完整代码2
tensorflow_cnn_mnist.py

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data


mnist = input_data.read_data_sets('MNIST_data_bak/', one_hot=True)
sess = tf.InteractiveSession()


# 截断的正太分布噪声,标准差设为0.1
# 同时因为我们使用ReLU,也给偏置项增加一些小的正值0.1用来避免死亡节点(dead neurons)
def weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initial)


def bias_variable(shape):
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)


# 卷积层和池化层也是接下来要重复使用的,因此也为它们定义创建函数

# tf.nn.conv2d是TensorFlow中的2维卷积函数,参数中x是输入,W是卷积的参数,比如[5, 5, 1, 32]
# 前面两个数字代表卷积核的尺寸,第三个数字代表有多少个channel,因为我们只有灰度单色,所以是1,如果是彩色的RGB图片,这里是3
# 最后代表核的数量,也就是这个卷积层会提取多少类的特征

# Strides代表卷积模板移动的步长,都是1代表会不遗漏地划过图片的每一个点!Padding代表边界的处理方式,这里的SAME代表给
# 边界加上Padding让卷积的输出和输入保持同样SAME的尺寸
def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')


# tf.nn.max_pool是TensorFlow中的最大池化函数,我们这里使用2*2的最大池化,即将2*2的像素块降为1*1的像素
# 最大池化会保留原始像素块中灰度值最高的那一个像素,即保留最显著的特征,因为希望整体上缩小图片尺寸,因此池化层
# strides也设为横竖两个方向以2为步长。如果步长还是1,那么我们会得到一个尺寸不变的图片
def max_pool_2x2(x):
    return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')


# 因为卷积神经网络会利用到空间结构信息,因此需要将1D的输入向量转为2D的图片结构,即从1*784的形式转为原始的28*28的结构
# 同时因为只有一个颜色通道,故最终尺寸为[-1, 28, 28, 1],前面的-1代表样本数量不固定,最后的1代表颜色通道数量
x = tf.placeholder(tf.float32, [None, 784])
y_ = tf.placeholder(tf.float32, [None, 10])
x_image = tf.reshape(x, [-1, 28, 28, 1])

# 定义我的第一个卷积层,我们先使用前面写好的函数进行参数初始化,包括weights和bias,这里的[5, 5, 1, 32]代表卷积
# 核尺寸为5*51个颜色通道,32个不同的卷积核,然后使用conv2d函数进行卷积操作,并加上偏置项,接着再使用ReLU激活函数进行
# 非线性处理,最后,使用最大池化函数max_pool_2*2对卷积的输出结果进行池化操作
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

# 第二层和第一个一样,但是卷积核变成了64
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

# 因为前面经历了两次步长为2*2的最大池化,所以边长已经只有1/4了,图片尺寸由28*28变成了7*7
# 而第二个卷积层的卷积核数量为64,其输出的tensor尺寸即为7*7*64
# 我们使用tf.reshape函数对第二个卷积层的输出tensor进行变形,将其转成1D的向量
# 然后连接一个全连接层,隐含节点为1024,并使用ReLU激活函数
W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

# 防止过拟合,使用Dropout层
keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

# 接 Softmax分类
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

# 定义损失函数
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y_conv),
                                              reduction_indices=[1]))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# 训练
tf.global_variables_initializer().run()
for i in range(20000):
    batch = mnist.train.next_batch(50)
    if i % 100 == 0:
        train_accuracy = accuracy.eval(feed_dict={x: batch[0], y_: batch[1],
                                                  keep_prob: 1.0})
        print("step %d, training accuracy %g" % (i, train_accuracy))
    train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

print("test accuracy %g" % accuracy.eval(feed_dict={
    x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0
}))

# 最后,这个CNN模型可以得到的准确率约为99.2%,基本可以满足对手写数字识别准确率的要求
# 相比之前的MLP的2%的错误率,CNN的错误率下降了大约60%,这里主要的性能提升都来自于更优秀的网络设计
# 即卷积网络对图像特征的提取和抽象能力,依靠卷积核的权值共享,CNN的参数数量并没有爆炸,降低计算量的同时
# 也减轻了过拟合,因此整个模型的性能有较大的提升,这里我们只是实现了一个简单的卷积神经网络,没有复杂的Trick
# 接下来我们实现复杂一点的卷积网络,MNIST数据集已经不适合用来评测其性能
# 我们将使用CIFAR-10数据集进行训练,这也是深度学习可以大幅领先其它模型的一个数据集

完整代码3

tensorflow_cnn_cifar10.py

import tensorflow as tf
import numpy as np
import math
import time
from tutorials.image.cifar10 import cifar10
from tutorials.image.cifar10 import cifar10_input


# 本节使用的数据集是CIFAR-10,这是一个经典的数据集,包含6000032*32的彩色图像,其中训练集50000张,测试集10000张
# 一共标注为10类,每一类图片6000张。10类分别是 airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck

# 我们载入一些常用库,比如NumPy和time,并载入TensorFlow Models中自动下载、读取CIFAR-10数据的类
max_steps = 3000
batch_size = 128
# 下载cifar10数据集的默认路径
data_dir = 'D:/cifar10_data/cifar-10-batches-bin'


def variable_with_weight_losses(shape, stddev, wl):
    # 定义初始化weights的函数,和之前一样依然使用tf.truncated_normal截断的正太分布来初始化权值
    var = tf.Variable(tf.truncated_normal(shape, stddev=stddev))
    if wl is not None:
        # 给weight加一个L2的loss,相当于做了一个L2的正则化处理
        # 在机器学习中,不管是分类还是回归任务,都可能因为特征过多而导致过拟合,一般可以通过减少特征或者惩罚不重要特征的权重来缓解这个问题
        # 但是通常我们并不知道该惩罚哪些特征的权重,而正则化就是帮助我们惩罚特征权重的,即特征的权重也会成为模型的损失函数的一部分
        # 我们使用w1来控制L2 loss的大小
        weight_loss = tf.multiply(tf.nn.l2_loss(var), wl, name='weight_loss')
        # 我们使用tf.add_to_collection把weight loss统一存到一个collection,这个collection名为"losses",它会在后面计算神经网络
        # 总体loss时被用上
        tf.add_to_collection("losses", weight_loss)
    return var


# 下载cifar10类下载数据集,并解压,展开到其默认位置
cifar10.maybe_download_and_extract()
# 使用cifar10_input类中的distorted_inputs函数产生训练需要使用的数据,包括特征及其对应的label,这里是封装好的tensor,
# 每次执行都会生成一个batch_size的数量的样本。需要注意的是这里对数据进行了Data Augmentation数据增强
# 具体实现细节查看函数,其中数据增强操作包括随机水平翻转tf.image.random_flip_left_right()
# 随机剪切一块24*24大小的图片tf.random_crop,随机设置亮度和对比度,tf.image.random_brightness、tf.image.random_contrast
# 以及对数据进行标准化,白化 tf.image.per_image_standardization() 减去均值、除以方差,保证数据零均值,方差为1
images_train, labels_train = cifar10_input.distorted_inputs(
    data_dir=data_dir, batch_size=batch_size
)

# 生成测试数据,不过这里不需要进行太多处理,不需要对图片进行翻转或修改亮度、对比度,不过需要裁剪图片正中间的24*24大小的区块,
# 并进行数据标准化操作
images_test, labels_test = cifar10_input.inputs(eval_data=True, data_dir=data_dir, batch_size=batch_size)

# 因为batch_size在之后定义网络结构时被用到了,所以数据尺寸中的第一个值即样本条数需要被预先设定,而不能像以前那样设置为None
# 而数据尺寸中的图片尺寸为24*24即是剪裁后的大小,颜色通道数则设为3
image_holder = tf.placeholder(tf.float32, [batch_size, 24, 24, 3])
label_holder = tf.placeholder(tf.int32, [batch_size])

# 初始设置第一个卷积层,64个卷积核,卷积核大小是5*53通道
weight1 = variable_with_weight_losses(shape=[5, 5, 3, 64], stddev=5e-2, wl=0.0)
kernel1 = tf.nn.conv2d(image_holder, filter=weight1, strides=[1, 1, 1, 1], padding='SAME')
bias1 = tf.Variable(tf.constant(0.0, shape=[64]))
conv1 = tf.nn.relu(tf.nn.bias_add(kernel1, bias1))
# 使用尺寸3*3步长2*2的最大池化层处理数据,这里最大池化的尺寸和步长不一样,可以增加数据的丰富性
pool1 = tf.nn.max_pool(conv1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')
# 使用LRN对结果进行处理
# LRN最早见于Alex那篇用CNN参加ImageNet比赛的论文,Alex在论文中解释LRN层模仿了生物神经系统的"侧抑制"机制,
# 对局部神经元的活动创建竞争环境,使得其中响应比较大的值变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力
# Alex在ImageNet数据集上的实验表明,使用LRN后CNN在Top1的错误率可以降低1.4%,因此其在经典AlexNet中使用了LRN层
# LRN对ReLU这种没有上限边界的激活函数会比较有用,因为它会从附近的多个卷积核的响应中挑选比较大的反馈
# 但不适合Sigmoid这种有固定边界并且能抑制过大值得激活函数
norm1 = tf.nn.lrn(pool1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75)

# 创建第二个卷积层
# 上面64个卷积核,即输出64个通道,所以本层卷积核尺寸的第三个维度即输入的通道数也需要调整为64
weight2 = variable_with_weight_losses(shape=[5, 5, 64, 64], stddev=5e-2, wl=0.0)
kernel2 = tf.nn.conv2d(norm1, weight2, [1, 1, 1, 1], padding='SAME')
# 还有这里的bias值全部初始化为0.1,而不是0.最后,调换了最大池化层和LRN层的顺序,先进行LRN层处理,再使用最大池化层
bias2 = tf.Variable(tf.constant(0.1, shape=[64]))
conv2 = tf.nn.relu(tf.nn.bias_add(kernel2, bias2))
norm2 = tf.nn.lrn(conv2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75)
pool2 = tf.nn.max_pool(norm2, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')

# 两个卷积层之后,是全连接层
# 先把第二个卷积层之后的输出结果flatten,使用tf.reshape函数将每个样本都变成一维向量,使用get_shape函数获取数据扁平化之后的长度
reshape = tf.reshape(pool2, [batch_size, -1])
dim = reshape.get_shape()[1].value
# 接着初始化权值,隐含节点384个,正太分布的标准差设为0.04,bias的值也初始化为0.1
# 注意这里我们希望这个全连接层不要过拟合,因此设了一个非零的weight loss值0.04,让这一层具有L2正则所约束。
weight3 = variable_with_weight_losses(shape=[dim, 384], stddev=0.04, wl=0.004)
bias3 = tf.Variable(tf.constant(0.1, shape=[384]))
# 最后我们依然使用ReLU激活函数进行非线性化
local3 = tf.nn.relu(tf.matmul(reshape, weight3) + bias3)

# 接下来还是全连接层,只是隐含节点只有一半,其他一样
weight4 = variable_with_weight_losses(shape=[384, 192], stddev=0.04, wl=0.004)
bias4 = tf.Variable(tf.constant(0.1, shape=[192]))
local4 = tf.nn.relu(tf.matmul(local3, weight4) + bias4)

# 最后一层,依然先创建一层weight,其正太分布标准差设为一个隐含层节点数的倒数,并且不用L2正则
# 这里没有用之前的softmax输出最后结果,这里把softmax操作放在了计算loss部分,其实我们不需要对inference的输出进行softmax
# 处理就可以获得最终分类结果(直接比较inference输出的各类的数值大小即可),计算softmax主要是为了计算loss,因此softmax操作整合到后面合理
weight5 = variable_with_weight_losses(shape=[192, 10], stddev=1/192.0, wl=0.0)
bias5 = tf.Variable(tf.constant(0.0, shape=[10]))
logits = tf.add(tf.matmul(local4, weight5), bias5)


# 到这里就完成了整个网络inference的部分,梳理整个网络结构,设计性能良好的CNN是有一定规律可循的,但是想要针对某个问题设计最合适的
# 网络结构,是需要大量实际摸索的
# 完成模型inference的构建,接下来是计算CNN的loss,这里依然是用cross_entropy,这里我们把softmax的计算和cross_entropy的计算
# 合在了一起,即 tf.nn.sparse_softmax_cross_entropy_with_logits()
# 这里使用 tf.reduce_mean() 对 cross entropy计算均值,再使用 tf.add_to_collection()把cross entropy的loss添加到整体
# losses的collection中,最后,使用tf.add_n将整体losses的collection集合中的全部loss求和,得到最终的loss,其中包括
# cross entropy loss, 还有后两个全连接层中weight的L2 loss
def loss(logits, labels):
    labels = tf.cast(labels, tf.int64)
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
        logits=logits, labels=labels, name='cross_entropy_per_example'
    )
    cross_entropy_mean = tf.reduce_mean(cross_entropy,
                                        name='cross_entropy')
    tf.add_to_collection('losses', cross_entropy_mean)

    return tf.add_n(tf.get_collection('losses'), name='total_loss')


loss = loss(logits=logits, labels=label_holder)
# 优化器依然选择Adam Optimizer, 学习速率0.001
train_op = tf.train.AdamOptimizer(1e-3).minimize(loss)

# 使用 tf.nn.in_top_k()函数求输出结果中 top k的准确率,默认使用top 1,也就是输出分数最高的那一类的准确率
top_k_op = tf.nn.in_top_k(logits, label_holder, 1)

sess = tf.InteractiveSession()
tf.global_variables_initializer().run()

# 前面对图像进行数据增强的操作需要耗费大量CPU时间,因此distorted_inputs使用了16个独立的线程来加速任务,函数内部会产生线程池,
# 在需要使用时会通过TensorFlow queue进行调度
# 启动图片数据增强的线程队列,这里一共使用了16个线程来进行加速,如果不启动线程,那么后续inference以及训练的操作都是无法开始的
tf.train.start_queue_runners()

# 进行训练
for step in range(max_steps):
    start_time = time.time()
    image_batch, label_batch = sess.run([images_train, labels_train])
    _, loss_value = sess.run([train_op, loss],
                             feed_dict={image_holder: image_batch, label_holder: label_batch})
    duration = time.time() - start_time
    if step % 10 == 0:
        examples_per_sec = batch_size / duration
        sec_per_batch = float(duration)

        format_str = 'step %d, loss = %.2f (%.1f examples/sec; %.3f sec/batch)'
        print(format_str % (step, loss_value, examples_per_sec, sec_per_batch))


# 评测模型在测试集上的准确率
# 我们依然像训练时那样使用固定的batch_size,然后一个batch一个batch输入测试数据
num_examples = 10000
# 先计算一共要多少个batch才能将全部样本评测完
num_iter = int(math.ceil(num_examples / batch_size))
true_count = 0
total_sample_count = num_iter * batch_size
step = 0
while step < num_iter:
    image_batch, label_batch = sess.run([images_test, labels_test])
    predictions = sess.run([top_k_op], feed_dict={image_holder: image_batch,
                                                  label_holder: label_batch})
    true_count += np.sum(predictions)
    step += 1

precision = true_count / total_sample_count
print('precision @ 1 = %.3f' % precision)

# 最终,在cifar-10数据集上,通过一个短时间小迭代的训练,可以达到大致73%的准确率,持续增加max_steps,可以期望准确率逐渐增加
# 如果max_steps比较大,则推荐使用学习速率衰减decay的SGD进行训练,这样训练过程中能达到的准确率峰值会比较高,大致有86%
# 其中L2正则以及LRN层的使用都对模型准确率有提升作用,它们都可以提升模型的泛化能力

# 数据增强Data Augmentation在我们的训练中作用很大,它可以给单幅图增加多个副本,提高图片的利用率,防止对某一张图片结构的学习过拟合
# 这刚好是利用了图片数据本身的性质,图片的冗余信息量比较大,因此可以制造不同的噪声并让图片依然可以被识别出来。如果神经网络可以克服这些
# 噪声并准确识别,那么他的泛化能力必然很好。数据增强大大增加了样本量,而数据量的大小恰恰是深度学习最看重的,深度学习可以在图像识别上领先
# 其他算法的一大因素就是它对海量数据的利用效率非常高。其他算法,可能在数据量大到一定程度时,准确率就不再上升了,而深度学习只要提供足够
# 多的样本,准确率基本持续提升,所以说它是最适合大数据的算法

完整代码5
tensorflow_cnn_alexnet.py

from datetime import datetime
import math
import time
import tensorflow as tf


# ILSVRC(ImageNet Large Scale Visual Recognition Challenge)
# ImageNet项目于2007年由斯坦福大学华人教授李飞飞创办,目标是收集大量带有标注信息的图片数据供计算机视觉模型训练
# ImageNet拥有1500万张标注过的高清图片,总共拥有22000类,其中约有100万张标注了图片中主要物体的定位边框
# ImageNet项目最早的灵感来自于人类通过视觉学习世界的方式,如果假定儿童的眼睛是生物照相机,它们平均每200ms就拍照一次
# 眼球转动一次的平均时间200ms,那么3岁大的孩子就已经看过了上亿张真实时间的照片,使用亚马逊的土耳其机器人平台实现标注过程
# 来自世界上167个国家的5万名工作者帮忙一起筛选、标注

# 每年ILSVRC比赛数据集中大概有120万张图片,以及1000类的标注,是ImageNet全部数据的一个子集。比赛采用top-5和top-1分类错误率
# 作为模型性能的评测指标
# AlexNet比赛分类项目的2012年冠军,top5错误率16.4%8层神经网络
# 神经网络模型AlexNet可以算是LeNet的一种更深更宽的版本!AlexNet中包含了几个比较新的技术点,首次在CNN中成功应用了ReLU、Dropout、
# LRN等Trick,AlexNet包含了6亿3000万个连接,6000多万个参数!和65万个神经元,拥有5个卷积层,3个全连接层,其中3个卷积层后面
# 连接了最大池化层,输出层是1000类的softmax层做分类,LRN应用在了第一卷积层和第二卷积层后面,ReLU激活函数应用在8层每一层后面

# 1,运用ReLU,解决Sigmoid在网络层次较深时的梯度弥散
# 2,训练Dropout,随机忽略一些神经元,避免过拟合
# 3,使用重叠的最大池化,此前CNN普遍平均池化,最大池化避免平均池化的模糊化效果
# 4,提出了LRN层,局部神经元活动创建竞争机制,响应比较大的值变得更大,抑制其他反馈小的神经元,增强泛化能力
# 5,使用CUDA加速深度卷积网络的训练
# 6,数据增强,随机地从256*256的原始图像中截取224*224大小的区域,以及水平翻转的镜像,相当于增加了【(256-224^2*2=2048
# 倍的数据量。没有数据增强,紧靠原始的数据量,参数众多的CNN会陷入过拟合中

batch_size = 32
num_batchs = 100


# 定义一个现实网络每一层结构的函数print_actications,展示每一个卷积层或池化层输入tensor的尺寸
# 这个函数接受一个tensor作为输入,并显示其名称 t.op.name 和tensor尺寸 t.get_shape.as_list()
def print_activations(t):
    print(t.op.name, " ", t.get_shape().as_list())


def inference(images):
    parameters = []

    with tf.name_scope('conv1') as scope:
        kernel = tf.Variable(tf.truncated_normal([11, 11, 3, 64],
                                                 dtype=tf.float32, stddev=1e-1), name='weights')
        conv = tf.nn.conv2d(images, kernel, [1, 4, 4, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[64], dtype=tf.float32),
                             trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv1 = tf.nn.relu(bias, name=scope)
        print_activations(conv1)
        parameters += [kernel, biases]

    # 这里LRN参数基本都是AlexNet论文中的推荐值,不过目前除了AlexNet,其他经典的卷积神经网络基本都放弃了LRN
    # 主要是效果不明显,使用也会使得前馈、反馈的速度整体下降1/3,可以自主选择是否使用LRN
    lrn1 = tf.nn.lrn(conv1, depth_radius=4, bias=1.0, alpha=0.001/9, beta=0.75, name='lrn1')
    pool1 = tf.nn.max_pool(lrn1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='VALID', name='pool1')
    print_activations(pool1)

    with tf.name_scope('conv2') as scope:
        kernel = tf.Variable(tf.truncated_normal([5, 5, 64, 192],
                                                 dtype=tf.float32, stddev=1e-1, name='weights'))
        conv = tf.nn.conv2d(pool1, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[192],
                                         dtype=tf.float32), trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv2 = tf.nn.relu(bias, name=scope)
        parameters += [kernel, biases]
        print_activations(conv2)

    lrn2 = tf.nn.lrn(conv2, 4, bias=1.0, alpha=0.001/9, beta=0.75, name='lrn2')
    pool2 = tf.nn.max_pool(lrn2, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='VALID', name='pool2')
    print_activations(pool2)

    with tf.name_scope('conv3') as scope:
        kernel = tf.Variable(tf.truncated_normal([3, 3, 192, 384],
                                                 dtype=tf.float32, stddev=1e-1, name='weights'))
        conv = tf.nn.conv2d(pool2, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[384],
                                         dtype=tf.float32), trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv3 = tf.nn.relu(bias, name=scope)
        parameters += [kernel, biases]
        print_activations(conv3)

    with tf.name_scope('conv4') as scope:
        kernel = tf.Variable(tf.truncated_normal([3, 3, 384, 256],
                                                 dtype=tf.float32, stddev=1e-1, name='weights'))
        conv = tf.nn.conv2d(conv3, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[256],
                                         dtype=tf.float32), trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv4 = tf.nn.relu(bias, name=scope)
        parameters += [kernel, biases]
        print_activations(conv4)

    with tf.name_scope('conv5') as scope:
        kernel = tf.Variable(tf.truncated_normal([3, 3, 256, 256],
                                                 dtype=tf.float32, stddev=1e-1, name='weights'))
        conv = tf.nn.conv2d(conv4, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.Variable(tf.constant(0.0, shape=[256],
                                         dtype=tf.float32), trainable=True, name='biases')
        bias = tf.nn.bias_add(conv, biases)
        conv5 = tf.nn.relu(bias, name=scope)
        parameters += [kernel, biases]
        print_activations(conv5)

    pool5 = tf.nn.max_pool(conv5, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='VALID', name='pool5')
    print_activations(pool5)

    return pool5, parameters


# 最后我们返回这个池化层的输出pool5,这样inference函数就完成了,它可以创建AlexNet的卷积部分,在正式使用AlexNet来训练或预测时
# 还需要添加3个全连接层,隐含节点数分别为409640961000,由于最后3个全连接层的计算量很小,就没有房子计算速度评测中
# 大家在使用AlexNet时需要自行加上这3个全连接层

# 评估AlexNet每轮计算时间的函数
def time_tensorflow_run(session, target, info_string):
    num_steps_burn_in = 10  # 预热轮数
    total_duration = 0.0
    total_duration_squared = 0.0

    for i in range(num_batchs + num_steps_burn_in):
        start_time = time.time()
        _ = session.run(target)
        duration = time.time() - start_time
        if i >= num_steps_burn_in:
            if not i % 10:
                print('%s: step %d, duration = %.3f' %
                      (datetime.now(), i - num_steps_burn_in, duration))
            total_duration += duration
            total_duration_squared += duration * duration

    mn = total_duration / num_batchs
    vr = total_duration_squared / num_batchs - mn * mn
    sd = math.sqrt(vr)
    print('%s: %s across %d steps, %.3f +/- %.3f sec / batch' %
          (datetime.now(), info_string, num_batchs, mn, sd))


def run_benchmark():
    with tf.Graph().as_default():
        image_size = 224
        images = tf.Variable(tf.random_normal([batch_size,
                                               image_size,
                                               image_size,
                                               3],
                                              dtype=tf.float32,
                                              stddev=1e-1))

        pool5, parameters = inference(images)

        init = tf.global_variables_initializer()
        sess = tf.Session()
        sess.run(init)

        time_tensorflow_run(sess, pool5, "Forward")

        # 模拟训练过程
        objective = tf.nn.l2_loss(pool5)
        grad = tf.gradients(objective, parameters)
        time_tensorflow_run(sess, grad, "Forward-backward")


run_benchmark()
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

海宝7号

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

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

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

打赏作者

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

抵扣说明:

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

余额充值