Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(九)(2)

简介: Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(九)

Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(九)(1)https://developer.aliyun.com/article/1482472

张量数组

tf.TensorArray表示一个张量列表。这在包含循环的动态模型中可能很方便,用于累积结果并稍后计算一些统计数据。您可以在数组中的任何位置读取或写入张量:

array = tf.TensorArray(dtype=tf.float32, size=3)
array = array.write(0, tf.constant([1., 2.]))
array = array.write(1, tf.constant([3., 10.]))
array = array.write(2, tf.constant([5., 7.]))
tensor1 = array.read(1)  # => returns (and zeros out!) tf.constant([3., 10.])

默认情况下,读取一个项目也会用相同形状但全是零的张量替换它。如果不想要这样,可以将clear_after_read设置为False

警告

当您向数组写入时,必须将输出分配回数组,就像这个代码示例中所示。如果不这样做,尽管您的代码在急切模式下可以正常工作,但在图模式下会出错(这些模式在第十二章中讨论)。

默认情况下,TensorArray具有在创建时设置的固定大小。或者,您可以设置size=0dynamic_size=True,以便在需要时自动增长数组。但是,这会影响性能,因此如果您事先知道size,最好使用固定大小数组。您还必须指定dtype,并且所有元素必须与写入数组的第一个元素具有相同的形状。

您可以通过调用stack()方法将所有项目堆叠到常规张量中:

>>> array.stack()
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[1., 2.],
 [0., 0.],
 [5., 7.]], dtype=float32)>

集合

TensorFlow 支持整数或字符串的集合(但不支持浮点数)。它使用常规张量表示集合。例如,集合{1, 5, 9}只是表示为张量[[1, 5, 9]]。请注意,张量必须至少有两个维度,并且集合必须在最后一个维度中。例如,[[1, 5, 9], [2, 5, 11]]是一个包含两个独立集合的张量:{1, 5, 9}{2, 5, 11}

tf.sets包含几个用于操作集合的函数。例如,让我们创建两个集合并计算它们的并集(结果是一个稀疏张量,因此我们调用to_dense()来显示它):

>>> a = tf.constant([[1, 5, 9]])
>>> b = tf.constant([[5, 6, 9, 11]])
>>> u = tf.sets.union(a, b)
>>> u
<tensorflow.python.framework.sparse_tensor.SparseTensor at 0x132b60d30>
>>> tf.sparse.to_dense(u)
<tf.Tensor: [...], numpy=array([[ 1,  5,  6,  9, 11]], dtype=int32)>

还可以同时计算多对集合的并集。如果某些集合比其他集合短,必须用填充值(例如 0)填充它们:

>>> a = tf.constant([[1, 5, 9], [10, 0, 0]])
>>> b = tf.constant([[5, 6, 9, 11], [13, 0, 0, 0]])
>>> u = tf.sets.union(a, b)
>>> tf.sparse.to_dense(u)
<tf.Tensor: [...] numpy=array([[ 1,  5,  6,  9, 11],
 [ 0, 10, 13,  0,  0]], dtype=int32)>

如果您想使用不同的填充值,比如-1,那么在调用to_dense()时必须设置default_value=-1(或您喜欢的值)。

警告

默认的default_value是 0,所以在处理字符串集合时,必须设置这个参数(例如,设置为空字符串)。

tf.sets中还有其他可用的函数,包括difference()intersection()size(),它们都是不言自明的。如果要检查一个集合是否包含某些给定值,可以计算该集合和值的交集。如果要向集合添加一些值,可以计算集合和值的并集。

队列

队列是一种数据结构,您可以将数据记录推送到其中,然后再将它们取出。TensorFlow 在tf.queue包中实现了几种类型的队列。在实现高效的数据加载和预处理流水线时,它们曾经非常重要,但是 tf.data API 基本上使它们变得无用(也许在一些罕见情况下除外),因为使用起来更简单,并提供了构建高效流水线所需的所有工具。为了完整起见,让我们快速看一下它们。

