引言

TensorFlow 版本1.15pip3 install tensorflow==1.15.0

这是《TensorFlow实战Google深度学习框架(第2版)》的学习笔记,所有代码在TensorFlow 1.15版本中运行正常。虽然现在TensorFlow 2.x很流行了,但是个人觉得先学习下1.x也是很有必要的。

计算模型——计算图

计算图的概念

TensorFlow的名字说明了两个最重要的概念——Tensor和Flow。

第一个词Tensor表明了它的数据结构,张量,可理解为多维数组。
而Flow体现了它的计算模型,流。
TensorFlow是一个通过计算图的形式来表达计算的编程,TensorFlow中的每一个计算都是计算图上的一个节点,节点之间的边描述了计算之间的依赖关系。

计算图的使用

TensorFlow程序一般可以分为两个阶段。第一个阶段需要定义计算图中所有的计算。第二个阶段执行计算。以下代码给出了计算定义阶段的例子:

import tensorflow as tf
# 定义计算图中的计算
a = tf.constant([1.0,2.0], name='a')
b = tf.constant([2.0,3.0], name='b')
result = a + b

除了使用默认的计算图,TensorFlow支持通过tf.Graph函数来生成新的计算图。不同计算图上的张量和运算都不会共享。下面展示如何在不同计算图上定义和使用变量。

import tensorflow as tfg1 = tf.Graph()
with g1.as_default():# 在计算图g1中定义变量v,并设置初始值0v = tf.get_variable("v",initializer=tf.zeros_initializer()(shape=[1]))g2 = tf.Graph()
with g2.as_default():# 在计算图g2中定义变量v,并设置初始值1v = tf.get_variable("v",initializer=tf.ones_initializer()(shape=[1]))# 在计算图g1中读取变量v的取值
with tf.Session(graph=g1) as sess:# 初始化模型的参数 不加这句会报 Attempting to use uninitialized value v 异常# 当含有变量时,需要通过这句给变量进行初始化tf.global_variables_initializer().run()with tf.variable_scope("", reuse=True):print(sess.run(tf.get_variable('v'))) # [0.]# 在计算图g2中读取变量v的取值
with tf.Session(graph=g2) as sess:tf.global_variables_initializer().run()with tf.variable_scope("", reuse=True):print(sess.run(tf.get_variable('v'))) # [1.]

TensorFlow中的计算图不仅可以用来隔离张量和计算,还提供了管理张量和计算的机制。
计算图可以通过tf.Graph.device函数来指定运行计算的设备。以下程序可以将加法计算跑在GPU上:

a = tf.constant([1.0,2.0], name='a')
b = tf.constant([2.0,3.0], name='b')g = tf.Graph()
with g.device('/gpu:0'):result = a + b

数据模型——张量

张量是TensorFlow管理数据的形式。

张量的概念

张量可以理解为多维数组。零阶张量表示标量;第一阶张量为向量,即一维数组;第n阶张量可以理解为一个n维数组。

但在TensorFlow中张量并没有真正保存数字,保存的是如何得到这些数字的计算过程。以向量加法为例,当运行下面代码时,并不会得到加法的结果,而会得到对结果的一个引用。

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) # Tensor("add_1:0", shape=(2,), dtype=float32)

从上可以看出,TensorFlow计算的结果是一个张量的结构。一个张量中主要保存了三个属性:名字(name)、维度(shape)和类型(type)。

  • 名字是张量的唯一标识符,同时也体现了这个张量是如何计算出来的。
  • 维度描述了张量的维度信息,比如shape=(2,)说明张量result是一个一维数组,长度为2.
  • 每一个张量都会有一个唯一的类型,TensorFlow会对参与运算的所有张量进行类型的检查,若类型不匹配则会报错。

一般建议使用dtype来明确指定变量或者常量的类型。

a= tf.constant([l,2],name="a",dtype=tf.float32)

张量的使用

张量使用主要可以分为两类。

使用张量的第一类情况是对中间结果的引用。当一个计算包含很多中间结果时,使用张量可以大大提高代码的可读性。

# 使用张量记录中间结果
a = tf.constant([1.0,2.0], name='a')
b = tf.constant([2.0,3.0], name='b')
result = a + b# 直接计算向量的和,可读性较差
result = tf.constant([1.0,2.0], name='a') + tf.constant([2.0,3.0], name='b')

使用张量的第二类情况是当计算图构造完成之后,张量可以用来获取计算结果。虽然张量本身没有存储具体的结果数字,但通过会话就可以得到这些具体的数字。

运行模型——会话

