Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d8e8e1c
Initial work to accommodate different tr-vl split per parallel replica.
goord Jun 22, 2022
0184708
Merge pull request #1 from LHC-NLeSC/master
goord Dec 16, 2022
685af8a
Work on tr-vl masking layers, TF crashes with shape mismatch in gradient
goord Dec 19, 2022
94916ad
tr-vl masking layers working, validation needed
goord Dec 25, 2022
e9cf4bf
Merge pull request #2 from NNPDF/master
goord Jan 13, 2023
a43bd5d
Fixed issues with replica-specific training data and inverse covarian…
goord Feb 15, 2023
0584f56
Merge remote-tracking branch 'origin/trvl-mask-layers' into trvl-mask…
goord Feb 15, 2023
f5e9a45
Added input shape to build method args again
goord Feb 15, 2023
2ca63f0
Fixed broken sequential replica fits
goord Feb 16, 2023
20c8c9a
Merge branch 'NNPDF:master' into trvl-mask-layers
goord Mar 13, 2023
eb819af
Add single-point datasets to training set for all parallel replicas
goord Mar 13, 2023
7269c1d
Give better names to mask layers
goord Mar 13, 2023
a5f5534
Merge branch 'NNPDF:master' into trvl-mask-layers
goord Apr 17, 2023
194c34d
Merge branch 'NNPDF:master' into trvl-mask-layers
goord May 1, 2023
303024e
Fix mask layer kernel shape.
goord May 1, 2023
bc56876
Either apply masks to all observables or not
goord May 8, 2023
267d973
Wrapped load_with_cuts in lru cache function
goord May 10, 2023
7505f97
Better way of wrapping load_with_cuts in lru cache function
goord May 10, 2023
653f580
Fixed bug when running single replica in parallel
goord May 15, 2023
7e2dc71
Merge branch 'master' into trvl-mask-layers
goord Jun 5, 2023
84f56cd
Merge branch 'master' into trvl-mask-layers
goord Jun 5, 2023
0409cf3
Fixed incorrect merge in model_trainer.py
goord Jun 5, 2023
623a2d7
Attempt to fix tests with default argument in validphys
goord Jun 7, 2023
7edad32
Fix 1-D mask test
goord Jun 7, 2023
79240ad
Reverted accidental commits
goord Jun 7, 2023
57c2111
Merge pull request #1759 from NNPDF/master
goord Jun 19, 2023
49404d1
Merge pull request #1775 from NNPDF/master
goord Jul 17, 2023
1aab479
various caches speeding up multii-replica init
goord Jul 24, 2023
495ed3e
Merge branch 'trvl-mask-layers' of github.com:NNPDF/nnpdf into trvl-m…
goord Jul 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,4 @@ integrability:

############################################################
debug: false
maxcores: 4
maxcores: 4
4 changes: 4 additions & 0 deletions n3fit/src/n3fit/backends/keras_backend/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ def flatten(x):
"""Flatten tensor x"""
return tf.reshape(x, (-1,))

@tf.function
def reshape(x, shape):
""" reshape tensor x """
return tf.reshape(x, shape)

def boolean_mask(*args, **kwargs):
"""
Expand Down
10 changes: 5 additions & 5 deletions n3fit/src/n3fit/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,11 +362,11 @@ def check_consistent_parallel(parameters, parallel_models, same_trvl_per_replica
"""
if not parallel_models:
return
if not same_trvl_per_replica:
raise CheckError(
"Replicas cannot be run in parallel with different training/validation "
" masks, please set `same_trvl_per_replica` to True in the runcard"
)
# if not same_trvl_per_replica:
# raise CheckError(
# "Replicas cannot be run in parallel with different training/validation "
# " masks, please set `same_trvl_per_replica` to True in the runcard"
# )
if parameters.get("layer_type") != "dense":
raise CheckError("Parallelization has only been tested with layer_type=='dense'")