最简单的队列是先进先出(FIFO)队列。要构建它,您需要指定它可以包含的记录的最大数量。此外,每个记录都是张量的元组,因此您必须指定每个张量的类型,以及可选的形状。例如,以下代码示例创建了一个最多包含三条记录的 FIFO 队列,每条记录包含一个 32 位整数和一个字符串的元组。然后将两条记录推送到队列中,查看大小(此时为 2),并取出一条记录:

>>> q = tf.queue.FIFOQueue(3, [tf.int32, tf.string], shapes=[(), ()])
>>> q.enqueue([10, b"windy"])
>>> q.enqueue([15, b"sunny"])
>>> q.size()
<tf.Tensor: shape=(), dtype=int32, numpy=2>
>>> q.dequeue()
[<tf.Tensor: shape=(), dtype=int32, numpy=10>,
 <tf.Tensor: shape=(), dtype=string, numpy=b'windy'>]

还可以使用enqueue_many()dequeue_many()一次入队和出队多个记录(要使用dequeue_many(),必须在创建队列时指定shapes参数,就像我们之前做的那样):

>>> q.enqueue_many([[13, 16], [b'cloudy', b'rainy']])
>>> q.dequeue_many(3)
[<tf.Tensor: [...], numpy=array([15, 13, 16], dtype=int32)>,
 <tf.Tensor: [...], numpy=array([b'sunny', b'cloudy', b'rainy'], dtype=object)>]

其他队列类型包括:

PaddingFIFOQueue

FIFOQueue相同,但其dequeue_many()方法支持出队不同形状的多个记录。它会自动填充最短的记录,以确保批次中的所有记录具有相同的形状。

PriorityQueue

一个按优先级顺序出队记录的队列。优先级必须作为每个记录的第一个元素包含在其中,是一个 64 位整数。令人惊讶的是,优先级较低的记录将首先出队。具有相同优先级的记录将按照 FIFO 顺序出队。

RandomShuffleQueue

一个记录以随机顺序出队的队列。在 tf.data 出现之前,这对实现洗牌缓冲区很有用。

如果队列已满并且您尝试入队另一个记录,则enqueue*()方法将冻结,直到另一个线程出队一条记录。同样,如果队列为空并且您尝试出队一条记录,则dequeue*()方法将冻结,直到另一个线程将记录推送到队列中。

如果您不熟悉 Unicode 代码点,请查看https://homl.info/unicode

附录 D:TensorFlow 图

在本附录中,我们将探索由 TF 函数生成的图形(请参阅第十二章)。

TF 函数和具体函数

TF 函数是多态的,意味着它们支持不同类型(和形状)的输入。例如,考虑以下tf_cube()函数:

@tf.function
def tf_cube(x):
    return x ** 3

每次您调用一个 TF 函数并使用新的输入类型或形状组合时,它会生成一个新的具体函数,具有为这种特定组合专门优化的图形。这样的参数类型和形状组合被称为输入签名。如果您使用它之前已经见过的输入签名调用 TF 函数,它将重用之前生成的具体函数。例如,如果您调用tf_cube(tf.constant(3.0)),TF 函数将重用用于tf_cube(tf.constant(2.0))(对于 float32 标量张量)的相同具体函数。但是,如果您调用tf_cube(tf.constant([2.0]))tf_cube(tf.constant([3.0]))(对于形状为[1]的 float32 张量),它将生成一个新的具体函数,对于tf_cube(tf.constant([[1.0, 2.0], [3.0, 4.0]]))(对于形状为[2, 2]的 float32 张量),它将生成另一个新的具体函数。您可以通过调用 TF 函数的get_concrete_function()方法来获取特定输入组合的具体函数。然后可以像普通函数一样调用它,但它只支持一个输入签名(在此示例中为 float32 标量张量):

>>> concrete_function = tf_cube.get_concrete_function(tf.constant(2.0))
>>> concrete_function
<ConcreteFunction tf_cube(x) at 0x7F84411F4250>
>>> concrete_function(tf.constant(2.0))
<tf.Tensor: shape=(), dtype=float32, numpy=8.0>

