Repository: hand10ryo/PyTorchCML (language: python)

Issue:
Implementation of domain adaptation
Corrects embedding with reference to user and item features. 
See 3.3 of the paper, "Collaborative Metric Learning".


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
index 764cb47..5dd1d6b 100644
--- a/PyTorchCML/losses/BaseLoss.py
+++ b/PyTorchCML/losses/BaseLoss.py
@@ -11,6 +11,13 @@ def __init__(self, regularizers: list = []):
 
     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:
@@ -34,18 +41,18 @@ def forward(
 
          ---   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,
-        #}
+        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)
-        reg = self.regularize(embeddings_dict)
-        return loss + reg
+
+        return loss
         """
 
         raise NotImplementedError
diff --git a/PyTorchCML/losses/LogitPairwiseLoss.py b/PyTorchCML/losses/LogitPairwiseLoss.py
index 94f8762..29f66c4 100644
--- a/PyTorchCML/losses/LogitPairwiseLoss.py
+++ b/PyTorchCML/losses/LogitPairwiseLoss.py
@@ -11,10 +11,10 @@ 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.
@@ -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 5853b0f..177b143 100644
--- a/PyTorchCML/losses/MSEPairwiseLoss.py
+++ b/PyTorchCML/losses/MSEPairwiseLoss.py
@@ -6,10 +6,10 @@
 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.
@@ -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 4bc3423..1f7ec7d 100644
--- a/PyTorchCML/losses/MinTripletLoss.py
+++ b/PyTorchCML/losses/MinTripletLoss.py
@@ -11,10 +11,10 @@ def __init__(self, margin: float = 1, regularizers: list = []):
         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.
@@ -41,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 a85877c..0e398ac 100644
--- a/PyTorchCML/losses/RelevancePairwiseLoss.py
+++ b/PyTorchCML/losses/RelevancePairwiseLoss.py
@@ -28,10 +28,10 @@ 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.
@@ -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 6a626d2..88af0ef 100644
--- a/PyTorchCML/losses/SumTripletLoss.py
+++ b/PyTorchCML/losses/SumTripletLoss.py
@@ -12,10 +12,10 @@ def __init__(self, margin: float = 1, regularizers: list = []):
         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
@@ -39,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/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 366f36a..924393d 100644
--- a/PyTorchCML/trainers/BaseTrainer.py
+++ b/PyTorchCML/trainers/BaseTrainer.py
@@ -61,7 +61,7 @@ def fit(
         for ep in range(n_epoch):
             accum_loss = 0
 
-            # start mini-batch
+            # start epoch
             with tqdm(range(n_batch), total=n_batch) as pbar:
                 for b in pbar:
                     # batch sampling
@@ -84,6 +84,20 @@ def fit(
 
                     # 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
diff --git a/README.md b/README.md
index 0300472..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.
 
@@ -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.
 
@@ -223,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 975a94a..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)
@@ -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)
 
 最も単純化した実装は以下の通りです。
 
@@ -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_-" />の抽出
 
 デフォルトでは両者のサンプリングを一様ランダムに行います。
 
@@ -224,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 e96b9bf..0760bbb 100644
--- a/examples/notebooks/movielens_cml.ipynb
+++ b/examples/notebooks/movielens_cml.ipynb
@@ -3,17 +3,15 @@
   {
    "cell_type": "code",
    "execution_count": 1,
-   "metadata": {},
-   "outputs": [],
    "source": [
     "# !pip install PyTorchCML"
-   ]
+   ],
+   "outputs": [],
+   "metadata": {}
   },
   {
    "cell_type": "code",
    "execution_count": 1,
-   "metadata": {},
-   "outputs": [],
    "source": [
     "import sys\n",
     "sys.path.append(\"../../\")\n",
@@ -28,13 +26,13 @@
     "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",
@@ -86,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",
@@ -115,12 +113,16 @@
     "}\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": 4,
-   "metadata": {},
+   "source": [
+    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
+   ],
    "outputs": [
     {
      "output_type": "stream",
@@ -152,46 +154,108 @@
      ]
     }
    ],
-   "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.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"
-      ],
-      "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>"
+      ]
      },
      "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",
@@ -207,14 +271,16 @@
     "}\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,
-   "metadata": {
-    "tags": []
-   },
+   "source": [
+    "trainer.fit(n_batch=256, n_epoch=20, valid_evaluator = evaluator, valid_per_epoch=10)"
+   ],
    "outputs": [
     {
      "output_type": "stream",
@@ -246,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",
@@ -302,12 +432,16 @@
     "}\n",
     "evaluator = evaluators.UserwiseEvaluator(test_set, score_function_dict, ks=[3,5])\n",
     "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",
@@ -339,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",
@@ -412,12 +608,16 @@
     "}\n",
     "evaluator = evaluators.UserwiseEvaluator(test_set, score_function_dict, ks=[3,5])\n",
     "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",
@@ -449,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",
@@ -507,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",
@@ -529,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",
@@ -554,12 +816,16 @@
     "}\n",
     "evaluator = evaluators.UserwiseEvaluator(test_set, score_function_dict, ks=[3,5])\n",
     "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",
@@ -591,40 +857,631 @@
      ]
     }
    ],
-   "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": {
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)