Expand Down
15 changes: 11 additions & 4 deletions n3fit/src/n3fit/layers/losses.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def build(self, input_shape):
weights of the layers"""
init = MetaLayer.init_constant(self._invcovmat)
self.kernel = self.builder_helper(
"invcovmat", (self._ndata, self._ndata), init, trainable=False
"invcovmat", self._invcovmat.shape, init, trainable=False
)
mask_shape = (1, 1, self._ndata)
if self._mask is None:
Expand Down Expand Up @@ -85,10 +85,17 @@ def call(self, y_pred, **kwargs):
tmp = op.op_multiply([tmp_raw, self.mask])
if tmp.shape[1] == 1:
# einsum is not well suited for CPU, so use tensordot if not multimodel
right_dot = op.tensor_product(self.kernel, tmp[0, 0, :], axes=1)
res = op.tensor_product(tmp[0, :, :], right_dot, axes=1)
if len(self.kernel.shape) == 3:
right_dot = op.tensor_product(self.kernel[0, ...], tmp[0, 0, :], axes=1)
res = op.tensor_product(tmp[0, :, :], right_dot, axes=1)
else:
right_dot = op.tensor_product(self.kernel, tmp[0, 0, :], axes=1)
res = op.tensor_product(tmp[0, :, :], right_dot, axes=1)
else:
res = op.einsum("bri, ij, brj -> r", tmp, self.kernel, tmp)
if len(self.kernel.shape) == 3:
res = op.einsum("bri, rij, brj -> r", tmp, self.kernel, tmp)
else:
res = op.einsum("bri, ij, brj -> r", tmp, self.kernel, tmp)
return res


Expand Down
15 changes: 12 additions & 3 deletions n3fit/src/n3fit/layers/mask.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from n3fit.backends import MetaLayer
from n3fit.backends import operations as op

from numpy import count_nonzero

class Mask(MetaLayer):
"""
Expand All @@ -19,14 +19,20 @@ class Mask(MetaLayer):
c: float
constant multiplier for every output
axis: int
axis in which to apply the mask
axis in which to apply the mask. Currently,
only the last axis gives the correct output shape
"""

def __init__(self, bool_mask=None, c=None, axis=None, **kwargs):
if bool_mask is None:
self.mask = None
self.last_dim = -1
else:
self.mask = op.numpy_to_tensor(bool_mask, dtype=bool)
if len(bool_mask.shape) == 1:
self.last_dim = count_nonzero(bool_mask)
else:
self.last_dim = count_nonzero(bool_mask[0, ...])
self.c = c
self.axis = axis
super().__init__(**kwargs)
Expand All @@ -41,7 +47,10 @@ def build(self, input_shape):

def call(self, ret):
if self.mask is not None:
ret = op.boolean_mask(ret, self.mask, axis=self.axis)
flat_res = op.boolean_mask(ret, self.mask, axis=self.axis)
output_shape = ret.get_shape().as_list()
output_shape[-1] = self.last_dim
ret = op.reshape(flat_res, shape=output_shape)
if self.c is not None:
ret = ret * self.kernel
return ret
116 changes: 55 additions & 61 deletions n3fit/src/n3fit/model_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
FlavourToEvolution,
ObsRotation,
Preprocessing,
Mask,
losses,
)
from n3fit.layers.observable import is_unique
from n3fit.msr import generate_msr_model_and_grid
from validphys.photon.compute import Photon # only used for type hint here


@dataclass
class ObservableWrapper:
"""Wraps many observables into an experimental layer once the PDF model is prepared
Expand All @@ -47,6 +47,7 @@ class ObservableWrapper:

name: str
observables: list
trvl_mask_layers: list
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can have this be optional = None, and if it is None then it doesn't get applied.

