tensorflow第三四章

initializer  初始化程序,初始化设定

第三章 TensorFlow入门

3.1 Tensorflow计算模型--计算图

3.1.1 计算图的概念

flow 流动;流量;涨潮,泛滥

tensor 张量

1.计算图是Tensorflow最基本的一个概念;

Tensorflow所有的计算都会转化为计算图上的节点;

tensor  张量的意思,可以理解为多维数组,可以理解为它的数据结构;

flow  流,体现了计算模型;

tensorflow是通过计算图的形式来表述计算的编程系统;

每一个计算都是计算图上的一个节点,节点之间的变描述为计算之间的依赖关系;

每一个节点就是一个运算,而每一条边代表了计算之间的依赖关系;

如果一个运算的输入依赖另一个运算的输出,这这两个运算由依赖关系;

3.1.2 计算图的使用

1.TensorFlow程序分为两个阶段:

第一个阶段定义计算图中所有的计算;

例:

import tensorflow as tf    

#常用载入tensorflow的技巧,这样可以用tf来代替tensorflow作为模块名称;

a=tf.constant([1,2],name="a")       #定义向量

b=tf.constant([1,2],name="b")

rusult=a+b

tensorflow会自动将定义的计算转化为计算图上的节点;

第二个阶段是执行计算;

:在tensorflow程序中,系统会自动维护一个默认的计算图;

通过tf.get_default_graph函数可获取当前默认的计算图;

例:

import tensorflow as tf

a=tf.constant([1.0,2.0],name="a")

b=tf.constant([3.0,4.0],name="b")

result=a+b

print(a.graph is tf.get_default_graph())

运行结果:

true

2. 不同计算图上的张量和运算都不会共享;

例:

import tensorflow as tf

 

g1=tf.Graph()

with g1.as_default():

    v=tf.get_variable(

            "v",initializer=tf.zeros_initializer(shape=[1]))

    

g2=tf.Graph()

with g2.as_default():

    v=tf.get_variable(

            "v",initializer=tf.ones_initializer(shape=[1]))

 

with tf.Session(graph=g1) as sess:

   initialize_all_variables().run()

   with tf.variable_scope("",reuse=True):

       print(sess.run(tf.get_variable("v")))

       

with tf.Session(graph=g2) as sess:

   initialize_all_variables().run()

   with tf.variable_scope("",reuse=True):

       print(sess.run(tf.get_variable("v")))

tensorflow的计算图可以用来隔离张量和计算,也提供了管理和计算张量的机制;

tf.Graph.device函数指定运行计算的设备;

例:

将加法计算跑在GPU上:

g=tf.Graph()

with g.device(/gpu:0):

  result=a+b

3. 通过tf.add_to_collection函数将资源加入一个或多个集合中;

   通过tf.get_collection获取一个集合里面的所有资源(这里的资源可以是张量、变量或者运行tensorflow程度所需要的队列资源);

3.2 tensorflow数据模型--张量

1.零阶张量表示标量(scalar),也就是一个数;

  第一阶张量为向量(vector),也就是一个一维数组;

  n阶张量可以理解为一个n维数组;

  在张量中并没有真正保存数字,它保存的是如何得到这些数字的计算过程;

2. tensorflow计算的结果不是一个具体的数字,而是一个张量的结构;

3. 一个张量主要保存了三个属性:名字(name)、维度(shape)、类型(type);

例:

import tensorflow as tf

a=tf.constant([1.0,2.0],name="a")

b=tf.constant([2.0,3.0],name="b")

result=tf.add(a,b,name="add")

print(result)     #注:因为版本的升级,这里应该是print(内容)加上括号!

输出结果:

Tensor("add:0", shape=(2,), dtype=float32)

4.张量的第一个属性名字(name)不仅是一个张量的唯一的标识符,它同样给出了这个张量是如何计算出来的;

张量和计算图上节点所代表的计算结果是对应的;

  张量的命名形式:“node:src_output”;其中node为节点的名称,src_output表示当前张量来自节点的第几个输出;

如:“add:0”说明了result这个张量是计算节点“add”输出的第一个结果(编号从0开始);

4. 张量的第二个属性是张量的维度(shape);这个属性描述了一个张量的维度信息;

如:shape=(2,)说明张量result是一个一维数组,这个数组的长度是2;

5. 张量的第三个属性是类型(type);每一个张量会有一个唯一的类型;

  tensorflow会对参与运算的所有张量进行类型的检查,当发现类型不匹配时会报错;

6. 一般通过指定dtype来明确指出变量或者常量的类型;

  如:a=tf.constant([1,2],name=a,dtype=tf.float32) #将一个加数指定成实数类型

7. tensorflow支持14种不同的类型:

 实数(tf.float32、tf.float64)

 整数(tf.int8、tf.int16、tf.int32、tf.int64、tf.uint8)

 布尔型(tf.bool)

复数(tf.complex64、tf.complex128)

3.2.2 张量的使用

1.如:在卷积神经网络中,卷积层或者池化层有可能改变张量的维度,通过result.get_shape函数来获取结果张量的维度信息可以免去人工计算的麻烦;

2.使用tf.Session().run(result)语句来得到计算结果;

3.3 tensorflow运行模型--会话

1.会话拥有并管理tensorflow程序运行时的所有资源;

2.当所有计算完成之后需要关闭会话来帮助系统回收资源,否则可能出现资源泄漏的问题;

3.TensorFlow中使用会话的模式有两种,第一种模式需要明确调用会话生成函数和关闭会话函数;

这种模式的代码流程如下:

#创建一个会话

sess=tf.Session()

#使用这个创建好的会话来得到关心的运算的结果;如可以调用sess.run(result),

sess.run(...)

#关闭会话使本次运行中使用到的资源可以被释放

sess.close()

4. 当程序因为异常而退出时,关闭会话的函数可能就不会被执行从而导致资源泄漏;

5.为了解决异常退出时资源释放的问题,TensorFlow可以通过Python的上下文管理器来使用会话:

以下代码展示了如何使用这种模式:

#创建一个会话,并通过Python中的上下文管理器来管理这个会话

with tf.Session() as sess:

#使用这创建好的会话来计算关心的结果

sess.run(...)

#不需要再调用“Session.close()”函数来关闭会话

#当上下文退出时会话关闭和资源释放也自动完成

