一日目 Xenonpy内の記述子関連クラスについて
動作確認
Jupyter notebookをvisual studio codeの中で利用すると、わざわざコマンドで実行しなくても良いので便利です。
最低限の機能があるので、利用には必要十分だと思います。早速、Xenonpyをインストールしたので、github上のサンプルに従って実行してみます。最初はimportして、versionを確認する、RDkitが動くかどうかも確認してみます。
import xenonpy from rdkit import Chem from rdkit.Chem import Draw xenonpy.__version__
'0.6.7'
mol=Chem.MolFromSmiles("C1CCOC1")
Draw.MolToImage(mol)
しっかりと実行できたと思います。最後に構造も描画できていましたね。
iQSPR.ipynb:記述子クラス関連
次は、Xenonpyのgithubサイト(https://github.com/yoshida-lab/XenonPy)のsampleフォルダにあるiQSPR.ipynbを使って実行し、コードを理解した上で、将来的には改変していこうと思います。
最初に、tools.ipynbを実行します。こちらには、実行に必要なライブラリのimport文やmatplotlibによる定型プロッターが準備されております。実行するために、sampleフォルダ内のtools.ipynbをこれから作るコードと同じ場所に格納してください。
run tools.ipynb
次に、トレーニング用のサンプルデータベースをダウンロードします。
https://github.com/yoshida-lab/XenonPy/releases/download/v0.4.1/iQSPR_sample_data.csv
このデータには、有機化合物の化学構造を表現したSMILES文字列と、内部エネルギー(internal energy E(kJ/mol))と、バンドギャップ(HOMO-LUMO gap (eV, or eV/particle))が格納されています。これは量子化学計算パッケージであるGAMESSを用いて計算されたもので、用いた理論はB3LYP/6-31G(d)です。データ数として16674個あるので、これを用いてANN/GcNNなどの学習を行って転移学習用途にも使えるかもしれません。
このブログでは、エネルギーの単位が違うので、eVに統一したいと思います。
また、未取得データがある場合にNaNという文字列を与えれば、Xenonpy上で無視してくれたりする機能があるみたいです。
ダウンロードしたcsvはここで述べるコードと同じフォルダに格納してあるものとします。
# load in-house data data = pd.read_csv("./iQSPR_sample_data.csv", index_col=0) # take a look at the data data.columns data.head()
1 KJ/mol = 0.0104 eV/particleの関係があるので、次のようにします。
data["E"]=data["E"]*0.0104 data
これで、物性値として使っている値の単位があったので、数字の大きさが似通ってきました。次に、データベース内の内容を理解するために、matplotlibのhistコマンドを用いて、SMILESの文字数に対する分布グラフを描いてみましょう。ピークがおおよそ20-30くらいなので、iQSPR実行時の文字列として20文字くらいあれば十分かなという目算を行うことができます。ここで、ピークの位置を求めてみました。そうすると、おおよそ23.2くらいにあることがわかります。binの数を変えてもほとんど値は変わりません。(一部誤りがあり、2023/03/19修正)
count = [len(x) for x in data['SMILES']] y,x,_=plt.hist(count, bins=20) y=list(y) x=list(x) print("Peak position of SMILES length histogram" ,x[y.index(max(y))+1])
二つの物性値が格納されているので、どのようにデータが分布しているか調べめてみましょう。
# check target properties: E & HOMO-LUMO gap _ = plt.figure(figsize=(5,5)) _ = plt.scatter(data['E'],data['HOMO-LUMO gap'],s=15,c='b',alpha = 0.1) _ = plt.title('iQSPR sample data') _ = plt.xlabel('Internal energy') _ = plt.ylabel('HOMO-LUMO gap') _ = plt.show()
内部エネルギーは0-6eV、バンドギャップは0-18eVくらいに分布していることがわかります。薄い色のところにデータがない領域があることに着目しましょう。sampleフォルダのコードでは、内部エネルギーが低く、バンドギャップも低いエネルギーを返す、SMILES文字列を探しています。
記述子クラス
XenonpyにはRDKit由来のmolecular finger printを利用するためのラッパーがあります。チュートリアルに従い、ECFPとMACCSを連結したFinger printを用いて記述子クラスを定義しています。サンプルとして下記のようなものが与えられています。
from xenonpy.descriptor import Fingerprints RDKit_FPs = Fingerprints(featurizers=['ECFP', 'MACCS'], input_type='smiles')
finger printに関する脱線
上記までの簡素な方法で記述子を得ることができます。この記述子を自分の好きなように変えたいとなると、これだけでは情報が足りません。
Xenonpyのソースコードを確認し、Fingerprintsとして何が定義されているのかみてみましょう。
コードの冒頭部分に記載されているように、RDkit由来で利用できるFingerprintは以下のものです。
- 'RDKitFP'
- 'AtomPairFP'
- 'TopologicalTorsionFP'
- 'MACCS'
- 'FCFP'
- 'ECFP'
- 'PatternFP'
- 'LayeredFP'
- 'MHFP'
Fingerprintsクラスは750行目くらいから記載されています。
class Fingerprints(BaseDescriptor): """ Calculate fingerprints or descriptors of organic molecules. Note that MHFP currently does not support parallel computing, so n_jobs is fixed to 1. """
利用できる主要な引数として、以下のものがあります(詳しくはソースコードの英語を読んでください)。
- n_jobs:計算するスレッド数
- radius:CircularタイプのFinger printであるMorgan finger print向けの半径
- n_bits:平たくいうとFingerprintの要素数
- counting:0/1の値を要素数に入れる場合はFalse
- featurizers:デフォルトは全てのfingerprintを考慮
- input_type:SMILES記法を入力とするのか、RDKitのmolオブジェクト(rdkit.Chem.rdchem.Mol)をそのまま入力とするのか。
これがわかったら、次のようにfinger printを設定することもできます。
ECFP6_FPs = Fingerprints(featurizers=['ECFP',"MACCS"],radius=3,n_bits=128*4,counting=True, input_type='smiles')
このクラスを理解するにはBaseDescriptorから理解する必要があります。
https://github.com/yoshida-lab/XenonPy/blob/master/xenonpy/descriptor/base.py
from sklearn.base import TransformerMixin, BaseEstimator class BaseDescriptor(BaseEstimator, TransformerMixin, metaclass=TimedMetaClass):
このBaseDescriptorクラスを利用することで、各featurizerをタスクリストのように格納して、Xenonpy内で利用できるようです。このクラスのtransform関数を用いて、SMILESなどから、finger printを算出することができるようになります。
ECFPの例の場合、
https://github.com/yoshida-lab/XenonPy/blob/master/xenonpy/descriptor/fingerprint.py
class ECFP(BaseFeaturizer): def __init__(self, n_jobs=-1, *, radius=3, n_bits=2048, counting=False, input_type='mol', on_errors='raise', return_type='any', target_col=None): """ Morgan (Circular) fingerprints (ECFP) The algorithm used is described in the paper Rogers, D. & Hahn, M. Extended-Connectivity Fingerprints. JCIM 50:742-54 (2010) Parameters
が呼ばれます。
さらに、クラス継承元のBaseFeaturizerを見ると、
https://github.com/yoshida-lab/XenonPy/blob/master/xenonpy/descriptor/base.py
このクラスのtransform内で変換が行われており、結果としてECFPのfeaturize関数を呼び出している部分があります。ここまでわかったら、SMILESから、ある数値リストを出力するような関数を作り、Xenonpyの中に組み込むことは簡単です。次に、DummyFuncと、DummyClassを定義して使うこともできます。この仕組みを利用することで、ANN/GcNNによる学習モデルから転移学習させる際にDummyFuncに組み込んでやれば、Xenonpy向けの記述子クラスを定義できるようになります。以下に実装例を記載します。適当に作ったので、使用する際にはしっかりと作り込むことが必要です。
from xenonpy.descriptor.base import BaseDescriptor, BaseFeaturizer def DummyFunc(x, nBits=1): # mol オブジェクトからcanonical smilesに直して、n_bitsの数だけの長さの数列を返す無意味な関数。 dummy=Chem.MolToSmiles(x) return np.ones(nBits)*len(dummy) class DummyClass(BaseFeaturizer): def __init__(self, n_jobs=-1, *, radius=3, n_bits=2048, counting=False, input_type='mol', on_errors='raise', return_type='any', target_col=None): """ """ super().__init__(n_jobs=n_jobs, on_errors=on_errors, return_type=return_type, target_col=target_col) def featurize(self, x): if self.input_type == 'smiles': x_ = x x = Chem.MolFromSmiles(x) if x is None: raise ValueError('cannot convert Mol from SMILES %s' % x_) if self.input_type == 'any': if not isinstance(x, Chem.rdchem.Mol): x_ = x x = Chem.MolFromSmiles(x) if x is None: raise ValueError('cannot convert Mol from SMILES %s' % x_) if self.counting: return count_fp(dummyFunc(x,nBits=self.n_bits) ) else: return list(dummyFunc(x,nBits=self.n_bits) ) @property def feature_labels(self): if self.counting: return [f'Dummy_c:' + str(i) for i in range(self.n_bits)] else: return [f'Dummy:' + str(i) for i in range(self.n_bits)] # prepare descriptor function from XenonPy (possible to combine multiple descriptors) class DummyDesc(BaseDescriptor): def __init__(self, n_jobs=-1, on_errors='nan'): super().__init__() self.n_jobs = n_jobs self.rdkit_fp = DummyClass(n_jobs, on_errors=on_errors, input_type='smiles') Desc = DummyDesc()
記述子の適用
csvファイル内に"FBr(F)(F)(F)F"があるので、このデータは抜く必要があります。最初の記述子に話を戻して、SMILES文字列から、記述子を算出します。
data=data[ data["SMILES"] != "FBr(F)(F)(F)F" ] tmpFP = ECFP6_FPs.transform(data['SMILES']) tmpFP
まとめ
記述子関連のやれたことを短くまとめると、下記になります。
run tools.ipynb
from xenonpy.descriptor import Fingerprints # load in-house data data = pd.read_csv("./iQSPR_sample_data.csv", index_col=0) count = [len(x) for x in data['SMILES']] y,x,_=plt.hist(count, bins=20) y=list(y) x=list(x) print("Peak position of SMILES length histogram" ,x[y.index(max(y))+1]) # Convert energy unit data["E"]=data["E"]*0.0104 # check target properties: E & HOMO-LUMO gap _ = plt.figure(figsize=(5,5)) _ = plt.scatter(data['E'],data['HOMO-LUMO gap'],s=15,c='b',alpha = 0.1) _ = plt.title('iQSPR sample data') _ = plt.xlabel('Internal energy') _ = plt.ylabel('HOMO-LUMO gap') _ = plt.show() # Descriptor ECFP6_FPs = Fingerprints(featurizers=['ECFP',"MACCS"],radius=3,n_bits=128*4,counting=True, input_type='smiles') data=data[ data["SMILES"] != "FBr(F)(F)(F)F" ] tmpFP = ECFP6_FPs.transform(data['SMILES'])