From 6d9c29c23edf80ee28aaecf4f32e48b8eb4eb178 Mon Sep 17 00:00:00 2001 From: Shxiao101 Date: Sun, 13 Sep 2026 03:11:57 +0900 Subject: [PATCH 1/2] Fix DMRL save() failing when log_metrics=True DMRL created a tensorboard SummaryWriter in __init__ and kept it as self.tb_writer. Recommender.save() deep-copies the model, and the writer holds thread locks and an open event file, so saving raised "TypeError: cannot pickle '_thread.lock' object" (#656). Constructing the model also created temp/tb_data/run_1 even if it was never trained. Create the writer locally in _fit_dmrl, where all logging happens, and close it when training finishes. Add a save/load test that is skipped when the DMRL requirements or tensorboard are missing. Co-Authored-By: Claude Opus 5 --- cornac/models/dmrl/recom_dmrl.py | 36 +++++----- tests/cornac/models/dmrl/test_recom_dmrl.py | 74 +++++++++++++++++++++ 2 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 tests/cornac/models/dmrl/test_recom_dmrl.py diff --git a/cornac/models/dmrl/recom_dmrl.py b/cornac/models/dmrl/recom_dmrl.py index e87f5906b..bb8d19d52 100644 --- a/cornac/models/dmrl/recom_dmrl.py +++ b/cornac/models/dmrl/recom_dmrl.py @@ -114,10 +114,6 @@ def __init__( self.num_neg = num_neg self.num_factors = num_factors self.log_metrics = log_metrics - if log_metrics: - from torch.utils.tensorboard import SummaryWriter - - self.tb_writer = SummaryWriter("temp/tb_data/run_1") if self.num_factors == 1: # deactivate disentangled portion of loss if theres only 1 factor @@ -261,9 +257,13 @@ def _fit_dmrl(self, train_set: Dataset, val_set: Dataset = None): decay_c=1e-3, num_factors=self.num_factors, num_neg=self.num_neg ) - # add hyperparams to tensorboard if self.log_metrics: - self.tb_writer.add_hparams( + from torch.utils.tensorboard import SummaryWriter + + # kept local: a writer attribute would break Recommender.save/deepcopy + tb_writer = SummaryWriter("temp/tb_data/run_1") + # add hyperparams to tensorboard + tb_writer.add_hparams( { "learning_rate": self.learning_rate, "decay_c": self.decay_c, @@ -379,38 +379,38 @@ def _fit_dmrl(self, train_set: Dataset, val_set: Dataset = None): if self.log_metrics: # tb_x = epoch * len(dataloader) + i + 1 - self.tb_writer.add_scalar("Loss/train", last_loss, j) - self.tb_writer.add_scalar( + tb_writer.add_scalar("Loss/train", last_loss, j) + tb_writer.add_scalar( "Loss/val", running_loss_val / devider, j ) - self.tb_writer.add_scalar( + tb_writer.add_scalar( "Gradient Norm/train", np.mean(self.model.grad_norms), j ) - self.tb_writer.add_scalar( + tb_writer.add_scalar( "Param Norm/train", np.mean(self.model.param_norms), j ) - self.tb_writer.add_scalar( + tb_writer.add_scalar( "User-Item based rating", np.mean(self.model.ui_ratings), j ) - self.tb_writer.add_scalar( + tb_writer.add_scalar( "User-Text based rating", np.mean(self.model.ut_ratings), j ) - self.tb_writer.add_scalar( + tb_writer.add_scalar( "User-Itm Attention", np.mean(self.model.ui_attention), j ) - self.tb_writer.add_scalar( + tb_writer.add_scalar( "User-Text Attention", np.mean(self.model.ut_attention), j ) for name, param in self.model.named_parameters(): - self.tb_writer.add_scalar( + tb_writer.add_scalar( name + "/grad_norm", np.mean(self.model.grad_dict[name]), j, ) - self.tb_writer.add_histogram( + tb_writer.add_histogram( name + "/grad", param.grad, global_step=epoch ) - self.tb_writer.add_scalar( + tb_writer.add_scalar( "Learning rate", optimizer.param_groups[0]["lr"], j ) self.model.reset_grad_metrics() @@ -426,6 +426,8 @@ def _fit_dmrl(self, train_set: Dataset, val_set: Dataset = None): print(f"Epoch: {epoch} is done") # scheduler.step() print("Finished training!") + if self.log_metrics: + tb_writer.close() # self.eval_train_set_performance() # evaluate the model on the training set after training if necessary def eval_train_set_performance(self) -> Tuple[float, float]: diff --git a/tests/cornac/models/dmrl/test_recom_dmrl.py b/tests/cornac/models/dmrl/test_recom_dmrl.py new file mode 100644 index 000000000..230847bff --- /dev/null +++ b/tests/cornac/models/dmrl/test_recom_dmrl.py @@ -0,0 +1,74 @@ +# Tests for saving and loading a trained DMRL model. +# They are skipped if the DMRL requirements or tensorboard are missing. To run them: +# pip install -r cornac/models/dmrl/requirements.txt tensorboard + +import os +import tempfile +import unittest + +import numpy as np + +try: + import torch + import sentence_transformers # noqa: F401 (imported by DMRL.fit) + import tensorboard # noqa: F401 + + run_dmrl_test_funcs = True +except ImportError: + run_dmrl_test_funcs = False + +from cornac.data import ImageModality +from cornac.eval_methods import BaseMethod +from cornac.models import DMRL, Recommender + + +@unittest.skipUnless(run_dmrl_test_funcs, "DMRL requirements or tensorboard are not installed") +class TestDMRLSaveLoad(unittest.TestCase): + def setUp(self): + rng = np.random.RandomState(0) + items = [f"i{j}" for j in range(30)] + data = [ + (f"u{i}", items[j], 1.0) + for i in range(20) + for j in rng.choice(30, 6, replace=False) + ] + image = ImageModality(features=rng.rand(30, 16).astype(np.float32), ids=items) + self.train_set = BaseMethod.from_splits( + train_data=data, + test_data=data[:10], + exclude_unknowns=True, + item_image=image, + seed=1, + ).train_set + self.tmp_dir = tempfile.TemporaryDirectory() + self.cwd = os.getcwd() + os.chdir(self.tmp_dir.name) # the tensorboard writer logs under ./temp + + def tearDown(self): + os.chdir(self.cwd) + self.tmp_dir.cleanup() + + def test_save_and_load_with_log_metrics(self): + model = DMRL( + batch_size=16, + epochs=1, + log_metrics=True, + bert_text_dim=0, + image_dim=16, + embedding_dim=8, + num_neg=2, + num_factors=2, + ) + model.fit(self.train_set) + + model_file = model.save(os.path.join(self.tmp_dir.name, "saved")) + loaded = Recommender.load(model_file) + + for p1, p2 in zip( + model.model.state_dict().values(), loaded.model.state_dict().values() + ): + self.assertTrue(torch.equal(p1, p2)) + + +if __name__ == "__main__": + unittest.main() From 1b77daf58e86caac93953b398cbd8653d9f2bb94 Mon Sep 17 00:00:00 2001 From: Shxiao101 Date: Sun, 13 Sep 2026 12:54:43 +0900 Subject: [PATCH 2/2] Remove DMRL save/load test per review Co-Authored-By: Claude Opus 5 --- tests/cornac/models/dmrl/test_recom_dmrl.py | 74 --------------------- 1 file changed, 74 deletions(-) delete mode 100644 tests/cornac/models/dmrl/test_recom_dmrl.py diff --git a/tests/cornac/models/dmrl/test_recom_dmrl.py b/tests/cornac/models/dmrl/test_recom_dmrl.py deleted file mode 100644 index 230847bff..000000000 --- a/tests/cornac/models/dmrl/test_recom_dmrl.py +++ /dev/null @@ -1,74 +0,0 @@ -# Tests for saving and loading a trained DMRL model. -# They are skipped if the DMRL requirements or tensorboard are missing. To run them: -# pip install -r cornac/models/dmrl/requirements.txt tensorboard - -import os -import tempfile -import unittest - -import numpy as np - -try: - import torch - import sentence_transformers # noqa: F401 (imported by DMRL.fit) - import tensorboard # noqa: F401 - - run_dmrl_test_funcs = True -except ImportError: - run_dmrl_test_funcs = False - -from cornac.data import ImageModality -from cornac.eval_methods import BaseMethod -from cornac.models import DMRL, Recommender - - -@unittest.skipUnless(run_dmrl_test_funcs, "DMRL requirements or tensorboard are not installed") -class TestDMRLSaveLoad(unittest.TestCase): - def setUp(self): - rng = np.random.RandomState(0) - items = [f"i{j}" for j in range(30)] - data = [ - (f"u{i}", items[j], 1.0) - for i in range(20) - for j in rng.choice(30, 6, replace=False) - ] - image = ImageModality(features=rng.rand(30, 16).astype(np.float32), ids=items) - self.train_set = BaseMethod.from_splits( - train_data=data, - test_data=data[:10], - exclude_unknowns=True, - item_image=image, - seed=1, - ).train_set - self.tmp_dir = tempfile.TemporaryDirectory() - self.cwd = os.getcwd() - os.chdir(self.tmp_dir.name) # the tensorboard writer logs under ./temp - - def tearDown(self): - os.chdir(self.cwd) - self.tmp_dir.cleanup() - - def test_save_and_load_with_log_metrics(self): - model = DMRL( - batch_size=16, - epochs=1, - log_metrics=True, - bert_text_dim=0, - image_dim=16, - embedding_dim=8, - num_neg=2, - num_factors=2, - ) - model.fit(self.train_set) - - model_file = model.save(os.path.join(self.tmp_dir.name, "saved")) - loaded = Recommender.load(model_file) - - for p1, p2 in zip( - model.model.state_dict().values(), loaded.model.state_dict().values() - ): - self.assertTrue(torch.equal(p1, p2)) - - -if __name__ == "__main__": - unittest.main()