6.TensorFlow会自动生成一个默认的计算图,但是TensorFlow不会自动生成默认的会话,而是需要手动指定;

当默认的会话被指定之后可以通过tf.Tensor.eval函数计算一个张量的取值;

以下代码展示了通过设定默认会话计算张量的取值:

sess=tf.Session()

with sess.as_default():

  print(result.eval())

以下代码也可以完成相同的功能:

sess=tf.Session()

#下面的两个命令有相同的功能

print(sess.run(result))

print(result.eval(session=sess))

8. 使用tf.InteractiveSession函数会自动将生成的会话注册为默认会话:

以下代码展示了tf.InteractiveSession函数的用法

sess=tf.InteractiveSession()

print(result.eval())

sess.close()

9. 通过ConfigProto Protocol Buffer来配置需要生成的会话:

下面是通过ConfigProto配置会话的方法:

config=tf.ConfigProto(allow_soft_placement=True,log_device_placement=True)

sess1=tf.InteractiveSession(config=config)

sess2=tf.Session(config=config)

10. 通过ConfigProto可以配置类似并行的线程数、GPY分配策略等参数;

这些参数中,最常用的是:

第一个是allow_soft_placement,这是个布尔型的参数;

第二个是log_device_placement,也是一个布尔型的参数;

3.4 TensorFlow实现神经网络

propagation 增殖,繁殖,传播

3.4.1 TensorFlow 游乐场及神经网络简介

TensorFlow游乐场:http://playground.tensorflow.org

vector 矢量;带菌者;航线

neuron 神经元,神经细胞

regularization  规则化;调整;合法化;正则化

人工神经网络(Artificial Neural Networks,简写为ANNs)

1. 神经网络的前向传播算法:定义神经网络的结构,并定义如何从神经网络的输入得到输出,这个过程就是神经网络的前向传播算法。

3.4.2 前向传播算法简介

1.神经元是构成一个神经网络的最小单元;

2.一个神经元有多个输入和一个输出;

3.每个神经元的输入既可以是其他神经元的输出,也可以是整个神经网络的输入;

4.神经网络的结构:不同神经元之间的连接结构;

5.一个最简单的神经元结构的输出就是所有输入的加权和;

6.神经元的参数:不同输入的权重;

7.神经网络的优化过程就是优化神经元中参数取值的过程;

8.全连接神经网络:相邻两层之间任意两个节点之间都有连接;

9.计算神经网络的前向传播结果需要三部分信息:

第一部分:神经网络的输入;

第二部分:神经网络的连接结构;

第三部分:每个神经元的参数;

11. 使用tf.matmul实现矩阵乘法的功能;

如:a=tf.matmul(x,w1)

y=tf.matmul(a,w2)    #pdf 53页

3.4.3 神经网络参数与TensorFlow变量

1.变量(tf.Variable)的作用:保存和更新神经网络中的参数;

2.和其他编程语言类似,TensorFlow中的变量也需要指定初始值;

例:

TensorFlow中声明一个2x3的矩阵变量的方法:

weights=tf.Variable(tf.random_normal([2,3],stddev=2))

这段代码调用了TensorFlow变量的声明函数tf.Variable。

这段代码中的矩阵的元素是均值为0,标准差为2的随机数;

 

注:表中的“正太分布”应该是“正态分布”;

常用的常量声明方法如下:

 

3.在神经网络中,偏置项(bias)通常会使用常数设置初值;

例:

biases=tf.Variable(tf.zeros([3]))

这段代码会生成一个初始值全部为0且长度为3的变量;

4.例:

w2=tf.Variable(weights.initialized_value())

w3=tf.Variable(weights.initialized_value()*2.0)

分析:

w2的初始值被设置成了与weights变量相同,w3的初始值则是weights初始值的2倍;

5. 样例(介绍如何通过变量实现神经网络的参数并实现前向传播的过程)

import tensorflow as tf

#声明w1、w2两个变量;通过seed参数设定了随机种子;

w1=tf.Variable(tf.random_normal([2,3],stddev=1,seed=1))

w2=tf.Variable(tf.random_normal([3,1],stddev=1,seed=1))

#这里的x是一个1x2的矩阵;                                                                                                                

x=tf.constant([[0.7,0.9]])

a=tf.matmul(x,w1)

y=tf.matmul(a,w2)

sess=tf.Session()

sess.run(w1.initializer)  #初始化w1

sess.run(w2.initializer)  #初始化w2

print(sess.run(y))

sess.close()

结果:

[[ 3.95757794]]

6. 通过tf.global_variables_initializer函数实现初始化所有变量的过程;

例:

init_op=tf.global_variables_initializer()

sess.run(init_op)

注:通过tf.initialize_all_variables函数就不需要将变量一个一个初始化了,这个函数会自动处理变量之间的依赖关系;

7. 变量只是一种特殊的张量;

8. 神经网络可以通过变量声明函数中的trainable参数来区分需要优化的参数和其他参数;

如果声明变量时参数trainable为True,那么这个变量将会被加入GraphKeys.TRAINABLE_VARIABLES集合;

9.在TensorFlow中可以通过tf.trainable_variables函数得到所有需要优化的参数;

10.TensorFlow中提供的神经网络优化算法会将GraphKeys.TRAINABLE_VARIABLES集合中的变量作为默认的优化对象;

11.一个变量在构建之后,它的类型就不能改变了;

例:

#前面所说的w1的类型为random_normal结果的默认类型tf.float32;

w1=tf.Variable(tf.random_normal([2,3],stddev=1),name=w1)

w2=tf.Variable(tf.random_normal([2,3],dtype=tf.float64,stddev=1),name=w2)

w1.assign(w2)

分析:这个程序会报错:(会报出类型不匹配的错误)

维度在程序运行中是有可能改变的,但是需要通过设置参数validate_shape=False;

例:

w1=tf.Variable(tf.random_normal([2,3],stddev=1),name=w1)

w2=tf.Variable(tf.random_normal([2,2],stddev=1),name=w2)

#下面这句会报维度不匹配的错误:

#ValueError:Shape(2,3)and(2,2)are not compatible(兼容的;能共处的;可并立的)

tf.assign(w1,w2)

#这一句将会被成功执行

tf.assign(w1,w2,validate_shape=False)

3.4.4 通过TensorFlow训练神经网络模型

 

batch  分批处理;批处理脚本

