DeepChemで欠損値を含む列を除去するTransformerを書いてみる

#はじめに

前回[mordredをDeepChemのFeaturizerとして使う](https://qiita.com/kimisyo/items/31fbf1d8fb8fe58d90cf) の記事でmordredで計算エラーが発生した場合0に置き換えていた。しかし前回もお伝えしたように通常はnanを含む記述子そのものを取り除くか、nanをそのまま欠損値を処理できるアルゴリズムに投入することが一般的に行われる。今回は、DeepChemのTransformerを継承し、欠損値を除去するTransformerを作成し、このTransformerによりnp.nanが含まれる記述子を取り除いて学習できるようにしたのでここにメモっておく。

#環境
- python 3.6
- deepchem  2.2.1.dev54
- rdkit 2019.03.3.0 
- morded 1.1.2 


# 前回のソースの修正
まずは、前回のソースの修正として、MordredDescriptor.pyをエラーが発生した場合に0ではなくnumpyのnanを格納するよう修正する。

```MordredDescriptor.py
from deepchem.feat import Featurizer
from mordred import Calculator, descriptors
import numpy as np

class MordredDescriptors(Featurizer):
    """
    Mordred descriptors.
    """
    name = 'descriptors'

    def __init__(self, ignore_3D=True):

        self.mordred_calculator = Calculator(descriptors, ignore_3D=ignore_3D)

        # 記述子名-計算関数の対応を保持
        self.descs = {}
        self.counter = 0

        for i, desc in enumerate(self.mordred_calculator.descriptors):
          self.descs[desc.__str__()] = desc

    def _featurize(self, mol):
      ret = []
      for desc in self.descs:
        try:
          ret.append(self.descs[desc](mol))
        except Exception as e:
          ret.append(np.nan)

      self.counter = self.counter + 1
      print(self.counter)

      return ret

```

# Transformerの作成

準備は整ったので、欠損値を含む列を除去するTransformerを書いてみよう。
DeepChemのClippingTransformerや、NomalizationTransformerを参考にし、以下の通り作成した。
まず、コンストラクタでdatasetを受け取り、記述子データである"X"を取り出し、np.nanを含まない列かどうかのBoolean配列を取得し、private変数として保持する。
そして、tansoform_arrayメソッドで、そのBoolean配列の中でTrueをもつ列のみを記述子データを返却するようにする。

```MissingValueElminatorTransformer.py

import numpy as np
from deepchem.trans.transformers import Transformer

#----------------------------------------------------
# #参考文献
# -[numpy配列からnanの列を除去する参考URL](https://note.nkmk.me/python-numpy-nan-remove/)
# -[Source code for deepchem.trans.transformers](https://deepchem.io/docs/_modules/deepchem/trans/transformers.html)
#----------------------------------------------------

class MissingValueElminateTransformer(Transformer):

    def __init__(self,
                 transform_X=False,
                 transform_y=False,
                 transform_w=False,
                 dataset=None,
                 transform_gradients=False):

        """Initialize normalization transformation."""

        if transform_X:
            X = dataset.X
            self.X_support = ~np.isnan(X).any(axis=0)

        super().__init__(
        transform_X=transform_X,
        transform_y=transform_y,
        transform_w=transform_w,
        dataset=dataset)


    def transform(self, dataset, parallel=False):
        return super().transform(dataset, parallel=parallel)


    def transform_array(self, X, y, w):

        X = X[:, self.X_support]

        return (X, y, w)


    def untransform(self, z):
        raise NotImplementedError(
            "Cannot untransform datasets with MissingValueElminateTransformer.")

```

#使い方

```MissingValueElminateTransformer(transform_X=True, dataset=train_dataset)```のような感じで初期化してあげれれば、その後の使い方は他のTransformerと全く同様だ。例によってmetris以下は適当に書いているので各自修正してほしい。

```python
import rdkit.Chem
from rdkit import Chem

import deepchem as dc
from deepchem.models.tensorgraph.models.graph_models import GraphConvModel
from deepchem.feat import RDKitDescriptors, ConvMolFeaturizer, CoulombMatrix, RdkitGridFeaturizer

from MordredDescriptors import MordredDescriptors
from MissingValueElminateTransformer import MissingValueElminateTransformer

# mordredのfeaturizerを定義
featurizer = MordredDescriptors()

# 学習データの読み込み
loader = dc.data.CSVLoader(tasks=["LogS"],
                               id_field="CompoundID",
                               smiles_field="smiles",
                               featurizer=featurizer)

dataset = loader.featurize("../ESOL/data/delaney-processed.csv")
splitter = dc.splits.RandomSplitter(dataset)

train_dataset, valid_dataset, test_dataset = splitter.train_valid_test_split(dataset, frac_train=0.8,
                                                                               frac_valid=0.1, frac_test=0.1)

# 欠損値除去するTrnasformerを追加
transformers = [
        MissingValueElminateTransformer(transform_X=True, dataset=train_dataset),
        dc.trans.NormalizationTransformer(transform_y=True, dataset=train_dataset)
    ]

for transformer in transformers:
    train_dataset = transformer.transform(train_dataset)
    valid_dataset = transformer.transform(valid_dataset)
    test_dataset = transformer.transform(test_dataset)

metric = dc.metrics.Metric(dc.metrics.r2_score)
optimizer = dc.hyper.HyperparamOpt(get_builder(), verbose=True)
params_dict = {"xxx:yyy"}
best_model, best_model_hyperparams, all_model_results = optimizer.hyperparam_search(params_dict, train_dataset, valid_dataset, transformers, metric=metric)
```

#おわりに
DeepChemでここまで既存記述子の使用にこだわる必要があるのか？という気もするが、既存手法とGCN(Graph Convolution Network)をフェアに比較しようとした場合、DeepChemの上で既存手法を使えるにするのがよいと判断する。(と前向きに自分を奮い立たせてみる）

# 参考文献
- [numpy配列からnanの列を除去する参考URL](https://note.nkmk.me/python-numpy-nan-remove/)
- [Source code for deepchem.trans.transformers](https://deepchem.io/docs/_modules/deepchem/trans/transformers.html)