本教程我们来看一下如何从实验数据产生新的数据集。如我们将要看到的,产生数据集对象的机量只是一小部分过程。许多真实的数据集在它们适用于训练模型前需要清理和QA。
处理数据文件
假定你的实验室的同事给你数据。你想用这些数据来构建机器学习模型。你将如何变换这些数据到适合于创建机器学习模型的数据集呢?
从新的数据建立模型是有些挑战的。可能有些数据不是以便以使用的方式记录的。另外,有些数据有噪音。这通常随着大量外部变化的生物测定,收集样本的成本和困难而发生。这是个问题,因为你不想让你的模型拟合噪音。
因此,有两大挑战:
1.解析数据
2.数据去噪音
本教程,我们来看一下从药物测定实验的Excel电子表格中手工生成数据集。在深入学个例子之前,我们简单的回顾一下 DeepChem的输入文件处理和特征化能力。
输入格式
DeepChem支持多种输入文件。例如,可以接受的输入文件格式为.csv, .sdf, .fasta, .png, .tif及其它格式。加载特定的文件格式由相关的Loader
类控制。例如,加载
.csv文件使用CSVLoader
类。这里有个适合于
CSVLoader
的
.csv文件的要求。
这里有一个潜在的输入文件的例子。
化合物IDs |
测量的对数溶解度 mols/litre |
SMILES |
benzothiazole |
-1.5 |
c2ccc1scnc1c2 |
这里" SMILES "包含SMILES字串,"测量的对数溶解度mols/litre "包含实验测量,"化合物IDs "包含化合物的唯一标识。
数据特征化
大部分的机器学习算法要求输入数据为矢量。然而来自药物发现的数据集通常是分子和相关的实验结果。要加载这数据,我们使用dc.data.DataLoader
的子类如
dc.data.CSVLoader
或 dc.data.SDFLoader
。用户可以用
dc.data.DataLoader
子类来加载任意文件格式。所有的加载器必需传递
dc.feat.Featurizer
对象,它指明如何转换分子式到矢量。
DeepChem
提供了不同的
dc.feat.Featurizer
子类。
解析数据
为了读取数据,我们要使用pandas 数据分析库。为了转换药物名称到SMILES字串,我们要用pubchempy。这不是标准的DeepChem依赖,但你可以用conda install pubchempy
安装。
In [ ]:
!conda install pubchempy
In [1]:
import os
import pandas as pd
from pubchempy import get_cids, get_compounds
Pandas非常神奇,但是它不能自动的找到你感兴趣的数据。你可能需要通过GUI来查找。我们可以看一下LibreOffice渲染的数据集。
为了这个,我们要导入Image和os。
In [2]:
import os
from IPython.display import Image, display
current_dir = os.path.dirname(os.path.realpath('__file__'))
data_screenshot = os.path.join(current_dir, 'assets/dataset_preparation_gui.png')
display(Image(filename=data_screenshot))
我们看到感兴趣的数据在第二个表,包含于"TA ID", "N #1 (%)", and "N #2 (%)"列。
另外,看起来表格的大部分是人类可读的(多列表头、带空格的列标签和符号等)。这让纯净的dataframe对象的产生变得困难。基于这个原因,我们去掉一些不用的或不方便的东西。
In [3]:
import deepchem as dc
dc.utils.download_url(
'https://github.com/deepchem/deepchem/raw/master/datasets/Positive%20Modulators%20Summary_%20918.TUC%20_%20v1.xlsx',
current_dir,
'Positive Modulators Summary_ 918.TUC _ v1.xlsx'
)
In [4]:
raw_data_file = os.path.join(current_dir, 'Positive Modulators Summary_ 918.TUC _ v1.xlsx')
raw_data_excel = pd.ExcelFile(raw_data_file)
# second sheet only
raw_data = raw_data_excel.parse(raw_data_excel.sheet_names[1])
In [5]:
# preview 5 rows of raw dataframe
raw_data.loc[raw_data.index[:5]]
Out[5]:
注意实际的行头在第一行而不是第0行。
In [6]:
# remove column labels (rows 0 and 1), as we will replace them
# only take data given in columns "TA ID" "N #1 (%)" (3) and "N #2 (%)" (4)
raw_data = raw_data.iloc[2:, [2, 6, 7]]
# reset the index so we keep the label but number from 0 again
raw_data.reset_index(inplace=True)
## rename columns
raw_data.columns = ['label', 'drug', 'n1', 'n2']
In [7]:
# preview cleaner dataframe
raw_data.loc[raw_data.index[:5]]
Out[7]:
这种格式更接近我们的需求。
现在我们来看一下药物的名称并给它们SMILES字串(DeepChem要求的格式)
In [8]:
drugs = raw_data['drug'].values
For many of these, we can retreive the smiles string via the canonical_smiles attribute of the get_compounds object (using pubchempy)
In [9]:
get_compounds(drugs[1], 'name')
Out[9]:
[Compound(5281078)]
In [10]:
get_compounds(drugs[1], 'name')[0].canonical_smiles
Out[10]:
'CC1=C2COC(=O)C2=C(C(=C1OC)CC=C(C)CCC(=O)OCCN3CCOCC3)O'
然而,有些药物名有变化的空格和符号(·, (±),等),有些名称无法被pubchempy读取。
对于这个任务,我们要用正则表达式做一些破解。而且,我们注意到,所有的离子以缩写形式需要扩展。基于这一原因我们使用字典,映射离子缩写到pubchempy可以识别的版本。
不幸的是,你可能有一些困难需要更多的破解。
In [11]:
import re
ion_replacements = {
'HBr': ' hydrobromide',
'2Br': ' dibromide',
'Br': ' bromide',
'HCl': ' hydrochloride',
'2H2O': ' dihydrate',
'H20': ' hydrate',
'Na': ' sodium'
}
ion_keys = ['H20', 'HBr', 'HCl', '2Br', '2H2O', 'Br', 'Na']
def compound_to_smiles(cmpd):
# remove spaces and irregular characters
compound = re.sub(r'([^\s\w]|_)+', '', cmpd)
# replace ion names if needed
for ion in ion_keys:
if ion in compound:
compound = compound.replace(ion, ion_replacements[ion])
# query for cid first in order to avoid timeouterror
cid = get_cids(compound, 'name')[0]
smiles = get_compounds(cid)[0].canonical_smiles
return smiles
现在我们正式的转换这些化合物到SMILES。这种转换需要花费几分种时间,所在可以喝一杯咖啡或一杯茶休息一下。注意这种转换有时会失败,所以下面我们要抛出错误处理。
In [12]:
smiles_map = {}
for i, compound in enumerate(drugs):
try:
smiles_map[compound] = compound_to_smiles(compound)
except:
print("Errored on %s" % i)
continue
Errored on 162
Errored on 303
[13]:
smiles_data = raw_data
# map drug name to smiles string
smiles_data['drug'] = smiles_data['drug'].apply(lambda x: smiles_map[x] if x in smiles_map else None)
[14]:
# preview smiles data
smiles_data.loc[smiles_data.index[:5]]
Out[14]:
很好,我们已经映射所有的名称到相应的SMILES编码。
现在我们来看一下数据,尽可能的去掉噪音数据。
数据去噪音
在机器学习中,我们知道一些免费的午餐。你要花点时间来分析和理解你的数据以框定你的问题并确定合适的模型框架。对于你的数据的处理取决于你从这个过程中得到的结论。
你需要问的问题有:
你要完成什么任务?
你的测定是什么?
数据的结构如何?
数据合理吗?
以前做了哪些尝试?
对于这个项目:
我想要建立一个模型来预测任一个小分子与离子通道蛋白的亲和力。
对于输入的药物,描述通道抑制的数据。
几百个药物,n=2
需要更仔细的检查数据集
这的蛋白没有。
可能会涉及作图。因此我们导入matplotlib和seaborn。我们也要看一下分子结构,所以我们导入rdkit。我们也可能使用seaborn。你可以用conda install seaborn
命令安装。
In [15]:
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
sns.set_style('white')
from rdkit import Chem
from rdkit.Chem import AllChem
from rdkit.Chem import Draw, PyMol, rdFMCS
from rdkit.Chem.Draw import IPythonConsole
from rdkit import rdBase
import numpy as np
我们的目标是建立小分子模型,所以我们要确保所有分子是小的。这可以通过SMILES字串的长度估计。
In [16]:
smiles_data['len'] = [len(i) if i is not None else 0 for i in smiles_data['drug']]
smiles_lens = [len(i) if i is not None else 0 for i in smiles_data['drug']]
sns.histplot(smiles_lens)
plt.xlabel('len(smiles)')
plt.ylabel('probability')
Out[16]:
Text(0, 0.5, 'probability')
有些看起来相当大,len(smiles) > 150。我们来看一下它们像什么。
In [17]:
# indices of large looking molecules
suspiciously_large = np.where(np.array(smiles_lens) > 150)[0]
# corresponding smiles string
long_smiles = smiles_data.loc[smiles_data.index[suspiciously_large]]['drug'].values
# look
Draw._MolsToGridImage([Chem.MolFromSmiles(i) for i in long_smiles], molsPerRow=6)
Out[17]:
正如怀疑的,没有小分子存在,所以我们要从数据集中移除它们。这里的假定是这些分子可以登记作为抑制剂仅因为它们是大的。它们更像立体的通道阻断剂而不是扩散和结合(那是我们感兴趣的)。
本教程移除不适合你的数据。
In [18]:
# drop large molecules
smiles_data = smiles_data[~smiles_data['drug'].isin(long_smiles)]
现在看一下数据集的数值结构。
首先检查一下NaNs。
In [19]:
nan_rows = smiles_data[smiles_data.isnull().T.any().T]
nan_rows[['n1', 'n2']]
Out[19]:
我不相信n=1,所在我会抛掉它。
然后我们检查n1和n2的分布。
In [20]:
df = smiles_data.dropna(axis=0, how='any')
# seaborn jointplot will allow us to compare n1 and n2, and plot each marginal
sns.jointplot(x='n1', y='n2', data=smiles_data)
Out[20]:
我们看到大部分数据包含在高斯分布的中心略小于0。我们看到一些活动数据点位于左下部,有一个在右上部。它们不同于大部分的数据。我们如何处理这些数据呢?
由于n1和n2代表相同的测量,理想情况下它们是取值相同的。这个图应该紧紧的对齐于对角线,而且皮尔逊相关系数应为1。我们看到实际上不是。这有助于我们发现错误的测量。
我们更仔细的看一下错误,作一下(n1-n2)分布图。
In [21]:
diff_df = df['n1'] - df['n2']
sns.histplot(diff_df)
plt.xlabel('difference in n')
plt.ylabel('probability')
Out[21]:
Text(0, 0.5, 'probability')
这看起来非常正态,我们通过scipy拟合正态来得到95%置信区间,并得到2倍标准偏差。
In [22]:
from scipy import stats
mean, std = stats.norm.fit(np.asarray(diff_df, dtype=np.float32))
ci_95 = std*2
ci_95
Out[22]:
17.75387954711914
现在我不相信置信区间外的数据,因此从数据框中删除这些数据点。例如,上面的图,至少有一个点的n1-n2 > 60,这是不要的。
In [23]:
noisy = diff_df[abs(diff_df) > ci_95]
df = df.drop(noisy.index)
sns.jointplot(x='n1', y='n2', data=df)
Out[23]:
现在数据看起来好很多!我们平均一下n1 和n2,并取误差为区间为ci_95。
In [24]:
avg_df = df[['label', 'drug']].copy()
n_avg = df[['n1', 'n2']].mean(axis=1)
avg_df['n'] = n_avg
avg_df.sort_values('n', inplace=True)
现在看一下误差区间排序的数据。
In [25]:
plt.errorbar(np.arange(avg_df.shape[0]), avg_df['n'], yerr=ci_95, fmt='o')
plt.xlabel('drug, sorted')
plt.ylabel('activity')
Out[25]:
Text(0, 0.5, 'activity')
现在我们来识别一下活性物合物。
就我的情况,这需要专业知识。在这一领域工作,咨询这一行业的教授,我对活性绝对值大于25的化合物感兴趣。这与我们想要建立模型的药物强度有关。
如果你不知道活性与非活性之间的如何划分,可以把它当作超参数来处理。
In [26]:
actives = avg_df[abs(avg_df['n'])-ci_95 > 25]['n']
plt.errorbar(np.arange(actives.shape[0]), actives, yerr=ci_95, fmt='o')
Out[26]:
In [27]:
# summary
print (raw_data.shape, avg_df.shape, len(actives.index))
(430, 5) (392, 3) 6
总结一下,我们已经:
去掉了与我们想要回答的问题无关的数据。(仅小分子)
去掉了缺失值。
确定的测定误差。
去掉了噪音数据点。
识别了活性物化合(通过专业知识来确定阀值
确定模型的类型,最终的数据集形式,以及合理的加载
现在,我们要用哪种模型框架?
假定我们有392个数据点且6个有活性,这数据将用于建立小数据one-shot分类器(10.1021/acscentsci.6b00367)。如果数据集有相似的特性,可以使用迁移学习,但现在还不行。
我们对数据框实施logic以转换到适合分类的二值形式。
In [28]:
# 1 if condition for active is met, 0 otherwise
avg_df.loc[:, 'active'] = (abs(avg_df['n'])-ci_95 > 25).astype(int)
现在保存文件。
In [29]:
avg_df.to_csv('modulators.csv', index=False)
现在转换数据框到DeepChem数据集。
In [30]:
dataset_file = 'modulators.csv'
task = ['active']
featurizer_func = dc.feat.ConvMolFeaturizer()
loader = dc.data.CSVLoader(tasks=task, feature_field='drug', featurizer=featurizer_func)
dataset = loader.create_dataset(dataset_file)
最后,通常以某种方法数值化的转换数据是有好处的。例如,有时候正态化数据是有用的,或者转换到零均值。这取决于手头的任务。DeepChem内置很多有用的转换,在deepchem.transformers.transformers基类中。
因为这是分类模型,而且活性的数值低,我将应用平衡转换。当我训练模型时我把这个转换器当作超参数。事实证明它提高了模型的性能。
In [31]:
transformer = dc.trans.BalancingTransformer(dataset=dataset)
dataset = transformer.transform(dataset)
现在我们保存平衡数据集对象到磁盘,然后重新加载它作为合理的检查。
In [32]:
dc.utils.save_to_disk(dataset, 'balanced_dataset.joblib')
balanced_dataset = dc.utils.load_from_disk('balanced_dataset.joblib')