1. 反向传播算法实现了一个迭代的过程,在每次迭代的开始,首先需要选取一小部分训练数据,这一小部分数据叫做一个batch;

2. 通过TensorFlow实现反向传播算法的第一步是使用TensorFlow表达一个batch的数据;

3. TensorFlow提供了placeholder机制用于提供输入数据;placeholder相当于定义了一个位置,这个位置中的数据在程序运行时再指定;

4. placeholder定义时,这个位置上的数据类型是需要指定的;

5. placeholder的类型是不可以改变的;

6. placeholder中数据的维度信息可以根据提供的数据推导得出,所以不一定要给出;

例:

下面是通过placeholder实现前向传播算法的代码:

import tensorflow as tf

w1=tf.Variable(tf.random_normal([2,3],stddev=1))

w2=tf.Variable(tf.random_normal([3,1],stddev=1))

#定义placeholder作为存放输入数据的地方;这里维度也不一定要定义;

#但如果维度是确定的,那么给出维度可以降低出错的概率;

x=tf.placeholder(tf.float32,shape=(1,2),name="input")

a=tf.matmul(x,w1)

y=tf.matmul(a,w2)

sess=tf.Session()

init_op=tf.global_variables_initializer()

sess.run(init_op)

#下面这一行会报错:

#InvalidArgumentError: You must feed a value for placeholder

#tensor 'input_1' with dtype float and shape [1,2]

#print(sess.run(y))

#下面这一行将会得到结果:(注:运行的时候,每一次得到的结果都不一样

print(sess.run(y,feed_dict={x:[[0.7,0.9]]}))

分析:

feed_dict是一个字典(map),在字典中需要给出每个用到的placeholder的取值;

若某个需要的placeholder没有被指定取值,那么程序在运行时将会报错;

7. 在上例程序中,如果将输入的1x2矩阵改为nx2的矩阵,那么就可以得到n个样例的前向传播结果了;

其中nx2的矩阵的每一行为一个样例数据;

这样前向传播的结果为nx1的矩阵,这个矩阵的每一行就代表了一个样例的前向传播结果;

例:

import tensorflow as tf

w1=tf.Variable(tf.random_normal([2,3],stddev=1))

w2=tf.Variable(tf.random_normal([3,1],stddev=1))

x=tf.placeholder(tf.float32,shape=(3,2),name="input")

a=tf.matmul(x,w1)

y=tf.matmul(a,w2)

sess=tf.Session()

init_op=tf.initialize_all_variables()

sess.run(init_op)

#因为x在定义时指定了n为3,所以在运行前向传播过程时需要提供3个样例数据

print(sess.run(y,feed_dict={x:[[0.7,0.9],[0.1,0.4],[0.5,0.8]]}))

结果:

[[ 0.48611751]

 [-0.12917635]

 [ 0.23223528]]

8. 在得到一个batch的前向传播结果之后,需要定义一个损失函数来刻画当前的预测值和真实答案之间的差距;然后通过反向传播算法来调整神经网络参数的取值使得差距可以被缩小;

例:

以下代码定义了一个简单的损失函数,并通过TensorFlow定义了反向传播的算法:

#定义损失函数来刻画预测值与真实值的差距;

cross_entropy=-tf.reduce_mean(y_*tf.log(tf.clip_by_value(y,le-10,1.0)))

#定义学习率

learning_rate=0.001

#定义反向传播算法来优化神经网络中的参数

train_step=\tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy)

分析:

cross_entropy定义了真实值和预测值之间的交叉熵(cross_entropy),这是分类问题中一个常用的损失函数;

train_step定义了反向传播的优化方法;

9. TensorFlow中比较常用的优化方法有三种:tf.train.GradientDescentOptimizer、tf.train.AdamOptimizer、tf.train.MomentumOptimizer;

在定义了反向传播算法之后,通过运行sess.run(tf.train)就可以对所有的在GraphKeys.TRAINABLE_VARIABLES集合中的变量进行优化,使得在当前batch下损失函数更小。

3.4.5 完整神经网络样例程序

import tensorflow as tf

#Numpy是一个科学计算的工具包,这里通过Numpy工具包生成模拟数据集;

from numpy.random import RandomState

 

#定义训练数据batch的大小

batch_size=8

 

#定义神经网络的参数,还是用上面例子的程序;

w1=tf.Variable(tf.random_normal([2,3],stddev=1,seed=1))

w2=tf.Variable(tf.random_normal([3,1],stddev=1,seed=1))

 

#在shape的一个维度上使用None可以方便使用不同的batch大小。在训练时需要把

#数据分成比较小的batch,但是在测试时,可以一次性使用全部的数据。

#当数据集比较小时这样比较方便测试,但数据集比较大时,将大量数据放入一个

#batch可能会导致内存溢出

x=tf.placeholder(tf.float32,shape=(None,2),name='x-input')

y_=tf.placeholder(tf.float32,shape=(None,1),name='y-input')

 

#定义神经网络前向传播的过程

a=tf.matmul(x,w1)

y=tf.matmul(a,w2)

 

#定义损失函数和反向传播的算法

cross_entropy=-tf.reduce_mean(y_*tf.log(tf.clip_by_value(y,le-10,1.0)))

train_step=tf.train.AdamOptimizer(0.001).minimize(cross_entropy)

 

#通过随机数生成一个模拟数据集

rdm=RandomState(1)

dataset_size=128

X=rdm.rand(dataset_size,2)

#定义规则来给出样本的标签,在这里所以x1+x2<1的样例都被认为是正样本(如零件合格)

#其他为负样本(如零件不合格)

#这里使用0表示负样本,1表示正样本

#大部分解决分类问题的神经网络都会采用1和0的表示方法

Y=[[int(x1+x2<1)] for (x1,x2) in X]

 

#创建一个会话来运行TensorFlow程序

with tf.Session() as sess:

    init_op=tf.global_variables_initializer()

    #初始化变量

    sess.run(init_op)

print(sess.run(w1))

print(sess.run(w2))

运行结果(在训练之前神经网络参数的值):

[[-0.81131822  1.48459876  0.06532937]

 [-2.4427042   0.0992484   0.59122431]]

[[-0.81131822]

 [ 1.48459876]

 [ 0.06532937]]

#设定训练的次数

STEPS=5000

