Commit 3c1c3eb3 authored by nd-02110114's avatar nd-02110114
Browse files

overhaul one hot featurizer

parent a4c7d1a0
Loading
Loading
Loading
Loading
+0 −158
Original line number Diff line number Diff line
import numpy as np
from deepchem.feat.base_classes import MolecularFeaturizer

zinc_charset = [
    ' ', '#', ')', '(', '+', '-', '/', '1', '3', '2', '5', '4', '7', '6', '8',
    '=', '@', 'C', 'B', 'F', 'I', 'H', 'O', 'N', 'S', '[', ']', '\\', 'c', 'l',
    'o', 'n', 'p', 's', 'r'
]


class OneHotFeaturizer(MolecularFeaturizer):
  """Encodes a molecule as a one-hot array.

  This featurizer takes a molecule and encodes its Smiles string as a one-hot
  array.

  Note
  ----
  This class requires RDKit to be installed. Note that this featurizer is not
  Thread Safe in initialization of charset
  """

  def __init__(self, charset=None, padlength=120):
    """Initialize featurizer.

    Parameters
    ----------
    charset: list of str, optional (default None)
      A list of strings, where each string is length 1.
    padlength: int, optional (default 120)
      length to pad the smile strings to.
    """
    try:
      from rdkit import Chem
    except ModuleNotFoundError:
      raise ValueError("This class requires RDKit to be installed.")
    self.charset = charset
    self.pad_length = padlength

  def _featurize(self, mol):
    """Compute one-hot featurization of this molecule.

    Parameters
    ----------
    mol : RDKit Mol
        Molecule.

    Returns
    -------
    rval: np.ndarray
      Vector of RDKit descriptors for `mol`
    """
    from rdkit import Chem
    smiles = Chem.MolToSmiles(mol)
    if self.charset is None:
      self.charset = self._create_charset(smiles)
    return np.array([self.one_hot_encoded(smile) for smile in smiles])

  def one_hot_array(self, i):
    """Create a one hot array with bit i set to 1

    Parameters
    ----------
    i: int
      bit to set to 1

    Returns
    -------
    obj:`list` of obj:`int`
      length len(self.charset)
    """
    return [int(x) for x in [ix == i for ix in range(len(self.charset))]]

  def one_hot_index(self, c):
    """Compute one-hot index of charater.

    Parameters
    ----------
    c: char
      character whose index we want

    Returns
    -------
    index of c in self.charset
    """
    return self.charset.index(c)

  def pad_smile(self, smile):
    """Pad a smile string to `self.pad_length`

    Parameters
    ----------
    smile: str
      The smiles string to be padded.

    Returns
    -------
    str
      smile string space padded to self.pad_length
    """

    return smile.ljust(self.pad_length)

  def one_hot_encoded(self, smile):
    """One Hot Encode an entire SMILE string
    
    Parameters
    ----------
    smile: str
      smile string to encode

    Returns
    -------
    np.array of one hot encoded arrays for each character in smile
    """
    return np.array([
        self.one_hot_array(self.one_hot_index(x)) for x in self.pad_smile(smile)
    ])

  def untransform(self, z):
    """Convert from one hot representation back to SMILE

    Parameters
    ----------
    z: obj:`list`
      list of one hot encoded features

    Returns
    -------
    Smile Strings picking MAX for each one hot encoded array
    """
    z1 = []
    for i in range(len(z)):
      s = ""
      for j in range(len(z[i])):
        oh = np.argmax(z[i][j])
        s += self.charset[oh]
      z1.append([s.strip()])
    return z1

  def _create_charset(self, smiles):
    """Create the charset from smiles

    Parameters
    ----------
    smiles: obj:`list` of obj:`str`
      list of smile strings

    Returns
    -------
    obj:`list` of obj:`str`
      List of length one strings that are characters in smiles.  No duplicates
    """
    s = set()
    for smile in smiles:
      for c in smile:
        s.add(c)
    return [' '] + sorted(list(s))
+1 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ from deepchem.feat.molecule_featurizers import CoulombMatrix
from deepchem.feat.molecule_featurizers import CoulombMatrixEig
from deepchem.feat.molecule_featurizers import MordredDescriptors
from deepchem.feat.molecule_featurizers import Mol2VecFingerprint
from deepchem.feat.molecule_featurizers import OneHotFeaturizer
from deepchem.feat.molecule_featurizers import RawFeaturizer
from deepchem.feat.molecule_featurizers import RDKitDescriptors
from deepchem.feat.molecule_featurizers import SmilesToImage
+1 −0
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ from deepchem.feat.molecule_featurizers.coulomb_matrices import CoulombMatrix
from deepchem.feat.molecule_featurizers.coulomb_matrices import CoulombMatrixEig
from deepchem.feat.molecule_featurizers.mordred_descriptors import MordredDescriptors
from deepchem.feat.molecule_featurizers.mol2vec_fingerprint import Mol2VecFingerprint
from deepchem.feat.molecule_featurizers.one_hot_featurizer import OneHotFeaturizer
from deepchem.feat.molecule_featurizers.raw_featurizer import RawFeaturizer
from deepchem.feat.molecule_featurizers.rdkit_descriptors import RDKitDescriptors
from deepchem.feat.molecule_featurizers.smiles_to_image import SmilesToImage
+113 −0
Original line number Diff line number Diff line
import logging
from typing import List

import numpy as np

from deepchem.utils.typing import RDKitMol
from deepchem.utils.molecule_feature_utils import one_hot_encode
from deepchem.feat.base_classes import MolecularFeaturizer