会话拥有并管理TensorFlow程序运行时的所有资源。所以计算完成之后需要关闭会话来帮助系统回收资源,否则会出现资源泄漏的问题。

TensorFlow中使用会话的模型一般有两种,第一种需要明确调用会话生成函数和关闭会话函数,这种模式的代码流程如下。

# 定义计算图中的计算
a = tf.constant([1.0,2.0], name='a')
b = tf.constant([2.0,3.0], name='b')
result = a + b
# 创建一个会话
sess = tf.Session()
# 使用这个会话来得到运算的结果
print(sess.run(result)) # [3. 5.]
# 关闭会话,释放资源
sess.close()

使用这种模式,在完成所有计算之后,需要明确调用Session.close函数来关闭会话并释放资源。然而,当程序因为异常而退出时,关闭会话的函数可能就不会被执行从而导致资源泄漏。为了解决异常退出时资源释放的问题,TensorFlow可通过Python的上下文管理器来使用会话,以下代码展示了如何使用这种模式:

# 定义计算图中的计算
a = tf.constant([1.0,2.0], name='a')
b = tf.constant([2.0,3.0], name='b')
result = a + b
# 创建一个会话,通过上下文管理器来管理这个会话
with tf.Session() as sess:print(sess.run(result)) # [3. 5.]# 当上下文退出时会话关闭和资源释放也自动完成了

TensorFlow会自动生成一个默认的计算图,如果没有特殊指定,运算会自动加入这个计算图中。
TensorFlow中的会话也有类似的机制,但TensorFlow不会自动生成默认的会话,而是需要手动指定。
当默认的会话被指定之后可以通过tf.Tensor.eval函数来计算一个张量的取值。如下所示:

# 定义计算图中的计算
a = tf.constant([1.0,2.0], name='a')
b = tf.constant([2.0,3.0], name='b')
result = a + b
sess = tf.Session()
with sess.as_default(): #将sess作为默认会话print(result.eval()) # [3. 5.]

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

# 定义计算图中的计算
a = tf.constant([1.0,2.0], name='a')
b = tf.constant([2.0,3.0], name='b')
result = a + b
sess = tf.Session()
print(sess.run(result))
print(result.eval(session=sess))

在交互式环境下,通过设置默认会话的方式来获取张量的取值更加方便。TensorFlow提供了tf.InteractiveSession函数来在交互式环境下直接构建默认会话。使用这个函数会自动将生成的会话注册为默认会话:

# 定义计算图中的计算
a = tf.constant([1.0,2.0], name='a')
b = tf.constant([2.0,3.0], name='b')
result = a + b
sess = tf.InteractiveSession()
print(result.eval())
sess.close()

通过该函数可以省却将产生的会话注册为默认会话的过程。无论使用哪种方法都可以通过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)

allow_soft_placementTrue时,在以下任意一个条件成立时,GPU上的运算可以放到CPU上进行:

  • 运算无法再GPU上执行
  • 没有GPU资源
  • 运算输入包含对CPU计算结果的引用

log_device_placementTrue时将会在日志中记录每个节点被安排在哪个设备上,以方便调试。

TensorFlow实现神经网络

前向传播算法简介

上图是一个最简单的神经元结构,进行的计算就是矩阵乘法。TensorFlow中矩阵乘法是非常容易实现的。

a = tf.matmul(x,w)

神经网络参数与TensorFlow变量

在TensorFlow中,变量(tf.Variable)的作用就是保存和更新神经网络中的参数。和其他编程语言类似, TensorFlow中的变量也需要指定初始值。因为在神经网络中,给参数赋予随机初始值最为常见,所以一般也使用随机数给TensorFlow中的变量初始化。 下面给出了一种在TensorFlow中声明一个2×32 \times 32×3的矩阵变量的方法:

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

这段代码会产生一个2×32 \times 32×3的矩阵,矩阵中元素是均值为0,标准差为2的随机数。下表中给出了TensorFlow中常用的变量声明方法。

函数名称 功能 样例
tf.zeros 产生全0的数组 tf.zeros([2,3],int32) -> [[0,0,0],[0,0,0]]
tf.ones 产生全1的数组 tf.ones([2,3],int32) -> [[1,1,1],[1,1,1]]
tf.fill 产生一个全部为给定数字的数组 tf.fill([2,3],9) ->[[9,9,9],[9,9,9]]
tf.constant 产生一个给定值的常量 tf.constant([1,2,3]) -> [1,2,3]

在神经网络中,偏置(bias)通常会使用常数来设置初始值。以下代码给出了一个样例。

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