for i in range(STEPS):

  #每次选取batch_size个样本进行训练

   start=(i*batch_size) % dataset_size

   end=min(start+batch_size,dataset_size)

 

  #通过选取的样本训练神经网络并更新参数

   sess.run(train_step,feed_dict={x:X[start:end],y_:Y[start:end]})

   if i % 1000==0:

      #每隔一段时间计算在所有数据上的交叉熵并输出

      total_cross_entropy=sess.run(cross_entropy,feed_dict={x:X,y_:Y})

      print("After %d training steps(s),cross entropy on all data is %g"%

            (i,total_cross_entropy))

print(sess.run(w1))

print(sess.run(w2))

运行结果:(训练之后神经网络参数的值):

[[-0.81131822  1.48459876  0.06532937]

 [-2.4427042   0.0992484   0.59122431]]

[[-0.81131822]

 [ 1.48459876]

 [ 0.06532937]]

After 0 training steps(s),cross entropy on all data is 0.0674925

[[-0.81231821  1.4855988   0.06632921]

 [-2.44370413  0.1002484   0.59222424]]

[[-0.81231821]

 [ 1.4855988 ]

 [ 0.06632937]]

After 1000 training steps(s),cross entropy on all data is 0.0163385

[[-1.27549374  1.93239319  0.71818316]

 [-2.82764411  0.47066161  1.14189851]]

[[-1.21198606]

 [ 1.95956361]

 [ 0.55081969]]

After 2000 training steps(s),cross entropy on all data is 0.00907547

[[-1.51397622  2.1591146   1.07429051]

 [-3.01708913  0.64845419  1.46364999]]

[[-1.40506411]

 [ 2.20634365]

 [ 0.83948904]]

After 3000 training steps(s),cross entropy on all data is 0.00714436

[[-1.65394425  2.29218411  1.27439237]

 [-3.14156055  0.76467752  1.66820383]]

[[-1.52613485]

 [ 2.35394239]

 [ 1.01985705]]

After 4000 training steps(s),cross entropy on all data is 0.00578471

[[-1.79143536  2.42184758  1.46388769]

 [-3.28938985  0.90241849  1.88527477]]

[[-1.66073918]

 [ 2.50406837]

 [ 1.20711744]]

注:运行过程中出现的问题:运行结果会出现Attempted to use a closed Session报错提示,原因是因为‘STEPS=5000’这步出现了问题。如果“STEPS=5000”与“with tf.Session() as sess1:”对齐就会有这样的提示。

要注意语句的对齐!!

上面的程序实现了训练神经网络的全部过程。从这段程序中可以总结出训练神经网络的过程可以分为以下3个步骤:

1. 定义神经网络的结构和前向传播的输出结果;

2. 定义损失函数以及选择反向传播优化的算法;

3. 生成会话(tf.Session)并且在训练数据上反复运行反向传播优化算法;

 

第三章总结:

本章介绍了TensorFlow里最基本的三个概念:计算图(tf.Graph)、张量(tf.Tensor)、会话(tf.Session);

计算图是TensorFlow的计算模型,所有TensorFlow的程序都会通过计算图的形式表示;

计算图上的每一个节点都是一个运算;而计算图上的边则表示了运算之间的数据传递关系;

计算图上还保存了运行每个运算的设备信息以及运算之间的依赖关系;

计算图提供了管理不同集合的功能,并且TensorFlow会自动维护五个不同的默认集合;

张量是TensorFlow的数据模型,TensorFlow中所有运算的输入、输出都是张量;

张量本身并不存储任何数据,它只是对运算结果的引用;通过张量,可以更好地组织TensorFlow的运算模型,它管理了一个TensorFlow程序拥有的系统资源,所有的运算要通过会话执行;

 

4章 深层神经网络

4.1 深度学习与深层神经网络

1.维基百科对深度学习的精确定义:一类通过多层非线性变换对高复杂性数据建模算法的合集;

2.深层神经网络是实现“多层非线性变换”最常用的一种方法,在实际中基本上可以认为深度学习就是深层神经网络的代名词;

3.深度学习有两个非常重要的特性:多层和非线性

4.1.1 线性模型的局限性

1.在线性模型中,模型的输出为输入的加权和;

2.线性变换:一个线性模型中通过输入得到输出的函数被称之为一个线性变换;

3.线性模型的最大特点是任意线性模型的组合仍然是线性模型;

4.前向传播算法实现的就是一个线性模型;

5.只通过线性变换,任意层的全连接神经网络和单层神经网络模型的表达能力没有任何区别,并且它们都是线性模型;

6.线性模型最大的局限性:线性模型能够解决的问题是有限的;

激活函数(activation)

7.使用ReLU非线性激活函数可以更好的区分;  #pdf68页

4.1.2 激活函数实现去线性化

1.如果将每一个神经元(也就是神经网络中的节点)的输出通过一个非线性函数(这个非线性函数就是激活函数),那么整个神经网络的模型也就不再是线性的了;

 

2. 偏置项(bias)

3. 

 

 

偏置项可以被表达为一个输出永远为1的节点;

4. tensorflow提供了7种不同的非线性激活函数tf.nn.relu、tf.sigmoid和tf.tanh是比较常用的几个;TensorFlow也支持使用自己定义的激活函数;

5. 以下代码展示了如何通过TensorFlow实现前面所说的神经网络的前向传播算法:

a=tf.nn.relu(tf.matmul(x,w1)+biases1)

b=tf.nn.relu(tf.matmul(a,w2)+biases2)

可以看出,TensorFlow可以很好的支持使用了激活函数和偏置项的神经网络;

4.1.3 多层网络解决异或运算

1.感知机(perceptron);感知机可以简单的理解为单层的神经网络;

2.感知机会先将输入进行加权和,再通过激活函数得到输出;这个结构就是一个没有隐藏层的神经网络;

3.异或:两个输入的符号相同时(同时为正或同时为负),则输出为0,否则(一个正一个负),则输出为1;

感知机无法模拟异或运算的功能;

4. 深层神经网络实际上有组合特征提取的功能;这个特性对于解决不易提取特征向量的问题(比如图片识别、语音识别等)有很大帮助;

4.2 损失函数定义

1.神经网络模型的效果以及优化的目标是通过损失函数(loss function)来定义的;

4.2.1 经典损失函数

1.分类问题和回归问题是监督学习的两大种类;

分类问题希望解决的是将不同的样本分到事先定义好的类别中;

