| #------------------------------------------------------------- |
| # |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| # |
| #------------------------------------------------------------- |
| |
| # Coreset selection for Classification via a depth-bounded decision tree. |
| # |
| # The method is adapted from the datamap-driven coreset approach of Hadar et al., |
| # "Datamap-Driven Tabular Coreset Selection for Classifier Training" |
| # (https://www.vldb.org/pvldb/vol18/p876-razmadze.pdf). |
| # |
| # A decision tree is trained on (X, y) and each leaf defines a region which |
| # is classified by its majority fraction and size. |
| # |
| # majority_fraction < psi -> hard (mixed labels, keep whole) |
| # majority_fraction >= psi, n <= tau -> ambiguous (pure but small, keep whole) |
| # majority_fraction >= psi, n > tau -> easy (large + pure, sample down) |
| # |
| # Hard and ambiguous regions are kept in full. Easy regions are sampled down |
| # to samp_ratio, processed largest-first, until the target size is reached, |
| # keeping any unseen regions whole once they fit. The result cannot |
| # shrink below: |
| # n_min ~ |hard rows| + |ambiguous rows| + (samp_ratio * |easy rows|) |
| # and requests below this floor return roughly the floor (with a warning). |
| # |
| # INPUT: |
| # ------------------------------------------------------------------------------ |
| # X Feature matrix in recoded/binned representation |
| # (as required by decisionTree) |
| # y Continuous label vector (as required by decisionTree) |
| # fraction Target coreset size as a fraction of nrow(X) (default 0.3) |
| # samp_ratio Thinning ratio applied to easy regions (default 0.03) |
| # psi Homogeneity threshold separating hard regions (default 0.90) |
| # tau Absolute size threshold separating easy from ambiguous |
| # regions among pure leaves (default 10) |
| # min_leaf Minimum samples per leaf (default 5) |
| # max_depth Bounded tree depth for the datamap (default 12) |
| # seed Seed for reproducible thinning, -1 for random (default -1) |
| # verbose Print information and summary (default FALSE) |
| # ------------------------------------------------------------------------------ |
| # |
| # OUTPUT: |
| # ------------------------------------------------------------------------------ |
| # Xc Coreset feature matrix |
| # yc Coreset label vector |
| # ------------------------------------------------------------------------------ |
| |
| m_coresetDT = function(Matrix[Double] X, Matrix[Double] y, |
| Double fraction = 0.3, Double samp_ratio = 0.03, Double psi = 0.90, Int tau = 10, |
| Int max_depth = 12, Int min_leaf = 5, |
| Int seed = -1, Boolean verbose = FALSE) |
| |
| return(Matrix[Double] Xc, Matrix[Double] yc) |
| { |
| n = nrow(X); |
| k = as.integer(max(y)); |
| nTarget = round(fraction * n) |
| |
| # Step 1: build the datamap |
| ctypes = cbind(matrix(1, 1, ncol(X)), matrix(2, 1, 1)); |
| tree = decisionTree(X=X, y=y, ctypes=ctypes, max_depth=max_depth, |
| min_leaf=min_leaf, min_split=2*min_leaf, max_features=1.0, seed=seed); |
| |
| # route every row to its leaf (tree traversal adapted from decisionTreePredict.dml) |
| numNodes = as.integer(ncol(tree) / 2); |
| nodes = matrix(tree, rows=numNodes, cols=2); |
| features = nodes[,1]; # split feature per node, 0 for leaves |
| values = nodes[,2]; # split value per node, class label for leaves |
| |
| Tidx = matrix(1, n, 1); |
| noChange = FALSE; |
| i = 1; |
| while(!noChange & i <= max_depth) { |
| P = table(seq(1,n), Tidx, n, numNodes); # one hot row->node |
| f = P %*% features; # split feature per row |
| v = P %*% values ; # split value per row |
| isNotLeaf = f > 0; # check if internal node |
| xv = rowSums(X * table(seq(1,n), max(f,1), n, ncol(X))); # xv[r] = X[r, f[r]] rows value for current split feature |
| Tidx_new = ifelse(isNotLeaf, 2*Tidx + (xv > v), Tidx); # descend to child |
| noChange = (sum(Tidx != Tidx_new) == 0); |
| Tidx = Tidx_new; |
| i = i + 1; |
| } |
| P = table(seq(1,n), Tidx, n, numNodes); |
| |
| # region stats |
| C = table(Tidx, y, numNodes, k); # label counts per region |
| s = rowSums(C); # region sizes |
| mf = rowMaxs(C) / max(s, 1); # majority fraction |
| exists = (s > 0); |
| isHard = exists & (mf < psi); |
| isAmb = exists & (mf >= psi) & (s <= tau); |
| isEasy = exists & (mf >= psi) & (s > tau); |
| |
| nHardRows = sum(isHard * s); |
| nAmbRows = sum(isAmb * s); |
| nEasyRows = sum(isEasy * s); |
| nFloor = nHardRows + nAmbRows + ceil(samp_ratio * nEasyRows); |
| |
| if(verbose) { |
| pHard = round(1000 * nHardRows / n) / 10; |
| pAmb = round(1000 * nAmbRows/ n) / 10; |
| pEasy = round(1000 * nEasyRows / n) / 10; |
| |
| print("coresetDT: datamap (psi=" + psi + ", tau=" + tau + ", max_depth=" + max_depth |
| + ", samp_ratio=" + samp_ratio + ") over " + n + " rows"); |
| |
| print("coresetDT: hard (mixed, mf<" + psi + "): " + sum(isHard) + " regions, " |
| + nHardRows + " rows (" + pHard + "% of data) -> kept whole"); |
| print("coresetDT: ambiguous (pure, size<=" + tau + "): " + sum(isAmb) + " regions, " |
| + nAmbRows + " rows (" + pAmb + "% of data) -> kept whole"); |
| print("coresetDT: easy (pure, size>" + tau + "): " + sum(isEasy) + " regions, " |
| + nEasyRows + " rows (" + pEasy + "% of data) -> thinned to "+ samp_ratio); |
| |
| print("coresetDT: size floor n_min = " + nFloor + " (" + (round(1000 * nFloor/ n) / 10) + "% of n)"); |
| } |
| |
| if(nTarget < nFloor) |
| print("coresetDT: WARN: requested size " + nTarget + " is below the floor " + nFloor + ". Returning ~ " + nFloor + |
| " rows. Lower samp_ratio, raise psi, or loosen the tree to go smaller."); |
| |
| # Step 2: select the coreset |
| keep = P %*% (isHard + isAmb); |
| cur = sum(keep); |
| |
| # Spend remaining budget on easy regions |
| nEasy = as.integer(sum(isEasy)); |
| |
| if(cur < nTarget) { |
| easyIdx = removeEmpty(target=seq(1,numNodes), margin="rows", select=isEasy); |
| easySize = removeEmpty(target=s, margin="rows", select=isEasy); |
| |
| ordered = order(target=easySize, by=1, decreasing=TRUE, index.return=TRUE); |
| randVals = rand(rows=n, cols=1, seed=seed); |
| visited = matrix(0, numNodes, 1); |
| remaining = nEasyRows; |
| |
| done = FALSE; |
| for(i in 1:nEasy) { |
| if(!done) { |
| pos = as.scalar(ordered[i,1]); |
| regid = as.scalar(easyIdx[pos,1]); |
| regSize = as.scalar(easySize[pos,1]); |
| |
| visited[regid,1] = 1; |
| remaining = remaining - regSize; |
| |
| # thin region to samp_ratio, its the k smallest random scores within region |
| keeprows = max(round(samp_ratio * regSize), 1); |
| I = P[,regid]; # rows in this region |
| regRows = removeEmpty(target=seq(1,n), margin="rows", select=I); # global ids of region rows |
| regScores = removeEmpty(target=randVals, margin="rows", select=I); # their random scores |
| rank = order(target=regScores, by=1, index.return=TRUE); # rank rows within region |
| |
| chosenIds = table(seq(1,keeprows), rank[1:keeprows,1], keeprows, nrow(regRows)) %*% regRows; |
| keep = keep + table(chosenIds, 1, n, 1); |
| cur = cur + keeprows; |
| |
| # if all unvisited easy regions fit into the budget, add them |
| if(cur + remaining <= nTarget) { |
| keep = keep + P %*% (isEasy *(1 - visited)); |
| cur = cur + remaining; |
| done = TRUE; |
| } |
| } |
| } |
| } |
| |
| # extract coreset rows |
| toKeepIdx = removeEmpty(target=seq(1,n), margin="rows", select=keep); |
| Psel = table(seq(1,nrow(toKeepIdx)), toKeepIdx, nrow(toKeepIdx), n); |
| Xc = Psel %*% X; |
| yc = Psel %*% y; |
| |
| if(verbose) { |
| sel = nrow(Xc); |
| mustKeep = nHardRows + nAmbRows; |
| sampledEasy = sel - mustKeep; |
| print("coresetDT: selected " + sel + " / " + n + " rows (" + (round(1000 * sel / n) / 10) + "% of data) " + mustKeep + |
| " must-keep (hard+ambiguous) + " + sampledEasy + " sampled from easy"); |
| } |
| } |