为机器学习打开ROOT NTuple的最快、最节省内存的方法



我正在用scikit learn建立一个机器学习项目。输入数据是平坦的ROOT N元组。

在过去,我一直在使用root_numpy将NT胞胎转换为熊猫。DataFrame保存在h5文件中。

我想知道我是否可以使用连根拔起来
a)完全跳过h5转换
b)是否使用与从h5加载DataFrame一样多的内存?

我天真的第一次尝试看起来像这样:

'''
Runs preselection, keeps only desired variables in DataFrame
'''
def dropAndKeep(df, dropVariables=None, keepVariables=None, presel=None, inplace=True):
if ((presel is not None) and (not callable(presel))):
print("Please either provide a function to 'presel' or leave blank")
raise ValueError
if callable(presel):
if not(inplace):
df = df.drop(df[~presel(df)].index, inplace=False)
else:
df.drop(df[~presel(df)].index, inplace=True)
if keepVariables is not None:
dropThese = list( set(df.columns) - set(keepVariables) )
return df.drop(columns=dropThese, inplace=inplace)
if dropVariables is not None:
return df.drop(columns=dropVariables, inplace=inplace)
'''
Loads a TTree from ROOT file into a DataFrame 
'''
def load_root(inFile, key, dropVariables=None, keepVariables=None, presel=None):
df = uproot.open(inFile)[key].pandas.df()
dropAndKeep(df, dropVariables, keepVariables, presel=presel, inplace=True)
return df

inFile = "path/to/file.root"
key = "ntuple"
df = load_root(inFile, key)

这需要很长时间。有更好的方法吗?

请注意,对uproot.open(...)file [key]的每次调用都使用纯Python加载TFile和TTree元数据,这是连根拔起的最慢部分。如果您多次调用此函数,请尝试保留TFile和/或TTree对象并重新使用它们。

此外,看起来dropAndKeep函数只是删除行(事件),但如果我读错了,它正在执行列(分支),那么使用连根拔的数组读取函数的branches参数只发送您想要的分支。由于ROOT文件中的数据是按列排列的,因此您无法避免读取不需要的事件——您必须在事实之后(在任何框架中)剪切它们。

接下来,请注意,对于过滤事件等简单操作,Pandas比NumPy慢得多。如果您想加快速度,请使用TTree.arrays而不是TTree.pandas.df获取数组,为您的选择构造一个布尔值的NumPy数组,并将其应用于TTree.arrays返回的dict中的每个数组。然后,您可以使用Pandas的DataFrame构造函数将所有这些放入DataFrame中(如果您真的需要Pandas)。

的确,你不需要经历HDF5,也不需要经历Pandas。你的机器学习框架(TensorFlow?Torch?)几乎肯定有一个接口,可以接受零拷贝(或一个拷贝到GPU)的NumPy数组。强调HDF5或Pandas的教程之所以这样做,是因为对于大多数用户(非HEP)来说,这些是最方便的界面。他们的数据可能已经在HDF5或Pandas中;我们的数据可能在ROOT中。

如果你的机器学习将在GPU上进行,也许你也想在GPU上选择事件。CuPy是一个完全在GPU上分配和操作的NumPy克隆,您的TensorFlow/Torch张量可能有一个到CuPy阵列的零拷贝接口。原则上,如果使用CuPy数组作为asarray解释的目的地,那么连根拔应该能够直接从ROOT文件写入CuPy阵列。不过我还没试过。

如果您可以控制要处理的ROOT文件,请尝试使它们的篮子变大(增加刷新大小),使它们的数据结构简单(例如,纯数字或数字数组/矢量,而不是更深)。也许最重要的是,使用像lz4这样的轻量级压缩,而不是重量级的卢克·勒兹马。

Uproot可以并行读取篮子,但只有当它有很多非Python计算要做时,比如解压lzma时,这才被证明是有用的。