2. 通过神经网络解决多分类问题最常用的方法是设置n个输出节点,其中n为类别的个数;

   对于每一个样例,神经网络可以得到的一个n维数组作为输出结果;

   数组中的每一个维度(也就是每一个输出节点)对应一个类别;

   在理想情况下,如果一个样本属于类别k,那么这个类别所对应的输出节点的输出值应该为1,而其他节点的输出都为0;

3. 交叉熵cross entropy)刻画了两个概率分布之间的距离,它是分类问题中使用比较广的一种损失函数;

 

 

注:交叉熵刻画的是两个概率分布之间的距离,然而神经网络的输出却不一定是一个概率分布;

概率分布刻画了不同事件发生的概率;

当事件总数是有限的情况下,概率分布函数p(X=x)满足:

 

也就是说,任何事件发生的概率都在0和1之间,且总有某一个事件发生(概率的和为1);

4. Softmax回归本身可以作为一个学习算法来优化分类结果,但在TensorFlow中,Softmax回归的参数被去掉了,它只是一层额外的处理层,将神经网络的输出变成一个概率分布;

 

 

 

 

从以上公式可以看出,原始神经网络的输出被用作置信度来生成新的输出,而新的输出满足概率分布的所有要求;

这个新的输出可以理解为经过神经网络的推导,一个样例为不同类别的概率分别是多大;

5. 从交叉熵的公式中可以看到交叉熵函数不是对称的(H(p,q)H(q,p)),它刻画的是通过概率分布q来表达概率分布p的困难程度;

因为正确答案是希望得到的结果,所以当交叉熵作为神经网络的损失函数时,p代表的是正确答案,q代表的是预测值;

交叉熵刻画的是两个概率分布的距离,也就是说交叉熵值越小,两个概率分布越接近;

6. 通过TensorFlow实现交叉熵,代码如下:

cross_entropy=-tf.reduce_mean(y_*tf.log(tf.clip_by_value(y,le-10,1.0)))

分析

y_代表正确结果,y代表预测结果;

通过tf.clip_by_value函数可以将一个张量中的数值限制在一个范围之内,这样可以避免一些运算错误(比如log0是无效的);

例:

v=tf.constant([[1.0,2.0,3.0],[4.0,5.0,6.0]])

print tf.clip_by_value(v,2.5,4.5).eval()

#输出:[[2.5 2.5 3.][4. 4.5 4.5]]

分析:小于2.5的数都被换成了2.5,而大于4.5的数都被换成了4.5;这样通过tf.clip_by_value函数就可以保证在进行log运算时,不会出现log0这样的错误或者大于1的概率;

tf.log函数完成对张量中所有元素依次求对数的功能;

例:

v=tf.constant([1.0,2.0,3.0])

print tf.log(v).eval()

#输出:[0.   0.69314718    1.09861231]

在实现交叉熵的代码中直接将两个矩阵通过*”操作相乘;这个操作不是矩阵乘法,而是元素之间直接相乘;

矩阵乘法需要使用tf.matmul函数来完成;

下面给出两个操作的区别:

v1=tf.constant([[1.0,2.0].[3.0,4.0]])

v2=tf.constant([[5.0,6.0].[7.0,8.0]])

print(v1*v2).eval()

#输出[[5.  12.][21. 32.]]

print tf.matmul(v1,v2).eval()

#输出[[19. 22.][43. 50.]]

注:tf.matmul函数完成的是矩阵乘法运算;

例:(1,1)这个元素的值是:v1[1,1]×v2[1,1]+v1[1,2]×v2[2,1]=1×5+2×7=19

这三步计算得到的结果是一个n×m的二维矩阵,其中n为一个batch中样例的数量,m为分类的类别数量;

根据交叉熵的公式,应该将每行中的m个结果相加得到所有样例的交叉熵,然后再对这n行取平均得到一个batch的平均交叉熵;

因为分类问题的类别数量是不变的,所以可以直接对整个矩阵做平均而并不改变计算结果的意义;

以下代码简单展示了tf.reduce_mean函数的使用方法:

v=tf.constant([[1.0,2.0,3.0],[4.0,5.0,6.0]])

print tf.reduce_mean(v).eval()

#输出3.5

7. 交叉熵一般会与softmax回归一起使用,TensorFlow对这两个功能进行了统一封装,并提供了tf.nn.softmax_cross_entropy_with_logits函数;比如可以直接通过下面的代码来实现使用了softmax回归之后的交叉熵损失函数:

cross_entropy=tf.nn.softmax_cross_entropy_with_logits(y,y_)

分析:

y代表了原始神经网络的输出结果,y_给出了标准答案;这样通过一个命令就可以得到使用了Softmax回归之后的交叉熵;

在只有一个正确答案的分类问题中,TensorFlow提供了tf.nn.sparse_softmax_cross_entropy_with_logits函数来进一步加速计算过程;

8. 回归问题解决的是对具体数值的预测,如房价预测、销量预测等都是回归问题;这些问题需要预测的不是一个事先定义好的类别,而是一个任意实数;

9. 解决回归问题的神经网络一般只有一个输出节点,这个节点的输出值就是预测值;

10. 对于回归问题,最常用的损失函数是均方误差(MSE,mean squared error)。它的定义如下:

 

 

以下代码展示了如何通过tensorflow实现均方误差损失函数:

mse=tf.reduce_mean(tf.square(y_ -y))

其中y代表了神经网络的输出答案,y_代表了标准答案;

这里的“-”是两个矩阵中对应元素的减法;

4.2.2 自定义损失函数

 

码来实现这个损失函数:

loss=tf.reduce_sum(tf.select(tf.greater(v1,v2),(v1-v2)*a,(v2-v1)*b))

分析:

tf.greater的输入是两个张量,此函数会比较这两个输入张量中每一个元素的大小,并返回比较结果;当tf.greater的输入张量维度不一样时,TensorFlow会进行类似NumPy广播操作(broadcasting)的处理;

tf.select函数有三个参数;第一个为选择条件依据,当选择条件为True时,tf.select函数会选择第二个参数中的值,否则使用第三个参数中的值;

注:tf.select函数判断和选择都是在元素级别进行;

1. 以下代码展示了tf.select函数和tf.greater函数的用法:

import tensorflow as tf

v1=tf.constant([1.0,2.0,3.0,4.0])

v2=tf.constant([4.0,3.0,2.0,1.0])

sess=tf.InteractiveSession()

