Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
RClusterLoader.cxx
Go to the documentation of this file.
1#include "RClusterLoader.hxx"
2
4
5RClusterLoader::RClusterLoader(std::vector<ROOT::RDF::RNode> &rdfs, const std::vector<std::string> &cols,
6 const std::vector<std::size_t> &vecSizes, float vecPadding, float validationSplit,
7 bool shuffle, std::size_t setSeed)
8 : fRdfs(rdfs),
9 fCols(cols),
10 fVecSizes(vecSizes),
11 fVecPadding(vecPadding),
12 fValidationSplit(validationSplit),
13 fShuffle(shuffle),
14 fSetSeed(setSeed)
15{
16 fNumCols = fCols.size();
17 fSumVecSizes = std::accumulate(fVecSizes.begin(), fVecSizes.end(), 0UL);
19
20 for (auto &rdf : fRdfs) {
21 // TODO(staider) We need a better API in RDF to detect generically whether there's a filter or not
22 if (!rdf.GetFilterNames().empty()) {
23 fIsFiltered = true;
24 break;
25 }
26 }
27
28 fRdfSizes.resize(fRdfs.size(), 0);
29
30 // scan cluster boundaries across files
31 // TODO(staider) Add progress bar to inform the user about this potentially long operation
32 for (std::size_t rdfIdx = 0; rdfIdx < fRdfs.size(); ++rdfIdx) {
34 fAllClusters.push_back({rdfIdx, r.first, r.second});
35 auto numEntries = r.second - r.first;
36 fRdfSizes[rdfIdx] += numEntries;
37 fTotalEntries += numEntries;
38 }
39 }
40}
41
42//////////////////////////////////////////////////////////////////////////
43/// \brief Distribute the clusters into training and validation datasets
44/// No-op for filtered RDataFrames, the split is discovered lazily during the first epoch.
46{
47 if (fAllClusters.empty())
48 throw std::runtime_error("RClusterLoader::SplitDataset: no clusters found.");
49
50 if (fIsFiltered) {
51 return;
52 }
53
54 if (fShuffle) {
55 // --- Shuffled path
56 // Every cluster contributes a prefix to training and a suffix to validation.
57 // Cost: Each cluster is read twice per epoch, only when validation split is more than 0.
58 // We generate a random boolean value to decide whether the training set gets the prefix
59 // or suffix of each cluster to ensure better shuffling across runs when splitting.
60 std::mt19937 g(fSetSeed);
61 std::uniform_int_distribution<int> coin(0, 1);
62
63 std::size_t cumulativeEntries = 0;
64 std::size_t currentCumulativeTrain = 0;
65 // We iterate over clusters and accumulate the entry counts to assign training and validation sizes
66 // proportionally to the cluster size. Filtered clusters have varying sizes, so instead of calculating
67 // the training size as a fraction of each cluster's size independently, we take into account
68 // the cumulative counts of previous clusters in each calculation.
69 for (const RClusterRange &c : fAllClusters) {
70 const std::size_t sz = c.GetNumEntries();
71 cumulativeEntries += sz;
72 // the validation entries are rounded down, as in the unshuffled split and the eager loading
73 const std::size_t targetCumulativeTrain =
74 cumulativeEntries - static_cast<std::size_t>(fValidationSplit * cumulativeEntries);
75 const std::size_t trainSz = targetCumulativeTrain - currentCumulativeTrain;
76 currentCumulativeTrain = targetCumulativeTrain;
77 const std::size_t valSz = sz - trainSz;
78
79 // Randomly assign prefix or suffix to training
80 bool trainIsPrefix = coin(g);
81 const uint64_t trainStart = trainIsPrefix ? c.start : c.start + static_cast<std::uint64_t>(valSz);
82 const uint64_t valStart = trainIsPrefix ? c.start + static_cast<std::uint64_t>(trainSz) : c.start;
83
84 if (trainSz > 0) {
85 fTrainingClusters.push_back({c.rdfIdx, trainStart, trainStart + static_cast<std::uint64_t>(trainSz)});
86 fNumTrainingEntries += trainSz;
87 }
88 if (valSz > 0) {
89 fValidationClusters.push_back({c.rdfIdx, valStart, valStart + static_cast<std::uint64_t>(valSz)});
90 fNumValidationEntries += valSz;
91 }
92 }
93 } else {
94 // --- Unshuffled path
95 // Contiguous split: first (1 - validationSplit) fraction of entries go to
96 // training, the remainder to validation. At most one cluster is split at
97 // the boundary.
98 const std::size_t targetTraining = fTotalEntries - static_cast<std::size_t>(fValidationSplit * fTotalEntries);
99
100 std::size_t accumulated = 0;
101 std::size_t splitIdx = 0;
102 for (; splitIdx < fAllClusters.size(); ++splitIdx) {
103 const std::size_t sz = fAllClusters[splitIdx].GetNumEntries();
104 if (accumulated + sz > targetTraining) {
105 break;
106 }
107 accumulated += sz;
108 }
109
110 // Assign whole train/val clusters
111 fTrainingClusters.assign(fAllClusters.begin(), fAllClusters.begin() + splitIdx);
112 fNumTrainingEntries = accumulated;
113
114 if (splitIdx < fAllClusters.size() && accumulated < targetTraining) {
115 // Split the boundary cluster
116 const RClusterRange &boundary = fAllClusters[splitIdx];
117 const std::uint64_t splitPoint = boundary.start + static_cast<std::uint64_t>(targetTraining - accumulated);
118
119 fTrainingClusters.push_back({boundary.rdfIdx, boundary.start, splitPoint});
120 fValidationClusters.push_back({boundary.rdfIdx, splitPoint, boundary.end});
121 fValidationClusters.insert(fValidationClusters.end(), fAllClusters.begin() + splitIdx + 1, fAllClusters.end());
122
123 fNumTrainingEntries += splitPoint - boundary.start;
124 } else {
125 fValidationClusters.assign(fAllClusters.begin() + splitIdx, fAllClusters.end());
126 }
127
129 }
130
131 if (fTrainingClusters.empty())
132 throw std::runtime_error("RClusterLoader::SplitDataset: no entries for training after split. "
133 "Reduce validation_split.");
134
135 if (fValidationSplit > 0.0f && fValidationClusters.empty())
136 throw std::runtime_error("RClusterLoader::SplitDataset: no entries for validation after split. "
137 "Increase validation_split.");
138}
139
140//////////////////////////////////////////////////////////////////////////
141/// \brief Re-order training clusters for the upcoming epoch
143{
144 if (!fShuffle) {
145 return;
146 }
147
148 std::mt19937 g(fSetSeed == 0 ? std::random_device{}() : fSetSeed ^ epochIdx);
149 std::shuffle(fTrainingClusters.begin(), fTrainingClusters.end(), g);
150}
151
152//////////////////////////////////////////////////////////////////////////
153/// \brief Re-order validation clusters for the upcoming epoch
155{
156 if (!fShuffle) {
157 return;
158 }
159 std::mt19937 g(fSetSeed == 0 ? std::random_device{}() : fSetSeed ^ epochIdx);
160 std::shuffle(fValidationClusters.begin(), fValidationClusters.end(), g);
161}
162
163void RClusterLoader::LoadClusterInto(RFlat2DMatrix &dest, std::size_t rdfIdx, std::uint64_t startRow,
164 std::uint64_t endRow)
165{
166 ROOT::RDF::RNode &rdf = fRdfs[rdfIdx];
168 // the values are appended after the rows already in dest
171}
172
173//////////////////////////////////////////////////////////////////////////
174/// \brief Load one training cluster and return the number of rows written.
175///
176/// **Unfiltered**: delegates directly to `LoadClusterInto()`
177/// **Filtered**, epoch 1 (!fSplitDiscovered):
178/// - On the first call, Count() is called across all RDFs to obtain
179/// the total filtered entry count, fNumTrainingEntries and
180/// fNumValidationEntries are set as targets.
181/// - A single Foreach on the full raw cluster range loads data and captures
182/// rdfentry_ simultaneously. The real train/val boundary is computed from
183/// the accumulated filtered count vs the target, then the train sub-range
184/// is pushed to fTrainingClusters and the val sub-range to fValidationClusters.
185/// - Only the train rows are written into \p dest.
186/// -All subsequent epochs: delegates directly to `LoadClusterInto()`
187std::size_t RClusterLoader::LoadTrainingClusterInto(RFlat2DMatrix &dest, std::size_t rdfIdx, std::uint64_t startRow,
188 std::uint64_t endRow)
189{
191 // First call: discover total filtered count and set split targets.
193 std::vector<ROOT::RDF::RResultPtr<ULong64_t>> counts;
194 counts.reserve(fRdfs.size());
195 for (auto &rdf : fRdfs) {
196 counts.push_back(rdf.Count());
197 }
198 ROOT::RDF::RunGraphs({counts.begin(), counts.end()});
199
200 std::size_t totalFiltered = 0;
201 for (auto &c : counts) {
202 totalFiltered += c.GetValue();
203 }
204 // the validation entries are rounded down, as in SplitDataset() and the eager loading
205 fNumValidationEntries = static_cast<std::size_t>(fValidationSplit * totalFiltered);
207 }
208
209 ROOT::RDF::RNode &rdf = fRdfs[rdfIdx];
210 const std::size_t numLoadedValues = dest.fRVec.size();
211
212 // load the whole cluster and capture rdfentry_ in the same event loop
214 auto entries = rdf.Take<ULong64_t>("rdfentry_");
217 auto &rdfEntries = *entries;
218
219 const std::size_t totalFiltered = rdfEntries.size();
220 if (totalFiltered == 0) {
221 return 0;
222 }
223 std::sort(rdfEntries.begin(), rdfEntries.end());
224
225 // same logic as SplitDataset():
226 // if shuffled, every cluster is split proportionally
227 // if unshuffled, the training target follows the cumulative count until it reaches fNumTrainingEntries,
228 // the clusters before go entirely to training, the cluster reaching it is the only one split at the boundary
229 // computed below, and the clusters after go entirely to validation
230 // the counts here are used to compute the train/val cluster ranges below
231 const std::size_t cumulativeFiltered = fAccumulatedFilteredForTrain + fAccumulatedFilteredForVal + totalFiltered;
232 const std::size_t targetCumulativeTrain =
233 std::min(fShuffle ? cumulativeFiltered - static_cast<std::size_t>(fValidationSplit * cumulativeFiltered)
234 : cumulativeFiltered,
236 const std::size_t trainCount = targetCumulativeTrain - fAccumulatedFilteredForTrain;
237 const std::size_t valCount = totalFiltered - trainCount;
238
239 bool trainIsPrefix = true;
240 if (fShuffle) {
241 // If shuffling is enabled, we generate a random boolean value to decide whether the training set
242 // gets the prefix or suffix of each cluster to ensure better shuffling across runs when splitting.
243 std::mt19937 g(fSetSeed + fAccumulatedFilteredForTrain); // vary per cluster
244 std::uniform_int_distribution<int> coin(0, 1);
245 trainIsPrefix = coin(g);
246 }
247
248 // The boundary is the raw entry index that splits train and val sub-ranges within the
249 // cluster. Stable across epochs since the same filter always produces the same ordered
250 // entries. When one side has no filtered entries we fall back to the cluster endpoint that
251 // collapses that side to an empty range, avoiding an out-of-bounds access into rdfEntries
252 // (whose size is totalFiltered, so rdfEntries[totalFiltered] is OOB and trips libstdc++
253 // hardened-mode assertions).
254 std::uint64_t boundary;
255 if (trainIsPrefix) {
256 // train = [startRow, boundary), val = [boundary, endRow)
257 boundary = (trainCount < totalFiltered) ? rdfEntries[trainCount] : endRow;
258 } else {
259 // train = [boundary, endRow), val = [startRow, boundary)
260 boundary = (valCount < totalFiltered) ? rdfEntries[valCount] : endRow;
261 }
262
263 const std::uint64_t trainStart = trainIsPrefix ? startRow : boundary;
264 const std::uint64_t trainEnd = trainIsPrefix ? boundary : endRow;
265 const std::uint64_t valStart = trainIsPrefix ? boundary : startRow;
266 const std::uint64_t valEnd = trainIsPrefix ? endRow : boundary;
267
268 if (trainCount > 0)
269 fTrainingClusters.push_back({rdfIdx, trainStart, trainEnd, trainCount});
270 if (valCount > 0)
271 fValidationClusters.push_back({rdfIdx, valStart, valEnd, valCount});
272
273 fAccumulatedFilteredForTrain += trainCount;
274 fAccumulatedFilteredForVal += valCount;
275
276 // keep only the training rows of the cluster, the validation ones are loaded by the validation epoch
277 float *rows = dest.fRVec.data() + numLoadedValues;
278 if (!trainIsPrefix)
279 std::copy(rows + valCount * fNumChunkCols, rows + totalFiltered * fNumChunkCols, rows);
280 dest.fRVec.resize(numLoadedValues + trainCount * fNumChunkCols);
281
282 return trainCount;
283 }
284
285 LoadClusterInto(dest, rdfIdx, startRow, endRow);
286 return endRow - startRow;
287}
288
289//////////////////////////////////////////////////////////////////////////
290/// \brief Load one validation cluster at the end of \p dest
291void RClusterLoader::LoadValidationClusterInto(RFlat2DMatrix &dest, std::size_t rdfIdx, std::uint64_t startRow,
292 std::uint64_t endRow)
293{
294 LoadClusterInto(dest, rdfIdx, startRow, endRow);
295}
296
297//////////////////////////////////////////////////////////////////////////
298/// \brief Mark the train/val split as finalised after the first epoch
304
305} // namespace ROOT::Experimental::Internal::ML
#define c(i)
Definition RSha256.hxx:101
#define g(i)
Definition RSha256.hxx:105
unsigned long long ULong64_t
Portable unsigned long integer 8 bytes.
Definition RtypesCore.h:85
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t dest
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t r
void FinaliseSplitDiscovery()
Mark the train/val split as finalised after the first epoch.
void SplitDataset()
Distribute the clusters into training and validation datasets No-op for filtered RDataFrames,...
void LoadClusterInto(RFlat2DMatrix &dest, std::size_t rdfIdx, std::uint64_t startRow, std::uint64_t endRow)
std::size_t LoadTrainingClusterInto(RFlat2DMatrix &dest, std::size_t rdfIdx, std::uint64_t startRow, std::uint64_t endRow)
Load one training cluster and return the number of rows written.
void ShuffleValidationClusters(std::size_t epochIdx)
Re-order validation clusters for the upcoming epoch.
void LoadValidationClusterInto(RFlat2DMatrix &dest, std::size_t rdfIdx, std::uint64_t startRow, std::uint64_t endRow)
Load one validation cluster at the end of dest.
RClusterLoader(std::vector< ROOT::RDF::RNode > &rdfs, const std::vector< std::string > &cols, const std::vector< std::size_t > &vecSizes, float vecPadding, float validationSplit, bool shuffle, std::size_t setSeed)
void ShuffleTrainingClusters(std::size_t epochIdx)
Re-order training clusters for the upcoming epoch.
The public interface to the RDataFrame federation of classes.
RResultPtr< COLL > Take(std::string_view column="")
Return a collection of values of a column (lazy action, returns a std::vector by default).
void LoadCustomValues(ROOT::RDF::RNode &node, const std::vector< std::string > &cols, const std::vector< std::size_t > &vecSizes, float vecPadding, ROOT::RVecF &dest)
std::vector< std::pair< std::uint64_t, std::uint64_t > > GetDatasetGlobalClusterBoundaries(const RNode &node)
Retrieve the cluster boundaries for each cluster in the dataset, across files, with a global offset.
void ChangeBeginAndEndEntries(const RNode &node, Long64_t begin, Long64_t end)
unsigned int RunGraphs(std::vector< RResultHandle > handles)
Run the event loops of multiple RDataFrames concurrently.
Describes a contiguous range of entries within a single RDataFrame, corresponding to one TTree/RNTupl...
Wrapper around ROOT::RVec<float> representing a 2D matrix.