【问题标题】:How should Euler integration be implemented in TensorFlow?TensorFlow中应该如何实现欧拉积分?
【发布时间】:2019-02-11 16:39:18
【问题描述】:

我想编写一组 PDE 的粗略欧拉模拟。我读了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_))

问题 1

这里没有错误吗?一旦评估了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_stepupdate_step 来运行欧拉积分步骤。如果我理解正确,这可以通过单独调用 Session.run() 来完成:

with tf.Session() as sess:
    ...
    for i in range(1000):
        sess.run(delta_step)
        sess.run(update_step)

问题 2

似乎令人沮丧的是,无法定义以固定顺序组合两个步骤的单个操作,例如

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() 调用之外,还有其他方法可以控制这些张量的评估顺序吗?

更新(2019 年 12 月 2 日)

更新:根据 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。这些都是欧拉积分的错误答案:我们应该得到的是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 相同的结果。但大多数时候,有一个完全出乎意料的结果:欧拉积分步骤的正确答案。这种行为既不一致又令人惊讶。

我尝试通过引入 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}

正如预期的那样,这始终正确地计算欧拉积分步长。

我可以理解为什么有时tf.group(assignA, assignB) 会产生与运行assignA 后跟assignB 一致的答案,以及为什么有时会产生与运行assignB 后跟assignA 一致的答案,但我不明白理解为什么它通常会产生一个神奇地正确的答案(对于欧拉积分案例)并且与这些顺序都不一致。怎么回事?

【问题讨论】:

    标签: python tensorflow operator-precedence


    【解决方案1】:

    确实,您可以使用control dependencies 确保事情按照您想要的顺序运行。在这种情况下,您只需要确保在执行赋值操作之前计算出U_Ut_。我认为(尽管我不确定)本教程中的代码可能是正确的,并且要使用更新后的 U 计算 Ut_,您需要类似以下内容:

    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_ 都将首先被计算。

    编辑:关于新的 sn-ps 的一些附加说明。

    在您的更新(2019 年 12 月 2 日)的第一个 sn-p 中,代码首先运行一个分配,然后是下一个。正如你所说,这显然是错误的,因为第二次更新将使用另一个变量已经更新的值。

    第二个sn-p,如果我没记错的话(如果我错了,请纠正我)是教程提出的,对赋值操作进行分组。既然你说你已经看到了这种产生错误结果的实例,我想像这样评估它并不总是安全的。但是,您经常得到正确的结果也就不足为奇了。在这里,TensorFlow 将计算所有必要的值来更新这两个变量。由于评估顺序不是确定性的(当没有明确的依赖关系时),a 的更新可能发生在计算 b_ 之前,例如,在这种情况下你会得到错误的结果。但可以合理地预期,a_b_ 将在 ab 更新之前得到计算。

    在第三个 sn-p 中,您使用控制依赖关系,但不是以有效的方式。您用代码表示的是,在计算 a_b_ 之前不应运行组操作。然而,这并不意味着什么。组操作几乎是一个依赖于其输入的无操作。那里的控制依赖关系只影响这个无操作,但不会阻止分配操作在之前的任何时候运行。正如我最初建议的那样,您应该将分配操作放在控制依赖项块中,以确保分配不会比应有的更早发生(在我的 sn-p 中,为了方便起见,我还将组操作放在块中,但不管是进还是出都没有关系。

    【讨论】:

    • 我已编辑问题以说明为什么控制依赖项似乎无法解决问题。
    • @SimonMcGMcG 我用更多解释编辑了我的答案,希望对您有所帮助。
    • 谢谢,将赋值运算符移动到控制依赖块内解决了这个问题。我认为我的一些困惑来自于没有真正理解在连续调用Session.run() 之间持续存在哪些对象。假设某些操作x 定义为具有对y 的控制依赖。我曾想象这意味着 y 必须在 会话中 的某个时间点进行评估,然后才能评估 x(这不会很有帮助)。但这实际上意味着必须(重新)评估y在当前对Session.run()的调用中,然后才能评估x,对吧?
    • @SimonMcGMcG 没错,控制依赖关系(或一般的依赖关系)是指对Session.run() 的每个单独调用的控制流。中间节点值不会在调用run() 之间保存(只有变量和一些其他stateful objects 像随机数)所以,正如你所说,如果你有with tf.control_depencies([y]): x = ... 这意味着x 直到@987654351 才会被计算@ 已被评估此调用Session.run()
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-10-25
    • 1970-01-01
    • 1970-01-01
    • 2015-08-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多