图 D-1 显示了tf_cube() TF 函数,在我们调用tf_cube(2)tf_cube(tf.constant(2.0))之后:生成了两个具体函数,每个签名一个,每个具有自己优化的函数图FuncGraph)和自己的函数定义FunctionDef)。函数定义指向与函数的输入和输出对应的图的部分。在每个FuncGraph中,节点(椭圆形)表示操作(例如,幂运算,常量,或用于参数的占位符如x),而边(操作之间的实箭头)表示将在图中流动的张量。左侧的具体函数专门用于x=2,因此 TensorFlow 成功将其简化为始终输出 8(请注意,函数定义甚至没有输入)。右侧的具体函数专门用于 float32 标量张量,无法简化。如果我们调用tf_cube(tf.constant(5.0)),将调用第二个具体函数,x的占位符操作将输出 5.0,然后幂运算将计算5.0 ** 3,因此输出将为 125.0。

图 D-1。tf_cube() TF 函数,及其ConcreteFunction和它们的FuncGraph

这些图中的张量是符号张量,意味着它们没有实际值,只有数据类型、形状和名称。它们代表将在实际值被馈送到占位符x并执行图形后流经图形的未来张量。符号张量使得可以预先指定如何连接操作,并且还允许 TensorFlow 递归推断所有张量的数据类型和形状,鉴于它们的输入的数据类型和形状。

现在让我们继续窥探底层,并看看如何访问函数定义和函数图,以及如何探索图的操作和张量。

探索函数定义和图形

您可以使用graph属性访问具体函数的计算图,并通过调用图的get_operations()方法获取其操作列表:

>>> concrete_function.graph
<tensorflow.python.framework.func_graph.FuncGraph at 0x7f84411f4790>
>>> ops = concrete_function.graph.get_operations()
>>> ops
[<tf.Operation 'x' type=Placeholder>,
 <tf.Operation 'pow/y' type=Const>,
 <tf.Operation 'pow' type=Pow>,
 <tf.Operation 'Identity' type=Identity>]

在这个例子中,第一个操作代表输入参数 x(它被称为 占位符),第二个“操作”代表常数 3,第三个操作代表幂运算(**),最后一个操作代表这个函数的输出(它是一个恒等操作,意味着它不会做任何比幂运算输出的更多的事情⁠^(1))。每个操作都有一个输入和输出张量的列表,您可以通过操作的 inputsoutputs 属性轻松访问。例如,让我们获取幂运算的输入和输出列表:

>>> pow_op = ops[2]
>>> list(pow_op.inputs)
[<tf.Tensor 'x:0' shape=() dtype=float32>,
 <tf.Tensor 'pow/y:0' shape=() dtype=float32>]
>>> pow_op.outputs
[<tf.Tensor 'pow:0' shape=() dtype=float32>]

这个计算图在 图 D-2 中表示。

图 D-2. 计算图示例

请注意每个操作都有一个名称。它默认为操作的名称(例如,"pow"),但当调用操作时您可以手动定义它(例如,tf.pow(x, 3, name="other_name"))。如果名称已经存在,TensorFlow 会自动添加一个唯一的索引(例如,"pow_1""pow_2" 等)。每个张量也有一个唯一的名称:它总是输出该张量的操作的名称,如果它是操作的第一个输出,则为 :0,如果它是第二个输出,则为 :1,依此类推。您可以使用图的 get_operation_by_name()get_tensor_by_name() 方法按名称获取操作或张量:

>>> concrete_function.graph.get_operation_by_name('x')
<tf.Operation 'x' type=Placeholder>
>>> concrete_function.graph.get_tensor_by_name('Identity:0')
<tf.Tensor 'Identity:0' shape=() dtype=float32>

具体函数还包含函数定义(表示为协议缓冲区⁠^(2)),其中包括函数的签名。这个签名允许具体函数知道要用输入值填充哪些占位符,以及要返回哪些张量:

