pruning! turns a split node back into a leaf via delete-children!, but it does not restore the node's sample-indices. Since make-forest defaults to :remove-sample-indices? t, every node that was split already had its sample-indices set to nil, so the newly created leaves have none. Any subsequent predict-forest / test-forest then fails.
Reproduction
Self-contained, no dataset download needed (SBCL 2.6.7, master @ b4e5ec5):
(ql:quickload :cl-random-forest)
(defparameter *seed* 42)
(defun nextrand ()
(setf *seed* (mod (+ (* 1103515245 *seed*) 12345) 2147483648))
(/ (float *seed* 1.0) 2147483648.0))
(defun noise () (* 2.0 (- (+ (nextrand) (nextrand) (nextrand)) 1.5)))
(defun gen (n-datum n-dim n-class signal)
(let ((dm (make-array (list n-datum n-dim) :element-type 'single-float))
(tg (make-array n-datum :element-type 'fixnum)))
(dotimes (i n-datum)
(let ((c (mod i n-class)))
(setf (aref tg i) c)
(dotimes (j n-dim)
(setf (aref dm i j) (+ (noise) (if (= (mod j n-class) c) signal 0.0))))))
(values dm tg)))
(multiple-value-bind (x y) (gen 2000 20 5 1.0)
(defparameter *x* x)
(defparameter *y* y))
;; NOTE: :remove-sample-indices? defaults to T
(defparameter *forest*
(clrf:make-forest 5 *x* *y* :n-tree 30 :bagging-ratio 0.2 :max-depth 8 :n-trial 15))
(defparameter *rd* (clrf:make-refine-dataset *forest* *x*))
(defparameter *learner* (clrf:make-refine-learner *forest*))
(clrf:train-refine-learner *learner* *rd* *y*)
(clrf:test-forest *forest* *x* *y* :quiet-p t) ; => 85.1 (OK)
(clrf:pruning! *forest* *learner* 0.2)
(clrf:test-forest *forest* *x* *y* :quiet-p t) ; => TYPE-ERROR
Result:
The value
NIL
is not of type
(SIMPLE-ARRAY FIXNUM)
from the function type declaration.
0: CL-RANDOM-FOREST/SRC/RANDOM-FOREST::NODE-CLASS-DISTRIBUTION (src/random-forest.lisp:221)
1: CL-RANDOM-FOREST/SRC/RANDOM-FOREST::CLASS-DISTRIBUTION-FOREST (src/random-forest.lisp:707)
2: CL-RANDOM-FOREST/SRC/RANDOM-FOREST:PREDICT-FOREST (src/random-forest.lisp:730)
3: CL-RANDOM-FOREST/SRC/RANDOM-FOREST:TEST-FOREST (src/random-forest.lisp:739)
Cause
set-best-children! nils out the parent's sample-indices when the tree was built with the default :remove-sample-indices? t (src/random-forest.lisp:414-415):
(when (dtree-remove-sample-indices? dtree)
(setf (node-sample-indices node) nil))
delete-children! (src/random-forest.lisp:1212-1217) makes that node a leaf again but never puts sample-indices back:
(defun delete-children! (node)
(setf (node-test-attribute node) nil
(node-test-threshold node) nil
(node-left-node node) nil
(node-right-node node) nil)
node)
node-class-distribution (src/random-forest.lisp:221-224) recomputes the leaf distribution from sample-indices on every prediction, so it receives nil.
Why it is easy to miss
The refine-learner path keeps working after pruning, because make-refine-vector only reads node-leaf-index. So make-refine-dataset -> train-refine-learner -> test-refine-learner all succeed, and the failure only shows up when predict-forest / test-forest is called on the pruned forest.
Regression forests have the same structure through node-regression-mean (src/random-forest.lisp:515).
Workaround
Build the forest with :remove-sample-indices? nil. Verified that the exact same pruning sequence then completes and test-forest works (71.2% before pruning -> 71.5% after, on a 100-tree forest).
Suggested fix
Either
- make
delete-children! restore the node's sample-indices by concatenating the sample indices of the subtree leaves being discarded, or
- have
pruning! assert that the forest was built with :remove-sample-indices? nil, and document the requirement (the same way src/feature-importance.lisp needs it for the OOB set).
The first keeps pruning! usable with the memory-saving default; the second is the minimal change.
Related: #15 (forest-n-leaf is stale after pruning!).
pruning!turns a split node back into a leaf viadelete-children!, but it does not restore the node'ssample-indices. Sincemake-forestdefaults to:remove-sample-indices? t, every node that was split already had itssample-indicesset tonil, so the newly created leaves have none. Any subsequentpredict-forest/test-forestthen fails.Reproduction
Self-contained, no dataset download needed (SBCL 2.6.7, master @ b4e5ec5):
Result:
Cause
set-best-children!nils out the parent'ssample-indiceswhen the tree was built with the default:remove-sample-indices? t(src/random-forest.lisp:414-415):delete-children!(src/random-forest.lisp:1212-1217) makes that node a leaf again but never putssample-indicesback:node-class-distribution(src/random-forest.lisp:221-224) recomputes the leaf distribution fromsample-indiceson every prediction, so it receivesnil.Why it is easy to miss
The refine-learner path keeps working after pruning, because
make-refine-vectoronly readsnode-leaf-index. Somake-refine-dataset->train-refine-learner->test-refine-learnerall succeed, and the failure only shows up whenpredict-forest/test-forestis called on the pruned forest.Regression forests have the same structure through
node-regression-mean(src/random-forest.lisp:515).Workaround
Build the forest with
:remove-sample-indices? nil. Verified that the exact same pruning sequence then completes andtest-forestworks (71.2% before pruning -> 71.5% after, on a 100-tree forest).Suggested fix
Either
delete-children!restore the node'ssample-indicesby concatenating the sample indices of the subtree leaves being discarded, orpruning!assertthat the forest was built with:remove-sample-indices? nil, and document the requirement (the same waysrc/feature-importance.lispneeds it for the OOB set).The first keeps
pruning!usable with the memory-saving default; the second is the minimal change.Related: #15 (
forest-n-leafis stale afterpruning!).