本文翻译自: 《Broadcasting the good and the ugly》, 如有侵权请联系删除, 仅限于学术交流, 请勿商用。 如有谬误, 请联系指出。
TensorFlow 支持广播元素操作。 通常, 当你想做加法或乘法的运算时, 你需要确保操作数的形状(shape)是匹配的, 例如: 你不能将一个形状为[3, 2]的张量和一个形状为[3, 4]的张量相加。 但是, 这里有一个特殊情况, 那就是当你的其中一个操作数是一个具有单独维度(singular dimension)的张量的时候, TF 会隐式地在它的单独维度方向填满(tile), 以确保和另一个操作数的形状相匹配。 所以, 对一个[3, 2]的张量和一个[3, 1]的张量相加在 TF 中是合法的。
import tensorflow as tf
a = tf.constant([
[1., 2.],
[3., 4.]
])
b = tf.constant([
[1.],
[2.]
])# c = a + tf.tile(b, [1, 2])
c = a + b
广播机制允许我们在隐式情况下进行填充(tile), 这种操作可以使得我们的代码更加简洁, 并且更有效率地利用内存, 因为我们不需要储存填充操作的结果。 一个可以表现这个优势的应用场景就是在结合具有不同长度的特征向量的时候。 为了拼接具有不同长度的特征向量, 我们一般都先填充输入向量, 拼接这个结果然后进行之后的一系列非线性操作等。 这是各种神经网络架构的常见模式: :
a = tf.random_uniform([5, 3, 5])
b = tf.random_uniform([5, 1, 6])
# concat a and b and apply nonlinearity
tiled_b = tf.tile(b, [1, 3, 1])
c = tf.concat([a, tiled_b], 2)
d = tf.layers.dense(c, 10, activation = tf.nn.relu)
但如果利用了广播机制, 这种操作就可以更有效地完成。 举个例子, 因为我们知道$f(m(x+y))=f(mx+my)$的事实, 所以我们可以分别进行线性操作, 并使用广播进行隐式连接:
pa = tf.layers.dense(a, 10, activation = None)
pb = tf.layers.dense(b, 10, activation = None)
d = tf.nn.relu(pa + pb)
事实上, 这段代码是通用的, 只要张量之间能够进行广播, 就可以应用于任意形状的张量:
def merge(a, b, units, activation = tf.nn.relu):
pa = tf.layers.dense(a, units, activation = None)
pb = tf.layers.dense(b, units, activation = None)
c = pa + pb
if activation is not None:
c = activation(c)
return c
一个更通用的函数在这。
目前为止, 我们讨论了广播机制的优点, 但是同样的广播机制也有其缺点, 隐式假设几乎总是使得调试变得更加困难, 考虑下面的例子:
a = tf.constant([
[1.],
[2.]
])
b = tf.constant([1., 2.])
c = tf.reduce_sum(a + b)
你猜这个结果是多少? 如果你说是 6, 那么你就错了, 答案应该是 12。 这是因为当两个张量的秩不匹配的时候, TensorFlow 将会在元素操作之前自动展开秩较低的张量的第一维, 所以这个加法的结果将会变为[[2, 3], [3, 4]], 对所有参数进行约化后得到 12。
解决这种麻烦的方法就是尽可能地显示使用。 我们在需要 reduce 某些张量的时候, 显式地指定维度, 然后寻找这个 bug 就会变得简单:
a = tf.constant([
[1.],
[2.]
])
b = tf.constant([1., 2.])
c = tf.reduce_sum(a + b, 0)
这样, c 的值就是[5, 7], 我们就容易猜到其出错的原因。 一个更通用的法则就是: 在进行 reduce 操作和使用 tf.squeeze
时我们必须指定维度。