在我的数据流中,我从数据库中查询一个小子集,使用这些结果构建大约十二个数组,然后根据一些参数值计算一个似然值。接着对数据库的另一个子集重复这一过程。我希望计算似然函数相对于参数而非数据的梯度。但是,ReverseDiff会计算相对于所有输入的梯度。我该如何解决这个问题?具体来说,我该如何构造一个ReverseDiff.Tape对象?
简而言之:如何将随机梯度下降与ReverseDiff结合?(我并不执着于使用ReverseDiff。它只是看起来像是这项工作的合适工具。)
这似乎应该是一个常见的编码模式。在我的领域中经常使用。但我似乎遗漏了什么。Julia的作用域规则似乎破坏了作用域/匿名函数的方法,而ReverseDiff在生成的磁带中保留了原始数据值,而不是使用修改后的值。
一些不工作的样本代码
using ReverseDiffusing Base.Testmutable struct data X::Array{Float64, 2}endconst D = data(zeros(Float64, 2, 2))# 基线已知数据用于比较function f1(params) X = float.([1 2; 3 4]) f2(params, X)end# X是数据,只想对params求导function f2(params, X) sum(params[1]' * X[:, 1] - (params[1] .* params[2])' * X[:, 2].^2)end# 将感兴趣的数据存储在D.X中,以便我们只调用f2(params)就能得到我们的# 梯度f2(params) = f2(params, D.X)# 使用内部函数并替换Z的数据function scope_test() function f2_only_params(params) f2(params, Z) end Z = float.([6 7; 1 3]) f2_tape = ReverseDiff.GradientTape(f2_only_params, [1, 2]) Z[:] = float.([1 2; 3 4]) grad = ReverseDiff.gradient!(f2_tape, [3,4]) return gradendfunction struct_test() D.X[:] = float.([6 7; 1 3]) f2_tape = ReverseDiff.GradientTape(f2, [1., 2.]) D.X[:] = float.([1 2; 3 4]) grad = ReverseDiff.gradient!(f2_tape, [3., 4.]) return gradendfunction struct_test2() D.X[:] = float.([1 2; 3 4]) f2_tape = ReverseDiff.GradientTape(f2, [3., 4.]) D.X[:] = float.([1 2; 3 4]) grad = ReverseDiff.gradient!(f2_tape, [3., 4.]) return gradendD.X[:] = float.([1 2; 3 4])@test f1([3., 4.]) == f2([3., 4.], D.X)@test f1([3., 4.]) == f2([3., 4.])f1_tape = ReverseDiff.GradientTape(f1, [3,4])f1_grad = ReverseDiff.gradient!(f1_tape, [3,4])# 失败!使用第33行的值@test scope_test() == f1_grad# 失败,使用第42行的值@test struct_test() == f1_grad# 成功,所以,不是完全随机的@test struct_test2() == f1_grad
回答:
谢谢Alex,你的回答已经解决了90%的问题。AutoGrad(Knet在撰写时使用的东西)确实提供了一个非常好的界面,我认为对大多数用户来说这是自然的。然而,使用ReverseDiff的匿名函数实际上比AutoGrad采用的方法更快,原因我还不太明白。
如果你跟随你所链接的问题链,这似乎是ReverseDiff/ForwardDiff团队希望人们做的事情:
ReverseDiff.gradient(p -> f(p, non_differentiated_data), params)
不能在这种非常常见的使用场景中获得预编译的磁带确实令人失望,也许未来的工作会改变这一点。但这就是目前的情况。
对那些有兴趣进一步阅读的人提供一些参考资料: