Julia 中的 "closure" 是什么?
What is a "closure" in Julia?
我正在学习如何在 Julia
中编写最大似然实现,目前,我正在关注 this material(顺便说一句,强烈推荐!)。
所以问题是我不完全理解 Julia 中的 closure 是什么,也不知道我什么时候应该真正使用它。即使阅读了 official documentation 这个概念对我来说仍然有点模糊。
例如,在教程中,我提到作者将对数似然函数定义为:
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
然而,后来他声称
The log-likelihood as we've written is a function of both the data and the parameters, but mathematically it should only depend on the parameters. In addition to that mathematical reason for creating a new function, we want a function only of the parameters because the optimization algorithms in Optim assume the inputs have that property. To achieve both goals, we'll construct a closure that partially applies the log-likelihood function for us and negates it to give us the negative log-likelihood we want to minimize.
# 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
算法的工作原理,但我仍然不明白它是什么广义上的闭包。如果有人能对此有所了解,我将不胜感激。非常感谢。
在您询问的上下文中,您可以认为闭包是一个引用某些在其外部范围内定义的变量的函数(对于其他情况,请参阅@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
在其外部范围内定义;它需要存储此引用,因为变量 x
在 fun
函数体内使用。
在估计的上下文中,正如您所指出的,这很有用,因为在我的例子中 find_zero
要求函数只接受一个参数 - 在我的例子中是 m
变量,而你希望 return 值取决于传递的 m
和 x
.
重要的是,一旦 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
变量被我绑定到 fun2
变量的匿名函数 v -> push!(x, v)
捕获。稍后当您调用 fun2
时,绑定到 x
变量的对象得到更新(并且可以被引用),尽管它是在 gen
函数范围内定义的。虽然我们离开了 gen
范围,但绑定到 x
变量的对象比范围长,因为它被我们定义的匿名函数捕获。
如有不明之处请评论。
我将通过向您展示他故意遗漏的内容来补充 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
我正在学习如何在 Julia
中编写最大似然实现,目前,我正在关注 this material(顺便说一句,强烈推荐!)。
所以问题是我不完全理解 Julia 中的 closure 是什么,也不知道我什么时候应该真正使用它。即使阅读了 official documentation 这个概念对我来说仍然有点模糊。
例如,在教程中,我提到作者将对数似然函数定义为:
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
然而,后来他声称
The log-likelihood as we've written is a function of both the data and the parameters, but mathematically it should only depend on the parameters. In addition to that mathematical reason for creating a new function, we want a function only of the parameters because the optimization algorithms in Optim assume the inputs have that property. To achieve both goals, we'll construct a closure that partially applies the log-likelihood function for us and negates it to give us the negative log-likelihood we want to minimize.
# 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
算法的工作原理,但我仍然不明白它是什么广义上的闭包。如果有人能对此有所了解,我将不胜感激。非常感谢。
在您询问的上下文中,您可以认为闭包是一个引用某些在其外部范围内定义的变量的函数(对于其他情况,请参阅@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
在其外部范围内定义;它需要存储此引用,因为变量 x
在 fun
函数体内使用。
在估计的上下文中,正如您所指出的,这很有用,因为在我的例子中 find_zero
要求函数只接受一个参数 - 在我的例子中是 m
变量,而你希望 return 值取决于传递的 m
和 x
.
重要的是,一旦 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
变量被我绑定到 fun2
变量的匿名函数 v -> push!(x, v)
捕获。稍后当您调用 fun2
时,绑定到 x
变量的对象得到更新(并且可以被引用),尽管它是在 gen
函数范围内定义的。虽然我们离开了 gen
范围,但绑定到 x
变量的对象比范围长,因为它被我们定义的匿名函数捕获。
如有不明之处请评论。
我将通过向您展示他故意遗漏的内容来补充 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