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 const std::size_t targetCumulativeTrain =
73 static_cast<std::size_t>(cumulativeEntries * (1.0f - fValidationSplit));
74 const std::size_t trainSz = targetCumulativeTrain - currentCumulativeTrain;
75 currentCumulativeTrain = targetCumulativeTrain;
76 const std::size_t valSz = sz - trainSz;
77
78 // Randomly assign prefix or suffix to training
79 bool trainIsPrefix = coin(g);
80 const uint64_t trainStart = trainIsPrefix ? c.start : c.start + static_cast<std::uint64_t>(valSz);
81 const uint64_t valStart = trainIsPrefix ? c.start + static_cast<std::uint64_t>(trainSz) : c.start;
82
83 if (trainSz > 0) {
84 fTrainingClusters.push_back({c.rdfIdx, trainStart, trainStart + static_cast<std::uint64_t>(trainSz)});
85 fNumTrainingEntries += trainSz;
86 }
87 if (valSz > 0) {
88 fValidationClusters.push_back({c.rdfIdx, valStart, valStart + static_cast<std::uint64_t>(valSz)});
89 fNumValidationEntries += valSz;
90 }
91 }
92 } else {
93 // --- Unshuffled path
94 // Contiguous split: first (1 - validationSplit) fraction of entries go to
95 // training, the remainder to validation. At most one cluster is split at
96 // the boundary.
97 const std::size_t targetTraining = fTotalEntries - static_cast<std::size_t>(fValidationSplit * fTotalEntries);
98
99 std::size_t accumulated = 0;
100 std::size_t splitIdx = 0;
101 for (; splitIdx < fAllClusters.size(); ++splitIdx) {
102 const std::size_t sz = fAllClusters[splitIdx].GetNumEntries();
103 if (accumulated + sz > targetTraining) {
104 break;
105 }
106 accumulated += sz;
107 }
108
109 // Assign whole train/val clusters
110 fTrainingClusters.assign(fAllClusters.begin(), fAllClusters.begin() + splitIdx);
111 fNumTrainingEntries = accumulated;
112
113 if (splitIdx < fAllClusters.size() && accumulated < targetTraining) {
114 // Split the boundary cluster
115 const RClusterRange &boundary = fAllClusters[splitIdx];
116 const std::uint64_t splitPoint = boundary.start + static_cast<std::uint64_t>(targetTraining - accumulated);
117
118 fTrainingClusters.push_back({boundary.rdfIdx, boundary.start, splitPoint});
119 fValidationClusters.push_back({boundary.rdfIdx, splitPoint, boundary.end});
120 fValidationClusters.insert(fValidationClusters.end(), fAllClusters.begin() + splitIdx + 1, fAllClusters.end());
121
122 fNumTrainingEntries += splitPoint - boundary.start;
123 } else {
124 fValidationClusters.assign(fAllClusters.begin() + splitIdx, fAllClusters.end());
125 }
126
128 }
129
130 if (fTrainingClusters.empty())
131 throw std::runtime_error("RClusterLoader::SplitDataset: no entries for training after split. "
132 "Reduce validation_split.");
133
134 if (fValidationSplit > 0.0f && fValidationClusters.empty())
135 throw std::runtime_error("RClusterLoader::SplitDataset: no entries for validation after split. "
136 "Increase validation_split.");
137}
138
139//////////////////////////////////////////////////////////////////////////
140/// \brief Re-order training clusters for the upcoming epoch
142{
143 if (!fShuffle) {
144 return;
145 }
146
147 std::mt19937 g(fSetSeed == 0 ? std::random_device{}() : fSetSeed ^ epochIdx);
148 std::shuffle(fTrainingClusters.begin(), fTrainingClusters.end(), g);
149}
150
151//////////////////////////////////////////////////////////////////////////
152/// \brief Re-order validation clusters for the upcoming epoch
154{
155 if (!fShuffle) {
156 return;
157 }
158 std::mt19937 g(fSetSeed == 0 ? std::random_device{}() : fSetSeed ^ epochIdx);
159 std::shuffle(fValidationClusters.begin(), fValidationClusters.end(), g);
160}
161
162void RClusterLoader::LoadClusterInto(RFlat2DMatrix &dest, std::size_t rdfIdx, std::uint64_t startRow,
163 std::uint64_t endRow)
164{
165 ROOT::RDF::RNode &rdf = fRdfs[rdfIdx];
167 // the values are appended after the rows already in dest
170}
171
172//////////////////////////////////////////////////////////////////////////
173/// \brief Load one training cluster and return the number of rows written.
174///
175/// **Unfiltered**: delegates directly to `LoadClusterInto()`
176/// **Filtered**, epoch 1 (!fSplitDiscovered):
177/// - On the first call, Count() is called across all RDFs to obtain
178/// the total filtered entry count, fNumTrainingEntries and
179/// fNumValidationEntries are set as targets.
180/// - A single Foreach on the full raw cluster range loads data and captures
181/// rdfentry_ simultaneously. The real train/val boundary is computed from
182/// the accumulated filtered count vs the target, then the train sub-range
183/// is pushed to fTrainingClusters and the val sub-range to fValidationClusters.
184/// - Only the train rows are written into \p dest.
185/// -All subsequent epochs: delegates directly to `LoadClusterInto()`
186std::size_t RClusterLoader::LoadTrainingClusterInto(RFlat2DMatrix &dest, std::size_t rdfIdx, std::uint64_t startRow,
187 std::uint64_t endRow)
188{
190 // First call: discover total filtered count and set split targets.
192 std::vector<ROOT::RDF::RResultPtr<ULong64_t>> counts;
193 counts.reserve(fRdfs.size());
194 for (auto &rdf : fRdfs) {
195 counts.push_back(rdf.Count());
196 }
197 ROOT::RDF::RunGraphs({counts.begin(), counts.end()});
198
199 std::size_t totalFiltered = 0;
200 for (auto &c : counts) {
201 totalFiltered += c.GetValue();
202 }
203 fNumTrainingEntries = static_cast<std::size_t>(totalFiltered * (1.0f - fValidationSplit));
205 }
206
207 ROOT::RDF::RNode &rdf = fRdfs[rdfIdx];
208
209 std::vector<ULong64_t> rdfEntries;
210 rdfEntries.reserve(endRow - startRow);
211
213 rdf.Foreach([&](ULong64_t entry) { rdfEntries.push_back(entry); }, {"rdfentry_"});
215
216 const std::size_t totalFiltered = rdfEntries.size();
217 if (totalFiltered == 0) {
218 return 0;
219 }
220 std::sort(rdfEntries.begin(), rdfEntries.end());
221
222 const std::size_t cumulativeFiltered = fAccumulatedFilteredForTrain + fAccumulatedFilteredForVal + totalFiltered;
223 const std::size_t targetCumulativeTrain =
224 std::min(static_cast<std::size_t>(cumulativeFiltered * (1.0f - fValidationSplit)), fNumTrainingEntries);
225 const std::size_t trainCount = targetCumulativeTrain - fAccumulatedFilteredForTrain;
226 const std::size_t valCount = totalFiltered - trainCount;
227
228 bool trainIsPrefix = true;
229 if (fShuffle) {
230 // If shuffling is enabled, we generate a random boolean value to decide whether the training set
231 // gets the prefix or suffix of each cluster to ensure better shuffling across runs when splitting.
232 std::mt19937 g(fSetSeed + fAccumulatedFilteredForTrain); // vary per cluster
233 std::uniform_int_distribution<int> coin(0, 1);
234 trainIsPrefix = coin(g);
235 }
236
237 // The boundary is the raw entry index that splits train and val sub-ranges within the
238 // cluster. Stable across epochs since the same filter always produces the same ordered
239 // entries. When one side has no filtered entries we fall back to the cluster endpoint that
240 // collapses that side to an empty range, avoiding an out-of-bounds access into rdfEntries
241 // (whose size is totalFiltered, so rdfEntries[totalFiltered] is OOB and trips libstdc++
242 // hardened-mode assertions).
243 std::uint64_t boundary;
244 if (trainIsPrefix) {
245 // train = [startRow, boundary), val = [boundary, endRow)
246 boundary = (trainCount < totalFiltered) ? rdfEntries[trainCount] : endRow;
247 } else {
248 // train = [boundary, endRow), val = [startRow, boundary)
249 boundary = (valCount < totalFiltered) ? rdfEntries[valCount] : endRow;
250 }
251
252 const std::uint64_t trainStart = trainIsPrefix ? startRow : boundary;
253 const std::uint64_t trainEnd = trainIsPrefix ? boundary : endRow;
254 const std::uint64_t valStart = trainIsPrefix ? boundary : startRow;
255 const std::uint64_t valEnd = trainIsPrefix ? endRow : boundary;
256
257 if (trainCount > 0)
258 fTrainingClusters.push_back({rdfIdx, trainStart, trainEnd, trainCount});
259 if (valCount > 0)
260 fValidationClusters.push_back({rdfIdx, valStart, valEnd, valCount});
261
262 fAccumulatedFilteredForTrain += trainCount;
263 fAccumulatedFilteredForVal += valCount;
264
265 if (trainCount > 0)
266 LoadClusterInto(dest, rdfIdx, trainStart, trainEnd);
267
268 return trainCount;
269 }
270
271 LoadClusterInto(dest, rdfIdx, startRow, endRow);
272 return endRow - startRow;
273}
274
275//////////////////////////////////////////////////////////////////////////
276/// \brief Load one validation cluster at the end of \p dest
277void RClusterLoader::LoadValidationClusterInto(RFlat2DMatrix &dest, std::size_t rdfIdx, std::uint64_t startRow,
278 std::uint64_t endRow)
279{
280 LoadClusterInto(dest, rdfIdx, startRow, endRow);
281}
282
283//////////////////////////////////////////////////////////////////////////
284/// \brief Mark the train/val split as finalised after the first epoch
290
291} // 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.
void Foreach(F f, const ColumnNames_t &columns={})
Execute a user-defined function on each entry (instant action).
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.