print(tf.greater(v1,v2).eval())

#输出:[False False  True  True]

print(tf.where(tf.greater(v1,v2),v1,v2).eval())

#输出:[4. 3. 3. 4.]

sess.close()

注:将tf.select换成tf.where

 

2. 下面通过一个简单的神经网络程序来讲解损失函数对模型训练结果的影响:

import tensorflow as tf

from numpy.random import RandomState

 

batch_size=8

 

#两个输入节点

x=tf.placeholder(tf.float32,shape=(None,2),name='x-input')

#回归问题一般只有一个输出节点

y_=tf.placeholder(tf.float32,shape=(None,1),name='y-input')

 

#定义了一个单层的神经网络前向传播的过程,这里就是简单加权和

w1=tf.Variable(tf.random_normal([2,1],stddev=1,seed=1))

y=tf.matmul(x,w1)

 

#定义预测多了和预测少了的成本

loss_less=10

loss_more=1

loss=tf.reduce_sum(tf.where(tf.greater(y,y_),(y-y_)*loss_more,(y-y_)*loss_less))

train_step=tf.train.AdamOptimizer(0.001).minimize(loss)

 

#通过随机数生成一个模拟数据集

rdm=RandomState(1)

dataset_size=128

X=rdm.rand(dataset_size,2)

#设置回归的正确值为两个输入的和加上一个随机变量。之所以要加上一个随机量是为了加入不可预测的噪音,否则不同损失函数的意义就不大了,因为不同损失函数都会在能完全预测正确的时候最低。一般来说噪音为一个均值为0的小量,所以这里的噪音设置为-0.05~0.05的随机数

Y=[[x1+x2+rdm.rand()/10.0-0.05] for (x1,x2) in X]

 

#训练神经网络

with tf.Session() as sess:

    init_op=tf.initialize_all_variables()

    sess.run(init_op)

    STEPS=5000

    for i in range(STEPS):

        start=(i*batch_size) % dataset_size

        end=min(start+batch_size,dataset_size)

        sess.run(train_step,feed_dict={x:X[start:end],y_:Y[start:end]})

        print(sess.run(w1))

结果:pdf80页

注:对于相同的神经网络,不同的损失函数会对训练得到的模型产生重要影响;

4.3 神经网络优化算法

1.反向传播算法(backpropagation)

梯度下降算法(gradient decent)

3. 梯度下降算法主要用于优化单个参数的取值;

  反向传播算法给出了一个高效的方式在所有参数上使用梯度下降算法,从而使神经网络模型在训练数据上的损失函数尽可能小;

  反向传播算法是训练神经网络的核心算法,它可以根据定义好的损失函数优化神经网络中参数的取值,从而使神经网络模型在训练数据集上的损失函数达到一个较小值;

4. 神经网络模型中参数的优化过程直接决定了模型的质量,是使用神经网络时非常重要的一步;

 

 

 

 

学习率ηlearning rate)

 

 

从表中可以看出,经过5次迭代后,参数x的值变成了0.0512,这个和参数最优值0已经比较接近了;

5. 神经网络的优化过程分为两个阶段

     第一个阶段先通过前向传播算法计算得到预测值,并将预测值和真实值作对比得出两者之间的差距;

     第二个阶段通过反向传播算法计算损失函数对每一个参数的梯度,再根据梯度和学习率使用梯度下降算法更新每一个参数;

6. 注意:梯度下降算法并不能保证被优化的函数达到全局最优解,如下图4-12所示:

   图中给出的函数就有可能只能得到局部最优解而不是全局最优解;

在小黑点处,损失函数的偏导为0,于是参数就不会再进一步更新;

   在这个样例中,如果参数x的初始值落在右侧深色的区间中,那么通过梯度下降得到的结果都会落到小黑点代表的局部最优解;只有当x的初始值落在左侧浅色的区间时梯度下降才能给出全局最优答案;

   由此可见,在训练神经网络时,参数的初始值会很大程度影响最后得到的结果;

   只有当损失函数为凸函数时,梯度下降算法才能保证达到全局最优解;

 

7. 梯度下降算法的另外一个问题是计算时间太长;

因为要在全部训练数据上最小化损失,所以损失函数J(θ)是在所有训练数据上的损失和;这样在每一轮迭代中都需要计算在全部训练数据上的损失函数;

在海量训练数据下,要计算所有训练数据的损失函数是非常消耗时间的;

为了加速训练过程,可以使用随机梯度下降的算法(stochastic gradient decent),这个算法优化的不是在全部训练数据上的损失函数,而是在每一轮迭代中,随机优化某一条训练数据上的损失函数;

因为随机梯度下降算法每次优化的只是某一条数据上的损失函数,所以他的问题非常明显,在某一条数据上损失函数更小并不代表在全部数据上损失函数更小,于是使用随机梯度下降优化得到的神经网络甚至可能无法达到局部最优;

为了综合梯度下降算法和随机梯度下降算法的优缺点,在实际应用中一般采用这两个算法的折中,每次计算一小部分训练数据的损失函数;这一小部分数据被称之为一个batch;

8. 以下代码给出了TensorFlow中如何实现神经网络的训练过程:(在本书的所有样例中,神经网络的训练都大致遵循以下过程)

batch_size=n

 

#每次读取一小部分数据作为当前的训练数据来执行反向传播算法

x=tf.placeholder(tf.float32,shape=(batch_size,2),name='x-input')

y_=tf.placeholder(tf.float32,shape=(batch_size,1),name='y-input')

 

#定义神经网络结构和优化算法

loss=...

train_step=tf.train.AdamOptimizer(0.001).minimize(loss)

 

#训练神经网络

with tf.Session() as sess:

    #参数初始化

    ...

    #迭代的更新参数

    for i in range(STEPS):

        #准备batch_size个训练数据;一般将所有训练数据随机打乱之后再选取可以得到更好的优化效果

        current_X,current_Y=...

        sess.run(train_step,feed_dict={x:current_X,y_:current_Y})

4.4 神经网络进一步优化

1.通过指数衰减的学习率可以让模型在训练的前期快速接近较优解,又可以保证模型在训练后期不会有太大的波动,从而更加接近局部最优;

2.滑动平均模型会将每一轮迭代得到的模型综合起来,从而使得最终得到的模型更加健壮(robust);

4.4.1 学习率的设置

1.设置学习率(learning rate)控制参数更新的速度;