>>> concrete_function.function_def.signature
name: "__inference_tf_cube_3515903"
input_arg {
 name: "x"
 type: DT_FLOAT
}
output_arg {
 name: "identity"
 type: DT_FLOAT
}

现在让我们更仔细地看一下跟踪。

更仔细地看一下跟踪

让我们调整 tf_cube() 函数以打印其输入:

@tf.function
def tf_cube(x):
    print(f"x = {x}")
    return x ** 3

现在让我们调用它:

>>> result = tf_cube(tf.constant(2.0))
x = Tensor("x:0", shape=(), dtype=float32)
>>> result
<tf.Tensor: shape=(), dtype=float32, numpy=8.0>

result 看起来不错,但看看打印出来的内容:x 是一个符号张量!它有一个形状和数据类型,但没有值。而且它有一个名称("x:0")。这是因为 print() 函数不是一个 TensorFlow 操作,所以它只会在 Python 函数被跟踪时运行,这发生在图模式下,参数被替换为符号张量(相同类型和形状,但没有值)。由于 print() 函数没有被捕获到图中,所以下一次我们用 float32 标量张量调用 tf_cube() 时,什么也不会被打印:

>>> result = tf_cube(tf.constant(3.0))
>>> result = tf_cube(tf.constant(4.0))

但是,如果我们用不同类型或形状的张量,或者用一个新的 Python 值调用 tf_cube(),函数将再次被跟踪,因此 print() 函数将被调用:

>>> result = tf_cube(2)  # new Python value: trace!
x = 2
>>> result = tf_cube(3)  # new Python value: trace!
x = 3
>>> result = tf_cube(tf.constant([[1., 2.]]))  # new shape: trace!
x = Tensor("x:0", shape=(1, 2), dtype=float32)
>>> result = tf_cube(tf.constant([[3., 4.], [5., 6.]]))  # new shape: trace!
x = Tensor("x:0", shape=(None, 2), dtype=float32)
>>> result = tf_cube(tf.constant([[7., 8.], [9., 10.]]))  # same shape: no trace
警告

如果您的函数具有 Python 副作用(例如,将一些日志保存到磁盘),请注意此代码只会在函数被跟踪时运行(即每次用新的输入签名调用 TF 函数时)。最好假设函数可能在调用 TF 函数时随时被跟踪(或不被跟踪)。

在某些情况下,您可能希望将 TF 函数限制为特定的输入签名。例如,假设您知道您只会用 28 × 28 像素图像的批次调用 TF 函数,但是批次的大小会有很大的不同。您可能不希望 TensorFlow 为每个批次大小生成不同的具体函数,或者依赖它自行决定何时使用 None。在这种情况下,您可以像这样指定输入签名:

@tf.function(input_signature=[tf.TensorSpec([None, 28, 28], tf.float32)])
def shrink(images):
    return images[:, ::2, ::2]  # drop half the rows and columns

这个 TF 函数将接受任何形状为 [*, 28, 28] 的 float32 张量,并且每次都会重用相同的具体函数:

img_batch_1 = tf.random.uniform(shape=[100, 28, 28])
img_batch_2 = tf.random.uniform(shape=[50, 28, 28])
preprocessed_images = shrink(img_batch_1)  # works fine, traces the function
preprocessed_images = shrink(img_batch_2)  # works fine, same concrete function

然而,如果您尝试用 Python 值调用这个 TF 函数,或者用意外的数据类型或形状的张量调用它,您将会得到一个异常:

img_batch_3 = tf.random.uniform(shape=[2, 2, 2])
preprocessed_images = shrink(img_batch_3)  # ValueError! Incompatible inputs

使用 AutoGraph 捕获控制流

如果您的函数包含一个简单的 for 循环,您期望会发生什么?例如,让我们编写一个函数,通过连续添加 1 来将 10 添加到其输入中:

@tf.function
def add_10(x):
    for i in range(10):
        x += 1
    return x

它运行正常,但当我们查看它的图时,我们发现它不包含循环:它只包含 10 个加法操作!