logger = logging.getLogger(__name__)

ZINC_CHARSET = [
    '#', ')', '(', '+', '-', '/', '1', '3', '2', '5', '4', '7', '6', '8', '=',
    '@', 'C', 'B', 'F', 'I', 'H', 'O', 'N', 'S', '[', ']', '\\', 'c', 'l', 'o',
    'n', 'p', 's', 'r'
]


class OneHotFeaturizer(MolecularFeaturizer):
  """Encodes SMILES as a one-hot array.

  This featurizer encodes its SMILES string as a one-hot array.

  Notes
  ----
  This class requires RDKit to be installed.
  """

  def __init__(self, charset: List[str] = ZINC_CHARSET, max_length: int = 100):
    """Initialize featurizer.

    Parameters
    ----------
    charset: List[str], optional (default ZINC_CHARSET)
      A list of strings, where each string is length 1 and unique.
    max_length: int, optional (default 100)
      length to pad the SMILES strings to.
    """
    if len(charset) != len(set(charset)):
      raise ValueError("All values in charset must be unique.")
    self.charset = charset
    self.max_length = max_length

  def _featurize(self, mol: RDKitMol) -> np.ndarray:
    """Compute one-hot featurization of this molecule.

    Parameters
    ----------
    mol: rdkit.Chem.rdchem.Mol
      RDKit Mol object

    Returns
    -------
    np.ndarray
      An one hot vector encoded from SMILES.
      The shape is `(max_length, len(charset) + 1)`.
      The index of unknown character is `len(charset)`.
    """
    try:
      from rdkit import Chem
    except ModuleNotFoundError:
      raise ValueError("This class requires RDKit to be installed.")

    smiles = Chem.MolToSmiles(mol)
    # validation
    if len(smiles) > self.max_length:
      logger.info(
          "The length of {} is longer than `max_length`. So we return an empty array."
      )
      return np.array([])

    smiles = self.pad_smile(smiles)
    return np.array([
        one_hot_encode(val, self.charset, include_unknown_set=True)
        for val in smiles
    ])

  def pad_smile(self, smiles: str) -> str:
    """Pad SMILES string to `self.pad_length`

    Parameters
    ----------
    smiles: str
      The smiles string to be padded.

    Returns
    -------
    str
      SMILES string space padded to self.pad_length
    """
    return smiles.ljust(self.max_length)

  def untransform(self, one_hot_vectors: np.ndarray) -> str:
    """Convert from one hot representation back to SMILES

    Parameters
    ----------
    one_hot_vectors: np.ndarray
      An array of one hot encoded features.

    Returns
    -------
    str
      SMILES string for an one hot encoded array.
    """
    smiles = ""
    for one_hot in one_hot_vectors:
      try:
        idx = np.argmax(one_hot)
        smiles += self.charset[idx]
      except IndexError:
        smiles += ""
    return smiles
+64 −0
Original line number Diff line number Diff line
import unittest

import numpy as np

from deepchem.feat import OneHotFeaturizer
from deepchem.feat.molecule_featurizers.one_hot_featurizer import ZINC_CHARSET


class TestOneHotFeaturizert(unittest.TestCase):
  """
  Test OneHotFeaturizer.
  """

  def test_onehot_featurizer(self):
    """
    Test simple one hot encoding.
    """
    from rdkit import Chem
    length = len(ZINC_CHARSET) + 1
    smiles = 'CC(=O)Oc1ccccc1C(=O)O'
    mol = Chem.MolFromSmiles(smiles)
    featurizer = OneHotFeaturizer()
    feature = featurizer([mol])
    assert feature.shape == (1, 100, length)

    # untranform
    undo_smiles = featurizer.untransform(feature[0])
    assert smiles == undo_smiles

  def test_onehot_featurizer_with_max_length(self):
    """
    Test one hot encoding with max_length.
    """
    from rdkit import Chem
    length = len(ZINC_CHARSET) + 1
    smiles = 'CC(=O)Oc1ccccc1C(=O)O'
    mol = Chem.MolFromSmiles(smiles)
    featurizer = OneHotFeaturizer(max_length=120)
    feature = featurizer([mol])
    assert feature.shape == (1, 120, length)

    # untranform
    undo_smiles = featurizer.untransform(feature[0])
    assert smiles == undo_smiles

  def test_correct_transformation(self):
    """
    Test correct one hot encoding.
    """
    from rdkit import Chem
    charset = ['C', 'N', '=', ')', '(', 'O']
    smiles = 'CN=C=O'
    mol = Chem.MolFromSmiles(smiles)
    featurizer = OneHotFeaturizer(charset=charset, max_length=100)
    feature = featurizer([mol])
    assert np.allclose(feature[0][0], np.array([1, 0, 0, 0, 0, 0, 0]))
    assert np.allclose(feature[0][1], np.array([0, 1, 0, 0, 0, 0, 0]))
    assert np.allclose(feature[0][2], np.array([0, 0, 1, 0, 0, 0, 0]))
    assert np.allclose(feature[0][3], np.array([1, 0, 0, 0, 0, 0, 0]))
    assert np.allclose(feature[0][4], np.array([0, 0, 1, 0, 0, 0, 0]))
    assert np.allclose(feature[0][5], np.array([0, 0, 0, 0, 0, 1, 0]))
    # untranform
    undo_smiles = featurizer.untransform(feature[0])
    assert smiles == undo_smiles