返回列表

7th Place Solution for the Open Problems – Single-Cell Perturbations

584. Open Problems – Single-Cell Perturbations | open-problems-single-cell-perturbations

开始: 2023-09-12 结束: 2023-11-30 基因组学与生物信息 数据算法赛
单细胞扰动竞赛第七名解决方案

单细胞扰动竞赛第七名解决方案

作者:李志健(专家级)
比赛排名:第7名
发布时间:2023年12月6日

感谢主办方举办这次有趣的竞赛,也祝贺所有获奖者。我的方案最终获得第七名,这对我来说是个惊喜。

背景

本次竞赛主题为"开放式问题 - 单细胞扰动",目标是预测不同细胞类型在化学物质扰动下的基因表达变化。

方法概述

我的方法分为两个主要步骤:i) 为所有细胞类型、小分子化合物和基因学习嵌入表示;ii) 使用这些嵌入作为特征训练深度学习模型预测目标值,同时有效防止过拟合。整个竞赛过程中,我只使用了三层全连接网络,最终提交也是基于单个FC模型。

学习嵌入表示

这一步的目标是为每种细胞类型、化合物和基因学习特定的嵌入表示。我深受这篇论文的启发,作者使用深度张量分解技术来学习细胞类型、实验方法和基因组位置的密集且信息丰富的表示。

我采用了相同的基本方法,但对模型架构和参数进行了大量调整,包括潜在因子数量、网络维度以及特征组合方式(如拼接与相加)。

最终模型结构如下:

class DeepTensorFactorization(torch.nn.Module):
    def __init__(self, 
                 cell_types, 
                 compounds, 
                 genes, 
                 n_cell_type_factors: int=4, 
                 n_compounds_factors: int=16, 
                 n_gene_factors: int=128,
                 n_hiddens: int=2048,
                 dropout: float=0.1):
        super().__init__()
        
        self.cell_types = cell_types
        self.compounds = compounds
        self.genes = genes
        
        self.n_cell_types = len(cell_types)
        self.n_compounds = len(compounds)
        self.n_genes = len(genes)
        
        self.n_cell_type_factors = n_cell_type_factors
        self.n_compounds_factors = n_compounds_factors
        self.n_gene_factors = n_gene_factors
        
        self.cell_type_embedding = torch.nn.Embedding(self.n_cell_types, self.n_cell_type_factors)
        self.compound_embedding = torch.nn.Embedding(self.n_compounds, self.n_compounds_factors)
        self.gene_embedding = torch.nn.Embedding(self.n_genes, self.n_gene_factors)
        
        self.n_hiddens = n_hiddens
        self.dropout = dropout
        self.n_factors = n_cell_type_factors + n_compounds_factors + n_gene_factors
        
        self.model = nn.Sequential(nn.Linear(self.n_factors, self.n_hiddens),
                                   nn.BatchNorm1d(self.n_hiddens),
                                   nn.ReLU(),
                                   nn.Dropout(self.dropout),
                                   nn.Linear(self.n_hiddens, self.n_hiddens),
                                   nn.BatchNorm1d(self.n_hiddens),
                                   nn.ReLU(),
                                   nn.Dropout(self.dropout),
                                   nn.Linear(self.n_hiddens, 1))
        
    def forward(self, cell_type_indices, compound_indices, gene_indices):
        cell_type_vec = self.cell_type_embedding(cell_type_indices)
        compound_vec = self.compound_embedding(compound_indices)
        gene_vec = self.gene_embedding(gene_indices)
        
        x = torch.concat([cell_type_vec, compound_vec, gene_vec], dim=1)
        x = self.model(x)
        
        return x

为了训练这个模型,我使用了`de_train.parquet`中的所有数据,并按如下方式转换数据格式:

df = pd.read_parquet('de_train.parquet')
df = df.sort_values(['cell_type', 'sm_name'])
df = df.drop(['sm_lincs_id', 'SMILES', 'control'], axis=1)
df = pd.melt(df, id_vars=['cell_type', 'sm_name'], var_name='gene', value_name='target')

训练数据格式如下所示:

训练数据格式示例

我训练了100个epoch且没有使用验证集,因为这一步只关心嵌入层的表示质量。

为了验证嵌入表示的质量,我使用UMAP对基因嵌入进行了可视化。下图展示了基因嵌入的2D UMAP可视化结果:

基因嵌入UMAP可视化

通过目视检查,可以发现基因嵌入中存在明显的结构模式。

目标值预测

获得嵌入表示后,我训练了另一个模型来预测目标值,该模型直接用于生成最终提交结果。我继续使用全连接网络,结构如下:

class PerturbNet(torch.nn.Module):
    def __init__(self, 
                 n_input: int=148,
                 n_hiddens: int=2048,
                 dropout: float=0.5):
        super().__init__()
        
        self.n_input = n_input
        self.n_hiddens = n_hiddens
        self.dropout = dropout

        self.model = nn.Sequential(nn.Linear(self.n_input, self.n_hiddens),
                                   nn.BatchNorm1d(self.n_hiddens),
                                   nn.ReLU(),
                                   nn.Dropout(self.dropout),
                                   nn.Linear(self.n_hiddens, self.n_hiddens),
                                   nn.BatchNorm1d(self.n_hiddens),
                                   nn.ReLU(),
                                   nn.Dropout(self.dropout),
                                   nn.Linear(self.n_hiddens, 1))
        
    def forward(self, x):
        x = self.model(x)
        
        return x

为了防止过拟合,我将每种细胞类型的数据作为验证集,实现了4折交叉验证。对于化合物,我参考了官方notebook的方法,使用私有测试集中的化合物作为验证:

for key, cell_type in cell_type_names.items():
    print(cell_type)
    
    # 分割训练和验证数据,使用私有测试化合物作为验证
    df_train = df[(df['cell_type'] != key) | ~df['sm_lincs_id'].isin(privte_ids)]
    df_valid = df[(df['cell_type'] == key) & df['sm_lincs_id'].isin(privte_ids)]
    
    df_train = df_train.sort_values(['cell_type', 'sm_name'])
    df_valid = df_valid.sort_values('sm_name')
    
    df_train = convert_to_long_df(df_train)
    df_valid = convert_to_long_df(df_valid)
    
    df_train.to_csv(f'../../results/PerturbNet/splited_data/train_{cell_type}.csv')
    df_valid.to_csv(f'../../results/PerturbNet/splited_data/valid_{cell_type}.csv')

模型训练策略如下:

# 设置损失函数和优化器
criterion = torch.nn.MSELoss()
optimizer = AdamW(model.parameters(), lr=args.lr, weight_decay=1e-4)
scheduler = ReduceLROnPlateau(optimizer, 'min', min_lr=1e-5)

我以每种细胞类型作为验证数据分别训练模型,最终提交时对四个模型的预测结果取平均值。

未奏效的方法

在比赛期间,我尝试了很多基于先验知识的特征工程方法,包括单细胞数据特征、分子结构嵌入以及geneFormer等预训练模型的基因嵌入。然而这些方法都没有带来效果提升。因此最终模型仅使用了第一步学习到的嵌入特征。

同比赛其他方案