Because (I think, maybe I'm wrong!) that we should never be in a situation in which some of the masks exist and some are None, that way you can avoid the list of None.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in rev. bc56876. I did not insert the default value yet though

dataset_xsizes: list
invcovmat: np.array = None
covmat: np.array = None
Expand All @@ -61,8 +62,7 @@ def _generate_loss(self, mask=None):
was initialized with"""
if self.invcovmat is not None:
loss = losses.LossInvcovmat(
self.invcovmat, self.data, mask, covmat=self.covmat, name=self.name
)
self.invcovmat, self.data, mask, covmat=self.covmat, name=self.name)
elif self.positivity:
loss = losses.LossPositivity(name=self.name, c=self.multiplier)
elif self.integrability:
Expand All @@ -86,8 +86,15 @@ def _generate_experimental_layer(self, pdf):
else:
output_layers = [obs(pdf) for obs in self.observables]

# Finally concatenate all observables (so that experiments are one single entitiy)
ret = op.concatenate(output_layers, axis=2)
masked_output_layers = []
if self.trvl_mask_layers is not None:
for output_layer, mask_layer in zip(output_layers, self.trvl_mask_layers):
masked_output_layers.append(mask_layer(output_layer))
else:
masked_output_layers = output_layers

# Finally concatenate all observables (so that experiments are one single entity)
ret = op.concatenate(masked_output_layers)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the axis removed? (I guess the default is exactly the right axis, but I'd like to have it explicit, it makes debugging easier)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was a bit explicit, but since all tensor shapes have to be as explicit as possible for tensorflow to do the correct thing, I will re-insert it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More than for tensorflow is for the person reading the code in this case, since it is hard to keep track of which axis is what :P

if self.rotation is not None:
ret = self.rotation(ret)
return ret
Expand All @@ -98,8 +105,14 @@ def __call__(self, pdf_layer, mask=None):
return loss_f(experiment_prediction)


def observable_generator(
spec_dict, positivity_initial=1.0, integrability=False
def observable_generator(spec_dict,
mask_array=None,
training_data=None,
validation_data=None,
invcovmat_tr=None,
invcovmat_vl=None,
positivity_initial=1.0,
integrability=False
): # pylint: disable=too-many-locals
"""
This function generates the observable models for each experiment.
Expand Down Expand Up @@ -148,10 +161,13 @@ def observable_generator(
spec_name = spec_dict["name"]
dataset_xsizes = []
model_inputs = []
model_obs_tr = []
model_obs_vl = []
model_obs_ex = []
model_observables = []
tr_mask_layers = []
vl_mask_layers = []
offset = 0
apply_masks = spec_dict.get("data_transformation_tr") is None and mask_array is not None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit worried. If mask_array is not None but there is a data_transformation_tr then the masks will not be applied. If this is necessary then it should fail at the beginning.

We usually do that by adding a check before the fit starts. In this case it should check whether the run options are a parallel fit and data_transformation and if so validphys will raise an exception telling the user which options are inconsistent.

(for the time being you can put just a raise Exception here to stop it and create the check at the end)

# The first step is to compute the observable for each of the datasets
masks = []
for dataset in spec_dict["datasets"]:
# Get the generic information of the dataset
dataset_name = dataset.name
Expand All @@ -165,56 +181,29 @@ def observable_generator(
# Set the operation (if any) to be applied to the fktables of this dataset
operation_name = dataset.operation

# Extract the masks that will end up in the observable wrappers...
if apply_masks:
trmask = mask_array[:, offset:offset + dataset.ndata]
masks.append(trmask)
tr_mask_layers.append(Mask(trmask, axis=1, name=f"trmask_{dataset_name}"))
vl_mask_layers.append(Mask(~trmask, axis=1, name=f"vlmask_{dataset_name}"))

# Now generate the observable layer, which takes the following information:
# operation name
# dataset name
# list of validphys.coredata.FKTableData objects
# these will then be used to check how many different pdf inputs are needed
# (and convolutions if given the case)

if spec_dict["positivity"]:
# Positivity (and integrability, which is a special kind of positivity...)
# enters only at the "training" part of the models
obs_layer_tr = Obs_Layer(
dataset.fktables_data,
dataset.training_fktables(),
operation_name,
name=f"dat_{dataset_name}",
)
obs_layer_ex = obs_layer_vl = None
elif spec_dict.get("data_transformation_tr") is not None:
# Data transformation needs access to the full array of output data
obs_layer_ex = Obs_Layer(
dataset.fktables_data,
dataset.fktables(),
operation_name,
name=f"exp_{dataset_name}",
)
obs_layer_tr = obs_layer_vl = obs_layer_ex
else:
obs_layer_tr = Obs_Layer(
dataset.fktables_data,
dataset.training_fktables(),
operation_name,
name=f"dat_{dataset_name}",
)
obs_layer_ex = Obs_Layer(
dataset.fktables_data,
dataset.fktables(),
operation_name,
name=f"exp_{dataset_name}",
)
obs_layer_vl = Obs_Layer(
dataset.fktables_data,
dataset.validation_fktables(),
operation_name,
name=f"val_{dataset_name}",
)
obs_layer = Obs_Layer(
dataset.fktables_data,
dataset.fktables(),
operation_name,
name=f"dat_{dataset_name}")

# If the observable layer found that all input grids are equal, the splitting will be None
# otherwise the different xgrids need to be stored separately
# Note: for pineappl grids, obs_layer_tr.splitting should always be None
if obs_layer_tr.splitting is None:
if obs_layer.splitting is None:
xgrid = dataset.fktables_data[0].xgrid
model_inputs.append(xgrid)
dataset_xsizes.append(len(xgrid))
Expand All @@ -223,9 +212,10 @@ def observable_generator(
model_inputs += xgrids
dataset_xsizes.append(sum([len(i) for i in xgrids]))

model_obs_tr.append(obs_layer_tr)
model_obs_vl.append(obs_layer_vl)
model_obs_ex.append(obs_layer_ex)
model_observables.append(obs_layer)

# shift offset for new mask array
offset = offset + dataset.ndata
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would there be a way to have a list of arrays from the onset (instead of having an offset that we move?) such that to each dataset in the list correspond an array.


# Check whether all xgrids of all observables in this experiment are equal
# if so, simplify the model input
Expand All @@ -240,7 +230,8 @@ def observable_generator(
if spec_dict["positivity"]:
out_positivity = ObservableWrapper(
spec_name,
model_obs_tr,
model_observables,
tr_mask_layers if apply_masks else None,
dataset_xsizes,
multiplier=positivity_initial,
positivity=not integrability,
Expand All @@ -265,23 +256,26 @@ def observable_generator(

out_tr = ObservableWrapper(
spec_name,
model_obs_tr,
model_observables,
tr_mask_layers if apply_masks else None,
dataset_xsizes,
invcovmat=spec_dict["invcovmat"],
data=spec_dict["expdata"],
invcovmat=invcovmat_tr,
data=training_data,
rotation=obsrot_tr,
)
out_vl = ObservableWrapper(
f"{spec_name}_val",
model_obs_vl,
model_observables,
vl_mask_layers if apply_masks else None,
dataset_xsizes,
invcovmat=spec_dict["invcovmat_vl"],
data=spec_dict["expdata_vl"],
invcovmat=invcovmat_vl,
data=validation_data,
rotation=obsrot_vl,
)
out_exp = ObservableWrapper(
f"{spec_name}_exp",
model_obs_ex,
model_observables,
None,
dataset_xsizes,
invcovmat=spec_dict["invcovmat_true"],
covmat=spec_dict["covmat"],
Expand Down
Loading