2.学习率决定了参数每次更新的幅度;如果幅度过大,可能导致参数在极优值的两侧来回移动;当学习率过小时,虽然能保证收敛性,但是会大大降低优化速度;

3.学习率既不能过大,也不能过小;

4.为了解决设定学习率的问题,TensorFlow提供了一种更加灵活的学习率设置方法--指数衰减法

5.tf.train.exponential_decay函数实现了指数衰减学习率;通过这个函数,可以先使用较大的学习率来快速得到一个比较优的解,然后随着迭代的继续逐步减小学习率,使得模型在训练后期更加稳定;exponential_decay函数会指数级地减小学习率,它实现了以下代码的功能:

decayed_learning_rate=\

   learning_rate*decay_rate^(global_step/decay_steps)

分析:

decayed_learning_rate为每一轮优化时使用的学习率,learning_rate为事先设定的初始学习率,decay_rate为衰减系数,decay_steps为衰减速度;

5. tf.train.exponential_decay函数可以通过设置参数staircase选择不同的衰减方式;staircase的默认值为False,这时学习率随迭代轮数变化的趋势如图4-13中灰色曲线所示;staircase的值被设置为True时global_step/decay_steps会被转化成整数;这使得学习率成为一个阶梯函数(staircase function);图中4-13黑色曲线显示了阶梯状的学习率;

在这样的设置下,decay_steps通常代表了完整的使用一遍训练数据所需要的迭代轮数;这个迭代轮数就是总训练样本数除以每一个batch中的训练样本数;

这种设置的常用场景是每完整地过完一遍训练数据,学习率就减少一次;

这可以使得训练数据集中的所有数据对模型训练有相等的作用;

当使用连续的指数衰减学习率时,不同的训练数据有不同的学习率,而当学习率减小时,对应的训练数据对模型训练结果的影响也就小了。

 

6. 下面给出了一段代码示范如何在TensorFlow中使用tf.train.exponential_decay函数:

 

global_step=tf.Variable(0)

#通过exponential_decay函数生成学习率

learning_rate=tf.train.exponential_decay(0.1,global_step,100,0.96,staircase=True)

#使用指数衰减的学习率,在minimize函数中传入global_step将自动更新global_step参数,从而使得学习效率也得到相应更新

learning_step=tf.train.GradientDescentOptimizer(learning_rate).minimize(...my loss...,global_step=global_step)

分析:

上面的代码设定了初始学习率为0.1,因为指定了staircase=True,所以每训练100轮后学习率乘以0.96;

一般来说,初始学习率,衰减系数,衰减速度都是根据经验来设置的;

损失函数下降的速度和迭代结束之后总损失的大小没有必然的联系,也就是说并不能通过前几轮损失函数下降的速度来比较不同神经网络的效果;

4.4.2 过拟合问题

1.在真实应用中,希望通过训练出来的模型对未知的数据给出判断;

2.模型在训练数据上的表现并不一定代表了它在未知数据上的表现;

3.过拟合:当一个模型过于复杂之后,它可以很好地“记忆”每一个训练数据中随机噪音的部分而忘记了要去“学习”训练数据中通用的趋势;

4.过度拟合训练数据中的随机噪音虽然可以得到非常小的损失函数,但是对于未知数据可能无法做出可靠的判断;

 

 

 

 

 

 

 

6. 为了避免过拟合问题,一个非常常用的方法是正则化(regularization);

7. 正则化的思想就是在损失函数中加入刻画模型复杂程度的指标

假设用于刻画模型在训练数据上表现的损失函数为J(θ),那么在优化时不是直接优化J(θ),而是优化J(θ)+λR(w);其中R(w)刻画的是模型的复杂程度,而λ表示模型复杂损失在总损失中的比例;

θ表示的是一个神经网络中所有的参数,它包括边上的权重w和偏置项b

一般来说,模型复杂度只由权重w决定;

 

无论哪种正则化方式,基本的思想都是希望通过限制权重的大小,使得模型不能任意拟合训练数据中的随机噪音;

但是这两种正则化的方法有很大的区别

首先,L1正则化会让参数变得更稀疏,而L2正则化不会;

所谓参数变得更稀疏是指会有更多的参数变为0,这样可以达到类似特征选取的功能;

L2正则化不会让参数变得稀疏的原因是当参数很小时,如0.001,这个参数的平方基本上就可以忽略了,于是模型不会进一步将这个参数调整为0;

其次,L1正则化的计算公式不可导,而L2正则化公式可导;因为在优化时需要计算损失函数的偏导数,所以对含有L2正则化损失函数的优化要更加简洁;优化带L1正则化的损失函数要更加复杂,而且优化方法也有很多种;

在实践中,可以将L1正则化和L2正则化同时使用:

 

8. TensorFlow可以优化任意形式的损失函数;TensorFlow也可以优化带正则化的损失函数;

以下代码给出了一个简单的带L2正则化的损失函数定义:

w=tf.Variable(tf.random_normal([2,1],stddev=1,seed=1))

y=tf.matmul(x,w)

loss=tf.reduce_mean(tf.square(y_-y))+tf.contrib.layers.l2_regularizer(lambda)(w)

分析:

上面的程序中,loss为定义的损失函数,它由两个部分组成;

第一个部分是均方误差损失函数,它刻画了模型在训练数据上的表现;

第二个部分是正则化,它防止模型过渡模拟训练数据中的随机噪音;

lambda参数表示了正则化项的权重,也就是公式J(θ)+λR(w)中的λ

w为需要计算正则化损失的参数;

TensorFlow提供的tf.contrib.layers.l2_regularizer函数,它可以返回一个函数,这个函数可以计算一个给定参数的L2正则化项的值;

tf.contrib.layers.l1_regularizer可以计算L1正则化项的值;

  以下代码给出了使用这两个函数的样例:

import tensorflow as tf

weights=tf.constant([[1.0,-2.0],[-3.0,4.0]])

with tf.Session() as sess:

   #输出5   具体见pdf88页

   print(sess.run(tf.contrib.layers.l1_regularizer(.5)(weights)))

   #输出7.5

   print(sess.run(tf.contrib.layers.l2_regularizer(.5)(weights)))

:当网络结构复杂之后定义网络结构的部分和计算损失函数的部分可能不在同一个函数中,这样通过变量这种方式计算损失函数就不方便了;

