Repository: hand10ryo/PyTorchCML (language: python)

Issue:
Feature/adaptors
#25 implement domain adaptation


Discussion / hints:

Fix patch:
diff --git a/PyTorchCML/adaptors/BaseAdaptor.py b/PyTorchCML/adaptors/BaseAdaptor.py
new file mode 100644
index 0000000..563f14c
--- /dev/null
+++ b/PyTorchCML/adaptors/BaseAdaptor.py
@@ -0,0 +1,26 @@
+from torch import nn
+
+
+class BaseAdaptor(nn.Module):
+    """Astract class of module for domain adaptation."""
+
+    def __init__(self, weight):
+        """Set some parameters
+
+        Args:
+            weight (float, optional): Loss weights for domain adaptation. Defaults to 1e-3.
+        """
+        super().__init__()
+        self.weight = weight
+
+    def forward(self, indices, embeddings):
+        """Method to calculate loss for domain adaptation.
+
+        Args:
+            indices (torch.Tensor): Indices of users or items. size = (n_user, n_sample)
+            embeddings (torch.Tensor): The embeddings corresponding to indices. size = (n_user, n_sample, n_dim)
+
+        Raises:
+            NotImplementedError: [description]
+        """
+        raise NotImplementedError
diff --git a/PyTorchCML/adaptors/MLPAdaptor.py b/PyTorchCML/adaptors/MLPAdaptor.py
new file mode 100644
index 0000000..0c1c724
--- /dev/null
+++ b/PyTorchCML/adaptors/MLPAdaptor.py
@@ -0,0 +1,55 @@
+import torch
+
+from torch import nn
+
+from .BaseAdaptor import BaseAdaptor
+
+
+class MLPAdaptor(BaseAdaptor):
+    """Class of module for domain adaptation with MLP."""
+
+    def __init__(
+        self,
+        features: torch.Tensor,
+        n_dim: int = 20,
+        n_hidden: list = [100],
+        weight: float = 1e-3,
+    ):
+        """Set MLP model for domain adaptation.
+
+        Args:
+            features (torch.Tensor): A feature of users or items. size = (n_user, n_feature)
+            n_dim (int, optional): A number of dimention of embeddings. Defaults to 20.
+            n_hidden (list, optional): A list of numbers of neuron for each hidden layers. Defaults to [100].
+            weight (float, optional): Loss weights for domain adaptation. Defaults to 1e-3.
+        """
+        super().__init__(weight)
+        self.features_embeddings = nn.Embedding.from_pretrained(features)
+        self.features_embeddings.weight.requires_grad = False
+
+        self.n_input = features.shape[1]
+        self.n_hidden = n_hidden
+        self.n_output = n_dim
+
+        projection_layers = [nn.Linear(self.n_input, self.n_hidden[0]), nn.ReLU()]
+        for i in range(len(self.n_hidden) - 1):
+            layer = [nn.Linear(self.n_hidden[i], self.n_hidden[i + 1]), nn.ReLU()]
+            projection_layers += layer
+        projection_layers += [nn.Linear(self.n_hidden[-1], self.n_output)]
+
+        self.projector = nn.Sequential(*projection_layers)
+
+    def forward(self, indices: torch.Tensor, embeddings: torch.Tensor):
+        """Method to calculate loss for domain adaptation.
+
+        Args:
+            indices (torch.Tensor): Indices of users or items. size = (n_user, n_sample)
+            embeddings (torch.Tensor): The embeddings corresponding to indices. size = (n_user, n_sample, n_dim)
+
+        Returns:
+            [torch.Tensor]: loss for domain adaptation. dim = 0.
+        """
+        features = self.features_embeddings(indices)
+        projection = self.projector(features)
+        dist = torch.sqrt(torch.pow(projection - embeddings, 2).sum(axis=2))
+        return self.weight * dist.sum()
diff --git a/PyTorchCML/adaptors/__init__.py b/PyTorchCML/adaptors/__init__.py
new file mode 100644
index 0000000..bf41016
--- /dev/null
+++ b/PyTorchCML/adaptors/__init__.py
@@ -0,0 +1,3 @@
+# flake8: noqa
+from .BaseAdaptor import BaseAdaptor
+from .MLPAdaptor import MLPAdaptor
diff --git a/PyTorchCML/losses/BaseLoss.py b/PyTorchCML/losses/BaseLoss.py
new file mode 100644
index 0000000..5dd1d6b
--- /dev/null
+++ b/PyTorchCML/losses/BaseLoss.py
@@ -0,0 +1,65 @@
+import torch
+from torch import nn
+
+
+class BaseLoss(nn.Module):
+    """Class of abstract loss module for pairwise loss like matrix factorization."""
+
+    def __init__(self, regularizers: list = []):
+        super().__init__()
+        self.regularizers = regularizers
+
+    def forward(
+        self, embeddings_dict: dict, batch: torch.Tensor, column_names: dict
+    ) -> torch.Tensor:
+        loss = self.main(embeddings_dict, batch, column_names)
+        loss += self.regularize(embeddings_dict)
+        return loss
+
+    def main(
+        self, embeddings_dict: dict, batch: torch.Tensor, column_names: dict
+    ) -> torch.Tensor:
+        """
+        Args:
+            embeddings_dict (dict): A dictionary of embddings.
+            (e.g. It has following key and values.)
+                user_embedding : embeddings of user, size (n_batch, 1, d)
+                pos_item_embedding : embeddings of positive item, size (n_batch, 1, d)
+                neg_item_embedding : embeddings of negative item, size (n_batch, n_neg_samples, d)
+                user_bias : bias of user, size (n_batch, 1)
+                pos_item_bias : bias of positive item, size (n_batch, 1)
+                neg_item_bias : bias of negative item, size (n_batch, n_neg_samples)
+
+            batch (torch.Tensor) : A tensor of batch, size (n_batch, *).
+            column_names (dict) : A dictionary that maps names to indices of rows of batch.
+
+        Raises:
+            NotImplementedError: [description]
+
+        Returns:
+            torch.Tensor: [description]
+
+         ---   example code   ---
+
+        embeddings_dict = {
+           "user_embedding": user_embedding,
+            "pos_item_embedding": pos_item_embedding,
+            "neg_item_embedding": neg_item_embedding,
+            "user_bias": user_bias,
+            "pos_item_bias": pos_item_bias,
+            "neg_item_bias": neg_item_bias,
+        }
+
+        loss = loss_function(embeddings_dict, batch, column_names)
+
+        return loss
+        """
+
+        raise NotImplementedError
+
+    def regularize(self, embeddings_dict: dict):
+        reg = 0
+        for regularizer in self.regularizers:
+            reg += regularizer(embeddings_dict)
+
+        return reg
diff --git a/PyTorchCML/losses/BasePairwiseLoss.py b/PyTorchCML/losses/BasePairwiseLoss.py
deleted file mode 100644
index cd0a082..0000000
--- a/PyTorchCML/losses/BasePairwiseLoss.py
+++ /dev/null
@@ -1,50 +0,0 @@
-import torch
-from torch import nn
-
-
-class BasePairwiseLoss(nn.Module):
-    """Class of abstract loss module for pairwise loss like matrix factorization."""
-
-    def __init__(self, regularizers: list = []):
-        super().__init__()
-        self.regularizers = regularizers
-
-    def forward(
-        self, embeddings_dict: dict, batch: torch.Tensor, column_names: dict
-    ) -> torch.Tensor:
-        """
-        Args:
-            embeddings_dict (dict): A dictionary of embddings which has following key and values.
-            batch (torch.Tensor) : A tensor of batch, size (n_batch, *).
-            column_names (dict) : A dictionary that maps names to indices of rows of batch.
-
-        Raises:
-            NotImplementedError: [description]
-
-        Returns:
-            torch.Tensor: [description]
-
-         ---   example code   ---
-
-        # embeddings_dict = {
-        #   "user_embedding": user_emb,
-        #    "pos_item_embedding": pos_item_emb,
-        #    "neg_item_embedding": neg_item_emb,
-        #    "user_bias": user_bias,
-        #    "pos_item_bias": pos_item_bias,
-        #    "neg_item_bias": neg_item_bias,
-        #}
-
-        loss = loss_function(embeddings_dict, batch, column_names)
-        reg = self.regularize(embeddings_dict)
-        return loss + reg
-        """
-
-        raise NotImplementedError
-
-    def regularize(self, embeddings_dict: dict):
-        reg = 0
-        for regularizer in self.regularizers:
-            reg += regularizer(embeddings_dict)
-
-        return reg
diff --git a/PyTorchCML/losses/BaseTripletLoss.py b/PyTorchCML/losses/BaseTripletLoss.py
deleted file mode 100644
index 932c91b..0000000
--- a/PyTorchCML/losses/BaseTripletLoss.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import torch
-from torch import nn
-
-
-class BaseTripletLoss(nn.Module):
-    """Class of Abstract Loss for Triplet"""
-
-    def __init__(self, margin: float = 1, regularizers: list = []):
-        """Set margin size and ReLU function
-
-        Args:
-            margin (float, optional): safe margin size. Defaults to 1.
-            regularizers (list, optional): list of regularizer
-        """
-        super().__init__()
-        self.margin = margin
-        self.ReLU = nn.ReLU()
-        self.regularizers = regularizers
-
-    def forward(
-        self, embeddings_dict: dict, batch: torch.Tensor, column_names: dict
-    ) -> torch.Tensor:
-        """Method of forward
-
-        Args:
-            embeddings_dict (dict): A dictionary of embddings which has following key and values.
-            batch (torch.Tensor) : A tensor of batch, size (n_batch, *).
-            column_names (dict) : A dictionary that maps names to indices of rows of batch.
-
-        Raises:
-            NotImplementedError: [description]
-
-        Returns:
-            torch.Tensor: [description]
-
-
-        ---- example code ---
-
-        # embeddings_dict = {
-        #   "user_embedding": user_emb,
-        #    "pos_item_embedding": pos_item_emb,
-        #    "neg_item_embedding": neg_item_emb,
-        #}
-
-        loss = some_function(embeddings_dict, batch, column_names)
-        reg = self.regularize(embeddings_dict)
-        return loss + reg
-
-        """
-
-        raise NotImplementedError
-
-    def regularize(self, embeddings_dict: dict):
-        reg = 0
-        for regularizer in self.regularizers:
-            reg += regularizer(embeddings_dict)
-
-        return reg
diff --git a/PyTorchCML/losses/LogitPairwiseLoss.py b/PyTorchCML/losses/LogitPairwiseLoss.py
index 6e515bf..29f66c4 100644
--- a/PyTorchCML/losses/LogitPairwiseLoss.py
+++ b/PyTorchCML/losses/LogitPairwiseLoss.py
@@ -1,25 +1,25 @@
 import torch
 from torch import nn
 
-from .BasePairwiseLoss import BasePairwiseLoss
+from .BaseLoss import BaseLoss
 
 
-class LogitPairwiseLoss(BasePairwiseLoss):
+class LogitPairwiseLoss(BaseLoss):
     """Class of pairwise logit loss for Logistic Matrix Factorization"""
 
     def __init__(self, regularizers: list = []):
         super().__init__(regularizers)
         self.LogSigmoid = nn.LogSigmoid()
 