>>> add_10(tf.constant(0))
<tf.Tensor: shape=(), dtype=int32, numpy=15>
>>> add_10.get_concrete_function(tf.constant(0)).graph.get_operations()
[<tf.Operation 'x' type=Placeholder>, [...],
 <tf.Operation 'add' type=AddV2>, [...],
 <tf.Operation 'add_1' type=AddV2>, [...],
 <tf.Operation 'add_2' type=AddV2>, [...],
 [...]
 <tf.Operation 'add_9' type=AddV2>, [...],
 <tf.Operation 'Identity' type=Identity>]

实际上这是有道理的:当函数被跟踪时,循环运行了 10 次,因此x += 1操作运行了 10 次,并且由于它处于图模式下,它在图中记录了这个操作 10 次。您可以将这个for循环看作是一个在创建图表时被展开的“静态”循环。

如果您希望图表包含一个“动态”循环(即在执行图表时运行的循环),您可以手动使用tf.while_loop()操作创建一个,但这并不直观(请参见第十二章笔记本的“使用 AutoGraph 捕获控制流”部分以获取示例)。相反,使用 TensorFlow 的AutoGraph功能要简单得多,详见第十二章。AutoGraph 实际上是默认激活的(如果您需要关闭它,可以在tf.function()中传递autograph=False)。因此,如果它是开启的,为什么它没有捕获add_10()函数中的for循环呢?它只捕获对tf.data.Dataset对象的张量进行迭代的for循环,因此您应该使用tf.range()而不是range()。这是为了给您选择:

  • 如果使用range()for循环将是静态的,这意味着仅在跟踪函数时才会执行。循环将被“展开”为每次迭代的一组操作,正如我们所见。
  • 如果使用tf.range(),循环将是动态的,这意味着它将包含在图表本身中(但在跟踪期间不会运行)。

让我们看看如果在add_10()函数中将range()替换为tf.range()时生成的图表:

>>> add_10.get_concrete_function(tf.constant(0)).graph.get_operations()
[<tf.Operation 'x' type=Placeholder>, [...],
 <tf.Operation 'while' type=StatelessWhile>, [...]]

如您所见,图现在包含一个While循环操作,就好像我们调用了tf.while_loop()函数一样。

在 TF 函数中处理变量和其他资源

在 TensorFlow 中,变量和其他有状态对象,如队列或数据集,被称为资源。TF 函数对它们进行特殊处理:任何读取或更新资源的操作都被视为有状态的,并且 TF 函数确保有状态的操作按照它们出现的顺序执行(与无状态操作相反,后者可能并行运行,因此它们的执行顺序不被保证)。此外,当您将资源作为参数传递给 TF 函数时,它会通过引用传递,因此函数可能会对其进行修改。例如:

counter = tf.Variable(0)
@tf.function
def increment(counter, c=1):
    return counter.assign_add(c)
increment(counter)  # counter is now equal to 1
increment(counter)  # counter is now equal to 2

如果查看函数定义,第一个参数被标记为资源:

>>> function_def = increment.get_concrete_function(counter).function_def
>>> function_def.signature.input_arg[0]
name: "counter"
type: DT_RESOURCE

还可以在函数外部使用定义的tf.Variable,而无需显式将其作为参数传递:

counter = tf.Variable(0)
@tf.function
def increment(c=1):
    return counter.assign_add(c)

TF 函数将将其视为隐式的第一个参数,因此实际上最终会具有相同的签名(除了参数的名称)。但是,使用全局变量可能会很快变得混乱,因此通常应该将变量(和其他资源)封装在类中。好消息是@tf.function也可以很好地与方法一起使用:

class Counter:
    def __init__(self):
        self.counter = tf.Variable(0)
    @tf.function
    def increment(self, c=1):
        return self.counter.assign_add(c)
警告

不要使用=+=-=或任何其他 Python 赋值运算符与 TF 变量。相反,您必须使用assign()assign_add()assign_sub()方法。如果尝试使用 Python 赋值运算符,当调用该方法时将会出现异常。

这种面向对象的方法的一个很好的例子当然是 Keras。让我们看看如何在 Keras 中使用 TF 函数。

