在Julia中,“closure”是什么?

6

我正在学习如何在 Julia 中编写最大似然实现,目前我正在遵循 这个教程(强烈推荐!)。

问题是,我不完全理解 Julia 中的闭包是什么,也不知道何时应该使用它。即使阅读了官方文档,概念对我来说仍然有点模糊。

例如,在教程中,作者定义了对数似然函数为:

function log_likelihood(X, y, β)
    ll = 0.0
    @inbounds for i in eachindex(y)
        zᵢ = dot(X[i, :], β)
        c = -log1pexp(-zᵢ) # Conceptually equivalent to log(1 / (1 + exp(-zᵢ))) == -log(1 + exp(-zᵢ))
        ll += y[i] * c + (1 - y[i]) * (-zᵢ + c) # Conceptually equivalent to log(exp(-zᵢ) / (1 + exp(-zᵢ)))
    end
    ll
end

然而,后来他声称:

我们写的对数似然函数是数据和参数的函数,但从数学上讲,它只应该依赖于参数。除了这个数学原因创建一个新函数之外,我们还希望得到一个仅由参数组成的函数,因为Optim中的优化算法假定输入具有该属性。为了实现这两个目标,我们将构造一个闭包,它部分地应用对数似然函数,并取反以给出我们想要最小化的负对数似然。

# Creating the closure
make_closures(X, y) = β -> -log_likelihood(X, y, β)
nll = make_closures(X, y)

# Define Initial Values equal to zero
β₀ = zeros(2 + 1)
# Ignite the optimization routine using `nll`
res = optimize(nll, β₀, LBFGS(), autodiff=:forward)

从这段文字中,我理解我们需要使用它,因为这是 Optim 算法工作的方式,但我仍然不明白在更广泛的意义下什么是闭包。如果有人能够给予解释,我将不胜感激。非常感谢。

2个回答

7
在你所提到的上下文中,可以将闭包看作是引用其外部作用域中定义的某些变量的函数(对于其他情况,请参见 @phipsgabler 的答案)。以下是一个最简示例:
julia> function est_mean(x)
           function fun(m)
               return m - mean(x)
           end
           val = find_zero(fun, 0.0)
           @show val, mean(x)
           return fun # explicitly return the inner function to inspect it
       end
est_mean (generic function with 1 method)

julia> x = rand(10)
10-element Vector{Float64}:
 0.6699650145575134
 0.8208379672036165
 0.4299946498764684
 0.1321653923513042
 0.5552854476018734
 0.8729613266067378
 0.5423030870674236
 0.15751882823315777
 0.4227087678654101
 0.8594042895489912

julia> fun = est_mean(x)
(val, mean(x)) = (0.5463144770912497, 0.5463144770912497)
fun (generic function with 1 method)

julia> dump(fun)
fun (function of type var"#fun#3"{Vector{Float64}})
  x: Array{Float64}((10,)) [0.6699650145575134, 0.8208379672036165, 0.4299946498764684, 0.1321653923513042, 0.5552854476018734, 0.8729613266067378, 0.5423030870674236, 0.15751882823315777, 0.4227087678654101, 0.8594042895489912]

julia> fun.x
10-element Vector{Float64}:
 0.6699650145575134
 0.8208379672036165
 0.4299946498764684
 0.1321653923513042
 0.5552854476018734
 0.8729613266067378
 0.5423030870674236
 0.15751882823315777
 0.4227087678654101
 0.8594042895489912

julia> fun(10)
9.453685522908751

如您所见,fun 持有对外部作用域(在本例中由 est_mean 函数引入的作用域)变量 x 的引用。而且,我已经向您展示了,即使从 fun 外部检索此值作为其字段(通常不建议这样做,但我向您展示了这一点,以证明确实 fun 存储对其外部作用域定义的对象 x 的引用;它需要存储此引用,因为变量 xfun 函数体内部使用)。

在估计的上下文中,正如您所指出的那样,这是有用的,因为在我的情况下,find_zero 要求函数仅采用一个参数 - 在我的情况下是 m 变量,而您希望返回值取决于传递的 mx

重要的是,一旦 x 被捕获在 fun 闭包中,它就不必在当前作用域中。例如,当我调用 fun(10) 时,代码会正确执行,尽管我们在 est_mean 函数的作用域之外。但这不是问题,因为 fun 函数已经捕获了 x 变量。

让我再举一个例子:

julia> function gen()
          x = []
          return v -> push!(x, v)
       end
gen (generic function with 1 method)

julia> fun2 = gen()
#4 (generic function with 1 method)

julia> fun2.x
Any[]

julia> fun2(1)
1-element Vector{Any}:
 1

julia> fun2.x
1-element Vector{Any}:
 1

julia> fun2(100)
2-element Vector{Any}:
   1
 100

julia> fun2.x
2-element Vector{Any}:
   1
 100

在这里,你可以看到在gen函数内定义的x变量被匿名函数v -> push!(x, v)捕获,并且将其绑定到fun2变量上。稍后,当你调用fun2时,x变量绑定的对象得到更新(并且可以被引用),尽管它是在gen函数范围内定义的。即使我们离开了gen的作用域,由匿名函数捕获的绑定到x变量的对象仍然存在,因为我们定义了这个匿名函数。

如果有不清楚的地方,请评论说明。


非常好的回复!我只是认为值得一提的是,第二种方法(使用匿名函数)会生成多个内部签名,我不知道它是否能够很好地编译... - Diogo Santiago
性能并不取决于使用匿名函数还是命名函数。Julia 会同样高效地处理两者。从性能的角度来看,重要的是编译器是否能够证明闭包中捕获的变量类型是具体的(如果不能,则必须对此类变量执行装箱操作,这会导致性能大幅降低)。 - Bogumił Kamiński

4

我将补充Bogumił的回答,向您展示他故意省略的内容:闭包不必严格意义上是一个函数。实际上,如果在Julia中禁止嵌套函数,您可以自己编写它们:

struct LikelihoodClosure
    X
    y
end

(l::LikelihoodClosure)(β) = -log_likelihood(l.X, l.y, β)
make_closures(X, y) = LikelihoodClosure(X, y)
nll = make_closures(X, y)

现在你可以调用nll(β₀),它是一个LikelihoodClosure类型的对象,并有一个定义好的应用方法。
这就是全部内容了。匿名函数只是语法糖,用于创建存储上下文中“固定变量”的对象实例。
julia> f(x) = y -> x + y
f (generic function with 1 method)

julia> f(1) # that's the closure value
#1 (generic function with 1 method)

julia> typeof(f(1)) # that's the closure type
var"#1#2"{Int64}

julia> f(1).x
1

julia> propertynames(f(1)) # behold, it has a field `x`!
(:x,)

我们甚至可以有点作弊,构造一个实例:

julia> eval(Expr(:new, var"#1#2"{Int64}, 22))
#1 (generic function with 1 method)

julia> eval(Expr(:new, var"#1#2"{Int64}, 22))(2)
24

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接