Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding support for LGBM_BoosterUpdateOneIterCustom #114

Merged
merged 3 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions src/wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ sparsedatatype(x::SparseArrays.SparseMatrixCSC{Float64, <:Integer}) = C_API_DTYP
sparsedatatype(x::SparseArrays.SparseMatrixCSC{<:Any, <:Integer}) = throw(TypeError(:sparsedatatype, AbstractFloat, one(eltype(x.nzval))))



# Floating point conversion helpers
tofloat32(x::Vector{<:AbstractFloat}) = Float32.(x)
tofloat32(x::Vector{Float32}) = x


macro lightgbm(f, params...)
Expand Down Expand Up @@ -465,7 +467,38 @@ function LGBM_BoosterUpdateOneIter(bst::Booster)
return is_finished[]
end

# function LGBM_BoosterUpdateOneIterCustom()
"""
LGBM_BoosterUpdateOneIterCustom
Pass grads and 2nd derivatives corresponding to some custom loss function
grads and 2nd derivatives must be same cardinality as training data

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as training data * number of trees per iteration

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Also, trying to run this on a booster without data will fail.
"""
function LGBM_BoosterUpdateOneIterCustom(bst::Booster, grads::Vector{<:AbstractFloat}, hessian::Vector{<:AbstractFloat})

if length(bst.datasets) == 0
throw(ErrorException("Booster does not have any training data associated"))
end
numdata = LGBM_DatasetGetNumData(first(bst.datasets))
nummodels = LGBM_BoosterNumModelPerIteration(bst)

if !((numdata*nummodels) == length(grads) == length(hessian))
throw(DimensionMismatch(
"Gradients sizes ($(length(grads)), $(length(hessian))) don't match training data size ($numdata) * ($nummodels)"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

number of trees per iteration * ($nummodels)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm? Not sure what you're expecting here, number of trees per iteration is the number of models, so the message would be like "doesnt match training data (22) * (3)" for example

))
end

grads = tofloat32(grads)
hessian = tofloat32(hessian)

is_finished = Ref{Cint}()
@lightgbm(:LGBM_BoosterUpdateOneIterCustom,
bst.handle => BoosterHandle,
grads => Ptr{Cfloat},
hessian => Ptr{Cfloat},
is_finished => Ref{Cint})
return is_finished[]

end

