我想写一组PDE的粗略Euler模拟。我读了PDE tutorial on tensorflow.org后,对如何正确执行此操作感到有些困惑。我有两个具体问题,但是如果有任何我被忽视或误解的事情,欢迎进一步的反馈。
以下代码来自教程:
# Discretized PDE update rules
U_ = U + eps * Ut
Ut_ = Ut + eps * (laplace(U) - damping * Ut)
# Operation to update the state
step = tf.group(
U.assign(U_),
Ut.assign(Ut_))
这里没有bug吗?对U.assign(U_)
进行评估后,确定对Ut_
的下一次评估将使用U
的更新值,而不是来自同一时间步长的值吗?我会认为正确的方法如下:
delta_U = tf.Variable(dU_init)
delta_Ut = tf.Variable(dUt_init)
delta_step = tf.group(
delta_U.assign(Ut)
delta_Ut.assign(laplace(U) - damping * Ut)
)
update_step = tf.group(
U.assign_add(eps * delta_U),
Ut.assign_add(eps * delta_Ut)
)
然后我们可以通过交替评估delta_step
和update_step
来运行Euler积分步骤。如果我理解正确,可以通过分别调用Session.run()
来完成此操作:
with tf.Session() as sess:
...
for i in range(1000):
sess.run(delta_step)
sess.run(update_step)
令人沮丧的是,无法定义以固定顺序组合两个步骤的单个操作,例如
combined_update = tf.group(delta_step, update_step)
with tf.Session() as sess:
...
for i in range(1000):
sess.run(combined_update)
但是根据this thread的回答,tf.group()
不保证任何特定的评估顺序。在该线程上描述的用于控制评估顺序的方法涉及一种称为“控制依赖项”的方法。可以在这种情况下使用它们吗?我们要确保对两个张量的重复求值以固定顺序进行?
如果没有,除了显式使用连续的Session.run()
调用之外,还有另一种方法可以控制这些张量的求值顺序吗?
更新:根据jdehesa的回答,我进行了更详细的调查。结果支持我的原始直觉,即PDE教程中存在一个错误,由于tf.assign()
调用的评估顺序不一致,导致错误的结果;这不能通过使用控件依赖项来解决。但是,PDE教程中的方法通常会产生正确的结果,我不明白为什么。
我使用以下代码检查了以明确的顺序运行分配操作的结果:
import tensorflow as tf
import numpy as np
# define two variables a and b, and the PDEs that govern them
a = tf.Variable(0.0)
b = tf.Variable(1.0)
da_dt_ = b * 2
db_dt_ = 10 - a * b
dt = 0.1 # integration step size
# after one step of Euler integration, we should have
# a = 0.2 [ = 0.0 + (1.0 * 2) * 0.1 ]
# b = 2.0 [ = 1.0 + (10 - 0.0 * 1.0) * 0.1 ]
# using the method from the PDE tutorial, define updated values for a and b
a_ = a + da_dt_ * dt
b_ = b + db_dt_ * dt
# and define the update operations
assignA = a.assign(a_)
assignB = b.assign(b_)
# define a higher-order function that runs a particular simulation n times
# and summarises the results
def summarise(simulation, n=500):
runs = np.array( [ simulation() for i in range(n) ] )
summary = dict( { (tuple(run), 0) for run in np.unique(runs, axis=0) } )
for run in runs:
summary[tuple(run)] += 1
return summary
# check the results of running the assignment operations in an explicit order
def explicitOrder(first, second):
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
sess.run(first)
sess.run(second)
return (sess.run(a), sess.run(b))
print( summarise(lambda: explicitOrder(assignA, assignB)) )
# prints {(0.2, 1.98): 500}
print( summarise(lambda: explicitOrder(assignB, assignA)) )
# prints {(0.4, 2.0): 500}
如预期的那样,如果我们首先评估assignA
,则a
更新为0.2,然后使用此更新的值将b
更新为1.98。如果我们首先评估assignB
,则b
首先更新为2.0,然后使用此更新后的值将a
更新为0.4。这些都是对Euler积分的错误答案:我们应该得到的是a
= 0.2,b
= 2.0。
我测试了当我们允许tf.group()
隐式控制评估顺序而不使用控制依赖项时会发生什么。
noCDstep = tf.group(assignA, assignB)
def implicitOrder():
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
sess.run(noCDstep)
return (sess.run(a), sess.run(b))
print( summarise(lambda: implicitOrder()) )
# prints, e.g. {(0.4, 2.0): 37, (0.2, 1.98): 1, (0.2, 2.0): 462}
有时,这会产生与评估assignB
后跟assignA
相同的结果,或(很少有)评估assignA
后跟assignB
的结果。但是在大多数情况下,会有一个完全出乎意料的结果:对Euler积分步骤的正确答案。这种行为既不一致又令人惊讶。
我尝试通过引入jdehesa建议的控件依赖项(使用以下代码)来解决这种不一致的行为:
with tf.control_dependencies([a_, b_]):
cdStep = tf.group(assignA, assignB)
def cdOrder():
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
sess.run(cdStep)
return (sess.run(a), sess.run(b))
print( summarise(lambda: cdOrder()) )
# prints, e.g. {(0.4, 2.0): 3, (0.2, 1.98): 3, (0.2, 2.0): 494}
似乎控件依赖项不能解决此不一致问题,并且不清楚它们之间是否有任何区别。然后,我尝试实施最初在问题中建议的方法,该方法使用其他变量来独立执行增量和更新的计算:
da_dt = tf.Variable(0.0)
db_dt = tf.Variable(0.0)
assignDeltas = tf.group( da_dt.assign(da_dt_), db_dt.assign(db_dt_) )
assignUpdates = tf.group( a.assign_add(da_dt * dt), b.assign_add(db_dt * dt) )
def explicitDeltas():
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
sess.run(assignDeltas)
sess.run(assignUpdates)
return (sess.run(a), sess.run(b))
print( summarise(lambda: explicitDeltas()) )
# prints {(0.2, 2.0): 500}
如预期的那样,这将正确正确地计算Euler积分步骤。
我可以理解为什么有时tf.group(assignA, assignB)
产生与运行assignA
后跟assignB
一致的答案,以及为什么有时有时产生与运行assignB
后跟{ {1}},但我不明白为什么它通常会产生一个魔术上正确的答案(对于Euler积分的情况),并且与这两个命令都不符合。发生了什么事?
答案 0 :(得分:0)
实际上,您可以使用control dependencies确保事物按您想要的顺序运行。在这种情况下,您只需要确保在执行分配操作之前已计算U_
和Ut_
。我认为(尽管我不确定),本教程中的代码可能是正确的,并且要使用更新后的Ut_
计算U
,您将需要执行以下操作:>
U_ = U + eps * Ut
U = U.assign(U_)
Ut_ = Ut + eps * (laplace(U) - damping * Ut)
step = Ut.assign(Ut_)
但是,只要要确保先执行某件事,就可以显式编写依赖项:
# Discretized PDE update rules
U_ = U + eps * Ut
Ut_ = Ut + eps * (laplace(U) - damping * Ut)
# Operation to update the state
with tf.control_dependencies([U_, Ut_]):
step = tf.group(
U.assign(U_),
Ut.assign(Ut_))
这将确保在执行任何分配操作之前,先计算U_
和Ut_
。
编辑:有关新代码段的一些其他说明。
在更新的第一个代码段(12/02/2019)中,代码将运行第一个任务,然后运行下一个任务。如您所说,这显然是错误的,因为第二次更新将使用另一个变量的已更新值。
第二段代码(如果我没记错的话(如果我记错了,请纠正我))是本教程建议的内容,将分配操作分组。既然您说您已经看到这种情况产生了错误的结果,那么我想这样评估它并不总是安全的。但是,经常得到正确的结果也就不足为奇了。这里TensorFlow将计算所有必要的值以更新两个变量。由于评估顺序不是确定性的(当没有显式依赖项时),因此可能会发生a
的更新发生在计算b_
之前的情况,例如,在这种情况下,您将得到错误的结果。但是可以预期,在a_
和b_
更新之前,将多次计算a
和b
。
在第三个代码段中,您使用控件依赖项,但不是有效的方式。您在代码中指出的是,在计算a_
和b_
之前,不应运行组操作。但是,这并不意味着什么。组操作几乎是无操作的,它依赖于其输入。那里的控件依赖项只会影响此无操作,而不会阻止分配操作在之前运行。就像我最初建议的那样,您应该将赋值操作放到控件依赖关系块中,以确保赋值不会比应有的发生更快(在我的摘录中,为了方便起见,我也将组操作放到了该块中到底是里面还是外面都没有关系。