如果要反复读取这些数组,您可能需要使用numpy.save写入中间文件,它本质上只是磁盘上的原始字节。这意味着在回读时没有反序列化,而不是解码ROOT或HDF5文件所需的工作。因为它是一种非常简单的格式,你甚至可以用numpy.memmap读取它,当操作系统懒洋洋地从磁盘加载数据时,它会窥探操作系统的页面缓存,甚至删除字节的显式副本。

并非所有这些技巧都同样有用。我试着把最重要的放在第一位,但在进行大规模的代码重写之前进行实验,这可能不会有多大区别。有些技巧不能与其他技巧组合,例如CuPy和memmap(memmap总是懒洋洋地加载到主内存中,而不是GPU内存)。但有些组合可能是富有成效的。

祝你好运!

每个人都忘记了一个明显的成分:RDataFrame.AsNumpy(),参见例如。https://root.cern.ch/doc/master/df026__AsNumpyArrays_8py.html

这样,就不需要临时文件,也不需要将所有内容加载到内存中。阅读是以原生C++的速度进行的。很高兴看到一份关于哪些方面效果更好的报告https://root-forum.cern.ch!

我的另一个侧面点。我在连根拔起->hdf5营地;这样,我可以一次性完成较慢的部分(将文件读取到hdf5中),也可以组合较小的文件并进行一些处理。我也保持低压缩或关闭。这可能需要4-5分钟将许多文件连根拔起读取到<10秒hdf5读取几个文件。

我可以补充的一点是,如果你有"锯齿状"数据,比如真相信息,那么直接使用AwkwardArray可以很好地工作,它对hdf5有本地支持。我使用h5py来处理HDF5文件。你可以在这里看到我在做什么:https://gitlab.cern.ch/LHCb-Reco-Dev/pv-finder.

这以前也是这样设计的,因为我没有一个可以在任何地方同时运行ROOT和ML工具的环境,但现在我使用了一个带有两者的environment.yml文件,使用Conda forge ROOT和XML工具(PyTorch等)。

Jim似乎提供了一个很好的选项概述,所以我将提供一个有点专业化的策略:

由于您似乎正在执行"预选"步骤,我认为您可能会从以NumPy、HDF5或Parquet格式保存中间文件中受益;这样可以避免每次在磁盘上处理数据时重复选择计算(将这些格式加载到NumPy或panda中与保存它们一样简单)。因此,我的建议是加载一次ROOT风格的数据(并且只读取您感兴趣的分支),执行预选步骤,并保存中间文件以备将来使用。我将举一个更具体的例子:

我的工作流程包括三个选项。我们可以把它们描绘成熊猫。DataFrame.eval/pandas.DataFrame.query字符串。(pandas.eval在可用时使用numexpr加速)。这些类似于TTree::Draw的选择。这里有一个任意的例子,我的树有列[electron_pt, regionA, regionB, regionC]

selectA = "(electron_pt >= 25) & (regionA == True)"
selectB = "(electron_pt >= 30) & (regionB == True)"
selectC = "(electron_pt >= 35) & (regionC == True)"

我可以将我的数据加载到数据帧中一次,然后应用选择:

keep_columns = [......] # some list of branches to keep, must contain selection branches
df = uproot.open("file.root").get("tree").pandas.df(branches=keep_columns)
selections = {
"A": selectA,
"B": selectB,
"C": selectC
}

现在,我们可以循环选择,查询数据帧,并保存仅包含特定选择的中间格式。

for name, selection in selections.items():
df.query(selection).to_hdf("file_selection{}.h5".format(name), name)
# or save to parquet (if pyarrow is installed):
# df.query(selection).to_parquet("file_section{}.parquet".format(name))

然后用pandas.read_hdfpandas.read_parquet将文件读回内存。

这是一种在过去对我非常有效的策略,当我对来自共同来源的数据训练ML分类器时,但需要将其分类为几个不同的选择。

最新更新