除了使用随机数或常数,TensorFlow也支持通过其他变量的初始值来初始化新的变量:

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

w2的初始值被设置为与weights变量相同。w3的初始值则是weights初始值的两倍。在TensorFlow中,一个变量的值在被使用之前,这个变量的初始化过程需要被明确地调用。以下样例介绍了如何通过变量实现神经网络的参数并实现前向传播过程。

import tensorflow as tf
# 声明w1,w2两个变量。指定随机种子
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 = tf.constant([[0.7, 0.9]]) # 1 x 2 的矩阵# 通过前向传播算法获得神经网络的输出
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)sess = tf.Session()
# 初始化w1和w2两个变量
sess.run(w1.initializer)
sess.run(w2.initializer)
print(sess.run(y)) # [[3.957578]]
sess.close()

在计算y之前,需要将所有用到的变量初始化。虽然在变量定义时给出了变量初始化的方法,但这个方法并没有被真正运行。
所以在计算y之前,需要通过运行w1.initializerw1.initializer来给变量赋值。

虽然这样直接调用每个变量的初始化过程是一个可行的方案,但是当变量数目增多,或变量之间存在依赖关系时,单个调用的方案就比价麻烦了。

为了解决这个问题,TensorFlow提供了一种更加便捷的方式来完成变量初始化过程。

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

通过tf.global_variables_initializer函数,就不需要将变量一个一个初始化了。我们知道,TensorFlow的核心概念是张量,那么变量和张量是什么关系呢?

在TensorFlow中,变量的声明函数tf.Variable是一个运算。这个运算的输出结果就是一个张量,这个张量也是本节介绍的变量。所以变量只是一种特殊的张量。

所有的变量都会被自动加入到GraphKeys.VARIABLES这个集合中。通过tf.global_variables()函数可以拿到当前计算图上所有的变量。
当构建机器学习模型时,可以通过变量声明函数中的trainable参数来区分需要优化的参数和其他参数。如果声明变量时参数trainableTrue,那么这个变量将会被加入到GraphKeys.TRAINABLE_VARIABLES集合。

可以通过tf.trainable_variables()函数得到所有需要优化的参数。TensorFlow中提供的神经网络优化算法会将GraphKeys.TRAINABLE_VARIABLES集合中的变量作为默认的优化对象。

一个变量在构建之后,它的类型就不能再改变了。它就不能被赋予其他类型的值,以下代码会报出类型不匹配错误:

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)

通过TensorFlow训练神经网络模型

通过TensorFlow实现反向传播算法的第一步是使用TensorFlow表达一个batch的数据。在前面我们尝试使用常量来表达过一个样例:

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

但是如果每轮迭代中的数据都要通过常量来表示,那么TensorFlow的计算图将会太大。因为每生成一个变量,TensorFlow都会在计算图中增加一个节点。一般来说,一个神经网络的训练过程会需要经过几百万轮甚至几亿轮的迭代,这样计算图就会非常大,而且利用率很低。

为了避免这个问题,TensorFlow提供了placeholder机制用于提供输入数据。placeholder占位符的意思,相当于定义了一个位置,这个位置中的数据在程序运行时再指定。这样在程序中就不需要生成大量常量来提供输入数据,而只需要将数据通过placeholder传入TensorFlow计算图。在placeholder定义时,这个位置上的数据类型是需要指定的。和其他张量一样,placeholder的类型也是不可改变的。

placeholder中数据的维度信息可以根据提供的数据推导得出,所以不一定要给出。下面给出了通过placeholder实现前向传播算法的代码。

import tensorflow as tfw1 = tf.Variable(tf.random_normal([2,3], stddev=1),name='w1')
w2 = tf.Variable(tf.random_normal([3,1], stddev=1),name='w2')# 定义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)
# 下面这行将报错
# print(sess.run(y))
# InvalidArgumentError: You must feed a value for placeholder tensor 'input' with dtype float and shape [1,2]
# 下面这行不会
print(sess.run(y, feed_dict={x: [[0.7, 0.9]]}))

在这段代码中计算前向传播结果时,需要提供一个feed_dict来指定x的取值。feed_dict是一个字典,在字典中需要给出每个用到的placeholder的取值。如果某个需要的placeholder没有被指定取值,那么程序将会报错。

以上代码只计算了一个样例的前向传播结果,但在训练神经网络时需要每次提供一个batch的训练样例。对于这样的需求,placeholder也可以很好地支持。
在上面的程序中,如果将输入的1×21 \times 21×2矩阵改为n×2n \times 2n×2的矩阵,那么就可以得到n个样例的前向传播结果了。 得到前向传播的结果为n×1n \times 1n×1的矩阵,这个矩阵的每一行就代表了一个样例的前向传播结果。以下代码给出了一个示例:

import tensorflow as tfw1 = tf.Variable(tf.random_normal([2,3], stddev=1),name='w1')
w2 = tf.Variable(tf.random_normal([3,1], stddev=1),name='w2')# 这里假设batch大小为3,n=3
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.global_variables_initializer()
sess.run(init_op)
# 下面这行将报错
# print(sess.run(y))
# InvalidArgumentError: You must feed a value for placeholder tensor 'input' with dtype float and shape [1,2]
# 下面这行不会
print(sess.run(y, feed_dict={x: [[0.7, 0.9],[0.1, 0.4],[0.5, 0.8]]}))

输出结果为:

[[-4.2367287][-1.478172 ][-3.5316126]]

在得到一个batch的前向传播结果之后,需要定义一个损失函数来刻画当前的预测值和真实值之间的差距。然后通过反向传播算法来调整神经网络参数的取值是的差距可以被缩小。下面定义一个简单的损失函数,并通过TensorFlow定义了反向传播的算法:

y = tf.sigmoid(y)
# 定义损失函数来刻画预测值与真实值的差距
cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)) + (1-y) * tf.log(tf.clip_by_value(1-y, 1e-10, 1.0))
)
# 定义学习率
learning_rate = 0.001
# 定义反向传播算法来优化神经网络中的参数
train_step = tr.train.AdamOptimizer(learning_rate).minimize(cross_entropy)

在定义了反向传播算法后,通过运行sess.run(train_step)就可以对所有在GraphKeys.TRAINABLE_VARIABLES集合中的变量进行初始化,使得在当前batch下损失函数更小。下面给出一个完整的训练神经网络样例程序。

完整神经网络样例程序

import tensorflow as tf
from numpy.random import RandomState# 定义训练数据batch的大小
batch_size = 8
# 定义神经网络的参数
w1 = tf.Variable(tf.random_normal([2,3], stddev=1),name='w1')
w2 = tf.Variable(tf.random_normal([3,1], stddev=1),name='w2')# 在shape的一个维度上用None可以方便使用不同的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)# 定义损失函数和反向传播的算法
y = tf.sigmoid(y)
cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)) + (1-y) * tf.log(tf.clip_by_value(1-y, 1e-10, 1.0))
)
# 定义学习率
learning_rate = 0.001
# 定义反向传播算法来优化神经网络中的参数
train_step = tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy)# 通过随机数生成一个模拟数据集
rdm = RandomState(1)
dataset_size = 128
X = rdm.rand(dataset_size, 2)
# 定义规则设置样本的标签 0表示负样本,1表示正样本
Y = [[int(x1 + x2 < 1)] for (x1, x2) in X]# 创建一个会话来运行程序
with tf.Session() as sess:init_op = tf.global_variables_initializer()# 初始化变量sess.run(init_op)print(sess.run(w1))print(sess.run(w2))# 设置训练的轮数STEPS = 5000for i in range(STEPS):# 每次选取batch_size个样本进行训练start = (i * batch_size) % dataset_sizeend = 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 step(s), cross entropy on all data is %g' % (i, total_cross_entropy))print(sess.run(w1))print(sess.run(w2))

输出为:

After 0 training step(s), cross entropy on all data is 0.763093
After 1000 training step(s), cross entropy on all data is 0.339521
After 2000 training step(s), cross entropy on all data is 0.0988844
After 3000 training step(s), cross entropy on all data is 0.0507982
After 4000 training step(s), cross entropy on all data is 0.0306918
[[ 1.3071561 -0.8735636  2.927468 ][ 3.5681686 -0.4556637  4.314473 ]]
[[ 1.750785 ][-1.4868999][ 2.7453477]]

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

  1. 定义神经网络的结构和前向传播的输出结果。
  2. 定义损失函数以及选择反向传播优化的算法。
  3. 生成会话并且在训练数据上反复运行反向传播优化算法。

