Skip to content

Commit f61c0dc

Browse files
authored
Merge pull request #88 from diningphil/minor-improvements
tqdm when debug, possibility to run multiple trainings for model sele…
2 parents b07f8a0 + f316ec1 commit f61c0dc

7 files changed

Lines changed: 173 additions & 18 deletions

File tree

.github/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
PyYAML
2+
tqdm
23
tensorboard
34
tqdm
45
ogb

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## [1.5.2] Some Improvements
4+
5+
### Added
6+
7+
- Implemented a convenient tqdm progress bar in debug mode to track speed of training and evaluation.
8+
- Created a new splitter class, `SameInnerSplitSplitter`, which allows you to average the validation scores of the
9+
same model selection configuration over multiple runs without changing the inner data split. Cannot be combined with a
10+
double/nested CV approach, for which you should use the base `Splitter` class to generate different inner data splits.
11+
- Trying out a helper mechanism to print to terminal information about the experiment that broke (if any)
12+
when you are not in debug mode.
13+
314
## [1.5.1] New default behavior - more efficient training
415

516
### Changed - PLEASE READ

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
author = "Federico Errica"
2525

2626
# The full version, including alpha/beta/rc tags
27-
release = "1.5.1"
27+
release = "1.5.2"
2828

2929

3030
# -- General configuration ---------------------------------------------------

pydgn/data/splitter.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,131 @@ def save(self, path: str):
437437
print("Done.")
438438

439439

