Random decision lists and ripper - Trombini's work#14
Conversation
IREP* implementation in src/algorithms/sequentialcovering.jl was fixed after beamsearch was changed to use the Antecedent structure. Also the optimizations made to IREP* made in the branch labels have been merged to irepnewtypes before rewriting IREP_Star to use the Antecedent structure most efficiently
The method unaryconditions_noneq has been replaced with the more efficient method alphabet2conditions, and the computation has been modified to only occurr once for the entire execution
the irepstar function in sequentialcovering.jl has been fixed after it was broken during code refactoring
Some basic code has been added to do some sanity check on the results of irepstar
the target_class parameter in beamsearch's findbestantecedent has been applied correctly. Some comments explaining the code and some docs have been added, a couple of features that were either disabled due to refactoring or bugs have been fixed, the first practical irepstar test has proven successful. The laplace_accuracy function in loss_functions.jl has been updated to also handle different types of labeling
| args; | ||
| kwargs... | ||
| ) | ||
| # Growing |
There was a problem hiding this comment.
I'm not sure if the rule growth criterion for RIPPER is the same as the one implemented in findbestantecedent.
To be checked.
According to the paper this function should be called "replace_rule"
There was a problem hiding this comment.
As for the call to findbestantecedent, I based myself on the following passage from the original RIPPER paper
For each rule Ri, two alternative rules are constructed. The replacement for Ri is formed by growing and then pruning a rule Ri', where pruning is guided so as to minimize the error of the entire ruleset R1, ..., Ri', ..., Rk on the pruning data. The revision of Ri is formed analogously, except that the revision is grown by greedily adding conditions to Ri, rather than the empty rule.
Then, as for which rule to keep, the paper writes
Finally a decision is made as to whether the finalnal theory should include the revised rule, the replacement
rule, or the original rule. This decision is made using the MDL heuristic.
I think it's implied that the process of creating a rule and/or expanding an existing one is the same as IREP*, which is described as follows
GrowRule repeatedly adds the condition that maximizes FOIL's information gain criterion until the rule covers no negative examples from the growing dataset.
This seems like what findbestantecedent was already doing, with the only difference being in the possibilities it allows in its parameters.
There was a problem hiding this comment.
From what I’ve gathered from the literature, CN2 is the 'cautious' one. During rule construction, it evaluates whether adding a new condition is actually useful or if it’s just chasing noise in the data (pre-pruning). If the test shows it’s not worth it, it stops immediately.
RIPPER, on the other hand, is the 'overachiever.' During the growth phase, it goes full speed ahead, specializing the rule until it’s 'perfect'—meaning it doesn't make a single error on the training data—without worrying that it might be becoming too specific or complex. Only later, and with more care, does it use a separate validation set to 'prune' away the unnecessary branches and simplify the rule.
It seems clear to me, then, that the stopping criterion for rule growth must be different and managed according to the specific case, or, at least, make sure that in the case of RIPPER the rules generated in the grow phase actually overfit the data.
There was a problem hiding this comment.
I agree with what you said.
On an implementation level, findbestantecedent seems to be using the greedy/"overachiever" RIPPER approach: it keeps specializing a rule until doing so would result in a coverage smaller than min_rule_coverage.
When that happens, the function sortedantecedents returns nothing, and the loop in findbestantecedent halts.
If other the "cautious" CN2-like type of rule finding is to be implemented, we could consider adding the possibility to pass a stopping condition as an optional user-definable parameter.
There was a problem hiding this comment.
I agree with what you said.
However, I'd say that in CN2 this hyperspecialization depends on the heuristic you want to use (e.g., Laplace accuracy, unlike entropy, doesn't always favor massively specific rules).
Do you agree ?
There was a problem hiding this comment.
The method for finding an antecedent in CN2 can still be implemented using the current findbestantecedent function.
The pseudocode for finding a rule that I'm referring to is the one in Find_BestConditionExpression in https://en.wikipedia.org/wiki/CN2_algorithm.
The method still has a beam of antecedents that are incrementally specialized, and the main loop stops when ConditionalExpressionSet is empty, which can only happen if TrialConditionalExpressionSet is empty, or in other words, if no further specializations are possible, just like in findbestantecedent.
The only "difference" with the current findbestantecedent function is that before selecting a new "best" test, there is a test for statistical significance as well as a scoring.
This could easily be implemented given the already existing framework, by simply returning an infinite loss value if the rule does not pass a test for statistical significance. In that case, a rule that does not satisfy the test could never be chosen to be returned.
Likewise, for any other type of "test" aimed to select or discard a rule, one could simply use the already-existing loss function system, and return Inf when the test is not passed, and that makes sure the rule must be discarded.
This, coupled with a custom stopping condition, seems like it should be enough to handle every case I've seen so far.
Overall I feel like the general structure for both RIPPER and CN2 is the same: incremental specialization of a beam of antecedents, using some kind of metric/test/loss to determine which one is best, and a predetermined stopping condition, which in both cases is if no further specializations are possible, but which can very well be implemented to be (optionally) defined in Sole.jl.
Trying to "force" certain combinations of stopping conditions or tests seems complicated, and I don't know how it would be useful.
| A vector of global indices representing all instances covered by the antecedent across both | ||
| grow and prune datasets. | ||
| """ | ||
| function compute_global_coverage( |
There was a problem hiding this comment.
The logic for tracking which samples are covered by the current ruleset is duplicated across irepstar, ripperk, and merge_ruleset_satmasks. A single abstraction responsible for maintaining and updating the coverage mask would eliminate the duplication and make the training loops easier to read.
edo-007
left a comment
There was a problem hiding this comment.
Overall the code looks good.
I would say it is algorithmically faithful to the original implementation( I left a comment about this).
The main thing worth working on is cleanliness — adding a few layers of abstraction in the right places would make it significantly easier to read and maintain. Example: wrapping the parallel training state variables into a struct, cleaning up how algorithm parameters are passed around, and consolidating the duplicated coverage tracking logic.
the number of selectors does not have to be updated at each RIPPER iteration. This was fixed in this commit
…ests with random forests/random decision lists and mixed model on crab dataset + 'reconstruct' call is now safe
…e models on various datasets + optimization in sortedantecedents The loop in build_rdl (now build_ensemble) was optimized through multi-threading. 'sortedantecedents' was also optimized by avoiding calls to deepcopy() where they aren't needed. The code to test ensemble models was separated into test/rdl_benchmark/test_functions.jl, and some more tests were written. Lastly, safe_reconstruct has been fixed as it carried a bug.
|
Be careful on dependencies: are freaking outdated! [compat]
BenchmarkTools = "1"
CSV = "0.10"
CategoricalArrays = "1"
DataFrames = "1.8"
DecisionTree = "0.12"
Distributions = "0.25"
FillArrays = "1"
Logging = "1.11"
MLJ = "0.21 - 0.23"
MLJBase = "1.12"
MLJDecisionTreeInterface = "0.4"
MLJModelInterface = "1.12"
MLJModels = "0.18"
Parameters = "0.12"
Printf = "1.11 - 1.12"
RDatasets = "0.8"
Reexport = "1"
Revise = "3.14"
SoleBase = "0.13"
SoleData = "0.16"
SoleLogics = "0.13"
SoleModels = "0.10"
StatsBase = "0.34"
Tables = "1.12"
Test = "1"
julia = "1"And remember that packages such as Now that I've updated deps in my local repo, I'm going to test this package, but at a first glance, looks awesome! |
|
I'm playing with tests and with every test I ended up with this error: ERROR: TaskFailedException
nested task error: MethodError: no method matching _multivariate_scalar_alphabet(::Vector{…}, ::Vector{…}, ::Vector{…}; sorted::Bool, discretizedomain::Bool, y::SubArray{…}, sortingmode::Symbol)
This method does not support all of the given keyword arguments (and may not support any).
Stacktrace:
[1] kwerr(::@NamedTuple{…}, ::Function, ::Vector{…}, ::Vector{…}, ::Vector{…})
@ Base ./error.jl:175
[2] alphabet(X::PropositionalLogiset{…}, sorted::Bool; force_i_variables::Bool, test_operators::Nothing, discretizedomain::Bool, unique::Bool, kwargs::@Kwargs{…})
@ SoleData ~/Documents/Aclai/SoleData.jl/src/scalar/propositional-logiset.jl:259
[3] initialize_antecedents(sm::BeamSearch, X::PropositionalLogiset{…}, y::SubArray{…}; discretizedomain::Bool, default_alphabet::Nothing)this leads me to think that you are also working on SoleData package, correct? |
@PasoStudio73 The reason for the errors you're encountering is that I'm running the code on the branch fix/alphabet in SoleData.jl. The function "alphabet" in "SoleData.jl/src/scalar/propositional-logiset.jl" has an issue in main: Here the function call In "src/utils/models/other.jl" in lines 147 - 153 I did the same. So they are now Whereas before they didn't allow the passing of kwargs. |
Thanks for the super quick reply @NickT42, and for all the details you provided me, very very appreciated! This makes sense and I agree with you: those fixes must be merged in main in SoleData, we'll take care do fix that! |
The debugging lines from irepstar() have been updated to use the 'DataSplit' function, fixing a potential bug. The file src/interfaces/MLJ.jl has been updated to work with the new loss function system. A bug was also fixed in loss_functions.jl, where symmetric loss functions were calling functions from StatsBase instead of those in the ModalDecisionLists package.Lastly, testing packages such as Revise or Test have been removed from the dependencies
The dependencies were updated, and specific versions for packages in [extras] have been inserted in [compat]
The 'sequentialcovering' method was fixed, as well as loss function calls in symmetric losses. The 'max_infogain_ratio' parameter's behavior was reintroduced with the new loss function system, and 'max_rulebase_length' was fixed in the ripperk binary version. The 'build_ensemble' method and its tests were removed from this package, as they were moved to a separate PR in SoleModels, where they belong.
The operators used on continuous variables when creating antecedents have been changed from [<=, >=] to [<, >=] to allow compatibility with LUMEN and to follow the standards in the rest of the Sole.jl framework
the optimization phase was previously being carried out only on uncovered data, now it's done on the whole dataset, as it should according to the original paper
d1f2e21 to
4caa06a
Compare
I'm opening this PR for Trombini's work so that it will be easier to review the code and then merge it.