使用 TF 函数与 Keras(或不使用)

默认情况下,您在 Keras 中使用的任何自定义函数、层或模型都将自动转换为 TF 函数;您无需做任何事情!但是,在某些情况下,您可能希望停用此自动转换——例如,如果您的自定义代码无法转换为 TF 函数,或者如果您只想调试代码(在急切模式下更容易)。为此,您只需在创建模型或其任何层时传递dynamic=True

model = MyModel(dynamic=True)

如果您的自定义模型或层将始终是动态的,可以使用dynamic=True调用基类的构造函数:

class MyDense(tf.keras.layers.Layer):
    def __init__(self, units, **kwargs):
        super().__init__(dynamic=True, **kwargs)
        [...]

或者,在调用compile()方法时传递run_eagerly=True

model.compile(loss=my_mse, optimizer="nadam", metrics=[my_mae],
              run_eagerly=True)

现在你知道了 TF 函数如何处理多态性(具有多个具体函数),如何使用 AutoGraph 和追踪自动生成图形,图形的样子,如何探索它们的符号操作和张量,如何处理变量和资源,以及如何在 Keras 中使用 TF 函数。

¹ 你可以安全地忽略它 - 它只是为了技术原因而在这里,以确保 TF 函数不会泄漏内部结构。

² 在第十三章中讨论的一种流行的二进制格式。

相关文章
|
11天前
|
机器学习/深度学习 TensorFlow API
TensorFlow与Keras实战:构建深度学习模型
本文探讨了TensorFlow和其高级API Keras在深度学习中的应用。TensorFlow是Google开发的高性能开源框架,支持分布式计算,而Keras以其用户友好和模块化设计简化了神经网络构建。通过一个手写数字识别的实战案例,展示了如何使用Keras加载MNIST数据集、构建CNN模型、训练及评估模型,并进行预测。案例详述了数据预处理、模型构建、训练过程和预测新图像的步骤,为读者提供TensorFlow和Keras的基础实践指导。
143 59
|
3天前
|
机器学习/深度学习 算法 数据挖掘
机器学习之sklearn基础——一个小案例,sklearn初体验
机器学习之sklearn基础——一个小案例,sklearn初体验
24 6
|
1月前
|
机器学习/深度学习 数据采集 算法
机器学习之sklearn基础教程
机器学习之sklearn基础教程
|
1天前
|
机器学习/深度学习 数据处理 计算机视觉
机器学习- Sklearn (交叉验证和Pipeline)
机器学习- Sklearn (交叉验证和Pipeline)
|
1月前
|
机器学习/深度学习 数据采集 人工智能
机器学习之sklearn基础教程
【5月更文挑战第9天】Sklearn是Python热门机器学习库,提供丰富算法和预处理工具。本文深入讲解基础概念、核心理论、常见问题及解决策略。内容涵盖模型选择与训练、预处理、交叉验证、分类回归、模型评估、数据集划分、正则化、编码分类变量、特征选择与降维、集成学习、超参数调优、模型评估、保存加载及模型解释。学习Sklearn是迈入机器学习领域的关键。
53 3
|
1月前
|
机器学习/深度学习 算法 算法框架/工具
Python深度学习基于Tensorflow(5)机器学习基础
Python深度学习基于Tensorflow(5)机器学习基础
26 2
|
25天前
|
机器学习/深度学习 自然语言处理 TensorFlow
构建高效的机器学习模型:基于Python和TensorFlow的实践
构建高效的机器学习模型:基于Python和TensorFlow的实践
39 0
|
1月前
|
机器学习/深度学习 数据可视化 数据处理
python 机器学习 sklearn——一起识别数字吧
python 机器学习 sklearn——一起识别数字吧
|
1月前
|
机器学习/深度学习 Python
python 机器学习 sklearn——手把手教你预测心脏病
python 机器学习 sklearn——手把手教你预测心脏病
|
1月前
|
机器学习/深度学习 数据采集 算法
机器学习之sklearn基础教程
机器学习之sklearn基础教程