-    def forward(
+    def main(
         self, embeddings_dict: dict, batch: torch.Tensor, column_names: dict
     ) -> torch.Tensor:
-        """Method of forwarding loss
+        """Method of forwarding main loss
 
         Args:
             embeddings_dict (dict): A dictionary of embddings which has following key and values.
-                user_embedding : embeddings of user, size (n_batch, d)
-                pos_item_embedding : embeddings of positive item, size (n_batch, d)
+                user_embedding : embeddings of user, size (n_batch, 1, d)
+                pos_item_embedding : embeddings of positive item, size (n_batch, 1, d)
                 neg_item_embedding : embeddings of negative item, size (n_batch, n_neg_samples, d)
                 user_bias : bias of user, size (n_batch, 1)
                 pos_item_bias : bias of positive item, size (n_batch, 1)
@@ -34,13 +34,13 @@ def forward(
         n_pos = 1
 
         pos_inner = torch.einsum(
-            "nd,nd->n",
+            "nid,nid->n",
             embeddings_dict["user_embedding"],
             embeddings_dict["pos_item_embedding"],
         )
 
         neg_inner = torch.einsum(
-            "nd,njd->nj",
+            "nid,njd->nj",
             embeddings_dict["user_embedding"],
             embeddings_dict["neg_item_embedding"],
         )
@@ -55,6 +55,5 @@ def forward(
         neg_loss = -nn.LogSigmoid()(-neg_y_hat).sum()
 
         loss = (pos_loss + neg_loss) / (n_batch * (n_pos + n_neg))
-        reg = self.regularize(embeddings_dict)
 
-        return loss + reg
+        return loss
diff --git a/PyTorchCML/losses/MSEPairwiseLoss.py b/PyTorchCML/losses/MSEPairwiseLoss.py
index 3a99e3c..177b143 100644
--- a/PyTorchCML/losses/MSEPairwiseLoss.py
+++ b/PyTorchCML/losses/MSEPairwiseLoss.py
@@ -1,20 +1,20 @@
 import torch
 
-from .BasePairwiseLoss import BasePairwiseLoss
+from .BaseLoss import BaseLoss
 
 
-class MSEPairwiseLoss(BasePairwiseLoss):
+class MSEPairwiseLoss(BaseLoss):
     """Class of loss for MSE in implicit feedback"""
 
-    def forward(
+    def main(
         self, embeddings_dict: dict, batch: torch.Tensor, column_names: dict
     ) -> torch.Tensor:
-        """Method of forwarding loss
+        """Method of forwarding main loss
 
         Args:
             embeddings_dict (dict): A dictionary of embddings which has following key and values.
-                "user_embedding" : embeddings of user, size (n_batch, d)
-                "pos_item_embedding" : embeddings of positive item, size (n_batch, d)
+                "user_embedding" : embeddings of user, size (n_batch, 1, d)
+                "pos_item_embedding" : embeddings of positive item, size (n_batch, 1, d)
                 "neg_item_embedding" : embeddings of negative item, size (n_batch, n_neg_samples, d)
                 "user_bias" : bias of user, size (n_batch, 1)
                 "pos_item_bias" : bias of positive item, size (n_batch, 1)
@@ -32,13 +32,13 @@ def forward(
         n_pos = 1
 
         pos_inner = torch.einsum(
-            "nd,nd->n",
+            "nid,nid->n",
             embeddings_dict["user_embedding"],
             embeddings_dict["pos_item_embedding"],
         )
 
         neg_inner = torch.einsum(
-            "nd,njd->nj",
+            "nid,njd->nj",
             embeddings_dict["user_embedding"],
             embeddings_dict["neg_item_embedding"],
         )
@@ -53,6 +53,5 @@ def forward(
         neg_loss = (torch.sigmoid(neg_r_hat) ** 2).sum()
 
         loss = (pos_loss + neg_loss) / (n_batch * (n_pos + n_neg))
-        reg = self.regularize(embeddings_dict)
 
-        return loss + reg
+        return loss
diff --git a/PyTorchCML/losses/MinTripletLoss.py b/PyTorchCML/losses/MinTripletLoss.py
index c8337a9..1f7ec7d 100644
--- a/PyTorchCML/losses/MinTripletLoss.py
+++ b/PyTorchCML/losses/MinTripletLoss.py
@@ -1,22 +1,25 @@
 import torch
+from torch import nn
 
-from .BaseTripletLoss import BaseTripletLoss
+from .BaseLoss import BaseLoss
 
 
-class MinTripletLoss(BaseTripletLoss):
+class MinTripletLoss(BaseLoss):
     def __init__(self, margin: float = 1, regularizers: list = []):
         """Class of Triplet Loss taking minimum negative sample."""
-        super().__init__(margin, regularizers)
+        super().__init__(regularizers)
+        self.margin = margin
+        self.ReLU = nn.ReLU()
 
-    def forward(
+    def main(
         self, embeddings_dict: dict, batch: torch.Tensor, column_names: dict
     ) -> torch.Tensor:
-        """Method of forwarding loss
+        """Method of forwarding main loss
 
         Args:
             embeddings_dict (dict): A dictionary of embddings which has following key and values.
-                user_embedding : embeddings of user, size (n_batch, d)
-                pos_item_embedding : embeddings of positive item, size (n_batch, d)
+                user_embedding : embeddings of user, size (n_batch, 1, d)
+                pos_item_embedding : embeddings of positive item, size (n_batch, 1, d)
                 neg_item_embedding : embeddings of negative item, size (n_batch, n_neg_samples, d)
 
             batch (torch.Tensor) : A tensor of batch, size (n_batch, *).
@@ -38,6 +41,5 @@ def forward(
         pairwiseloss = self.ReLU(self.margin + pos_dist ** 2 - min_neg_dist.values ** 2)
 
         loss = torch.mean(pairwiseloss)
-        reg = self.regularize(embeddings_dict)
 
-        return loss + reg
+        return loss
diff --git a/PyTorchCML/losses/RelevancePairwiseLoss.py b/PyTorchCML/losses/RelevancePairwiseLoss.py
index a4e3e01..0e398ac 100644
--- a/PyTorchCML/losses/RelevancePairwiseLoss.py
+++ b/PyTorchCML/losses/RelevancePairwiseLoss.py
@@ -1,10 +1,10 @@
 import torch
 from torch import nn
 
-from .BasePairwiseLoss import BasePairwiseLoss
+from .BaseLoss import BaseLoss
 
 
-class RelevancePairwiseLoss(BasePairwiseLoss):
+class RelevancePairwiseLoss(BaseLoss):
     """Class of loss for Relevance Matrix Factorization
 
     See below reference for detail.
@@ -28,15 +28,15 @@ def __init__(self, regularizers: list = [], delta: str = "logistic"):
         else:
             raise NotImplementedError
 
-    def forward(
+    def main(
         self, embeddings_dict: dict, batch: torch.Tensor, column_names: dict
     ) -> torch.Tensor:
-        """Method of forwarding loss
+        """Method of forwarding main loss
 
         Args:
             embeddings_dict (dict): A dictionary of embddings which has following key and values.
-                "user_embedding" : embeddings of user, size (n_batch, d)
-                "pos_item_embedding" : embeddings of positive item, size (n_batch, d)
+                "user_embedding" : embeddings of user, size (n_batch, 1, d)
+                "pos_item_embedding" : embeddings of positive item, size (n_batch, 1, d)
                 "neg_item_embedding" : embeddings of negative item, size (n_batch, n_neg_samples, d)
                 "user_bias" : bias of user, size (n_batch, 1)
                 "pos_item_bias" : bias of positive item, size (n_batch, 1)
@@ -55,13 +55,13 @@ def forward(
         pscore = batch[:, column_names["pscore"]]
 
         pos_inner = torch.einsum(
-            "nd,nd->n",
+            "nid,nid->n",
             embeddings_dict["user_embedding"],
             embeddings_dict["pos_item_embedding"],
         )
 
         neg_inner = torch.einsum(
-            "nd,njd->nj",
+            "nid,njd->nj",
             embeddings_dict["user_embedding"],
             embeddings_dict["neg_item_embedding"],
         )
@@ -79,6 +79,5 @@ def forward(
         neg_loss = self.delta_neg(neg_r_hat).sum()
 
         loss = (pos_loss + neg_loss) / (n_batch * (n_pos + n_neg))
-        reg = self.regularize(embeddings_dict)
 
-        return loss + reg
+        return loss
diff --git a/PyTorchCML/losses/SumTripletLoss.py b/PyTorchCML/losses/SumTripletLoss.py
index 633bf82..88af0ef 100644
--- a/PyTorchCML/losses/SumTripletLoss.py
+++ b/PyTorchCML/losses/SumTripletLoss.py
@@ -1,23 +1,26 @@
 import torch
+from torch import nn
 
-from .BaseTripletLoss import BaseTripletLoss
+from .BaseLoss import BaseLoss
 
 
-class SumTripletLoss(BaseTripletLoss):
+class SumTripletLoss(BaseLoss):
     """Class of Triplet Loss taking sum of negative sample."""
 
     def __init__(self, margin: float = 1, regularizers: list = []):
-        super().__init__(margin, regularizers)
+        super().__init__(regularizers)
+        self.margin = margin
+        self.ReLU = nn.ReLU()
 
-    def forward(
+    def main(
         self, embeddings_dict: dict, batch: torch.Tensor, column_names: dict
     ) -> torch.Tensor:
-        """Method of forwarding loss
+        """Method of forwarding main loss
 
         Args:
             embeddings_dict (dict): A dictionary of embddings which has following key and values
-                user_embedding : embeddings of user, size (n_batch, d)
-                pos_item_embedding : embeddings of positive item, size (n_batch, d)
+                user_embedding : embeddings of user, size (n_batch, 1, d)
+                pos_item_embedding : embeddings of positive item, size (n_batch, 1, d)
                 neg_item_embedding : embeddings of negative item, size (n_batch, n_neg_samples, d)
 
             batch (torch.Tensor) : A tensor of batch, size (n_batch, *).
@@ -36,6 +39,5 @@ def forward(
 
         tripletloss = self.ReLU(self.margin + pos_dist ** 2 - neg_dist ** 2)
         loss = torch.mean(tripletloss)
-        reg = self.regularize(embeddings_dict)
 
-        return loss + reg
+        return loss
diff --git a/PyTorchCML/losses/__init__.py b/PyTorchCML/losses/__init__.py
index 9a2e7ce..a4da933 100644
--- a/PyTorchCML/losses/__init__.py
+++ b/PyTorchCML/losses/__init__.py
@@ -1,8 +1,7 @@
 # flake8: noqa
-from .BaseTripletLoss import BaseTripletLoss
+from .BaseLoss import BaseLoss
 from .MinTripletLoss import MinTripletLoss
 from .SumTripletLoss import SumTripletLoss
-from .BasePairwiseLoss import BasePairwiseLoss
 from .LogitPairwiseLoss import LogitPairwiseLoss
 from .RelevancePairwiseLoss import RelevancePairwiseLoss
 from .MSEPairwiseLoss import MSEPairwiseLoss
diff --git a/PyTorchCML/models/BaseEmbeddingModel.py b/PyTorchCML/models/BaseEmbeddingModel.py
index ebf5a4b..4438939 100644
--- a/PyTorchCML/models/BaseEmbeddingModel.py
+++ b/PyTorchCML/models/BaseEmbeddingModel.py
@@ -3,6 +3,8 @@
 import torch
 from torch import nn
 
+from ..adaptors import BaseAdaptor
+
 
 class BaseEmbeddingModel(nn.Module):
     """Class of abstract embeddings model getting embedding or predict relevance from indices."""
@@ -15,6 +17,8 @@ def __init__(
         max_norm: Optional[float] = 1,
         user_embedding_init: Optional[torch.Tensor] = None,
         item_embedding_init: Optional[torch.Tensor] = None,
+        user_adaptor: Optional[BaseAdaptor] = None,
+        item_adaptor: Optional[BaseAdaptor] = None,
     ):
         """Set embeddings.
 
@@ -31,6 +35,8 @@ def __init__(
         self.n_item = n_item
         self.n_dim = n_dim
         self.max_norm = max_norm
+        self.user_adaptor = user_adaptor
+        self.item_adaptor = item_adaptor
 
         if user_embedding_init is None:
             self.user_embedding = nn.Embedding(
@@ -71,7 +77,7 @@ def predict(self, pairs: torch.Tensor) -> torch.Tensor:
         """Method of predicting relevance for each pair of user and item.
 
         Args:
-            pairs (torch.Tensor): Tensor which columns are [user_id, item_id]
+            pairs (torch.Tensor): Tensor whose columns are [user_id, item_id]
 
         Raises:
             NotImplementedError: [description]
diff --git a/PyTorchCML/models/MatrixFactorization.py b/PyTorchCML/models/MatrixFactorization.py
index 671a47c..5845702 100644
--- a/PyTorchCML/models/MatrixFactorization.py
+++ b/PyTorchCML/models/MatrixFactorization.py
@@ -4,6 +4,7 @@
 from torch import nn
 
 from .BaseEmbeddingModel import BaseEmbeddingModel
+from ..adaptors import BaseAdaptor
 
 
 class LogitMatrixFactorization(BaseEmbeddingModel):
@@ -18,6 +19,8 @@ def __init__(
         item_embedding_init: Optional[torch.Tensor] = None,
         user_bias_init: Optional[torch.Tensor] = None,
         item_bias_init: Optional[torch.Tensor] = None,
+        user_adaptor: Optional[BaseAdaptor] = None,
+        item_adaptor: Optional[BaseAdaptor] = None,
     ):
         """Set model parameters
 
@@ -34,7 +37,14 @@ def __init__(
         """
 
         super().__init__(
-            n_user, n_item, n_dim, max_norm, user_embedding_init, item_embedding_init
+            n_user,
+            n_item,
+            n_dim,
+            max_norm,
+            user_embedding_init,
+            item_embedding_init,
+            user_adaptor,
+            item_adaptor,
         )
         self.max_bias = max_bias
         self.weight_link = lambda x: torch.sigmoid(-x)
diff --git a/PyTorchCML/trainers/BaseTrainer.py b/PyTorchCML/trainers/BaseTrainer.py
index 0f70467..924393d 100644
--- a/PyTorchCML/trainers/BaseTrainer.py
+++ b/PyTorchCML/trainers/BaseTrainer.py
@@ -1,9 +1,13 @@
-from typing import Optional, Union
+from typing import Optional
 
 from torch import optim
 
+import numpy as np
+import pandas as pd
+from tqdm import tqdm
+
 from ..evaluators import BaseEvaluator
-from ..losses import BasePairwiseLoss, BaseTripletLoss
+from ..losses import BaseLoss
 from ..models import BaseEmbeddingModel
 from ..samplers import BaseSampler
 
@@ -15,7 +19,7 @@ def __init__(
         self,
         model: BaseEmbeddingModel,
         optimizer: optim,
-        criterion: Union[BasePairwiseLoss, BaseTripletLoss],
+        criterion: BaseLoss,
         sampler: BaseSampler,
         column_names: Optional[dict] = None,
     ):
@@ -46,4 +50,71 @@ def fit(
         valid_per_epoch: int = 5,
     ):
 
-        raise NotImplementedError
+        # set evaluator and log dataframe
+        valid_or_not = valid_evaluator is not None
+        if valid_or_not:
+            self.valid_scores = valid_evaluator.score(self.model)
+            self.valid_scores["epoch"] = 0
+            self.valid_scores["loss"] = np.nan
+
+        # start training
+        for ep in range(n_epoch):
+            accum_loss = 0
+
+            # start epoch
+            with tqdm(range(n_batch), total=n_batch) as pbar:
+                for b in pbar:
+                    # batch sampling
+                    batch = self.sampler.get_pos_batch()
+                    users = batch[:, self.column_names["user_id"]].reshape(-1, 1)
+                    pos_items = batch[:, self.column_names["item_id"]].reshape(-1, 1)
+
+                    if self.sampler.two_stage:
+                        neg_candidates = self.sampler.get_and_set_candidates()
+                        dist = self.model.spreadout_distance(pos_items, neg_candidates)
+                        self.sampler.set_candidates_weight(dist, self.model.n_dim)
+
+                    neg_items = self.sampler.get_neg_batch(users.reshape(-1))
+
+                    # initialize gradient
+                    self.model.zero_grad()
+
+                    # compute distance
+                    embeddings_dict = self.model(users, pos_items, neg_items)
+
+                    # compute loss
+                    loss = self.criterion(embeddings_dict, batch, self.column_names)
+
+                    # adding loss for domain adaptation
+                    if self.model.user_adaptor is not None:
+                        loss += self.model.user_adaptor(
+                            users, embeddings_dict["user_embedding"]
+                        )
+                    if self.model.item_adaptor is not None:
+                        loss += self.model.item_adaptor(
+                            pos_items, embeddings_dict["pos_item_embedding"]
+                        )
+                        loss += self.model.item_adaptor(
+                            neg_items, embeddings_dict["neg_item_embedding"]
+                        )
+
+                    accum_loss += loss.item()
+
+                    # gradient of loss
+                    loss.backward()
+
+                    # update model parameters
+                    self.optimizer.step()
+
+                    pbar.set_description_str(
+                        f"epoch{ep+1} avg_loss:{accum_loss / (b+1) :.3f}"
+                    )
+
+            # compute metrics for epoch
+            if valid_or_not and (
+                ((ep + 1) % valid_per_epoch == 0) or (ep == n_epoch - 1)
+            ):
+                valid_scores_sub = valid_evaluator.score(self.model)
+                valid_scores_sub["epoch"] = ep + 1
+                valid_scores_sub["loss"] = accum_loss / n_batch
+                self.valid_scores = pd.concat([self.valid_scores, valid_scores_sub])
diff --git a/PyTorchCML/trainers/CMLTrainer.py b/PyTorchCML/trainers/CMLTrainer.py
deleted file mode 100644
index 89b5f96..0000000
--- a/PyTorchCML/trainers/CMLTrainer.py
+++ /dev/null
@@ -1,73 +0,0 @@
-from typing import Optional
-
-import numpy as np
-import pandas as pd
-from tqdm import tqdm
-
-from ..evaluators import UserwiseEvaluator
-from .BaseTrainer import BaseTrainer
-
-
-class CMLTrainer(BaseTrainer):
-    def fit(
-        self,
-        n_batch: int = 500,
-        n_epoch: int = 10,
-        valid_evaluator: Optional[UserwiseEvaluator] = None,
-        valid_per_epoch: int = 5,
-    ):
-
-        # set evaluator and log dataframe
-        valid_or_not = valid_evaluator is not None
-        if valid_or_not:
-            self.valid_scores = valid_evaluator.score(self.model)
-            self.valid_scores["epoch"] = 0
-            self.valid_scores["loss"] = np.nan
-
-        # start training
-        for ep in range(n_epoch):
-            accum_loss = 0
-
-            # start mini-batch
-            with tqdm(range(n_batch), total=n_batch) as pbar:
-                for b in pbar:
-                    # batch sampling
-                    batch = self.sampler.get_pos_batch()
-                    users = batch[:, self.column_names["user_id"]].reshape(-1, 1)
-                    pos_items = batch[:, self.column_names["item_id"]].reshape(-1, 1)
-
-                    if self.sampler.two_stage:
-                        neg_candidates = self.sampler.get_and_set_candidates()
-                        dist = self.model.spreadout_distance(pos_items, neg_candidates)
-                        self.sampler.set_candidates_weight(dist, self.model.n_dim)
-
-                    neg_items = self.sampler.get_neg_batch(users.reshape(-1))
-
-                    # initialize gradient
-                    self.model.zero_grad()
-
-                    # compute distance
-                    embeddings_dict = self.model(users, pos_items, neg_items)
-
-                    # compute loss
-                    loss = self.criterion(embeddings_dict, batch, self.column_names)
-                    accum_loss += loss.item()
-
-                    # gradient of loss
-                    loss.backward()
-
-                    # update model parameters
-                    self.optimizer.step()
-
-                    pbar.set_description_str(
-                        f"epoch{ep+1} avg_loss:{accum_loss / (b+1) :.3f}"
-                    )
-
-            # compute metrics for epoch
-            if valid_or_not and (
-                ((ep + 1) % valid_per_epoch == 0) or (ep == n_epoch - 1)
-            ):
-                valid_scores_sub = valid_evaluator.score(self.model)
-                valid_scores_sub["epoch"] = ep + 1
-                valid_scores_sub["loss"] = accum_loss / n_batch
-                self.valid_scores = pd.concat([self.valid_scores, valid_scores_sub])
diff --git a/PyTorchCML/trainers/MFTrainer.py b/PyTorchCML/trainers/MFTrainer.py
deleted file mode 100644
index fe321f2..0000000
--- a/PyTorchCML/trainers/MFTrainer.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from typing import Optional
-
-import numpy as np
-import pandas as pd
-from tqdm import tqdm
-
-from ..evaluators import UserwiseEvaluator
-from .BaseTrainer import BaseTrainer
-
-
-class MFTrainer(BaseTrainer):
-    def fit(
-        self,
-        n_batch: int = 500,
-        n_epoch: int = 10,
-        valid_evaluator: Optional[UserwiseEvaluator] = None,
-        valid_per_epoch: int = 5,
-    ):
-
-        # set evaluator and log dataframe
-        valid_or_not = valid_evaluator is not None
-        if valid_or_not:
-            self.valid_scores = valid_evaluator.score(self.model)
-            self.valid_scores["epoch"] = 0
-            self.valid_scores["loss"] = np.nan
-
-        # start training
-        for ep in range(n_epoch):
-            accum_loss = 0
-
-            # start mini-batch
-            with tqdm(range(n_batch), total=n_batch) as pbar:
-                for b in pbar:
-                    # batch sampling
-                    batch = self.sampler.get_pos_batch()
-                    users = batch[:, self.column_names["user_id"]]
-                    pos_items = batch[:, self.column_names["item_id"]]
-                    neg_items = self.sampler.get_neg_batch(users)
-
-                    # initialize gradient
-                    self.model.zero_grad()
-
-                    # look up embeddings
-                    embeddings_dict = self.model(users, pos_items, neg_items)
-
-                    # compute loss
-                    loss = self.criterion(embeddings_dict, batch, self.column_names)
-                    accum_loss += loss.item()
-
-                    # gradient of loss
-                    loss.backward()
-
-                    # update model parameters
-                    self.optimizer.step()
-
-                    pbar.set_description_str(
-                        f"epoch{ep+1} avg_loss:{accum_loss / (b+1) :.3f}"
-                    )
-
-            # compute metrics for epoch
-            if valid_or_not and (
-                ((ep + 1) % valid_per_epoch == 0) or (ep == n_epoch - 1)
-            ):
-                valid_scores_sub = valid_evaluator.score(self.model)
-                valid_scores_sub["epoch"] = ep + 1
-                valid_scores_sub["loss"] = accum_loss / n_batch
-                self.valid_scores = pd.concat([self.valid_scores, valid_scores_sub])
diff --git a/PyTorchCML/trainers/__init__.py b/PyTorchCML/trainers/__init__.py
index bac95c1..760fc56 100644
--- a/PyTorchCML/trainers/__init__.py
+++ b/PyTorchCML/trainers/__init__.py
@@ -1,4 +1,2 @@
 # flake8: noqa
 from .BaseTrainer import BaseTrainer
-from .CMLTrainer import CMLTrainer
-from .MFTrainer import MFTrainer
diff --git a/README.md b/README.md
index 943440b..ceb9651 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
 # PyTorchCML
 
+![https://github.com/hand10ryo/PyTorchCML/blob/image/images/icon.png](https://github.com/hand10ryo/PyTorchCML/blob/image/images/icon.png)
+
 PyTorchCML is a library of PyTorch implementations of matrix factorization (MF) and collaborative metric learning (CML), algorithms used in recommendation systems and data mining.
 
 日本語版READMEは[こちら](https://github.com/hand10ryo/PyTorchCML/blob/main/README_ja.md)
@@ -60,7 +62,7 @@ By combining these modules, you can implement a variety of algorithms.
 
 The following figure shows the relationship between these modules.
 
-![https://github.com/hand10ryo/PyTorchCML/blob/main/images/diagram.png](https://github.com/hand10ryo/PyTorchCML/blob/main/images/diagram.png)
+![https://github.com/hand10ryo/PyTorchCML/blob/image/images/diagram.png](https://github.com/hand10ryo/PyTorchCML/blob/image/images/diagram.png)
 
 The most straightforward implementation is as follows.
 
@@ -82,7 +84,7 @@ model = models.CollaborativeMetricLearning(n_user, n_item, n_dim=10).to(device)
 optimizer = optim.Adam(model.parameters(), lr=1e-3)
 criterion = losses.MinTripletLoss(margin=1).to(device)
 sampler = samplers.BaseSampler(train_set_torch, n_user, n_item, device=device)
-trainer = trainers.CMLTrainer(model, optimizer, criterion, sampler)
+trainer = trainers.BaseTrainer(model, optimizer, criterion, sampler)
 
 # run 
 trainer.fit(n_batch=256, n_epoch=3)
@@ -92,7 +94,7 @@ The input `train_set` represents a two-column NumPy array whose records are the
 
 The `n_user` and `n_item` are the number of users and items. Here, we assume that user ID and item ID start from 0 and that all users and items are included in the train_set.
 
-Then, define model, optimizer, criterion, and sampler, input them to a trainer and run the trainer's fit method to start learning CML.
+Then, define model, optimizer, criterion, and sampler, input them to a trainer and run the trainer's fit method to start learning CM
 
 ## models
 
@@ -124,10 +126,10 @@ model = models.LogitMatrixFactorization(
 The losses module is for handling the loss function for learning embeddings.
 We can mainly divide the loss function into PairwiseLoss and TripletLoss.
 
-PairwiseLoss is the loss for each user-item pair (u, i).
+PairwiseLoss is the loss for each user-item pair <img src="https://latex.codecogs.com/gif.latex?\bg_black&space;(u,i)" title="(u, i)" />.
 
-TripletLoss is the loss per (u, i_p, i_n).
-Here, (u, i_p) is a positive pair, and (u, i_n) is a negative pair.
+TripletLoss is the loss per <img src="https://latex.codecogs.com/gif.latex?\bg_black&space;(u,i_+,i_-)" title="(u,i_+,i_-)" />.
+Here, <img src="https://latex.codecogs.com/gif.latex?\bg_black&space;(u,i_+)" title="(u,i_+)" /> is a positive pair, and <img src="https://latex.codecogs.com/gif.latex?\bg_black&space;(u,i_-)" title="(u,i_-)" /> is a negative pair.
 
 In general, CML uses triplet loss, and MF uses pairwise loss.
 
@@ -137,8 +139,8 @@ The samplers is a module that handles the sampling of mini-batches during traini
 
 There are two types of sampling done by the sampler.
 
-- Sampling of positive user-item pairs (u, i_p)
-- Sampling of negative items i_n
+- Sampling of positive user-item pairs <img src="https://latex.codecogs.com/gif.latex?\bg_black&space;(u,i_+)" title="(u,i_+)" />
+- Sampling of negative items <img src="https://latex.codecogs.com/gif.latex?\bg_black&space;i_-" title="i_-" />
 
 The default setting is to sample both with a uniform random probability.
 
@@ -160,8 +162,6 @@ The trainers is the module that handles training.
 
 You can train by setting up a model, optimizer, loss function, and sampler.
 
-Currently, there are two modules, one for CML and one for MF.
-
 ## evaluators
 
 The evaluators is a module for evaluating performance after learning.
@@ -225,6 +225,34 @@ criterion = losses.MinTripletLoss(margin=1, regularizers=regs).to(device)
 
 It is also possible to introduce multiple regularizations by increasing the length of the list.
 
+## adaptors
+
+The adaptors is a module for realizing domain adaptation.
+
+Domain adaptation in CML is achieved by adding <img src="https://latex.codecogs.com/gif.latex?\inline&space;\bg_black&space;L(v_i,&space;\theta)&space;=&space;\|f(x_i;\theta)-v_i\|^2" title="L(v_i, \theta) = \|f(x_i;\theta)-v_i\|^2" /> to the loss for feature <img src="https://latex.codecogs.com/gif.latex?\bg_black&space;x_i" title="x_i" /> of item  <img src="https://latex.codecogs.com/gif.latex?\bg_black&space;i" title="i" /> . The same is true for the user. This allows us to reflect attribute information in the embedding vector.
+
+MLPAdaptor is a class of adaptors that assumes a multilayer perceptron in function <img src="https://latex.codecogs.com/gif.latex?\inline&space;\bg_black&space;f(x_i;\theta)" title="f(x_i;\theta)" />.
+
+You can set up the adaptor as shown in the code below
+
+```python
+from PyTorchCML import adaptors
+
+# item_feature.shape = (n_item, n_feature)
+item_feature_torch = torch.Tensor(item_feature)
+adaptor = adaptors.MLPAdaptor(
+    item_feature_torch, 
+    n_dim=10, 
+    n_hidden=[20], 
+    weight=1e-4
+)
+
+model = models.CollaborativeMetricLearning(
+    n_user, n_item, n_dim, 
+    item_adaptor=adaptor
+).to(device)
+```
+
 # Development
 
 ```bash
diff --git a/README_ja.md b/README_ja.md
index 8da2270..3be0338 100644
--- a/README_ja.md
+++ b/README_ja.md
@@ -1,5 +1,7 @@
 # PyTorchCML
 
+![https://github.com/hand10ryo/PyTorchCML/blob/image/images/icon.png](https://github.com/hand10ryo/PyTorchCML/blob/image/images/icon.png)
+
 PyTorchCMLは、推薦システム・データマイニングのアルゴリズムである 行列分解(matrix factorization, MF) および collaborative metric learning (CML)を PyTorch で実装したライブラリです。
 
 English version of README is [here](https://github.com/hand10ryo/PyTorchCML/blob/main/README.md)
@@ -14,7 +16,7 @@ CML は metric learning と MF を組み合わせたアルゴリズムで、ユ
 
 # インストール
 
-PyTorchCMLは python のパッケージマネージャー pip でインストール可能です。
+PytorchCMLは python のパッケージマネージャー pip でインストール可能です。
 
 ```bash
 pip install PyTorchCML
@@ -60,7 +62,7 @@ Movielens 100k データセットを用いた jupyter notebook の例が[こち
 
 これらのモジュールは以下の図のような関係があります。
 
-![https://github.com/hand10ryo/PyTorchCML/blob/main/images/diagram.png](https://github.com/hand10ryo/PyTorchCML/blob/main/images/diagram.png)
+![https://github.com/hand10ryo/PyTorchCML/blob/image/images/diagram.png](https://github.com/hand10ryo/PyTorchCML/blob/image/images/diagram.png)
 
 最も単純化した実装は以下の通りです。
 
@@ -82,7 +84,7 @@ model = models.CollaborativeMetricLearning(n_user, n_item, n_dim=10).to(device)
 optimizer = optim.Adam(model.parameters(), lr=1e-3)
 criterion = losses.MinTripletLoss(margin=1).to(device)
 sampler = samplers.BaseSampler(train_set_torch, n_user, n_item, device=device)
-trainer = trainers.CMLTrainer(model, optimizer, criterion, sampler)
+trainer = trainers.BaseTrainer(model, optimizer, criterion, sampler)
 
 # run 
 trainer.fit(n_batch=256, n_epoch=3)
@@ -125,9 +127,9 @@ losses は埋め込みベクトル学習のための損失関数を司るモジ
 
 損失関数は主に、PairwiseLoss と TripletLoss に分けられます。
 
-PairwiseLoss は、ユーザーアイテムペア(u, i) ごとの損失です。
+PairwiseLoss は、ユーザーアイテムペア <img src="https://latex.codecogs.com/gif.latex?\bg_black&space;(u,i)" title="(u, i)" /> ごとの損失です。
 
-TripletLoss は、ポジティブなユーザーアイテムペア (u, i_p) に対してネガティブなアイテムi_nを加えた(u, i_p,i_n)ごとの損失です。
+TripletLoss は、ポジティブなユーザーアイテムペア <img src="https://latex.codecogs.com/gif.latex?\bg_black&space;(u,i_+)" title="(u,i_+)" />に対してネガティブなアイテム<img src="https://latex.codecogs.com/gif.latex?\bg_black&space;i_-" title="i_-" />を加えた<img src="https://latex.codecogs.com/gif.latex?\bg_black&space;(u,i_+,i_-)" title="(u,i_+,i_-)" />ごとの損失です。
 
 ## samplers
 
@@ -135,8 +137,8 @@ samplers は学習中のミニバッチのサンプリングを司るモジュ
 
 sampler が行うサンプリングは２種類あります。
 
-- ポジティブなユーザーアイテムペア (u, i_p) の抽出
-- ネガティブなアイテム i_n の抽出
+- ポジティブなユーザーアイテムペア<img src="https://latex.codecogs.com/gif.latex?\bg_black&space;(u,i_+)" title="(u,i_+)" />の抽出
+- ネガティブなアイテム<img src="https://latex.codecogs.com/gif.latex?\bg_black&space;i_-" title="i_-" />の抽出
 
 デフォルトでは両者のサンプリングを一様ランダムに行います。
 
@@ -158,8 +160,6 @@ trainers は学習を司るモジュールです。
 
 モデル、オプティマイザ、損失関数、サンプラーを設定すると学習ができます。
 
-現在はCMLとMFで分かれています。
-
 ## evaluators
 
 evaluators は学習後のパフォーマンス評価を行うためのモジュールです。
@@ -226,6 +226,34 @@ criterion = losses.MinTripletLoss(margin=1, regularizers=regs).to(device)
 
 リストの長さを増やせば複数の正則化を導入することも可能です。
 
+## adaptors
+
+adaptors はドメイン適合を実現するためのモジュールです。
+
+CMLにおけるドメイン適合はアイテム <img src="https://latex.codecogs.com/gif.latex?\bg_black&space;i" title="i" /> の特徴量 <img src="https://latex.codecogs.com/gif.latex?\bg_black&space;x_i" title="x_i" />に対して、<img src="https://latex.codecogs.com/gif.latex?\inline&space;\bg_black&space;L(v_i,&space;\theta)&space;=&space;\|f(x_i;\theta)-v_i\|^2" title="L(v_i, \theta) = \|f(x_i;\theta)-v_i\|^2" /> を損失に加えることで達成します。ユーザーについても同様です。これによって埋め込みベクトルに属性情報を反映することができます。
+
+MLPAdaptor は<img src="https://latex.codecogs.com/gif.latex?\inline&space;\bg_black&space;f(x_i;\theta)" title="f(x_i;\theta)" />に多層パーセプトロンを仮定した Adaptor クラスです。
+
+以下のようにモデルに組み込むことができます。
+
+```python
+from PyTorchCML import adaptors
+
+# item_feature.shape = (n_item, n_feature)
+item_feature_torch = torch.Tensor(item_feature)
+adaptor = adaptors.MLPAdaptor(
+    item_feature_torch, 
+    n_dim=10, 
+    n_hidden=[20], 
+    weight=1e-4
+)
+
+model = models.CollaborativeMetricLearning(
+    n_user, n_item, n_dim, 
+    item_adaptor=adaptor
+).to(device)
+```
+
 # 開発
 
 ```bash
diff --git a/examples/notebooks/movielens_cml.ipynb b/examples/notebooks/movielens_cml.ipynb
index 750271d..0760bbb 100644
--- a/examples/notebooks/movielens_cml.ipynb
+++ b/examples/notebooks/movielens_cml.ipynb
@@ -3,24 +3,22 @@
   {
    "cell_type": "code",
    "execution_count": 1,
-   "metadata": {},
-   "outputs": [],
    "source": [
-    "# !pip install PytorchCML"
-   ]
+    "# !pip install PyTorchCML"
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 1,
-   "metadata": {},
-   "outputs": [],
    "source": [
     "import sys\n",
-    "sys.path.append(\"../../src/\")\n",
+    "sys.path.append(\"../../\")\n",
     "\n",
     "from itertools import product\n",
     "\n",
-    "from PytorchCML import losses, models, samplers, regularizers, evaluators, trainers\n",
+    "from PyTorchCML import losses, models, samplers, regularizers, evaluators, trainers\n",
     "import torch\n",
     "from torch import nn, optim\n",
     "import pandas as pd\n",
@@ -28,25 +26,31 @@
     "from sklearn.model_selection import train_test_split\n",
     "from sklearn.decomposition import TruncatedSVD\n",
     "from scipy.sparse import csr_matrix"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 2,
-   "metadata": {},
-   "outputs": [],
    "source": [
+    "# download movielens dataset\n",
     "movielens = pd.read_csv(\n",
     "  'http://files.grouplens.org/datasets/movielens/ml-100k/u.data', \n",
-    "  sep='\\t', header=None, index_col=None,\n",
+    "    sep='\\t', header=None, index_col=None,\n",
+    "    names = [\"user_id\", \"item_id\", \"rating\", \"timestamp\"]\n",
     ")\n",
-    "movielens.columns = [\"user_id\", \"item_id\", \"rating\", \"timestamp\"]\n",
+    "# Set user/item id and number of users/items.\n",
     "movielens.user_id -= 1\n",
     "movielens.item_id -= 1\n",
-    "movielens.rating = (movielens.rating >= 4).astype(int)\n",
     "n_user = movielens.user_id.nunique()\n",
     "n_item = movielens.item_id.nunique()\n",
     "\n",
+    "# make implicit feedback\n",
+    "movielens.rating = (movielens.rating >= 4).astype(int)\n",
+    "\n",
+    "\n",
+    "# train test split\n",
     "train, test = train_test_split(movielens)\n",
     "\n",
     "\n",
@@ -80,20 +84,20 @@
     "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n",
     "train_set = torch.LongTensor(train_set).to(device)\n",
     "test_set = torch.LongTensor(test_set).to(device)\n"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
+   "cell_type": "markdown",
    "source": [
     "## Defalt"
    ],
-   "cell_type": "markdown",
    "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 3,
-   "metadata": {},
-   "outputs": [],
    "source": [
     "lr = 1e-3\n",
     "n_dim = 10\n",
@@ -108,84 +112,150 @@
     "    \"Recall\": evaluators.recall\n",
     "}\n",
     "evaluator = evaluators.UserwiseEvaluator(test_set, score_function_dict, ks=[3,5])\n",
-    "trainer = trainers.CMLTrainer(model, optimizer, criterion, sampler)\n"
-   ]
+    "trainer = trainers.BaseTrainer(model, optimizer, criterion, sampler)\n"
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 4,
-   "metadata": {},
+   "source": [
+    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
+   ],
    "outputs": [
     {
      "output_type": "stream",
      "name": "stderr",
      "text": [
-      "100%|██████████| 943/943 [00:22<00:00, 41.78it/s]\n",
-      "epoch1 avg_loss:0.967: 100%|██████████| 256/256 [00:06<00:00, 37.47it/s]\n",
-      "epoch2 avg_loss:0.824: 100%|██████████| 256/256 [00:05<00:00, 48.58it/s]\n",
-      "epoch3 avg_loss:0.741: 100%|██████████| 256/256 [00:05<00:00, 50.05it/s]\n",
-      "epoch4 avg_loss:0.685: 100%|██████████| 256/256 [00:06<00:00, 42.30it/s]\n",
-      "epoch5 avg_loss:0.646: 100%|██████████| 256/256 [00:09<00:00, 27.20it/s]\n",
-      "epoch6 avg_loss:0.622: 100%|██████████| 256/256 [00:07<00:00, 32.36it/s]\n",
-      "epoch7 avg_loss:0.600: 100%|██████████| 256/256 [00:05<00:00, 44.91it/s]\n",
-      "epoch8 avg_loss:0.581: 100%|██████████| 256/256 [00:05<00:00, 48.92it/s]\n",
-      "epoch9 avg_loss:0.563: 100%|██████████| 256/256 [00:06<00:00, 41.15it/s]\n",
-      "epoch10 avg_loss:0.550: 100%|██████████| 256/256 [00:06<00:00, 42.59it/s]\n",
-      "100%|██████████| 943/943 [00:22<00:00, 41.46it/s]\n",
-      "epoch11 avg_loss:0.532: 100%|██████████| 256/256 [00:06<00:00, 38.52it/s]\n",
-      "epoch12 avg_loss:0.517: 100%|██████████| 256/256 [00:07<00:00, 36.29it/s]\n",
-      "epoch13 avg_loss:0.499: 100%|██████████| 256/256 [00:08<00:00, 31.96it/s]\n",
-      "epoch14 avg_loss:0.480: 100%|██████████| 256/256 [00:05<00:00, 45.09it/s]\n",
-      "epoch15 avg_loss:0.463: 100%|██████████| 256/256 [00:05<00:00, 46.86it/s]\n",
-      "epoch16 avg_loss:0.447: 100%|██████████| 256/256 [00:05<00:00, 45.36it/s]\n",
-      "epoch17 avg_loss:0.424: 100%|██████████| 256/256 [00:07<00:00, 34.68it/s]\n",
-      "epoch18 avg_loss:0.409: 100%|██████████| 256/256 [00:05<00:00, 45.60it/s]\n",
-      "epoch19 avg_loss:0.390: 100%|██████████| 256/256 [00:05<00:00, 44.48it/s]\n",
-      "epoch20 avg_loss:0.375: 100%|██████████| 256/256 [00:06<00:00, 40.54it/s]\n",
-      "100%|██████████| 943/943 [00:23<00:00, 40.68it/s]\n"
+      "100%|██████████| 943/943 [00:20<00:00, 46.66it/s]\n",
+      "epoch1 avg_loss:0.931: 100%|██████████| 256/256 [00:06<00:00, 38.85it/s]\n",
+      "epoch2 avg_loss:0.753: 100%|██████████| 256/256 [00:06<00:00, 40.74it/s]\n",
+      "epoch3 avg_loss:0.658: 100%|██████████| 256/256 [00:06<00:00, 39.85it/s]\n",
+      "epoch4 avg_loss:0.597: 100%|██████████| 256/256 [00:05<00:00, 46.31it/s]\n",
+      "epoch5 avg_loss:0.558: 100%|██████████| 256/256 [00:07<00:00, 34.50it/s]\n",
+      "epoch6 avg_loss:0.525: 100%|██████████| 256/256 [00:05<00:00, 44.82it/s]\n",
+      "epoch7 avg_loss:0.500: 100%|██████████| 256/256 [00:06<00:00, 42.24it/s]\n",
+      "epoch8 avg_loss:0.476: 100%|██████████| 256/256 [00:07<00:00, 35.35it/s]\n",
+      "epoch9 avg_loss:0.455: 100%|██████████| 256/256 [00:07<00:00, 34.50it/s]\n",
+      "epoch10 avg_loss:0.433: 100%|██████████| 256/256 [00:06<00:00, 41.74it/s]\n",
+      "100%|██████████| 943/943 [00:21<00:00, 44.59it/s]\n",
+      "epoch11 avg_loss:0.412: 100%|██████████| 256/256 [00:06<00:00, 41.59it/s]\n",
+      "epoch12 avg_loss:0.387: 100%|██████████| 256/256 [00:06<00:00, 39.21it/s]\n",
+      "epoch13 avg_loss:0.368: 100%|██████████| 256/256 [00:06<00:00, 42.29it/s]\n",
+      "epoch14 avg_loss:0.350: 100%|██████████| 256/256 [00:06<00:00, 40.07it/s]\n",
+      "epoch15 avg_loss:0.334: 100%|██████████| 256/256 [00:06<00:00, 42.41it/s]\n",
+      "epoch16 avg_loss:0.319: 100%|██████████| 256/256 [00:06<00:00, 42.64it/s]\n",
+      "epoch17 avg_loss:0.303: 100%|██████████| 256/256 [00:06<00:00, 41.52it/s]\n",
+      "epoch18 avg_loss:0.294: 100%|██████████| 256/256 [00:06<00:00, 41.94it/s]\n",
+      "epoch19 avg_loss:0.283: 100%|██████████| 256/256 [00:05<00:00, 44.28it/s]\n",
+      "epoch20 avg_loss:0.274: 100%|██████████| 256/256 [00:06<00:00, 41.52it/s]\n",
+      "100%|██████████| 943/943 [00:21<00:00, 43.05it/s]\n"
      ]
     }
    ],
-   "source": [
-    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 5,
-   "metadata": {},
+   "source": [
+    "trainer.valid_scores"
+   ],
    "outputs": [
     {
      "output_type": "execute_result",
      "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>nDCG@3</th>\n",
+       "      <th>MAP@3</th>\n",
+       "      <th>Recall@3</th>\n",
+       "      <th>nDCG@5</th>\n",
+       "      <th>MAP@5</th>\n",
+       "      <th>Recall@5</th>\n",
+       "      <th>epoch</th>\n",
+       "      <th>loss</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.007423</td>\n",
+       "      <td>0.012902</td>\n",
+       "      <td>0.001568</td>\n",
+       "      <td>0.008634</td>\n",
+       "      <td>0.017922</td>\n",
+       "      <td>0.002917</td>\n",
+       "      <td>0</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.042387</td>\n",
+       "      <td>0.070078</td>\n",
+       "      <td>0.006008</td>\n",
+       "      <td>0.046897</td>\n",
+       "      <td>0.084353</td>\n",
+       "      <td>0.011537</td>\n",
+       "      <td>10</td>\n",
+       "      <td>0.432954</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.202032</td>\n",
+       "      <td>0.291888</td>\n",
+       "      <td>0.044877</td>\n",
+       "      <td>0.202131</td>\n",
+       "      <td>0.311188</td>\n",
+       "      <td>0.073077</td>\n",
+       "      <td>20</td>\n",
+       "      <td>0.274121</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
       "text/plain": [
        "     nDCG@3     MAP@3  Recall@3    nDCG@5     MAP@5  Recall@5  epoch      loss\n",
-       "0  0.019834  0.036144  0.002331  0.019754  0.042109  0.003795      0       NaN\n",
-       "0  0.055078  0.088017  0.004533  0.057107  0.101432  0.008006     10  0.549945\n",
-       "0  0.274714  0.365323  0.032301  0.271033  0.381550  0.054757     20  0.375043"
-      ],
-      "text/html": "<div>\n<style scoped>\n    .dataframe tbody tr th:only-of-type {\n        vertical-align: middle;\n    }\n\n    .dataframe tbody tr th {\n        vertical-align: top;\n    }\n\n    .dataframe thead th {\n        text-align: right;\n    }\n</style>\n<table border=\"1\" class=\"dataframe\">\n  <thead>\n    <tr style=\"text-align: right;\">\n      <th></th>\n      <th>nDCG@3</th>\n      <th>MAP@3</th>\n      <th>Recall@3</th>\n      <th>nDCG@5</th>\n      <th>MAP@5</th>\n      <th>Recall@5</th>\n      <th>epoch</th>\n      <th>loss</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <th>0</th>\n      <td>0.019834</td>\n      <td>0.036144</td>\n      <td>0.002331</td>\n      <td>0.019754</td>\n      <td>0.042109</td>\n      <td>0.003795</td>\n      <td>0</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.055078</td>\n      <td>0.088017</td>\n      <td>0.004533</td>\n      <td>0.057107</td>\n      <td>0.101432</td>\n      <td>0.008006</td>\n      <td>10</td>\n      <td>0.549945</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.274714</td>\n      <td>0.365323</td>\n      <td>0.032301</td>\n      <td>0.271033</td>\n      <td>0.381550</td>\n      <td>0.054757</td>\n      <td>20</td>\n      <td>0.375043</td>\n    </tr>\n  </tbody>\n</table>\n</div>"
+       "0  0.007423  0.012902  0.001568  0.008634  0.017922  0.002917      0       NaN\n",
+       "0  0.042387  0.070078  0.006008  0.046897  0.084353  0.011537     10  0.432954\n",
+       "0  0.202032  0.291888  0.044877  0.202131  0.311188  0.073077     20  0.274121"
+      ]
      },
      "metadata": {},
      "execution_count": 5
     }
    ],
-   "source": [
-    "trainer.valid_scores"
-   ]
+   "metadata": {}
   },
   {
+   "cell_type": "markdown",
    "source": [
     "## Strict Negative"
    ],
-   "cell_type": "markdown",
    "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 6,
-   "metadata": {},
-   "outputs": [],
    "source": [
     "lr = 1e-3\n",
     "n_dim = 10\n",
@@ -200,15 +270,17 @@
     "    \"Recall\": evaluators.recall\n",
     "}\n",
     "evaluator = evaluators.UserwiseEvaluator(test_set, score_function_dict, ks=[3,5])\n",
-    "trainer = trainers.CMLTrainer(model, optimizer, criterion, sampler)\n"
-   ]
+    "trainer = trainers.BaseTrainer(model, optimizer, criterion, sampler)\n"
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 7,
-   "metadata": {
-    "tags": []
-   },
+   "source": [
+    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
+   ],
    "outputs": [
     {
      "output_type": "stream",
@@ -240,46 +312,110 @@
      ]
     }
    ],
-   "source": [
-    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
-   ]
+   "metadata": {
+    "tags": []
+   }
   },
   {
    "cell_type": "code",
    "execution_count": 8,
-   "metadata": {},
+   "source": [
+    "trainer.valid_scores"
+   ],
    "outputs": [
     {
      "output_type": "execute_result",
      "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>nDCG@3</th>\n",
+       "      <th>MAP@3</th>\n",
+       "      <th>Recall@3</th>\n",
+       "      <th>nDCG@5</th>\n",
+       "      <th>MAP@5</th>\n",
+       "      <th>Recall@5</th>\n",
+       "      <th>epoch</th>\n",
+       "      <th>loss</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.020385</td>\n",
+       "      <td>0.036939</td>\n",
+       "      <td>0.001419</td>\n",
+       "      <td>0.018477</td>\n",
+       "      <td>0.041640</td>\n",
+       "      <td>0.002321</td>\n",
+       "      <td>0</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.059936</td>\n",
+       "      <td>0.091552</td>\n",
+       "      <td>0.004546</td>\n",
+       "      <td>0.068632</td>\n",
+       "      <td>0.110379</td>\n",
+       "      <td>0.009836</td>\n",
+       "      <td>10</td>\n",
+       "      <td>0.471340</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.273629</td>\n",
+       "      <td>0.369123</td>\n",
+       "      <td>0.034714</td>\n",
+       "      <td>0.272501</td>\n",
+       "      <td>0.385817</td>\n",
+       "      <td>0.059150</td>\n",
+       "      <td>20</td>\n",
+       "      <td>0.287908</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
       "text/plain": [
        "     nDCG@3     MAP@3  Recall@3    nDCG@5     MAP@5  Recall@5  epoch      loss\n",
        "0  0.020385  0.036939  0.001419  0.018477  0.041640  0.002321      0       NaN\n",
        "0  0.059936  0.091552  0.004546  0.068632  0.110379  0.009836     10  0.471340\n",
        "0  0.273629  0.369123  0.034714  0.272501  0.385817  0.059150     20  0.287908"
-      ],
-      "text/html": "<div>\n<style scoped>\n    .dataframe tbody tr th:only-of-type {\n        vertical-align: middle;\n    }\n\n    .dataframe tbody tr th {\n        vertical-align: top;\n    }\n\n    .dataframe thead th {\n        text-align: right;\n    }\n</style>\n<table border=\"1\" class=\"dataframe\">\n  <thead>\n    <tr style=\"text-align: right;\">\n      <th></th>\n      <th>nDCG@3</th>\n      <th>MAP@3</th>\n      <th>Recall@3</th>\n      <th>nDCG@5</th>\n      <th>MAP@5</th>\n      <th>Recall@5</th>\n      <th>epoch</th>\n      <th>loss</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <th>0</th>\n      <td>0.020385</td>\n      <td>0.036939</td>\n      <td>0.001419</td>\n      <td>0.018477</td>\n      <td>0.041640</td>\n      <td>0.002321</td>\n      <td>0</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.059936</td>\n      <td>0.091552</td>\n      <td>0.004546</td>\n      <td>0.068632</td>\n      <td>0.110379</td>\n      <td>0.009836</td>\n      <td>10</td>\n      <td>0.471340</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.273629</td>\n      <td>0.369123</td>\n      <td>0.034714</td>\n      <td>0.272501</td>\n      <td>0.385817</td>\n      <td>0.059150</td>\n      <td>20</td>\n      <td>0.287908</td>\n    </tr>\n  </tbody>\n</table>\n</div>"
+      ]
      },
      "metadata": {},
      "execution_count": 8
     }
    ],
-   "source": [
-    "trainer.valid_scores"
-   ]
+   "metadata": {}
   },
   {
+   "cell_type": "markdown",
    "source": [
     "## Global Orthogonal Regularization"
    ],
-   "cell_type": "markdown",
    "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 9,
-   "metadata": {},
-   "outputs": [],
    "source": [
     "lr = 1e-3\n",
     "n_dim = 10\n",
@@ -295,13 +431,17 @@
     "    \"Recall\": evaluators.recall\n",
     "}\n",
     "evaluator = evaluators.UserwiseEvaluator(test_set, score_function_dict, ks=[3,5])\n",
-    "trainer = trainers.CMLTrainer(model, optimizer, criterion, sampler)"
-   ]
+    "trainer = trainers.BaseTrainer(model, optimizer, criterion, sampler)"
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 10,
-   "metadata": {},
+   "source": [
+    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
+   ],
    "outputs": [
     {
      "output_type": "stream",
@@ -333,58 +473,120 @@
      ]
     }
    ],
-   "source": [
-    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 11,
-   "metadata": {},
+   "source": [
+    "trainer.valid_scores"
+   ],
    "outputs": [
     {
      "output_type": "execute_result",
      "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>nDCG@3</th>\n",
+       "      <th>MAP@3</th>\n",
+       "      <th>Recall@3</th>\n",
+       "      <th>nDCG@5</th>\n",
+       "      <th>MAP@5</th>\n",
+       "      <th>Recall@5</th>\n",
+       "      <th>epoch</th>\n",
+       "      <th>loss</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.017032</td>\n",
+       "      <td>0.029692</td>\n",
+       "      <td>0.002178</td>\n",
+       "      <td>0.017122</td>\n",
+       "      <td>0.036462</td>\n",
+       "      <td>0.003362</td>\n",
+       "      <td>0</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.074296</td>\n",
+       "      <td>0.115500</td>\n",
+       "      <td>0.005293</td>\n",
+       "      <td>0.075128</td>\n",
+       "      <td>0.125672</td>\n",
+       "      <td>0.008741</td>\n",
+       "      <td>10</td>\n",
+       "      <td>0.469893</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.276036</td>\n",
+       "      <td>0.380877</td>\n",
+       "      <td>0.035444</td>\n",
+       "      <td>0.276884</td>\n",
+       "      <td>0.395498</td>\n",
+       "      <td>0.060833</td>\n",
+       "      <td>20</td>\n",
+       "      <td>0.281246</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
       "text/plain": [
        "     nDCG@3     MAP@3  Recall@3    nDCG@5     MAP@5  Recall@5  epoch      loss\n",
        "0  0.017032  0.029692  0.002178  0.017122  0.036462  0.003362      0       NaN\n",
        "0  0.074296  0.115500  0.005293  0.075128  0.125672  0.008741     10  0.469893\n",
        "0  0.276036  0.380877  0.035444  0.276884  0.395498  0.060833     20  0.281246"
-      ],
-      "text/html": "<div>\n<style scoped>\n    .dataframe tbody tr th:only-of-type {\n        vertical-align: middle;\n    }\n\n    .dataframe tbody tr th {\n        vertical-align: top;\n    }\n\n    .dataframe thead th {\n        text-align: right;\n    }\n</style>\n<table border=\"1\" class=\"dataframe\">\n  <thead>\n    <tr style=\"text-align: right;\">\n      <th></th>\n      <th>nDCG@3</th>\n      <th>MAP@3</th>\n      <th>Recall@3</th>\n      <th>nDCG@5</th>\n      <th>MAP@5</th>\n      <th>Recall@5</th>\n      <th>epoch</th>\n      <th>loss</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <th>0</th>\n      <td>0.017032</td>\n      <td>0.029692</td>\n      <td>0.002178</td>\n      <td>0.017122</td>\n      <td>0.036462</td>\n      <td>0.003362</td>\n      <td>0</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.074296</td>\n      <td>0.115500</td>\n      <td>0.005293</td>\n      <td>0.075128</td>\n      <td>0.125672</td>\n      <td>0.008741</td>\n      <td>10</td>\n      <td>0.469893</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.276036</td>\n      <td>0.380877</td>\n      <td>0.035444</td>\n      <td>0.276884</td>\n      <td>0.395498</td>\n      <td>0.060833</td>\n      <td>20</td>\n      <td>0.281246</td>\n    </tr>\n  </tbody>\n</table>\n</div>"
+      ]
      },
      "metadata": {},
      "execution_count": 11
     }
    ],
-   "source": [
-    "trainer.valid_scores"
-   ]
+   "metadata": {}
   },
   {
+   "cell_type": "markdown",
    "source": [
     "## Two Stage"
    ],
-   "cell_type": "markdown",
    "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 3,
-   "metadata": {},
-   "outputs": [],
    "source": [
     "item_count = train.groupby(\"item_id\")[\"user_id\"].count()\n",
     "count_index = np.array(item_count.index)\n",
     "neg_weight = np.zeros(n_item)\n",
     "neg_weight[count_index] = item_count ** 0.1"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 4,
-   "metadata": {},
-   "outputs": [],
    "source": [
     "lr = 1e-3\n",
     "n_dim = 10\n",
@@ -405,13 +607,17 @@
     "    \"Recall\": evaluators.recall\n",
     "}\n",
     "evaluator = evaluators.UserwiseEvaluator(test_set, score_function_dict, ks=[3,5])\n",
-    "trainer = trainers.CMLTrainer(model, optimizer, criterion, sampler)"
-   ]
+    "trainer = trainers.BaseTrainer(model, optimizer, criterion, sampler)"
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 5,
-   "metadata": {},
+   "source": [
+    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
+   ],
    "outputs": [
     {
      "output_type": "stream",
@@ -443,46 +649,108 @@
      ]
     }
    ],
-   "source": [
-    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 6,
-   "metadata": {},
+   "source": [
+    "trainer.valid_scores"
+   ],
    "outputs": [
     {
      "output_type": "execute_result",
      "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>nDCG@3</th>\n",
+       "      <th>MAP@3</th>\n",
+       "      <th>Recall@3</th>\n",
+       "      <th>nDCG@5</th>\n",
+       "      <th>MAP@5</th>\n",
+       "      <th>Recall@5</th>\n",
+       "      <th>epoch</th>\n",
+       "      <th>loss</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.012986</td>\n",
+       "      <td>0.022004</td>\n",
+       "      <td>0.001167</td>\n",
+       "      <td>0.013278</td>\n",
+       "      <td>0.028190</td>\n",
+       "      <td>0.002582</td>\n",
+       "      <td>0</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.207166</td>\n",
+       "      <td>0.293655</td>\n",
+       "      <td>0.019897</td>\n",
+       "      <td>0.200064</td>\n",
+       "      <td>0.305768</td>\n",
+       "      <td>0.033076</td>\n",
+       "      <td>10</td>\n",
+       "      <td>1.043901</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.356546</td>\n",
+       "      <td>0.484093</td>\n",
+       "      <td>0.052573</td>\n",
+       "      <td>0.326033</td>\n",
+       "      <td>0.484409</td>\n",
+       "      <td>0.074481</td>\n",
+       "      <td>20</td>\n",
+       "      <td>1.001474</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
       "text/plain": [
        "     nDCG@3     MAP@3  Recall@3    nDCG@5     MAP@5  Recall@5  epoch      loss\n",
        "0  0.012986  0.022004  0.001167  0.013278  0.028190  0.002582      0       NaN\n",
        "0  0.207166  0.293655  0.019897  0.200064  0.305768  0.033076     10  1.043901\n",
        "0  0.356546  0.484093  0.052573  0.326033  0.484409  0.074481     20  1.001474"
-      ],
-      "text/html": "<div>\n<style scoped>\n    .dataframe tbody tr th:only-of-type {\n        vertical-align: middle;\n    }\n\n    .dataframe tbody tr th {\n        vertical-align: top;\n    }\n\n    .dataframe thead th {\n        text-align: right;\n    }\n</style>\n<table border=\"1\" class=\"dataframe\">\n  <thead>\n    <tr style=\"text-align: right;\">\n      <th></th>\n      <th>nDCG@3</th>\n      <th>MAP@3</th>\n      <th>Recall@3</th>\n      <th>nDCG@5</th>\n      <th>MAP@5</th>\n      <th>Recall@5</th>\n      <th>epoch</th>\n      <th>loss</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <th>0</th>\n      <td>0.012986</td>\n      <td>0.022004</td>\n      <td>0.001167</td>\n      <td>0.013278</td>\n      <td>0.028190</td>\n      <td>0.002582</td>\n      <td>0</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.207166</td>\n      <td>0.293655</td>\n      <td>0.019897</td>\n      <td>0.200064</td>\n      <td>0.305768</td>\n      <td>0.033076</td>\n      <td>10</td>\n      <td>1.043901</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.356546</td>\n      <td>0.484093</td>\n      <td>0.052573</td>\n      <td>0.326033</td>\n      <td>0.484409</td>\n      <td>0.074481</td>\n      <td>20</td>\n      <td>1.001474</td>\n    </tr>\n  </tbody>\n</table>\n</div>"
+      ]
      },
      "metadata": {},
      "execution_count": 6
     }
    ],
-   "source": [
-    "trainer.valid_scores"
-   ]
+   "metadata": {}
   },
   {
+   "cell_type": "markdown",
    "source": [
     "## model weighted negative sampler"
    ],
-   "cell_type": "markdown",
    "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 4,
-   "metadata": {},
-   "outputs": [],
    "source": [
     "def svd_init(X, dim):\n",
     "    \"\"\"\n",
@@ -501,13 +769,13 @@
     "    vb = (2 / n_dim) ** 0.5 * V_.sum(axis=0) * s\n",
     "\n",
     "    return U, V, ub, vb"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 5,
-   "metadata": {},
-   "outputs": [],
    "source": [
     "n_dim = 10\n",
     "X = csr_matrix(\n",
@@ -523,13 +791,13 @@
     "    item_bias_init = torch.Tensor(vb)\n",
     ").to(device)\n",
     "neg_weight_model.link_weight = lambda x : 1 - torch.sigmoid(x)"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 6,
-   "metadata": {},
-   "outputs": [],
    "source": [
     "lr = 1e-3\n",
     "model = models.CollaborativeMetricLearning(n_user, n_item, n_dim).to(device)\n",
@@ -547,13 +815,17 @@
     "    \"Recall\": evaluators.recall\n",
     "}\n",
     "evaluator = evaluators.UserwiseEvaluator(test_set, score_function_dict, ks=[3,5])\n",
-    "trainer = trainers.CMLTrainer(model, optimizer, criterion, sampler)"
-   ]
+    "trainer = trainers.BaseTrainer(model, optimizer, criterion, sampler)"
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 7,
-   "metadata": {},
+   "source": [
+    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
+   ],
    "outputs": [
     {
      "output_type": "stream",
@@ -585,45 +857,636 @@
      ]
     }
    ],
-   "source": [
-    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
-   ]
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 8,
-   "metadata": {},
+   "source": [
+    "trainer.valid_scores"
+   ],
    "outputs": [
     {
      "output_type": "execute_result",
      "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>nDCG@3</th>\n",
+       "      <th>MAP@3</th>\n",
+       "      <th>Recall@3</th>\n",
+       "      <th>nDCG@5</th>\n",
+       "      <th>MAP@5</th>\n",
+       "      <th>Recall@5</th>\n",
+       "      <th>epoch</th>\n",
+       "      <th>loss</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.016209</td>\n",
+       "      <td>0.030399</td>\n",
+       "      <td>0.001955</td>\n",
+       "      <td>0.01690</td>\n",
+       "      <td>0.038264</td>\n",
+       "      <td>0.003477</td>\n",
+       "      <td>0</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.051292</td>\n",
+       "      <td>0.078208</td>\n",
+       "      <td>0.004285</td>\n",
+       "      <td>0.05536</td>\n",
+       "      <td>0.094836</td>\n",
+       "      <td>0.007510</td>\n",
+       "      <td>10</td>\n",
+       "      <td>0.572125</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.233268</td>\n",
+       "      <td>0.322552</td>\n",
+       "      <td>0.030232</td>\n",
+       "      <td>0.23401</td>\n",
+       "      <td>0.336276</td>\n",
+       "      <td>0.049536</td>\n",
+       "      <td>20</td>\n",
+       "      <td>0.430135</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
       "text/plain": [
        "     nDCG@3     MAP@3  Recall@3   nDCG@5     MAP@5  Recall@5  epoch      loss\n",
        "0  0.016209  0.030399  0.001955  0.01690  0.038264  0.003477      0       NaN\n",
        "0  0.051292  0.078208  0.004285  0.05536  0.094836  0.007510     10  0.572125\n",
        "0  0.233268  0.322552  0.030232  0.23401  0.336276  0.049536     20  0.430135"
-      ],
-      "text/html": "<div>\n<style scoped>\n    .dataframe tbody tr th:only-of-type {\n        vertical-align: middle;\n    }\n\n    .dataframe tbody tr th {\n        vertical-align: top;\n    }\n\n    .dataframe thead th {\n        text-align: right;\n    }\n</style>\n<table border=\"1\" class=\"dataframe\">\n  <thead>\n    <tr style=\"text-align: right;\">\n      <th></th>\n      <th>nDCG@3</th>\n      <th>MAP@3</th>\n      <th>Recall@3</th>\n      <th>nDCG@5</th>\n      <th>MAP@5</th>\n      <th>Recall@5</th>\n      <th>epoch</th>\n      <th>loss</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <th>0</th>\n      <td>0.016209</td>\n      <td>0.030399</td>\n      <td>0.001955</td>\n      <td>0.01690</td>\n      <td>0.038264</td>\n      <td>0.003477</td>\n      <td>0</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.051292</td>\n      <td>0.078208</td>\n      <td>0.004285</td>\n      <td>0.05536</td>\n      <td>0.094836</td>\n      <td>0.007510</td>\n      <td>10</td>\n      <td>0.572125</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.233268</td>\n      <td>0.322552</td>\n      <td>0.030232</td>\n      <td>0.23401</td>\n      <td>0.336276</td>\n      <td>0.049536</td>\n      <td>20</td>\n      <td>0.430135</td>\n    </tr>\n  </tbody>\n</table>\n</div>"
+      ]
      },
      "metadata": {},
      "execution_count": 8
     }
    ],
+   "metadata": {}
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "source": [],
+   "outputs": [],
+   "metadata": {}
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "source": [],
+   "outputs": [],
+   "metadata": {}
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# Domain Adaptation"
+   ],
+   "metadata": {}
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "source": [
+    "from PyTorchCML import adaptors"
+   ],
+   "outputs": [],
+   "metadata": {}
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "source": [
+    "df_item = pd.read_csv('http://files.grouplens.org/datasets/movielens/ml-100k/u.item' , sep=\"|\", header=None, encoding='latin-1')\n",
+    "item_feature = df_item.iloc[:, -19:]\n",
+    "item_feature_torch = torch.Tensor(item_feature.values)"
+   ],
+   "outputs": [],
+   "metadata": {}
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "source": [
+    "item_feature"
+   ],
+   "outputs": [
+    {
+     "output_type": "execute_result",
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>5</th>\n",
+       "      <th>6</th>\n",
+       "      <th>7</th>\n",
+       "      <th>8</th>\n",
+       "      <th>9</th>\n",
+       "      <th>10</th>\n",
+       "      <th>11</th>\n",
+       "      <th>12</th>\n",
+       "      <th>13</th>\n",
+       "      <th>14</th>\n",
+       "      <th>15</th>\n",
+       "      <th>16</th>\n",
+       "      <th>17</th>\n",
+       "      <th>18</th>\n",
+       "      <th>19</th>\n",
+       "      <th>20</th>\n",
+       "      <th>21</th>\n",
+       "      <th>22</th>\n",
+       "      <th>23</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>...</th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1677</th>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1678</th>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1679</th>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1680</th>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1681</th>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "<p>1682 rows × 19 columns</p>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "      5   6   7   8   9   10  11  12  13  14  15  16  17  18  19  20  21  22  \\\n",
+       "0      0   0   0   1   1   1   0   0   0   0   0   0   0   0   0   0   0   0   \n",
+       "1      0   1   1   0   0   0   0   0   0   0   0   0   0   0   0   0   1   0   \n",
+       "2      0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   1   0   \n",
+       "3      0   1   0   0   0   1   0   0   1   0   0   0   0   0   0   0   0   0   \n",
+       "4      0   0   0   0   0   0   1   0   1   0   0   0   0   0   0   0   1   0   \n",
+       "...   ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..  ..   \n",
+       "1677   0   0   0   0   0   0   0   0   1   0   0   0   0   0   0   0   0   0   \n",
+       "1678   0   0   0   0   0   0   0   0   0   0   0   0   0   0   1   0   1   0   \n",
+       "1679   0   0   0   0   0   0   0   0   1   0   0   0   0   0   1   0   0   0   \n",
+       "1680   0   0   0   0   0   1   0   0   0   0   0   0   0   0   0   0   0   0   \n",
+       "1681   0   0   0   0   0   0   0   0   1   0   0   0   0   0   0   0   0   0   \n",
+       "\n",
+       "      23  \n",
+       "0      0  \n",
+       "1      0  \n",
+       "2      0  \n",
+       "3      0  \n",
+       "4      0  \n",
+       "...   ..  \n",
+       "1677   0  \n",
+       "1678   0  \n",
+       "1679   0  \n",
+       "1680   0  \n",
+       "1681   0  \n",
+       "\n",
+       "[1682 rows x 19 columns]"
+      ]
+     },
+     "metadata": {},
+     "execution_count": 6
+    }
+   ],
+   "metadata": {}
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "source": [
+    "lr = 1e-3\n",
+    "n_dim = 10\n",
+    "adaptor = adaptors.MLPAdaptor(item_feature_torch, n_dim, [20], 1e-4)\n",
+    "model = models.CollaborativeMetricLearning(n_user, n_item, n_dim, item_adaptor=adaptor).to(device)\n",
+    "optimizer = optim.Adam(model.parameters(), lr=lr)\n",
+    "criterion = losses.SumTripletLoss(margin=1).to(device)\n",
+    "sampler = samplers.BaseSampler(train_set, n_user, n_item, device=device, strict_negative=False)\n",
+    "\n",
+    "score_function_dict = {\n",
+    "    \"nDCG\" : evaluators.ndcg,\n",
+    "    \"MAP\" : evaluators.average_precision,\n",
+    "    \"Recall\": evaluators.recall\n",
+    "}\n",
+    "evaluator = evaluators.UserwiseEvaluator(test_set, score_function_dict, ks=[3,5])\n",
+    "trainer = trainers.BaseTrainer(model, optimizer, criterion, sampler)\n"
+   ],
+   "outputs": [],
+   "metadata": {}
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "source": [
+    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
+   ],
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stderr",
+     "text": [
+      "100%|██████████| 943/943 [00:18<00:00, 51.76it/s]\n",
+      "epoch1 avg_loss:1.192: 100%|██████████| 256/256 [00:05<00:00, 50.56it/s]\n",
+      "epoch2 avg_loss:0.992: 100%|██████████| 256/256 [00:05<00:00, 50.57it/s]\n",
+      "epoch3 avg_loss:0.877: 100%|██████████| 256/256 [00:05<00:00, 50.00it/s]\n",
+      "epoch4 avg_loss:0.804: 100%|██████████| 256/256 [00:04<00:00, 51.48it/s]\n",
+      "epoch5 avg_loss:0.750: 100%|██████████| 256/256 [00:05<00:00, 48.38it/s]\n",
+      "epoch6 avg_loss:0.707: 100%|██████████| 256/256 [00:05<00:00, 49.85it/s]\n",
+      "epoch7 avg_loss:0.669: 100%|██████████| 256/256 [00:04<00:00, 52.49it/s]\n",
+      "epoch8 avg_loss:0.633: 100%|██████████| 256/256 [00:05<00:00, 48.15it/s]\n",
+      "epoch9 avg_loss:0.604: 100%|██████████| 256/256 [00:07<00:00, 32.41it/s]\n",
+      "epoch10 avg_loss:0.575: 100%|██████████| 256/256 [00:07<00:00, 32.39it/s]\n",
+      "100%|██████████| 943/943 [00:21<00:00, 44.52it/s]\n",
+      "epoch11 avg_loss:0.552: 100%|██████████| 256/256 [00:05<00:00, 47.47it/s]\n",
+      "epoch12 avg_loss:0.525: 100%|██████████| 256/256 [00:05<00:00, 46.07it/s]\n",
+      "epoch13 avg_loss:0.506: 100%|██████████| 256/256 [00:05<00:00, 45.56it/s]\n",
+      "epoch14 avg_loss:0.482: 100%|██████████| 256/256 [00:05<00:00, 46.49it/s]\n",
+      "epoch15 avg_loss:0.462: 100%|██████████| 256/256 [00:05<00:00, 44.14it/s]\n",
+      "epoch16 avg_loss:0.448: 100%|██████████| 256/256 [00:06<00:00, 37.59it/s]\n",
+      "epoch17 avg_loss:0.433: 100%|██████████| 256/256 [00:07<00:00, 35.14it/s]\n",
+      "epoch18 avg_loss:0.421: 100%|██████████| 256/256 [00:06<00:00, 37.13it/s]\n",
+      "epoch19 avg_loss:0.412: 100%|██████████| 256/256 [00:06<00:00, 39.32it/s]\n",
+      "epoch20 avg_loss:0.401: 100%|██████████| 256/256 [00:06<00:00, 39.88it/s]\n",
+      "100%|██████████| 943/943 [00:21<00:00, 43.02it/s]\n"
+     ]
+    }
+   ],
+   "metadata": {}
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
    "source": [
     "trainer.valid_scores"
-   ]
+   ],
+   "outputs": [
+    {
+     "output_type": "execute_result",
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>nDCG@3</th>\n",
+       "      <th>MAP@3</th>\n",
+       "      <th>Recall@3</th>\n",
+       "      <th>nDCG@5</th>\n",
+       "      <th>MAP@5</th>\n",
+       "      <th>Recall@5</th>\n",
+       "      <th>epoch</th>\n",
+       "      <th>loss</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.010119</td>\n",
+       "      <td>0.017851</td>\n",
+       "      <td>0.001563</td>\n",
+       "      <td>0.009928</td>\n",
+       "      <td>0.021907</td>\n",
+       "      <td>0.002435</td>\n",
+       "      <td>0</td>\n",
+       "      <td>NaN</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.102117</td>\n",
+       "      <td>0.145104</td>\n",
+       "      <td>0.013161</td>\n",
+       "      <td>0.105093</td>\n",
+       "      <td>0.160152</td>\n",
+       "      <td>0.023478</td>\n",
+       "      <td>10</td>\n",
+       "      <td>0.574888</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>0.243347</td>\n",
+       "      <td>0.335101</td>\n",
+       "      <td>0.054578</td>\n",
+       "      <td>0.234080</td>\n",
+       "      <td>0.344372</td>\n",
+       "      <td>0.080134</td>\n",
+       "      <td>20</td>\n",
+       "      <td>0.400818</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "     nDCG@3     MAP@3  Recall@3    nDCG@5     MAP@5  Recall@5  epoch      loss\n",
+       "0  0.010119  0.017851  0.001563  0.009928  0.021907  0.002435      0       NaN\n",
+       "0  0.102117  0.145104  0.013161  0.105093  0.160152  0.023478     10  0.574888\n",
+       "0  0.243347  0.335101  0.054578  0.234080  0.344372  0.080134     20  0.400818"
+      ]
+     },
+     "metadata": {},
+     "execution_count": 8
+    }
+   ],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "source": [],
    "outputs": [],
-   "source": []
+   "metadata": {}
   }
  ],
  "metadata": {
   "kernelspec": {
-   "name": "python386jvsc74a57bd01a6e8c4c71356cfd7f7f45384d81183fdca12e98ad893ee020bd76249bbd6be9",
+   "name": "python3",
    "display_name": "Python 3.8.6 64-bit ('pytorchcml-MJCCLiEQ-py3.8': poetry)"
   },
   "language_info": {
@@ -637,6 +1500,9 @@
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
    "version": "3.8.6"
+  },
+  "interpreter": {
+   "hash": "1a6e8c4c71356cfd7f7f45384d81183fdca12e98ad893ee020bd76249bbd6be9"
   }
  },
  "nbformat": 4,
diff --git a/examples/notebooks/movielens_mf.ipynb b/examples/notebooks/movielens_mf.ipynb
index f979b01..ac3a4a8 100644
--- a/examples/notebooks/movielens_mf.ipynb
+++ b/examples/notebooks/movielens_mf.ipynb
@@ -6,7 +6,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# !pip install PytorchCML"
+    "# !pip install PyTorchCML"
    ]
   },
   {
@@ -16,11 +16,11 @@
    "outputs": [],
    "source": [
     "import sys\n",
-    "sys.path.append(\"../../src/\")\n",
+    "sys.path.append(\"../../\")\n",
     "\n",
     "from itertools import product\n",
     "\n",
-    "from PytorchCML import losses, models, samplers, evaluators, trainers\n",
+    "from PyTorchCML import losses, models, samplers, evaluators, trainers\n",
     "import torch\n",
     "from torch import nn, optim\n",
     "import numpy as np\n",
@@ -61,19 +61,24 @@
    "metadata": {},
    "outputs": [],
    "source": [
+    "# download movielens dataset\n",
     "movielens = pd.read_csv(\n",
     "  'http://files.grouplens.org/datasets/movielens/ml-100k/u.data', \n",
-    "  sep='\\t', header=None, index_col=None,\n",
+    "    sep='\\t', header=None, index_col=None,\n",
+    "    names = [\"user_id\", \"item_id\", \"rating\", \"timestamp\"]\n",
     ")\n",
-    "movielens.columns = [\"user_id\", \"item_id\", \"rating\", \"timestamp\"]\n",
+    "# Set user/item id and number of users/items.\n",
     "movielens.user_id -= 1\n",
     "movielens.item_id -= 1\n",
-    "movielens.rating = (movielens.rating >= 4).astype(int)\n",
     "n_user = movielens.user_id.nunique()\n",
     "n_item = movielens.item_id.nunique()\n",
     "\n",
-    "train, test = train_test_split(movielens.copy())\n",
+    "# make implicit feedback\n",
+    "movielens.rating = (movielens.rating >= 4).astype(int)\n",
+    "\n",
     "\n",
+    "# train test split\n",
+    "train, test = train_test_split(movielens)\n",
     "\n",
     "# all user item pairs\n",
     "df_all = pd.DataFrame(\n",
@@ -155,7 +160,7 @@
     "    \"Recall\": evaluators.recall\n",
     "}\n",
     "evaluator = evaluators.UserwiseEvaluator(torch.LongTensor(test_set).to(device), score_function_dict, ks=[3])\n",
-    "trainer = trainers.MFTrainer(model, optimizer, criterion, sampler)\n"
+    "trainer = trainers.BaseTrainer(model, optimizer, criterion, sampler)\n"
    ]
   },
   {
@@ -167,25 +172,25 @@
      "output_type": "stream",
      "name": "stderr",
      "text": [
-      "100%|██████████| 943/943 [00:15<00:00, 59.02it/s]\n",
-      "epoch1 avg_loss:0.483: 100%|██████████| 50/50 [00:01<00:00, 29.97it/s]\n",
-      "epoch2 avg_loss:0.432: 100%|██████████| 50/50 [00:01<00:00, 28.96it/s]\n",
-      "epoch3 avg_loss:0.397: 100%|██████████| 50/50 [00:01<00:00, 27.35it/s]\n",
-      "epoch4 avg_loss:0.374: 100%|██████████| 50/50 [00:01<00:00, 29.84it/s]\n",
-      "epoch5 avg_loss:0.356: 100%|██████████| 50/50 [00:01<00:00, 27.78it/s]\n",
-      "100%|██████████| 943/943 [00:16<00:00, 56.00it/s]\n",
-      "epoch6 avg_loss:0.342: 100%|██████████| 50/50 [00:01<00:00, 30.99it/s]\n",
-      "epoch7 avg_loss:0.333: 100%|██████████| 50/50 [00:01<00:00, 25.26it/s]\n",
-      "epoch8 avg_loss:0.323: 100%|██████████| 50/50 [00:02<00:00, 22.10it/s]\n",
-      "epoch9 avg_loss:0.316: 100%|██████████| 50/50 [00:04<00:00, 10.34it/s]\n",
-      "epoch10 avg_loss:0.309: 100%|██████████| 50/50 [00:02<00:00, 17.51it/s]\n",
-      "100%|██████████| 943/943 [00:18<00:00, 50.22it/s]\n",
-      "epoch11 avg_loss:0.305: 100%|██████████| 50/50 [00:01<00:00, 26.74it/s]\n",
-      "epoch12 avg_loss:0.300: 100%|██████████| 50/50 [00:02<00:00, 23.22it/s]\n",
-      "epoch13 avg_loss:0.296: 100%|██████████| 50/50 [00:02<00:00, 23.00it/s]\n",
-      "epoch14 avg_loss:0.293: 100%|██████████| 50/50 [00:02<00:00, 19.72it/s]\n",
-      "epoch15 avg_loss:0.290: 100%|██████████| 50/50 [00:02<00:00, 22.30it/s]\n",
-      "100%|██████████| 943/943 [00:20<00:00, 46.42it/s]\n"
+      "100%|██████████| 943/943 [00:20<00:00, 46.16it/s]\n",
+      "epoch1 avg_loss:451.035: 100%|██████████| 50/50 [00:03<00:00, 14.38it/s]\n",
+      "epoch2 avg_loss:351.008: 100%|██████████| 50/50 [00:04<00:00, 12.41it/s]\n",
+      "epoch3 avg_loss:277.473: 100%|██████████| 50/50 [00:02<00:00, 18.52it/s]\n",
+      "epoch4 avg_loss:223.029: 100%|██████████| 50/50 [00:03<00:00, 15.06it/s]\n",
+      "epoch5 avg_loss:180.195: 100%|██████████| 50/50 [00:03<00:00, 12.74it/s]\n",
+      "100%|██████████| 943/943 [00:28<00:00, 33.23it/s]\n",
+      "epoch6 avg_loss:147.858: 100%|██████████| 50/50 [00:05<00:00,  9.89it/s]\n",
+      "epoch7 avg_loss:124.041: 100%|██████████| 50/50 [00:04<00:00, 12.10it/s]\n",
+      "epoch8 avg_loss:105.032: 100%|██████████| 50/50 [00:04<00:00, 11.43it/s]\n",
+      "epoch9 avg_loss:90.234: 100%|██████████| 50/50 [00:04<00:00, 12.21it/s]\n",
+      "epoch10 avg_loss:77.848: 100%|██████████| 50/50 [00:05<00:00,  8.84it/s]\n",
+      "100%|██████████| 943/943 [00:27<00:00, 34.08it/s]\n",
+      "epoch11 avg_loss:67.927: 100%|██████████| 50/50 [00:05<00:00,  9.05it/s]\n",
+      "epoch12 avg_loss:60.175: 100%|██████████| 50/50 [00:05<00:00,  8.78it/s]\n",
+      "epoch13 avg_loss:52.815: 100%|██████████| 50/50 [00:07<00:00,  6.81it/s]\n",
+      "epoch14 avg_loss:47.541: 100%|██████████| 50/50 [00:04<00:00, 10.87it/s]\n",
+      "epoch15 avg_loss:42.113: 100%|██████████| 50/50 [00:05<00:00,  9.65it/s]\n",
+      "100%|██████████| 943/943 [00:24<00:00, 38.80it/s]\n"
      ]
     }
    ],
@@ -202,13 +207,13 @@
      "output_type": "execute_result",
      "data": {
       "text/plain": [
-       "     nDCG@3     MAP@3  Recall@3  epoch      loss\n",
-       "0  0.404477  0.545157  0.111933      0       NaN\n",
-       "0  0.408631  0.549399  0.115896      5  0.356066\n",
-       "0  0.399501  0.540827  0.115340     10  0.308907\n",
-       "0  0.395533  0.534553  0.116172     15  0.289745"
+       "     nDCG@3     MAP@3  Recall@3  epoch        loss\n",
+       "0  0.395128  0.529074  0.109667      0         NaN\n",
+       "0  0.385569  0.518293  0.106805      5  180.194864\n",
+       "0  0.370207  0.504065  0.102159     10   77.848219\n",
+       "0  0.356891  0.487186  0.097600     15   42.113500"
       ],
-      "text/html": "<div>\n<style scoped>\n    .dataframe tbody tr th:only-of-type {\n        vertical-align: middle;\n    }\n\n    .dataframe tbody tr th {\n        vertical-align: top;\n    }\n\n    .dataframe thead th {\n        text-align: right;\n    }\n</style>\n<table border=\"1\" class=\"dataframe\">\n  <thead>\n    <tr style=\"text-align: right;\">\n      <th></th>\n      <th>nDCG@3</th>\n      <th>MAP@3</th>\n      <th>Recall@3</th>\n      <th>epoch</th>\n      <th>loss</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <th>0</th>\n      <td>0.404477</td>\n      <td>0.545157</td>\n      <td>0.111933</td>\n      <td>0</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.408631</td>\n      <td>0.549399</td>\n      <td>0.115896</td>\n      <td>5</td>\n      <td>0.356066</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.399501</td>\n      <td>0.540827</td>\n      <td>0.115340</td>\n      <td>10</td>\n      <td>0.308907</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.395533</td>\n      <td>0.534553</td>\n      <td>0.116172</td>\n      <td>15</td>\n      <td>0.289745</td>\n    </tr>\n  </tbody>\n</table>\n</div>"
+      "text/html": "<div>\n<style scoped>\n    .dataframe tbody tr th:only-of-type {\n        vertical-align: middle;\n    }\n\n    .dataframe tbody tr th {\n        vertical-align: top;\n    }\n\n    .dataframe thead th {\n        text-align: right;\n    }\n</style>\n<table border=\"1\" class=\"dataframe\">\n  <thead>\n    <tr style=\"text-align: right;\">\n      <th></th>\n      <th>nDCG@3</th>\n      <th>MAP@3</th>\n      <th>Recall@3</th>\n      <th>epoch</th>\n      <th>loss</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <th>0</th>\n      <td>0.395128</td>\n      <td>0.529074</td>\n      <td>0.109667</td>\n      <td>0</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.385569</td>\n      <td>0.518293</td>\n      <td>0.106805</td>\n      <td>5</td>\n      <td>180.194864</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.370207</td>\n      <td>0.504065</td>\n      <td>0.102159</td>\n      <td>10</td>\n      <td>77.848219</td>\n    </tr>\n    <tr>\n      <th>0</th>\n      <td>0.356891</td>\n      <td>0.487186</td>\n      <td>0.097600</td>\n      <td>15</td>\n      <td>42.113500</td>\n    </tr>\n  </tbody>\n</table>\n</div>"
      },
      "metadata": {},
      "execution_count": 7
@@ -266,7 +271,7 @@
     "    \"Recall\": evaluators.recall\n",
     "}\n",
     "evaluator = evaluators.UserwiseEvaluator(torch.LongTensor(test_set).to(device), score_function_dict, ks=[3])\n",
-    "trainer = trainers.MFTrainer(\n",
+    "trainer = trainers.BaseTrainer(\n",
     "    model, optimizer, criterion, sampler, \n",
     "    column_names={\"user_id\":0, \"item_id\":1, \"pscore\":2}\n",
     ")\n"
@@ -342,7 +347,7 @@
  ],
  "metadata": {
   "kernelspec": {
-   "name": "python386jvsc74a57bd01a6e8c4c71356cfd7f7f45384d81183fdca12e98ad893ee020bd76249bbd6be9",
+   "name": "python3",
    "display_name": "Python 3.8.6 64-bit ('pytorchcml-MJCCLiEQ-py3.8': poetry)"
   },
   "language_info": {
@@ -356,6 +361,9 @@
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
    "version": "3.8.6"
+  },
+  "interpreter": {
+   "hash": "1a6e8c4c71356cfd7f7f45384d81183fdca12e98ad893ee020bd76249bbd6be9"
   }
  },
  "nbformat": 4,
diff --git a/images/diagram.png b/images/diagram.png
index 2c6b202..be11db0 100644
Binary files a/images/diagram.png and b/images/diagram.png differ
diff --git a/images/icon.png b/images/icon.png
new file mode 100644
index 0000000..4c7abe0
Binary files /dev/null and b/images/icon.png differ
diff --git a/poetry.lock b/poetry.lock
index d7f8014..2b1df6e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,19 +1,25 @@
 [[package]]
-name = "appdirs"
-version = "1.4.4"
-description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+name = "appnope"
+version = "0.1.2"
+description = "Disable App Nap on macOS >= 10.9"
 category = "dev"
 optional = false
 python-versions = "*"
 
 [[package]]
-name = "appnope"
-version = "0.1.2"
-description = "Disable App Nap on macOS >= 10.9"
+name = "argcomplete"
+version = "1.12.3"
+description = "Bash tab completion for argparse"
 category = "dev"
 optional = false
 python-versions = "*"
 
+[package.dependencies]
+importlib-metadata = {version = ">=0.23,<5", markers = "python_version == \"3.7\""}
+
+[package.extras]
+test = ["coverage", "flake8", "pexpect", "wheel"]
+
 [[package]]
 name = "autopep8"
 version = "1.5.7"
@@ -36,25 +42,26 @@ python-versions = "*"
 
 [[package]]
 name = "black"
-version = "21.7b0"
+version = "21.8b0"
 description = "The uncompromising code formatter."
 category = "dev"
 optional = false
 python-versions = ">=3.6.2"
 
 [package.dependencies]
-appdirs = "*"
 click = ">=7.1.2"
 mypy-extensions = ">=0.4.3"
-pathspec = ">=0.8.1,<1"
+pathspec = ">=0.9.0,<1"
+platformdirs = ">=2"
 regex = ">=2020.1.8"
 tomli = ">=0.2.6,<2.0.0"
 typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""}
-typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
+typing-extensions = ">=3.10.0.0"
 
 [package.extras]
 colorama = ["colorama (>=0.4.3)"]
 d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
 python2 = ["typed-ast (>=1.4.2)"]
 uvloop = ["uvloop (>=0.15.2)"]
 
@@ -89,9 +96,17 @@ category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
+[[package]]
+name = "debugpy"
+version = "1.4.3"
+description = "An implementation of the Debug Adapter Protocol for Python"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
+
 [[package]]
 name = "decorator"
-version = "5.0.9"
+version = "5.1.0"
 description = "Decorators for Humans"
 category = "dev"
 optional = false
@@ -121,7 +136,7 @@ pyflakes = ">=2.3.0,<2.4.0"
 
 [[package]]
 name = "importlib-metadata"
-version = "4.6.4"
+version = "4.8.1"
 description = "Read metadata from Python packages"
 category = "dev"
 optional = false
@@ -138,25 +153,30 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
 
 [[package]]
 name = "ipykernel"
-version = "5.5.5"
+version = "6.4.1"
 description = "IPython Kernel for Jupyter"
 category = "dev"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.7"
 
 [package.dependencies]
 appnope = {version = "*", markers = "platform_system == \"Darwin\""}
-ipython = ">=5.0.0"
-jupyter-client = "*"
-tornado = ">=4.2"
-traitlets = ">=4.1.0"
+argcomplete = {version = ">=1.12.3", markers = "python_version < \"3.8.0\""}
+debugpy = ">=1.0.0,<2.0"
+importlib-metadata = {version = "<5", markers = "python_version < \"3.8.0\""}
+ipython = ">=7.23.1,<8.0"
+ipython-genutils = "*"
+jupyter-client = "<8.0"
+matplotlib-inline = ">=0.1.0,<0.2.0"
+tornado = ">=4.2,<7.0"
+traitlets = ">=4.1.0,<6.0"
 
 [package.extras]
-test = ["pytest (!=5.3.4)", "pytest-cov", "flaky", "nose", "jedi (<=0.17.2)"]
+test = ["pytest (!=5.3.4)", "pytest-cov", "flaky", "nose", "ipyparallel"]
 
 [[package]]
 name = "ipython"
-version = "7.26.0"
+version = "7.27.0"
 description = "IPython: Productive Interactive Computing"
 category = "dev"
 optional = false
@@ -233,7 +253,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "jupyter-client"
-version = "7.0.1"
+version = "7.0.2"
 description = "Jupyter protocol implementation and client libraries"
 category = "dev"
 optional = false
@@ -266,7 +286,7 @@ traitlets = "*"
 
 [[package]]
 name = "matplotlib-inline"
-version = "0.1.2"
+version = "0.1.3"
 description = "Inline Matplotlib backend for Jupyter"
 category = "dev"
 optional = false
@@ -378,6 +398,18 @@ category = "dev"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "platformdirs"
+version = "2.3.0"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
+test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
+
 [[package]]
 name = "prompt-toolkit"
 version = "3.0.20"
@@ -478,7 +510,7 @@ py = {version = "*", markers = "implementation_name == \"pypy\""}
 
 [[package]]
 name = "regex"
-version = "2021.8.21"
+version = "2021.8.28"
 description = "Alternative regular expression module, to replace re."
 category = "dev"
 optional = false
@@ -572,15 +604,12 @@ telegram = ["requests"]
 
 [[package]]
 name = "traitlets"
-version = "5.0.5"
+version = "5.1.0"
 description = "Traitlets Python configuration system"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 
-[package.dependencies]
-ipython-genutils = "*"
-
 [package.extras]
 test = ["pytest"]
 
@@ -594,7 +623,7 @@ python-versions = "*"
 
 [[package]]
 name = "typing-extensions"
-version = "3.10.0.0"
+version = "3.10.0.2"
 description = "Backported and Experimental Type Hints for Python 3.5+"
 category = "main"
 optional = false
@@ -623,17 +652,17 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
 [metadata]
 lock-version = "1.1"
 python-versions = ">=3.7.10,<3.10"
-content-hash = "b8aed3388851c1a14f7c55bf57174d78b83b0dbf37077439ed2ab073d216359c"
+content-hash = "49d2a93c09435e77c67f81a11855a443d90513a50fd54508bf8113403faa5205"
 
 [metadata.files]
-appdirs = [
-    {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
-    {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
-]
 appnope = [
     {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"},
     {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
 ]
+argcomplete = [
+    {file = "argcomplete-1.12.3-py2.py3-none-any.whl", hash = "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81"},
+    {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"},
+]
 autopep8 = [
     {file = "autopep8-1.5.7-py2.py3-none-any.whl", hash = "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"},
     {file = "autopep8-1.5.7.tar.gz", hash = "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0"},
@@ -643,8 +672,8 @@ backcall = [
     {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
 ]
 black = [
-    {file = "black-21.7b0-py3-none-any.whl", hash = "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116"},
-    {file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"},
+    {file = "black-21.8b0-py3-none-any.whl", hash = "sha256:2a0f9a8c2b2a60dbcf1ccb058842fb22bdbbcb2f32c6cc02d9578f90b92ce8b7"},
+    {file = "black-21.8b0.tar.gz", hash = "sha256:570608d28aa3af1792b98c4a337dbac6367877b47b12b88ab42095cfc1a627c2"},
 ]
 cffi = [
     {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"},
@@ -701,9 +730,29 @@ colorama = [
     {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
     {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
 ]
+debugpy = [
+    {file = "debugpy-1.4.3-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:88b17d7c2130968f75bdc706a33f46a8a6bb90f09512ea3bd984659d446ee4f4"},
+    {file = "debugpy-1.4.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ded60b402f83df46dee3f25ae5851809937176afdafd3fdbaab60b633b77cad"},
+    {file = "debugpy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:c0fd1a66e104752f86ca2faa6a0194dae61442a768f85369fc3d11bacff8120f"},
+    {file = "debugpy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f907941ad7a460646773eb3baae4c88836e9256b390dfbfae8d92a3d3b849a7d"},
+    {file = "debugpy-1.4.3-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:135a77ac1a8f6ea49a69928f088967d36842bc492d89b45941c6b19222cffa42"},
+    {file = "debugpy-1.4.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f3dcc294f3b4d79fdd7ffe1350d5d1e3cc29acaec67dd1c43143a43305bbbc91"},
+    {file = "debugpy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:c3d7db37b7eb234e49f50ba22b3b1637e8daadd68985d9cd35a6152aa10faa75"},
+    {file = "debugpy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:dbda8f877c3dec1559c01c63a1de63969e51a4907dc308f4824238bb776026fe"},
+    {file = "debugpy-1.4.3-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:7c15014290150b76f0311debf7fbba2e934680572ea60750b0f048143e873b3e"},
+    {file = "debugpy-1.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8d488356cc66172f1ea29635fd148ad131f13fad0e368ae03cc5c0a402372756"},
+    {file = "debugpy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:7e7210a3721fc54b52d8dc2f325e7c937ffcbba02b808e2e3215dcbf0c0b8349"},
+    {file = "debugpy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:3e4de96c70f3398abd1777f048b47564d98a40df1f72d33b47ef5b9478e07206"},
+    {file = "debugpy-1.4.3-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:2019ffcd08d7e643c644cd64bee0fd53c730cb8f15ff37e6a320b5afd3785bfa"},
+    {file = "debugpy-1.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:847926f78c1e33f7318a743837adb6a9b360a825b558fd21f9240ba518fe1bb1"},
+    {file = "debugpy-1.4.3-cp39-cp39-win32.whl", hash = "sha256:c9665e58b80d839ae1b0815341c63d00cae557c018f198c0b6b7bc5de9eca144"},
+    {file = "debugpy-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:ab3f33499c597a2ce454b81088e7f9d56127686e003c4f7a1c97ad4b38a55404"},
+    {file = "debugpy-1.4.3-py2.py3-none-any.whl", hash = "sha256:0c523fcbb6fb395403ee8508853767b74949335d5cdacc9f83d350670c2c0db2"},
+    {file = "debugpy-1.4.3.zip", hash = "sha256:4d53fe5aecf03ba466aa7fa7474c2b2fe28b2a6c0d36688d1e29382bfe88dd5f"},
+]
 decorator = [
-    {file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"},
-    {file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"},
+    {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
+    {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
 ]
 entrypoints = [
     {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"},
@@ -714,16 +763,16 @@ flake8 = [
     {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
 ]
 importlib-metadata = [
-    {file = "importlib_metadata-4.6.4-py3-none-any.whl", hash = "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5"},
-    {file = "importlib_metadata-4.6.4.tar.gz", hash = "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f"},
+    {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
+    {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
 ]
 ipykernel = [
-    {file = "ipykernel-5.5.5-py3-none-any.whl", hash = "sha256:29eee66548ee7c2edb7941de60c0ccf0a7a8dd957341db0a49c5e8e6a0fcb712"},
-    {file = "ipykernel-5.5.5.tar.gz", hash = "sha256:e976751336b51082a89fc2099fb7f96ef20f535837c398df6eab1283c2070884"},
+    {file = "ipykernel-6.4.1-py3-none-any.whl", hash = "sha256:a3f6c2dda2ecf63b37446808a70ed825fea04790779ca524889c596deae0def8"},
+    {file = "ipykernel-6.4.1.tar.gz", hash = "sha256:df3355e5eec23126bc89767a676c5f0abfc7f4c3497d118c592b83b316e8c0cd"},
 ]
 ipython = [
-    {file = "ipython-7.26.0-py3-none-any.whl", hash = "sha256:892743b65c21ed72b806a3a602cca408520b3200b89d1924f4b3d2cdb3692362"},
-    {file = "ipython-7.26.0.tar.gz", hash = "sha256:0cff04bb042800129348701f7bd68a430a844e8fb193979c08f6c99f28bb735e"},
+    {file = "ipython-7.27.0-py3-none-any.whl", hash = "sha256:75b5e060a3417cf64f138e0bb78e58512742c57dc29db5a5058a2b1f0c10df02"},
+    {file = "ipython-7.27.0.tar.gz", hash = "sha256:58b55ebfdfa260dad10d509702dc2857cb25ad82609506b070cf2d7b7df5af13"},
 ]
 ipython-genutils = [
     {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"},
@@ -742,16 +791,16 @@ joblib = [
     {file = "joblib-1.0.1.tar.gz", hash = "sha256:9c17567692206d2f3fb9ecf5e991084254fe631665c450b443761c4186a613f7"},
 ]
 jupyter-client = [
-    {file = "jupyter_client-7.0.1-py3-none-any.whl", hash = "sha256:07b9566979546004c089afe7c9bf9e96224ec5f8421fe0ae460759fa593c6b1d"},
-    {file = "jupyter_client-7.0.1.tar.gz", hash = "sha256:48822a93d9d75daa5fde235c35cf7a92fc979384735962501d4eb60b197fb43a"},
+    {file = "jupyter_client-7.0.2-py3-none-any.whl", hash = "sha256:37a30c13d3655b819add61c830594090af7fca40cd2d74f41cad9e2e12118501"},
+    {file = "jupyter_client-7.0.2.tar.gz", hash = "sha256:0c6cabd07e003a2e9692394bf1ae794188ad17d2e250ed747232d7a473aa772c"},
 ]
 jupyter-core = [
     {file = "jupyter_core-4.7.1-py3-none-any.whl", hash = "sha256:8c6c0cac5c1b563622ad49321d5ec47017bd18b94facb381c6973a0486395f8e"},
     {file = "jupyter_core-4.7.1.tar.gz", hash = "sha256:79025cb3225efcd36847d0840f3fc672c0abd7afd0de83ba8a1d3837619122b4"},
 ]
 matplotlib-inline = [
-    {file = "matplotlib-inline-0.1.2.tar.gz", hash = "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"},
-    {file = "matplotlib_inline-0.1.2-py3-none-any.whl", hash = "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811"},
+    {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"},
+    {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"},
 ]
 mccabe = [
     {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
@@ -858,6 +907,10 @@ pickleshare = [
     {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
     {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
 ]
+platformdirs = [
+    {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"},
+    {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"},
+]
 prompt-toolkit = [
     {file = "prompt_toolkit-3.0.20-py3-none-any.whl", hash = "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c"},
     {file = "prompt_toolkit-3.0.20.tar.gz", hash = "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"},
@@ -946,47 +999,47 @@ pyzmq = [
     {file = "pyzmq-22.2.1.tar.gz", hash = "sha256:6d18c76676771fd891ca8e0e68da0bbfb88e30129835c0ade748016adb3b6242"},
 ]
 regex = [
-    {file = "regex-2021.8.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b0c211c55d4aac4309c3209833c803fada3fc21cdf7b74abedda42a0c9dc3ce"},
-    {file = "regex-2021.8.21-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d5209c3ba25864b1a57461526ebde31483db295fc6195fdfc4f8355e10f7376"},
-    {file = "regex-2021.8.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c835c30f3af5c63a80917b72115e1defb83de99c73bc727bddd979a3b449e183"},
-    {file = "regex-2021.8.21-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:615fb5a524cffc91ab4490b69e10ae76c1ccbfa3383ea2fad72e54a85c7d47dd"},
-    {file = "regex-2021.8.21-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9966337353e436e6ba652814b0a957a517feb492a98b8f9d3b6ba76d22301dcc"},
-    {file = "regex-2021.8.21-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a49f85f0a099a5755d0a2cc6fc337e3cb945ad6390ec892332c691ab0a045882"},
-    {file = "regex-2021.8.21-cp310-cp310-win32.whl", hash = "sha256:f93a9d8804f4cec9da6c26c8cfae2c777028b4fdd9f49de0302e26e00bb86504"},
-    {file = "regex-2021.8.21-cp310-cp310-win_amd64.whl", hash = "sha256:a795829dc522227265d72b25d6ee6f6d41eb2105c15912c230097c8f5bfdbcdc"},
-    {file = "regex-2021.8.21-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bca14dfcfd9aae06d7d8d7e105539bd77d39d06caaae57a1ce945670bae744e0"},
-    {file = "regex-2021.8.21-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41acdd6d64cd56f857e271009966c2ffcbd07ec9149ca91f71088574eaa4278a"},
-    {file = "regex-2021.8.21-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96f0c79a70642dfdf7e6a018ebcbea7ea5205e27d8e019cad442d2acfc9af267"},
-    {file = "regex-2021.8.21-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:45f97ade892ace20252e5ccecdd7515c7df5feeb42c3d2a8b8c55920c3551c30"},
-    {file = "regex-2021.8.21-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f9974826aeeda32a76648fc677e3125ade379869a84aa964b683984a2dea9f1"},
-    {file = "regex-2021.8.21-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea9753d64cba6f226947c318a923dadaf1e21cd8db02f71652405263daa1f033"},
-    {file = "regex-2021.8.21-cp36-cp36m-win32.whl", hash = "sha256:ef9326c64349e2d718373415814e754183057ebc092261387a2c2f732d9172b2"},
-    {file = "regex-2021.8.21-cp36-cp36m-win_amd64.whl", hash = "sha256:6dbd51c3db300ce9d3171f4106da18fe49e7045232630fe3d4c6e37cb2b39ab9"},
-    {file = "regex-2021.8.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a89ca4105f8099de349d139d1090bad387fe2b208b717b288699ca26f179acbe"},
-    {file = "regex-2021.8.21-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6c2b1d78ceceb6741d703508cd0e9197b34f6bf6864dab30f940f8886e04ade"},
-    {file = "regex-2021.8.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a34ba9e39f8269fd66ab4f7a802794ffea6d6ac500568ec05b327a862c21ce23"},
-    {file = "regex-2021.8.21-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ecb6e7c45f9cd199c10ec35262b53b2247fb9a408803ed00ee5bb2b54aa626f5"},
-    {file = "regex-2021.8.21-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:330836ad89ff0be756b58758878409f591d4737b6a8cef26a162e2a4961c3321"},
-    {file = "regex-2021.8.21-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:71a904da8c9c02aee581f4452a5a988c3003207cb8033db426f29e5b2c0b7aea"},
-    {file = "regex-2021.8.21-cp37-cp37m-win32.whl", hash = "sha256:b511c6009d50d5c0dd0bab85ed25bc8ad6b6f5611de3a63a59786207e82824bb"},
-    {file = "regex-2021.8.21-cp37-cp37m-win_amd64.whl", hash = "sha256:93f9f720081d97acee38a411e861d4ce84cbc8ea5319bc1f8e38c972c47af49f"},
-    {file = "regex-2021.8.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a195e26df1fbb40ebee75865f9b64ba692a5824ecb91c078cc665b01f7a9a36"},
-    {file = "regex-2021.8.21-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06ba444bbf7ede3890a912bd4904bb65bf0da8f0d8808b90545481362c978642"},
-    {file = "regex-2021.8.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b8d551f1bd60b3e1c59ff55b9e8d74607a5308f66e2916948cafd13480b44a3"},
-    {file = "regex-2021.8.21-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ebbceefbffae118ab954d3cd6bf718f5790db66152f95202ebc231d58ad4e2c2"},
-    {file = "regex-2021.8.21-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccd721f1d4fc42b541b633d6e339018a08dd0290dc67269df79552843a06ca92"},
-    {file = "regex-2021.8.21-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ae87ab669431f611c56e581679db33b9a467f87d7bf197ac384e71e4956b4456"},
-    {file = "regex-2021.8.21-cp38-cp38-win32.whl", hash = "sha256:38600fd58c2996829480de7d034fb2d3a0307110e44dae80b6b4f9b3d2eea529"},
-    {file = "regex-2021.8.21-cp38-cp38-win_amd64.whl", hash = "sha256:61e734c2bcb3742c3f454dfa930ea60ea08f56fd1a0eb52d8cb189a2f6be9586"},
-    {file = "regex-2021.8.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b091dcfee169ad8de21b61eb2c3a75f9f0f859f851f64fdaf9320759a3244239"},
-    {file = "regex-2021.8.21-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:640ccca4d0a6fcc6590f005ecd7b16c3d8f5d52174e4854f96b16f34c39d6cb7"},
-    {file = "regex-2021.8.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac95101736239260189f426b1e361dc1b704513963357dc474beb0f39f5b7759"},
-    {file = "regex-2021.8.21-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b79dc2b2e313565416c1e62807c7c25c67a6ff0a0f8d83a318df464555b65948"},
-    {file = "regex-2021.8.21-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b623fc429a38a881ab2d9a56ef30e8ea20c72a891c193f5ebbddc016e083ee"},
-    {file = "regex-2021.8.21-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8021dee64899f993f4b5cca323aae65aabc01a546ed44356a0965e29d7893c94"},
-    {file = "regex-2021.8.21-cp39-cp39-win32.whl", hash = "sha256:d6ec4ae13760ceda023b2e5ef1f9bc0b21e4b0830458db143794a117fdbdc044"},
-    {file = "regex-2021.8.21-cp39-cp39-win_amd64.whl", hash = "sha256:03840a07a402576b8e3a6261f17eb88abd653ad4e18ec46ef10c9a63f8c99ebd"},
-    {file = "regex-2021.8.21.tar.gz", hash = "sha256:faf08b0341828f6a29b8f7dd94d5cf8cc7c39bfc3e67b78514c54b494b66915a"},
+    {file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"},
+    {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"},
+    {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"},
+    {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"},
+    {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"},
+    {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"},
+    {file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"},
+    {file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"},
+    {file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"},
+    {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"},
+    {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"},
+    {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"},
+    {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"},
+    {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"},
+    {file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"},
+    {file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"},
+    {file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"},
+    {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"},
+    {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"},
+    {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"},
+    {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"},
+    {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"},
+    {file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"},
+    {file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"},
+    {file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"},
+    {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"},
+    {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"},
+    {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"},
+    {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"},
+    {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"},
+    {file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"},
+    {file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"},
+    {file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"},
+    {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"},
+    {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"},
+    {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"},
+    {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"},
+    {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"},
+    {file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"},
+    {file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"},
+    {file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"},
 ]
 scikit-learn = [
     {file = "scikit-learn-0.22.2.post1.tar.gz", hash = "sha256:57538d138ba54407d21e27c306735cbd42a6aae0df6a5a30c7a6edde46b0017d"},
@@ -1112,8 +1165,8 @@ tqdm = [
     {file = "tqdm-4.62.2.tar.gz", hash = "sha256:a4d6d112e507ef98513ac119ead1159d286deab17dffedd96921412c2d236ff5"},
 ]
 traitlets = [
-    {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"},
-    {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"},
+    {file = "traitlets-5.1.0-py3-none-any.whl", hash = "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4"},
+    {file = "traitlets-5.1.0.tar.gz", hash = "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d"},
 ]
 typed-ast = [
     {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
@@ -1148,9 +1201,9 @@ typed-ast = [
     {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
 ]
 typing-extensions = [
-    {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
-    {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
-    {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
+    {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
+    {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
+    {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
 ]
 wcwidth = [
     {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
diff --git a/pyproject.toml b/pyproject.toml
index 6780766..0a3c9a3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,7 +30,7 @@ autopep8 = "^1.5.6"
 black = "^21.4b2"
 isort = "^5.8.0"
 mypy = "^0.812"
-ipykernel = "^5.5.3"
+ipykernel = "^6.0.0"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]


Test patch:
diff --git a/tests/adaptors/__init__.py b/tests/adaptors/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/adaptors/test_MLPAdaptor.py b/tests/adaptors/test_MLPAdaptor.py
new file mode 100644
index 0000000..296a40e
--- /dev/null
+++ b/tests/adaptors/test_MLPAdaptor.py
@@ -0,0 +1,41 @@
+import unittest
+
+import torch
+from torch import nn
+
+from PyTorchCML.adaptors import MLPAdaptor
+
+
+class TestMLPAdaptor(unittest.TestCase):
+    """Test MLPAdaptor"""
+
+    def test_forward(self):
+        user_features = torch.ones(3, 4) / 10
+        item_features = torch.ones(5, 6) / 10
+
+        user_adaptor = MLPAdaptor(user_features, n_hidden=[10, 10], n_dim=4, weight=1)
+        item_adaptor = MLPAdaptor(item_features, n_hidden=[10, 10], n_dim=4, weight=1)
+
+        for param in user_adaptor.parameters():
+            nn.init.constant_(param, 0.1)
+
+        for param in item_adaptor.parameters():
+            nn.init.constant_(param, 0.1)
+
+        users = torch.LongTensor([[0], [1]])
+        pos_items = torch.LongTensor([[2], [3]])
+        neg_items = torch.LongTensor([[0, 1, 3, 4], [0, 2, 3, 4]])
+
+        embeddings_dict = {
+            "user_embedding": torch.ones(2, 1, 4) / 10,
+            "pos_item_embedding": torch.ones(2, 1, 4) / 10,
+            "neg_item_embedding": torch.ones(2, 4, 4) * 2 / 10,
+        }
+
+        user_loss = user_adaptor(users, embeddings_dict["user_embedding"])
+        pos_item_loss = item_adaptor(pos_items, embeddings_dict["pos_item_embedding"])
+        neg_item_loss = item_adaptor(neg_items, embeddings_dict["neg_item_embedding"])
+
+        self.assertAlmostEqual(user_loss.item(), 0.96, places=5)
+        self.assertAlmostEqual(pos_item_loss.item(), 1.04, places=5)
+        self.assertAlmostEqual(neg_item_loss.item(), 2.56, places=5)
diff --git a/tests/losses/test_LogitPairwiseLoss.py b/tests/losses/test_LogitPairwiseLoss.py
index 8341777..dda56b8 100644
--- a/tests/losses/test_LogitPairwiseLoss.py
+++ b/tests/losses/test_LogitPairwiseLoss.py
@@ -29,8 +29,8 @@ def test_forward(self):
         avg_loss = 5.5
         """
         embeddings_dict = {
-            "user_embedding": torch.ones(3, 5),
-            "pos_item_embedding": torch.ones(3, 5) * 2,
+            "user_embedding": torch.ones(3, 1, 5),
+            "pos_item_embedding": torch.ones(3, 1, 5) * 2,
             "neg_item_embedding": torch.ones(3, 2, 5),
             "user_bias": torch.zeros(3, 1),
             "pos_item_bias": torch.zeros(3, 1) * 2,
diff --git a/tests/losses/test_MSEPairwiseLoss.py b/tests/losses/test_MSEPairwiseLoss.py
index 5c7b8a4..8ae3c16 100644
--- a/tests/losses/test_MSEPairwiseLoss.py
+++ b/tests/losses/test_MSEPairwiseLoss.py
@@ -30,8 +30,8 @@ def test_forward(self):
         avg_loss = 26 / 3
         """
         embeddings_dict = {
-            "user_embedding": torch.zeros(3, 5),
-            "pos_item_embedding": torch.zeros(3, 5) * 2,
+            "user_embedding": torch.zeros(3, 1, 5),
+            "pos_item_embedding": torch.zeros(3, 1, 5) * 2,
             "neg_item_embedding": -torch.zeros(3, 2, 5),
             "user_bias": torch.zeros(3, 1),
             "pos_item_bias": torch.zeros(3, 1) * 2,
diff --git a/tests/losses/test_RelevancePairwiseLoss.py b/tests/losses/test_RelevancePairwiseLoss.py
index 5b65064..fa87033 100644
--- a/tests/losses/test_RelevancePairwiseLoss.py
+++ b/tests/losses/test_RelevancePairwiseLoss.py
@@ -30,8 +30,8 @@ def test_forward(self):
         avg_loss = 26 / 3
         """
         embeddings_dict = {
-            "user_embedding": torch.zeros(3, 5),
-            "pos_item_embedding": torch.zeros(3, 5) * 2,
+            "user_embedding": torch.zeros(3, 1, 5),
+            "pos_item_embedding": torch.zeros(3, 1, 5) * 2,
             "neg_item_embedding": -torch.zeros(3, 2, 5),
             "user_bias": torch.zeros(3, 1),
             "pos_item_bias": torch.zeros(3, 1) * 2,
diff --git a/tests/samplers/test_TwoStageSampler.py b/tests/samplers/test_TwoStageSampler.py
index d92b9d8..b108f06 100644
--- a/tests/samplers/test_TwoStageSampler.py
+++ b/tests/samplers/test_TwoStageSampler.py
@@ -41,7 +41,7 @@ def test_get_pos_batch(self):
         self.assertGreaterEqual(5, item_id_max)
 
         # pairwise weighted sampler
-        pos_weight_pair = np.array([1, 1, 1, 1, 1, 100])
+        pos_weight_pair = np.array([1, 1, 1, 1, 1, 1000])
         sampler = TwoStageSampler(
             train_set,
             n_user=3,
@@ -57,7 +57,7 @@ def test_get_pos_batch(self):
         self.assertGreaterEqual(cnt_heavy, cnt_lignt)
 
         # item wise weighted sampler
-        pos_weight_item = np.array([1, 1, 1, 1, 100])
+        pos_weight_item = np.array([1, 1, 1, 1, 1000])
         sampler = TwoStageSampler(
             train_set,
             n_user=3,
diff --git a/tests/trainers/test_BaseTrainer.py b/tests/trainers/test_BaseTrainer.py
new file mode 100644
index 0000000..8ccb436
--- /dev/null
+++ b/tests/trainers/test_BaseTrainer.py
@@ -0,0 +1,93 @@
+import unittest
+import torch
+from torch import optim
+
+from PyTorchCML import evaluators, losses, models, samplers, trainers
+
+
+class TestBaseTrainer(unittest.TestCase):
+    def test_fit_cml(self):
+        train_set = torch.LongTensor([[0, 0], [0, 2], [1, 1], [1, 3], [2, 3], [2, 4]])
+        test_set = torch.LongTensor(
+            [
+                [0, 1, 1],
+                [0, 3, 1],
+                [0, 4, 0],
+                [1, 0, 0],
+                [1, 2, 1],
+                [1, 4, 1],
+                [2, 0, 1],
+                [2, 1, 0],
+                [2, 2, 1],
+            ]
+        )
+
+        lr = 1e-3
+        n_user, n_item, n_dim = 3, 5, 10
+        model = models.CollaborativeMetricLearning(n_user, n_item, n_dim)
+        optimizer = optim.Adam(model.parameters(), lr=lr)
+        criterion = losses.SumTripletLoss(margin=1)
+        sampler = samplers.BaseSampler(train_set, n_user, n_item, n_neg_samples=2)
+
+        score_function_dict = {
+            "nDCG": evaluators.ndcg,
+            "MAP": evaluators.average_precision,
+            "Recall": evaluators.recall,
+        }
+
+        evaluator = evaluators.UserwiseEvaluator(
+            test_set, score_function_dict, ks=[2, 3]
+        )
+
+        trainer = trainers.BaseTrainer(model, optimizer, criterion, sampler)
+
+        trainer.fit(n_batch=3, n_epoch=3, valid_evaluator=evaluator, valid_per_epoch=1)
+
+        df_eval = trainer.valid_scores
+
+        self.assertEqual(df_eval.shape, (4, 8))
+
+    def test_fit_mf(self):
+        train_set = torch.LongTensor([[0, 0], [0, 2], [1, 1], [1, 3], [2, 3], [2, 4]])
+        test_set = torch.LongTensor(
+            [
+                [0, 1, 1],
+                [0, 3, 1],
+                [0, 4, 0],
+                [1, 0, 0],
+                [1, 2, 1],
+                [1, 4, 1],
+                [2, 0, 1],
+                [2, 1, 0],
+                [2, 2, 1],
+            ]
+        )
+
+        lr = 1e-3
+        n_user, n_item, n_dim = 3, 5, 10
+        model = models.LogitMatrixFactorization(n_user, n_item, n_dim)
+        optimizer = optim.Adam(model.parameters(), lr=lr)
+        criterion = losses.LogitPairwiseLoss()
+        sampler = samplers.BaseSampler(train_set, n_user, n_item, n_neg_samples=2)
+
+        score_function_dict = {
+            "nDCG": evaluators.ndcg,
+            "MAP": evaluators.average_precision,
+            "Recall": evaluators.recall,
+        }
+
+        evaluator = evaluators.UserwiseEvaluator(
+            test_set, score_function_dict, ks=[2, 3]
+        )
+
+        trainer = trainers.BaseTrainer(model, optimizer, criterion, sampler)
+
+        trainer.fit(n_batch=3, n_epoch=3, valid_evaluator=evaluator, valid_per_epoch=1)
+
+        df_eval = trainer.valid_scores
+
+        self.assertEqual(df_eval.shape, (4, 8))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/trainers/test_CMLTrainer.py b/tests/trainers/test_CMLTrainer.py
deleted file mode 100644
index 60ffe65..0000000
--- a/tests/trainers/test_CMLTrainer.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import unittest
-import torch
-from torch import optim
-
-from PyTorchCML import evaluators, losses, models, samplers, trainers
-
-
-class TestCMLTrainer(unittest.TestCase):
-    def test_fit(self):
-        train_set = torch.LongTensor([[0, 0], [0, 2], [1, 1], [1, 3], [2, 3], [2, 4]])
-        test_set = torch.LongTensor(
-            [
-                [0, 1, 1],
-                [0, 3, 1],
-                [0, 4, 0],
-                [1, 0, 0],
-                [1, 2, 1],
-                [1, 4, 1],
-                [2, 0, 1],
-                [2, 1, 0],
-                [2, 2, 1],
-            ]
-        )
-
-        lr = 1e-3
-        n_user, n_item, n_dim = 3, 5, 10
-        model = models.CollaborativeMetricLearning(n_user, n_item, n_dim)
-        optimizer = optim.Adam(model.parameters(), lr=lr)
-        criterion = losses.SumTripletLoss(margin=1)
-        sampler = samplers.BaseSampler(train_set, n_user, n_item, n_neg_samples=2)
-
-        score_function_dict = {
-            "nDCG": evaluators.ndcg,
-            "MAP": evaluators.average_precision,
-            "Recall": evaluators.recall,
-        }
-
-        evaluator = evaluators.UserwiseEvaluator(
-            test_set, score_function_dict, ks=[2, 3]
-        )
-
-        trainer = trainers.CMLTrainer(model, optimizer, criterion, sampler)
-
-        trainer.fit(n_batch=3, n_epoch=3, valid_evaluator=evaluator, valid_per_epoch=1)
-
-        df_eval = trainer.valid_scores
-
-        self.assertEqual(df_eval.shape, (4, 8))
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/tests/trainers/test_MFTrainer.py b/tests/trainers/test_MFTrainer.py
deleted file mode 100644
index f1ca199..0000000
--- a/tests/trainers/test_MFTrainer.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import unittest
-import torch
-from torch import optim
-
-from PyTorchCML import evaluators, losses, models, samplers, trainers
-
-
-class TestMFTrainer(unittest.TestCase):
-    def test_fit(self):
-        train_set = torch.LongTensor([[0, 0], [0, 2], [1, 1], [1, 3], [2, 3], [2, 4]])
-        test_set = torch.LongTensor(
-            [
-                [0, 1, 1],
-                [0, 3, 1],
-                [0, 4, 0],
-                [1, 0, 0],
-                [1, 2, 1],
-                [1, 4, 1],
-                [2, 0, 1],
-                [2, 1, 0],
-                [2, 2, 1],
-            ]
-        )
-
-        lr = 1e-3
-        n_user, n_item, n_dim = 3, 5, 10
-        model = models.LogitMatrixFactorization(n_user, n_item, n_dim)
-        optimizer = optim.Adam(model.parameters(), lr=lr)
-        criterion = losses.LogitPairwiseLoss()
-        sampler = samplers.BaseSampler(train_set, n_user, n_item, n_neg_samples=2)
-
-        score_function_dict = {
-            "nDCG": evaluators.ndcg,
-            "MAP": evaluators.average_precision,
-            "Recall": evaluators.recall,
-        }
-
-        evaluator = evaluators.UserwiseEvaluator(
-            test_set, score_function_dict, ks=[2, 3]
-        )
-
-        trainer = trainers.MFTrainer(model, optimizer, criterion, sampler)
-
-        trainer.fit(n_batch=3, n_epoch=3, valid_evaluator=evaluator, valid_per_epoch=1)
-
-        df_eval = trainer.valid_scores
-
-        self.assertEqual(df_eval.shape, (4, 8))
-
-
-if __name__ == "__main__":
-    unittest.main()