440+
class SameInnerSplitSplitter(Splitter):
441+
r"""
442+
Splitter subclass that can be used to have multiple training runs of the
443+
same configuration at model selection time. It is not meant to be combined
444+
with a double-nested CV, for which the different inner splits are already
445+
enough to gauge the training stability of each configuration.
446+
"""
447+
def split(
448+
self,
449+
dataset: pydgn.data.dataset.DatasetInterface,
450+
targets: np.ndarray = None,
451+
):
452+
r"""
453+
Computes the splits and stores them in the list fields
454+
``self.outer_folds`` and ``self.inner_folds``.
455+
IMPORTANT: calling split() sets the seed of numpy, torch, and
456+
random for reproducibility.
457+
458+
Args:
459+
dataset (:class:`~pydgn.data.dataset.DatasetInterface`):
460+
the Dataset object
461+
targets (np.ndarray]): targets used for stratification.
462+
Default is ``None``
463+
"""
464+
np.random.seed(self.seed)
465+
torch.manual_seed(self.seed)
466+
torch.cuda.manual_seed(self.seed)
467+
random.seed(self.seed)
468+
469+
idxs = range(len(dataset))
470+
471+
stratified = self.stratify
472+
outer_idxs = np.array(idxs)
473+
474+
outer_splitter = self._get_splitter(
475+
n_splits=self.n_outer_folds,
476+
stratified=stratified,
477+
eval_ratio=self.test_ratio,
478+
) # This is the true test (outer test)
479+
480+
for train_idxs, test_idxs in outer_splitter.split(
481+
outer_idxs, y=targets if stratified else None
482+
):
483+
484+
assert set(train_idxs) == set(outer_idxs[train_idxs])
485+
assert set(test_idxs) == set(outer_idxs[test_idxs])
486+
487+
inner_fold_splits = []
488+
inner_idxs = outer_idxs[
489+
train_idxs
490+
] # equals train_idxs because outer_idxs was ordered
491+
inner_targets = (
492+
targets[train_idxs] if targets is not None else None
493+
)
494+
495+
inner_splitter = self._get_splitter(
496+
n_splits=self.n_inner_folds,
497+
stratified=stratified,
498+
eval_ratio=self.inner_val_ratio,
499+
) # The inner "test" is, instead, the validation set
500+
501+
for inner_train_idxs, inner_val_idxs in inner_splitter.split(
502+
inner_idxs, y=inner_targets if stratified else None
503+
):
504+
inner_fold = InnerFold(
505+
train_idxs=inner_idxs[inner_train_idxs].tolist(),
506+
val_idxs=inner_idxs[inner_val_idxs].tolist(),
507+
)
508+
509+
# False if empty
510+
assert not bool(
511+
set(inner_train_idxs)
512+
& set(inner_val_idxs)
513+
& set(test_idxs)
514+
)
515+
assert not bool(
516+
set(inner_idxs[inner_train_idxs])
517+
& set(inner_idxs[inner_val_idxs])
518+
& set(test_idxs)
519+
)
520+
521+
# we ignore the different inner splits and use only the first
522+
# one to be reused multiple times (effectively simulating
523+
# multiple training runs of the same configuration on the same
524+
# training/validation data split
525+
for _ in range(self.n_inner_folds):
526+
inner_fold_splits.append(inner_fold)
527+
break
528+
529+
self.inner_folds.append(inner_fold_splits)
530+
531+
# Obtain outer val from outer train in an holdout fashion
532+
outer_val_splitter = self._get_splitter(
533+
n_splits=1,
534+
stratified=stratified,
535+
eval_ratio=self.outer_val_ratio,
536+
)
537+
outer_train_idxs, outer_val_idxs = list(
538+
outer_val_splitter.split(inner_idxs, y=inner_targets)
539+
)[0]
540+
541+
# False if empty
542+
assert not bool(
543+
set(outer_train_idxs) & set(outer_val_idxs) & set(test_idxs)
544+
)
545+
assert not bool(
546+
set(outer_train_idxs) & set(outer_val_idxs) & set(test_idxs)
547+
)
548+
assert not bool(
549+
set(inner_idxs[outer_train_idxs])
550+
& set(inner_idxs[outer_val_idxs])
551+
& set(test_idxs)
552+
)
553+
554+
np.random.shuffle(outer_train_idxs)
555+
np.random.shuffle(outer_val_idxs)
556+
np.random.shuffle(test_idxs)
557+
outer_fold = OuterFold(
558+
train_idxs=inner_idxs[outer_train_idxs].tolist(),
559+
val_idxs=inner_idxs[outer_val_idxs].tolist(),
560+
test_idxs=outer_idxs[test_idxs].tolist(),
561+
)
562+
self.outer_folds.append(outer_fold)
563+
564+
440565
class TemporalSplitter(Splitter):
441566
r"""
442567
Reads the entire dataset and returns the targets. In this case, each

pydgn/evaluation/evaluator.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,16 @@ def run_valid(
8686
"""
8787
if not osp.exists(fold_results_torch_path):
8888
start = time.time()
89-
experiment = experiment_class(config, fold_exp_folder, exp_seed)
90-
train_res, val_res = experiment.run_valid(dataset_getter, logger)
91-
elapsed = time.time() - start
92-
torch.save((train_res, val_res, elapsed), fold_results_torch_path)
89+
try:
90+
experiment = experiment_class(config, fold_exp_folder, exp_seed)
91+
train_res, val_res = experiment.run_valid(dataset_getter, logger)
92+
elapsed = time.time() - start
93+
torch.save((train_res, val_res, elapsed), fold_results_torch_path)
94+
except Exception as e:
95+
print(f'There has been an issue with configuration '
96+
f'in {fold_exp_folder}!')
97+
print(e)
98+
elapsed = -1
9399
else:
94100
_, _, elapsed = torch.load(fold_results_torch_path)
95101
return dataset_getter.outer_k, dataset_getter.inner_k, config_id, elapsed
@@ -138,17 +144,21 @@ def run_test(
138144
a tuple with outer fold id, final run id, and time elapsed
139145
"""
140146
if not osp.exists(final_run_torch_path):
141-
start = time.time()
142-
experiment = experiment_class(
143-
best_config[CONFIG], final_run_exp_path, exp_seed
144-
)
145-
res = experiment.run_test(dataset_getter, logger)
146-
elapsed = time.time() - start
147-
148-
train_res, val_res, test_res = res
149-
torch.save(
150-
(train_res, val_res, test_res, elapsed), final_run_torch_path
151-
)
147+
try:
148+
start = time.time()
149+
experiment = experiment_class(
150+
best_config[CONFIG], final_run_exp_path, exp_seed
151+
)
152+
res = experiment.run_test(dataset_getter, logger)
153+
elapsed = time.time() - start
154+
train_res, val_res, test_res = res
155+
torch.save(
156+
(train_res, val_res, test_res, elapsed), final_run_torch_path
157+
)
158+
except Exception as e:
159+
print(f'There has been an issue in {final_run_exp_path}!')
160+
print(e)
161+
elapse = -1
152162
else:
153163
res = torch.load(final_run_torch_path)
154164
elapsed = res[-1]

pydgn/training/engine.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from torch.utils.data import SequentialSampler
77
from torch_geometric.data import Data
88
from torch_geometric.loader import DataLoader
9+
from tqdm import tqdm
910

1011
import pydgn
1112
from pydgn.log.logger import Logger
@@ -146,6 +147,7 @@ def __init__(
146147
self.eval_training = eval_training
147148
self.store_last_checkpoint = store_last_checkpoint
148149
self.training = False
150+
self.logger = None
149151

150152
# For dynamic graph learning
151153
self.reset_eval_model_hidden_state = reset_eval_model_hidden_state
@@ -421,7 +423,11 @@ def _loop(self, loader: DataLoader):
421423
self.state.update(loader_iterable=iter(loader))
422424

423425
# Loop over data
424-
for id_batch in range(len(loader)):
426+
for id_batch in tqdm(range(len(loader)),
427+
desc=f'Epoch {self.state.epoch+1}, {self.state.set} set',
428+
unit='batch',
429+
disable=not self.logger.debug,
430+
leave=False):
425431
self.state.update(id_batch=id_batch)
426432
# EngineCallback will store fetched data in state.batch_input
427433
self._dispatch(EventHandler.ON_FETCH_DATA, self.state)
@@ -544,6 +550,7 @@ def train(
544550
validation_loss, validation_score, validation_embeddings,
545551
test_loss, test_score, test_embeddings)
546552
"""
553+
self.logger = logger
547554

548555
try:
549556
# Initialize variables

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "pydgn"
7-
version = "1.5.1"
7+
version = "1.5.2"
88
description = "A Python Package for Deep Graph Networks"
99
authors = [ { name="Federico Errica", email="[email protected]" } ]
1010
readme = "README.md"
@@ -18,6 +18,7 @@ requires-python = ">=3.8"
1818

1919
dependencies = [
2020
"PyYAML>=5.4",
21+
"tqdm>=4.66.1",
2122
"Requests>=2.31.0",
2223
"scikit_learn>=1.3.0",
2324
"tensorboard>=2.11.0",

0 commit comments

Comments
 (0)