どこから見てもメンダコ

軟体動物門頭足綱八腕類メンダコ科

kerasで化合物SeqToSeq Autoencoder

化合物VAEを作ろうとしたのですが、VAEは思ったよりややこしかったのでとりあえず化合物AEをしました。

はじめに

化合物構造(smiles)を入力し、次元圧縮した後に化合物構造(smiles)を復元するautoencoderモデルを作成します。 VAEではないので化合物生成には使えませんが、エンコードされた層(図中のcompressed representation)が分子フィンガープリントとして使えます。
f:id:horomary:20181230023912p:plain ※図は Building Autoencoders in Kerasより改変して作成

データセット

Maximum Unbiased Validation (MUV) Data Set(https://pubs.acs.org/doi/10.1021/ci8002649)を使用します。MUVはvirtual screeningのベンチマーク用に設計された9万ほどの多様な低分子化合物のデータセットです。

deepchem/datasets at master · deepchem/deepchem · GitHub

deepchem/datasets/にmuv.csv.gzとしてcsv形式で置いてあります。

データセットの前処理

前処理としてデータセットをone-hot encodingします。

#データセットのロード
import pandas as pd

dataset = pd.read_csv("data/muv.csv")
smiles = dataset["smiles"].values


# データセットに含まれる語彙を収集
chars = set()

for smi in smiles:
    chars = chars.union(set(char for char in smi))
chars = sorted(list(chars))

#語彙辞書の作成
char_dict = dict((char, i+1) for i,char in enumerate(chars))
char_dict["<EOS>"] = 0

num_dict = dict((i+1,char) for i,char in enumerate(chars))
num_dict[0]="<EOS>"

chars_size=len(char_dict)
max_length = max(len(s) for s in smiles)
>>>print(char_dict)
{'#': 1, '(': 2, ')': 3, '+': 4, '-': 5, '/': 6, '1': 7, '2': 8, '3': 9, '4': 10, '5': 11, '6': 12, '=': 13, 'B': 14, 'C': 15, 'F': 16, 'H': 17, 'N': 18, 'O': 19, 'S': 20, '[': 21, '\\': 22, ']': 23, 'c': 24, 'l': 25, 'n': 26, 'o': 27, 'r': 28, 's': 29, '<EOS>': 0}
>>>print(num_dict)
{1: '#', 2: '(', 3: ')', 4: '+', 5: '-', 6: '/', 7: '1', 8: '2', 9: '3', 10: '4', 11: '5', 12: '6', 13: '=', 14: 'B', 15: 'C', 16: 'F', 17: 'H', 18: 'N', 19: 'O', 20: 'S', 21: '[', 22: '\\', 23: ']', 24: 'c', 25: 'l', 26: 'n', 27: 'o', 28: 'r', 29: 's', 0: '<EOS>'}
>>>print(len(char_dict))
30
>>>print(max_length)
86

max_lengthが86であることから、データセット内で最長のsmilesは86文字であることがわかります。

またデータセット内のsmilesは29種類の文字+<EOS>で表現できることがわかります。
<EOS>は列の終わりを示すため追加

# One-Hot処理
X = np.zeros( (len(smiles), max_length+1, chars_size),dtype=np.bool)

for n, smi in enumerate(smiles):
    for m, char in enumerate(smi):
        char_idx = char_dict[char]
        X[n,m,char_idx] = 1
    else:
        X[n,len(smi),0]=1
        continue
>>>print(X.shape)
(93127, 87, 30)

one-hot処理によりすべてのサンプルが87(文字数)×30(語彙数)の行列になりました。
max_length+1としているのは列の終わりを示す<EOS>を追加したからです。

モデルの定義

GitHub - maxhodak/keras-molecules: Autoencoder network for learning a continuous representation of molecular structures.を参考に作成しました。

from keras.layers import Input, Dense, GRU, RepeatVector, TimeDistributed, Conv1D, Flatten
from keras.models import Model


def build_autoencoder(max_length,chars_size,pretrained_weight=None):
    #encoder
    shape=(max_length,chars_size)
    encoder_inputs = Input(shape=shape)
    conv1 = Conv1D(filters=27, kernel_size=9, strides=1, padding='same',activation='relu')(encoder_inputs)
    conv2 = Conv1D(filters=18, kernel_size=9, strides=1, padding='same',activation='relu')(conv1)
    conv3 = Conv1D(filters=9, kernel_size=9, strides=1, padding='same',activation='relu')(conv2)
    flat = Flatten()(conv3)
    dense = Dense(512)(flat)
    encoded = dense
    
    #decoder
    decoder_inputs = RepeatVector(shape[0])(encoded)
    gru1 = GRU(512, return_sequences=True)(decoder_inputs)
    gru2 = GRU(256, return_sequences=True)(gru1)
    gru3 = GRU(125, return_sequences=True)(gru2)
    dense = TimeDistributed(Dense(chars_size, activation='softmax'))(gru3)
    decoder_outputs = dense

    model = Model(encoder_inputs, decoder_outputs)
    
    
    if pretrained_weight:
        model.load_weights(pretrained_weight)
    
    return model
>>>model = build_autoencoder(max_length+1, chars_size, pretrained_weight=None)
>>>model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_21 (InputLayer)        (None, 87, 30)            0         
_________________________________________________________________
conv1d_58 (Conv1D)           (None, 87, 27)            7317      
_________________________________________________________________
conv1d_59 (Conv1D)           (None, 87, 18)            4392      
_________________________________________________________________
conv1d_60 (Conv1D)           (None, 87, 9)             1467      
_________________________________________________________________
flatten_16 (Flatten)         (None, 783)               0         
_________________________________________________________________
dense_27 (Dense)             (None, 512)               401408    
_________________________________________________________________
repeat_vector_13 (RepeatVect (None, 87, 512)           0         
_________________________________________________________________
gru_39 (GRU)                 (None, 87, 512)           1574400   
_________________________________________________________________
gru_40 (GRU)                 (None, 87, 256)           590592    
_________________________________________________________________
gru_41 (GRU)                 (None, 87, 125)           143250    
_________________________________________________________________
time_distributed_13 (TimeDis (None, 87, 30)            3780      
=================================================================
Total params: 2,726,606
Trainable params: 2,726,606
Non-trainable params: 0
_________________________________________________________________

DecoderではGRU層を3層重ねていますがユニット数は徐々に減少させていきます。これはRNN層が遅いので計算速度を確保するためです。 なぜかは知りませんが化合物のモデルではLSTMではなくGRUがよく使われるようです。

DeepChemのSeqToSeqモデル
https://deepchem.io/docs/_modules/deepchem/models/tensorgraph/models/seqtoseq.html
でもGRUが採用されています。

レーニン

from keras.callbacks import *

X_train = X[:70000, :, :]
X_test = X[70000:, :, :]

model = build_autoencoder(max_length+1,chars_size,pretrained_weight=None)
model.compile(optimizer='adam',
              loss='categorical_crossentropy')


callbacks = []
callbacks.append(ModelCheckpoint(filepath="checkpoints/AE_{epoch:02d}.hdf5",save_weights_only=True))

history = model.fit(X_train, X_train,
                    epochs=20,
                    batch_size=512,
                    shuffle=True,
                    callbacks=callbacks,
                    validation_data=(X_test, X_test))

f:id:horomary:20181230233542p:plain
もっと長く学習させれば精度向上しそうな感じですが面倒なので打ち切りました。

検証

One-hot表現をSmiles文字列に復元する関数を作成し、モデルの検証を行います。

def Onehot2smiles(sample):
    sample_onehot = sample.reshape(1,87,30)
    sample_onehot_pred = model.predict(sample_onehot)

    sample_truth = []
    for idx in range(max_length):
        char_idx = np.where(sample_onehot[0,idx,:]==1)[0]
        if not list(char_idx):
            break
        char_idx = char_idx[0]
        char = num_dict[char_idx]
        sample_truth.append(char)

    truth = "".join(sample_truth)


    sample_pred = []
    for idx in range(max_length):
        char_idx = np.where(sample_onehot_pred[0,idx,:]==max(sample_onehot_pred[0,idx,:]))[0]
        if not list(char_idx):
            break
        char_idx = char_idx[0]
        char = num_dict[char_idx]
        sample_pred.append(char)

    pred = "".join(sample_pred)


    print("Input:",truth)
    print("Output:",pred)
    print()
>>>for i in range(15):
>>>    idx = np.random.randint(X_test.shape[0])
>>>    Onehot2smiles(X_test[idx])

Input: CCN1C(=O)C(CC(=O)Nc2ccc(OC)cc2)N(Cc2cccnc2)C1=S<EOS>
Output: CCN1C(=O)C(CC(=O)Nc2ccc(OC)cc2)N(Cc2cccnc2)C1=S<EOS>

Input: Cc1cc(=O)oc2cc(OC(C)C(=O)O)c(Cl)cc12<EOS>
Output: Cc1cc(=O)oc2cc(OC(C)C(=O)O)c(Cl)cc12<EOS>

Input: CNC(=O)c1c(N)sc2c1CCCCC2<EOS>
Output: CNC(=O)c1c(N)sc2c1CCCCC2<EOS>

Input: COc1ccc(N2CCN(Cc3cc(=O)oc4cc(-c5ccccc5)c(O)cc34)CC2)cc1<EOS>
Output: COc1ccc(N2CCN(Cc3cc(=O)nc4cc(-c5ccccc4)c(C)ccc3)CC2)cc1<EOS>

Input: CC(C)(C)OC(=O)N1CC2Cc3[nH]c(=O)ccc3C(C2)C1<EOS>
Output: CC(C)(C)OC(=O)N1CCN(nnnnH]c(=O)ccc2C#C))C1<EOS>

Input: O=C(Nc1ccc(Cl)c(S(=O)(=O)N2CCOCC2)c1)c1ccccn1<EOS>
Output: O=C(Nc1ccc(Cl)c(S(=O)(=O)N2CCOCC2)c1)c1ccccn1<EOS>

Input: CCc1nc2c3cnn(-c4cc(Cl)ccc4C)c3ncn2n1<EOS>
Output: CCc1nc2c3c(c(-c4cc(Cl)ccc3C)c3cnc2n1<EOS>

Input: Cc1cccc(Nc2ccccc2C(=O)NCc2ccco2)c1C<EOS>
Output: Cc1cccc(Nc2ccccc2C(=O)NCc2ccco2)c1C<EOS>

Input: O=C(c1cccc(S(=O)(=O)N2CCCC2)c1)c1cccc(S(=O)(=O)N2CCCC2)c1<EOS>
Output: O=C(c1cccc(S(=O)(=O)N2CCCC2)c1)c1cccc(S(=O)(=O)N2CCOC2)cc1<EOS>

Input: C=CCc1cccc2cc(C(=O)NCc3cccnc3)c(=O)oc12<EOS>
Output: C=CCc1cccc2cc(C(=O)NCc3ccccc3)c(=O)oc12<EOS>

Input: COc1ccc(C(=O)Nc2nnc(Cc3cccs3)s2)cc1OC<EOS>
Output: COc1ccc(C(=O)Nc2nnc(Cc3cccs3)s2)cc1OC<EOS>

Input: CCC1CCCCN1CCCNC(=O)Cn1nc(-c2ccc(C)cc2)ccc1=O<EOS>
Output: CCC1CCCCN1CCNNC(=O)Cn1nc(-c2ccc(C)cc2)ccc1=O<EOS>

Input: O=C(Cn1[nH]c(=O)c2ccccc2c1=O)OCC(=O)N(Cc1ccccc1)Cc1ccccc1<EOS>
Output: O=C(Cn1[nH]c(=O)c2ccccc2c1=O)OCC(=O)N(Cc1ccccc1)cc1cccc1<EOS>

Input: CN1CCN(c2nc3c(c(=O)n(C)c(=O)n3C)n2Cc2cccc3ccccc23)CC1<EOS>
Output: CN1CCN(c2nc3c(c(=O)n(C)c(=O)n3C)n2Cc2cccc3ccccc23)CC1<EOS>

Input: CCCc1nc(SC2CCCC2)c2c(=O)n(C)c(=O)n(C)c2n1<EOS>
Output: CCCc1nc(SC2CCCC2)c2c(=O)n(C)c(=O)n(C)c2n1<EOS>

それなりに良い感じでしょうか?

おわりに

kerasでのRNNの日本語での解説ってあんまりないですね。TimeDistributedレイヤーとか理解するまでいろいろ調べました。

付録:試行錯誤の経過

lossが0.3あたりからまともにsmilesが復元するようになってきた印象です。

  1. encoder: LSTM decoder:LSTMのモデルを試すも全くうまくいかず (loss>1.3)
  2. データの表現を [C,N,=,C,=,O,<pad>,<pad>,<pad>]という形式から[C, N, =, C, =, O, <EOS>, 0, 0]に変更 (loss>1.0)
  3. encoder: GRU decoder:GRUに変更(精度向上せず)
  4. encoder: Conv1D decoder:GRUのモデルに変更

参考

GitHub - maxhodak/keras-molecules: Autoencoder network for learning a continuous representation of molecular structures.

Building Autoencoders in Keras

A ten-minute introduction to sequence-to-sequence learning in Keras