9. 集合可以在一个计算图(tf.Graph)中保存一组实体(比如张量);

 下面的代码给出了通过集合计算一个5层神经网络带L2正则化的损失函数的计算方法:

import tensorflow as tf

 

#获取一层神经网络边上的权重,并将这个权重的L2正则化损失加入名称为‘losses’的集合中

def get_weight(shape,lambda):

    #生成一个变量

    var=tf.Variable(tf.random_normal(shape),dtype=tf.float32)

    #add_to_collection函数将这个新生成变量的L2正则化损失项加入集合。

    #这个函数的第一个参数‘losses’是集合的名字,第二个参数是要加入这个集合的内容

    tf.add_to_collection('losses',tf.contrib.layers.l2_regularizer(lambda)(var))

    #返回生成的变量

    return var

x=tf.placeholder(tf.float32,shape=(None,2))

y_=tf.placeholder(tf.float32,shape=(None,1))

batch_size=8

#定义了每一层网络中节点的个数

layer_dimension=[2,10,10,10,1]

#神经网络的层数

n_layers=len(layer_dimension)

 

#这个变量维护前向传播时最深层的节点,开始的时候就是输入层

cur_layer=x

#当前层的节点个数

in_dimension=layer_dimension[0]

 

#通过一个循环来生成5层全连接的神经网络结构

for i in range(1,n_layers):

    #layer_dimension[i]为下一层的节点个数

    out_dimension=layer_dimension[i]

    #生成当前层中权重的变量,并将这个变量的L2正则化损失加入计算图上的集合。

    weight=get_weight([in_dimension,out_dimension],0.001)

    bias=tf.Variable(tf.constant(0.1,shape=[out_dimension]))

    #使用ReLU激活函数

    cur_layer=tf.nn.relu(tf.matmul(cur_layer,weight)+bias)

    #进入下一层之前将下一层的节点个数更新为当前节点个数

    in_dimension=layer_dimension[i]

    

#在定义神经网络前向传播的同时已经将所有的L2正则化损失加入了图上的集合,这里只需要计算刻画模型在训练数据上表现的损失函数

mse_loss=tf.reduce_mean(tf.square(y_-cur_layer))

 

#将均方误差损失函数加入损失集合

tf.add_to_collection('losses',mse_loss)

 

#get_collection返回一个列表,这个列表是所有这个集合中的元素。在这个样例中,这些元素就是损失函数的不同部分,将它们加起来就可以得到最终的损失函数

loss=tf.add_n(tf.get_collection('losses'))

4.4.3 滑动平均模型

1.在采用随机梯度下降算法训练神经网络时,使用滑动平均模型在很多应用中都可以在一定程度提高最终模型在测试数据上的表现;

2.在TensorFlow中提供了tf.train.ExponentialMovingAverage来实现滑动平均模型;

3.初始化ExponentialMovingAverage时,需要提供一个衰减率(decay);这个衰减率用于控制模型更新的速度;

4.ExponentialMovingAverage对每一个变量会维护一个影子变量(shadow variable),这个影子变量的初始值就是相应变量的初始值,而每次运行变量更新时,影子变量的值会更新为:

shadow_variable=decay×shadow_variable+(1-decay)×variable

其中,shadow_variable为影子变量,variable为待更新的变量,decay为衰减率;

从公式中可以看到,decay决定了模型更新的速度,decay越大模型越趋于稳定;

在实际应用中,decay一般会设成非常接近1的数(如0.999或0.9999);

为了使得模型在训练前期可以更新的更快,ExponentialMovingAverage还提供了num_updates参数来动态设置decay的大小;

 

下面通过一段代码解释ExponentialMovingAverage是如何被使用的:

import tensorflow as tf

 

#定义一个变量用于计算滑动平均,这个变量的初始值为0。注意这里手动指定了变量的类型为tf.float32,因为所有需要计算滑动平均的变量必须是实数型

v1=tf.Variable(0,dtype=tf.float32)

#这里step变量模拟神经网络中迭代的轮数,可以用于动态控制衰减率

step=tf.Variable(0,trainable=False)

 

#定义一个滑动平均的类(class)。初始化时给定了衰减率(0.99)和控制衰减率的变量step

ema=tf.train.ExponentialMovingAverage(0.99,step)

#定义一个更新变量滑动平均的操作。这里需要给定一个列表,每次执行这个操作时,这个列表中的变量都会被更新。

maintain_averages_op=ema.apply([v1])

with tf.Session() as sess:

    #初始化所有变量

    init_op=tf.global_variables_initializer()

    sess.run(init_op)

    

    #通过ema.average(v1)获取滑动平均之后变量的取值。在初始化

    #之后变量v1的值和v1的滑动平均都为0.

print(sess.run([v1,ema.average(v1)]))

#输出:[0.0, 0.0]

 

#更新变量v1的值到5

sess.run(tf.assign(v1,5))

#更新v1的滑动平均值。衰减率为min{0.99,(1+step)/(10+step)=0.1}=0.1,所以v1的滑动平均会被更新为0.1x0+0.9x5=4.5

sess.run(maintain_averages_op)

print(sess.run([v1,ema.average(v1)]))

#输出[5.0, 4.5]

 

#更新step的值为10000

sess.run(tf.assign(step,10000))

#更新v1的值为10

sess.run(tf.assign(v1,10))

#更新v1的滑动平均值。衰减率为min{0.99,(1+step)/(10+step)0.999}=0.99,所以v1的滑动平均会被更新为0.99x4.5+0.01x10=4.555

sess.run(maintain_averages_op)

print(sess.run([v1,ema.average(v1)]))

#输出:[10.0, 4.5549998]

 

#再次更新滑动平均值,得到的新滑动平均值为0.99x4.555+0.01x10=4.60945

sess.run(maintain_averages_op)

print(sess.run([v1,ema.average(v1)]))

#输出:[10.0, 4.6094499]

 

4章小结:

1. 深度学习基本上就是深层神经网络的代名词;

2. 神经网络结构的两个总体原则:非线性结构和多层结构;

3. 非线性结构和多层结构是解决复杂问题的必要方法;

4. 神经网络是一个优化问题,而损失函数刻画了神经网络需要优化的目标;

5. 正则化通过在损失函数中加入对模型复杂程度的因素,可以有效避免过拟合问题;

6. 使用滑动平均模型让最后得到的模型在未知数据上更加强壮;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值