function LGBM_BoosterRollbackOneIter(bst::Booster)
@lightgbm(:LGBM_BoosterRollbackOneIter,
Expand Down Expand Up @@ -752,3 +785,11 @@ end
# function LGBM_BoosterDumpModel()
# function LGBM_BoosterGetLeafValue()
# function LGBM_BoosterSetLeafValue()

function LGBM_BoosterNumModelPerIteration(bst::Booster)
out_models = Ref{Cint}()
@lightgbm(:LGBM_BoosterNumModelPerIteration,
bst.handle => BoosterHandle,
out_models => Ref{Cint})
return out_models[]
end
79 changes: 78 additions & 1 deletion test/ffi/booster.jl
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,63 @@ end
end


@testset "LGBM_BoosterUpdateOneIterCustom" begin

numdata = 1000
mymat = randn(numdata, 2)
labels = randn(numdata)
dataset = LightGBM.LGBM_DatasetCreateFromMat(mymat, verbosity)
LightGBM.LGBM_DatasetSetField(dataset, "label", labels)
# default params won't allow this to learn anything from this useless data set (i.e. splitting completes)
booster = LightGBM.LGBM_BoosterCreate(dataset, verbosity)

finished = LightGBM.LGBM_BoosterUpdateOneIterCustom(booster, randn(numdata), rand(numdata))
pred1 = LightGBM.LGBM_BoosterGetPredict(booster, 0)
# check both types of float work

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this randn(numdata) type of Vector{<:AbstractFloat}?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the test would fail with a MethodError if it was not

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is typeof Vector{Float64} which is the same as the following test Float32.(randn(numdata))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it's different -- the following test is Float32

The API takes only Float32 data, so if the user passes Float64, it needs to be converted. I test each way independently just to verify both entry points work as expected.

finished = LightGBM.LGBM_BoosterUpdateOneIterCustom(booster, Float32.(randn(numdata)), Float32.(rand(numdata)))
pred2 = LightGBM.LGBM_BoosterGetPredict(booster, 0)
@test !isapprox(pred1, pred2; rtol=1e-5) # show that the gradients caused an update

finished = LightGBM.LGBM_BoosterUpdateOneIterCustom(booster, zeros(numdata), ones(numdata))
pred3 = LightGBM.LGBM_BoosterGetPredict(booster, 0)
@test isapprox(pred2, pred3; rtol=1e-16) # show that the gradients did not cause an update

@test_throws DimensionMismatch LightGBM.LGBM_BoosterUpdateOneIterCustom(booster, zeros(1), zeros(1))

existing_booster = LightGBM.LGBM_BoosterCreateFromModelfile(joinpath(@__DIR__, "data", "test_tree"))

# can't exactly match the size if there is no size (no training data) to match
@test_throws ErrorException LightGBM.LGBM_BoosterUpdateOneIterCustom(existing_booster, zeros(1), zeros(1))

# handle multiclass too
num_class = 3
mymat = randn(numdata, 2)
labels = rand((1:num_class) .- 1, numdata)

dataset = LightGBM.LGBM_DatasetCreateFromMat(mymat, verbosity)
LightGBM.LGBM_DatasetSetField(dataset, "label", labels)
booster = LightGBM.LGBM_BoosterCreate(dataset, "objective=multiclass num_class=$(num_class) $verbosity")

finished = LightGBM.LGBM_BoosterUpdateOneIterCustom(booster, randn(numdata*num_class), rand(numdata*num_class))
pred1 = LightGBM.LGBM_BoosterGetPredict(booster, 0)
# check both types of float work

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can remove this comment

finished = LightGBM.LGBM_BoosterUpdateOneIterCustom(booster, randn(numdata*num_class), rand(numdata*num_class))
pred2 = LightGBM.LGBM_BoosterGetPredict(booster, 0)

@test !isapprox(pred1, pred2; rtol=1e-5) # show that the gradients caused an update

# check the naive silly thing does in fact not get accepted
@test_throws DimensionMismatch LightGBM.LGBM_BoosterUpdateOneIterCustom(booster, Float32.(randn(numdata)), Float32.(rand(numdata)))

end


@testset "LGBM_BoosterRollbackOneIter" begin

# Arrange
mymat = randn(10000, 2)
labels = randn(10000)
dataset = LightGBM.LGBM_DatasetCreateFromMat(mymat, verbosity)
dataset = LightGBM.LGBM_DatasetCreateFromMat(mymat, verbosity)
LightGBM.LGBM_DatasetSetField(dataset, "label", labels)
booster = LightGBM.LGBM_BoosterCreate(dataset, verbosity)

Expand Down Expand Up @@ -401,4 +452,30 @@ end

end


@testset "LGBM_BoosterNumModelPerIteration" begin


mymat = [1. 2.; 3. 4.; 5. 6.]
dataset = LightGBM.LGBM_DatasetCreateFromMat(mymat, verbosity)
v_dataset = LightGBM.LGBM_DatasetCreateFromMat(mymat .+ 1., verbosity)
FatemehTahavori marked this conversation as resolved.
Show resolved Hide resolved

booster = LightGBM.LGBM_BoosterCreate(dataset, "objective=binary $verbosity")

@test LightGBM.LGBM_BoosterNumModelPerIteration(booster) == 1

booster = LightGBM.LGBM_BoosterCreate(dataset, "objective=regression $verbosity")

@test LightGBM.LGBM_BoosterNumModelPerIteration(booster) == 1

for n in 2:20

booster = LightGBM.LGBM_BoosterCreate(dataset, "objective=multiclass num_class=$(n) $verbosity")

@test LightGBM.LGBM_BoosterNumModelPerIteration(booster) == n

end

end

end # module