TensorFlow学习笔记——TensorFlow入门相关推荐

  1. TensorFlow学习笔记(二):快速理解Tutorial第一个例子-MNIST机器学习入门 标签: 机器学习SoftmaxTensorFlow教程 2016-08-02 22:12 3729人阅

    TensorFlow学习笔记(二):快速理解Tutorial第一个例子-MNIST机器学习入门 标签: 机器学习SoftmaxTensorFlow教程 2016-08-02 22:12 3729人阅读 ...

  2. tensorflow学习笔记——使用TensorFlow操作MNIST数据(1)

    续集请点击我:tensorflow学习笔记--使用TensorFlow操作MNIST数据(2) 本节开始学习使用tensorflow教程,当然从最简单的MNIST开始.这怎么说呢,就好比编程入门有He ...

  3. TensorFlow学习笔记--第三节张量(tensor)及其定义方法

    目录 在TensorFlow中,所有的数据通过张量的形式来表示 1张量及属性: 1.1维数(阶) 1.2 形状 1.3数据类型 TensorFlow 支持以下三种类型的张量: **1.常量** **2 ...

  4. TensorFlow 深度学习笔记 TensorFlow实现与优化深度神经网络

    TensorFlow 深度学习笔记 TensorFlow实现与优化深度神经网络 转载请注明作者:梦里风林 Github工程地址:https://github.com/ahangchen/GDLnote ...

  5. Tensorflow学习笔记6:解决tensorflow训练过程中GPU未调用问题

    Tensorflow学习笔记6:解决tensorflow训练过程中GPU未调用问题 参考文章: (1)Tensorflow学习笔记6:解决tensorflow训练过程中GPU未调用问题 (2)http ...

  6. tensorflow学习笔记(三十二):conv2d_transpose (解卷积)

    tensorflow学习笔记(三十二):conv2d_transpose ("解卷积") deconv解卷积,实际是叫做conv_transpose, conv_transpose ...

  7. tensorflow学习笔记二——建立一个简单的神经网络拟合二次函数

    tensorflow学习笔记二--建立一个简单的神经网络 2016-09-23 16:04 2973人阅读 评论(2) 收藏 举报  分类: tensorflow(4)  目录(?)[+] 本笔记目的 ...

  8. Tensorflow学习笔记2:About Session, Graph, Operation and Tensor

    简介 上一篇笔记:Tensorflow学习笔记1:Get Started 我们谈到Tensorflow是基于图(Graph)的计算系统.而图的节点则是由操作(Operation)来构成的,而图的各个节 ...

  9. Win10:tensorflow学习笔记(4)

    前言 学以致用,以学促用.输出检验,完整闭环. 经过前段时间的努力,已经在电脑上搭好了深度学习系统,接下来就要开始跑程序了,将AI落地了. 安装win10下tensforlow 可以参照之前的例子:w ...

  10. Win10: tensorflow 学习笔记(3)

    前言 学以致用,以学促用.输出检验,完整闭环. 怕什么真理无穷,进一寸有一寸的欢喜--胡适 经过前段时间的努力,已经在电脑上搭好了深度学习系统,接下来就要开始跑程序了,将AI落地了. 安装win10下 ...

最新文章

  1. 【java】staitc
  2. 使用xslt将.xml,转换成一个html网页时中文显示乱码怎么办,用dom将数据写到xml中 设置xml文件gbk编码时中文会出现乱码...
  3. Go基础编程:基础数据类型
  4. windows下执行testng用例
  5. 9. grouped product
  6. 网易面试总结(2014.12.31)
  7. 测量脉冲宽度仿真proteus
  8. 我国网络广告的现状及未来发展趋势分析
  9. 薪火相传,构建生态——记约束求解基础与应用训练营
  10. FPGA的工作原理简述
  11. 戴维斯分校 计算机硕士,2017年美国加州大学戴维斯分校研究生申请之计算机......
  12. Python迭代器iter使用及python iter( )函数介绍
  13. 修改Windows的Internet时间服务器使时间同步
  14. sap客户信贷_SAP信贷控制功能与配置详解
  15. 骚操作之rsync下行同步+lnotify实时同步详解
  16. 连接池的原理以及分析
  17. 光纤激光输出的功率均匀性测试
  18. Week 6 Maze
  19. 台铃48V锂电电动车充电器维修一例
  20. 希赛教育计算机网络基础,2020年9月网络教育统考《计算机应用基础》计算机网络模拟题试卷1...

热门文章

  1. java day48【 Maven 介绍 、 Maven 的使用 、Maven 常用命令 、 maven 工程运行调试 、总结】...
  2. 网络操作系统第224页作业
  3. 51-nod(1443)(最短路)
  4. java调用C++代码
  5. 软件因丢失.dll文件(未找到)而无法启动?
  6. SharePoint 2007 在Windows Server 2008上列表Open with Windows Explorer失效 解决
  7. iOS 25个性能优化/内存优化常用方法
  8. Python--网络编程-----通过socket收发文件软件开发--面向对象开发
  9. Ajax与jQuery、json
  10. Intent在Activity间的传值