使用Optim优化含样条近似的非线性函数性能低下问题排查
Julia Optim 性能优化方案
核心性能瓶颈分析
你的代码存在几个关键性能损耗点:
- 重复计算样条:
obj和grad!各自调用link_approx,每次迭代都会重复计算两次样条(一次算目标值、一次算梯度),这是最大的性能浪费。 - 低效的storage赋值:
grad!里用循环逐个给storage赋值,完全没必要,批量赋值效率远高于循环。 - 循环内定义函数:在
for j in 1:r循环内部定义grad!和obj,每次循环都会触发Julia重新编译函数,额外消耗时间。 - 泛型类型拖慢编译:
link_approx的参数x_v::Array过于宽泛,没有指定具体维度和类型,影响JIT编译优化。 - mapslices的低效使用:
mapslices(std, Z_c, dims = 1)比直接按列计算标准差慢很多。
优化后的代码
using Splines2 using LinearAlgebra using Optim using Distributions ## 基础设置优化 n=200 p=100 q=100 r=10 X = randn(n, p) Y = randn(n, q) B = ones(p, r) ./ p G = ones(q, r) ./ q Z = (X * B + Y * G) Z_c = Z .- mean(Z, dims = 1) # 替换mapslices为更高效的按列计算方式 Z = Z_c ./ std.(eachcol(Z_c))' theta = LinRange(-3.8, 4, 5) |> Vector{Float64} knots = LinRange(-7.2, 7.5, 9) |> Vector{Float64} coef = (3*diff(vcat(0, theta, 0)) ./ (knots[4:end] - knots[1:end-3]))[2:end-1] # 明确参数类型,提升编译效率 function link_approx(x_v::Vector{Float64}) est = bs(x_v, knots = knots, order = 4)[:, 3:end-3] * theta der = bs(x_v, knots = knots, order = 3)[:, 3:end-3] * coef return (est=est, der=der) # 用NamedTuple替代Dict,内存和访问效率更高 end # 用闭包封装每个j对应的目标/梯度函数,避免循环内重复编译 function make_obj_grad(j, X, B, Y, Z, n) # 合并目标值与梯度计算,避免重复调用link_approx function obj_grad!(F, G, gamma) linkfit = link_approx(Y*gamma) residual = Z[:,j] - X*B[:,j] - linkfit.est # 计算目标值(如果需要) if F !== nothing F = dot(residual, residual)/(2*n) end # 计算梯度(如果需要) if G !== nothing G .= transpose(Y) * (-residual .* linkfit.der) ./ n end return F end return obj_grad! end @time for j in 1:r obj_grad! = make_obj_grad(j, X, B, Y, Z, n) temp = optimize(obj_grad!, G[:,j], BFGS(), Optim.Options(iterations = Int(5e1))) G[:,j] = Optim.minimizer(temp) end
关键优化点说明
- 消除重复计算:用
obj_grad!合并目标值和梯度的计算逻辑,每次迭代只调用一次link_approx,直接砍掉一半的样条计算开销。 - 移除低效循环:
grad!部分直接用storage .= output批量赋值,替代循环逐个赋值,大幅提升内存操作效率。 - 避免重复编译:通过闭包
make_obj_grad生成每个j对应的计算函数,把函数定义移到循环外,避免每次循环都重新编译。 - 类型与结构优化:给
link_approx的参数指定Vector{Float64}类型,用NamedTuple替代Dict,减少类型推断开销和内存分配。 - 替换低效函数:用
std.(eachcol(Z_c))'替代mapslices,按列计算标准差的效率更高。
额外性能建议
- 尝试用
LBFGS算法替代BFGS:LBFGS在处理高维问题时内存占用更低,可能带来额外性能提升。 - 预计算样条基矩阵:如果knots和order固定,可以提前预计算样条基的相关信息,进一步减少重复计算。
- 启用多线程:设置
ENV["JULIA_NUM_THREADS"] = 6(根据你的CPU核心数调整)后重启Julia,利用多核加速矩阵运算。
内容的提问来源于stack exchange,提问作者Sungmin Ji
相关产品推荐
相关产品推荐

