├── HIST2ST.py ├── HIST2ST_train.py ├── NB_module.py ├── README.md ├── Workflow.png ├── data ├── download.sh ├── her_hvg_cut_1000.npy └── skin_hvg_cut_1000.npy ├── dataset.py ├── gcn.py ├── graph_construction.py ├── predict.py ├── run_trained_models.ipynb ├── transformer.py ├── tutorial.ipynb └── utils.py /HIST2ST.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | import pytorch_lightning as pl 4 | import torchvision.transforms as tf 5 | from gcn import * 6 | from NB_module import * 7 | from transformer import * 8 | from scipy.stats import pearsonr 9 | from torch.utils.data import DataLoader 10 | from copy import deepcopy as dcp 11 | from collections import defaultdict as dfd 12 | from sklearn.metrics import adjusted_rand_score as ari_score 13 | from sklearn.metrics.cluster import normalized_mutual_info_score as nmi_score 14 | class convmixer_block(nn.Module): 15 | def __init__(self,dim,kernel_size): 16 | super().__init__() 17 | self.dw=nn.Sequential( 18 | nn.Conv2d(dim, dim, kernel_size, groups=dim, padding="same"), 19 | nn.BatchNorm2d(dim), 20 | nn.GELU(), 21 | nn.Conv2d(dim, dim, kernel_size, groups=dim, padding="same"), 22 | nn.BatchNorm2d(dim), 23 | nn.GELU(), 24 | ) 25 | self.pw=nn.Sequential( 26 | nn.Conv2d(dim, dim, kernel_size=1), 27 | nn.GELU(), 28 | nn.BatchNorm2d(dim), 29 | ) 30 | def forward(self,x): 31 | x=self.dw(x)+x 32 | x=self.pw(x) 33 | return x 34 | class mixer_transformer(nn.Module): 35 | def __init__(self,channel=32, kernel_size=5, dim=1024, 36 | depth1=2, depth2=8, depth3=4, 37 | heads=8, dim_head=64, mlp_dim=1024, dropout = 0., 38 | policy='mean',gcn=True 39 | ): 40 | super().__init__() 41 | self.layer1=nn.Sequential( 42 | *[convmixer_block(channel,kernel_size) for i in range(depth1)], 43 | ) 44 | self.layer2=nn.Sequential(*[attn_block(dim,heads,dim_head,mlp_dim,dropout) for i in range(depth2)]) 45 | self.layer3=nn.ModuleList([gs_block(dim,dim,policy,gcn) for i in range(depth3)]) 46 | self.jknet=nn.Sequential( 47 | nn.LSTM(dim,dim,2), 48 | SelectItem(0), 49 | ) 50 | self.down=nn.Sequential( 51 | nn.Conv2d(channel,channel//8,1,1), 52 | nn.Flatten(), 53 | ) 54 | def forward(self,x,ct,adj): 55 | x=self.down(self.layer1(x)) 56 | g=x.unsqueeze(0) 57 | g=self.layer2(g+ct).squeeze(0) 58 | jk=[] 59 | for layer in self.layer3: 60 | g=layer(g,adj) 61 | jk.append(g.unsqueeze(0)) 62 | g=torch.cat(jk,0) 63 | g=self.jknet(g).mean(0) 64 | return g 65 | class ViT(nn.Module): 66 | def __init__(self, channel=32,kernel_size=5,dim=1024, 67 | depth1=2, depth2=8, depth3=4, 68 | heads=8, mlp_dim=1024, dim_head = 64, dropout = 0., 69 | policy='mean',gcn=True 70 | ): 71 | super().__init__() 72 | self.dropout = nn.Dropout(dropout) 73 | self.transformer = mixer_transformer( 74 | channel, kernel_size, dim, 75 | depth1, depth2, depth3, 76 | heads, dim_head, mlp_dim, dropout, 77 | policy,gcn, 78 | ) 79 | 80 | def forward(self,x,ct,adj): 81 | x = self.dropout(x) 82 | x = self.transformer(x,ct,adj) 83 | return x 84 | 85 | class Hist2ST(pl.LightningModule): 86 | def __init__(self, learning_rate=1e-5, fig_size=112, label=None, 87 | dropout=0.2, n_pos=64, kernel_size=5, patch_size=7, n_genes=785, 88 | depth1=2, depth2=8, depth3=4, heads=16, channel=32, 89 | zinb=0, nb=False, bake=0, lamb=0, policy='mean', 90 | ): 91 | super().__init__() 92 | # self.save_hyperparameters() 93 | dim=(fig_size//patch_size)**2*channel//8 94 | self.learning_rate = learning_rate 95 | 96 | self.nb=nb 97 | self.zinb=zinb 98 | 99 | self.bake=bake 100 | self.lamb=lamb 101 | 102 | self.label=label 103 | self.patch_embedding = nn.Conv2d(3,channel,patch_size,patch_size) 104 | self.x_embed = nn.Embedding(n_pos,dim) 105 | self.y_embed = nn.Embedding(n_pos,dim) 106 | self.vit = ViT( 107 | channel=channel, kernel_size=kernel_size, heads=heads, 108 | dim=dim, depth1=depth1,depth2=depth2, depth3=depth3, 109 | mlp_dim=dim, dropout = dropout, policy=policy, gcn=True, 110 | ) 111 | self.channel=channel 112 | self.patch_size=patch_size 113 | self.n_genes=n_genes 114 | if self.zinb>0: 115 | if self.nb: 116 | self.hr=nn.Linear(dim, n_genes) 117 | self.hp=nn.Linear(dim, n_genes) 118 | else: 119 | self.mean = nn.Sequential(nn.Linear(dim, n_genes), MeanAct()) 120 | self.disp = nn.Sequential(nn.Linear(dim, n_genes), DispAct()) 121 | self.pi = nn.Sequential(nn.Linear(dim, n_genes), nn.Sigmoid()) 122 | if self.bake>0: 123 | self.coef=nn.Sequential( 124 | nn.Linear(dim,dim), 125 | nn.ReLU(), 126 | nn.Linear(dim,1), 127 | ) 128 | self.gene_head = nn.Sequential( 129 | nn.LayerNorm(dim), 130 | nn.Linear(dim, n_genes), 131 | ) 132 | self.tf=tf.Compose([ 133 | tf.RandomGrayscale(0.1), 134 | tf.RandomRotation(90), 135 | tf.RandomHorizontalFlip(0.2), 136 | ]) 137 | def forward(self, patches, centers, adj, aug=False): 138 | B,N,C,H,W=patches.shape 139 | patches=patches.reshape(B*N,C,H,W) 140 | patches = self.patch_embedding(patches) 141 | centers_x = self.x_embed(centers[:,:,0]) 142 | centers_y = self.y_embed(centers[:,:,1]) 143 | ct=centers_x + centers_y 144 | h = self.vit(patches,ct,adj) 145 | x = self.gene_head(h) 146 | extra=None 147 | if self.zinb>0: 148 | if self.nb: 149 | r=self.hr(h) 150 | p=self.hp(h) 151 | extra=(r,p) 152 | else: 153 | m = self.mean(h) 154 | d = self.disp(h) 155 | p = self.pi(h) 156 | extra=(m,d,p) 157 | if aug: 158 | h=self.coef(h) 159 | return x,extra,h 160 | def aug(self,patch,center,adj): 161 | bake_x=[] 162 | for i in range(self.bake): 163 | new_patch=self.tf(patch.squeeze(0)).unsqueeze(0) 164 | x,_,h=self(new_patch,center,adj,True) 165 | bake_x.append((x.unsqueeze(0),h.unsqueeze(0))) 166 | return bake_x 167 | def distillation(self,bake_x): 168 | new_x,coef=zip(*bake_x) 169 | coef=torch.cat(coef,0) 170 | new_x=torch.cat(new_x,0) 171 | coef=F.softmax(coef,dim=0) 172 | new_x=(new_x*coef).sum(0) 173 | return new_x 174 | def training_step(self, batch, batch_idx): 175 | patch, center, exp, adj, oris, sfs, *_ = batch 176 | adj=adj.squeeze(0) 177 | exp=exp.squeeze(0) 178 | pred,extra,h = self(patch, center, adj) 179 | 180 | mse_loss = F.mse_loss(pred, exp) 181 | self.log('mse_loss', mse_loss,on_epoch=True, prog_bar=True, logger=True) 182 | bake_loss=0 183 | if self.bake>0: 184 | bake_x=self.aug(patch,center,adj) 185 | new_pred=self.distillation(bake_x) 186 | bake_loss+=F.mse_loss(new_pred,pred) 187 | self.log('bake_loss', bake_loss,on_epoch=True, prog_bar=True, logger=True) 188 | zinb_loss=0 189 | if self.zinb>0: 190 | if self.nb: 191 | r,p=extra 192 | zinb_loss = NB_loss(oris.squeeze(0),r,p) 193 | else: 194 | m,d,p=extra 195 | zinb_loss = ZINB_loss(oris.squeeze(0),m,d,p,sfs.squeeze(0)) 196 | self.log('zinb_loss', zinb_loss,on_epoch=True, prog_bar=True, logger=True) 197 | 198 | loss=mse_loss+self.zinb*zinb_loss+self.lamb*bake_loss 199 | return loss 200 | 201 | def validation_step(self, batch, batch_idx): 202 | patch, center, exp, adj, oris, sfs, *_ = batch 203 | def cluster(pred,cls): 204 | sc.pp.pca(pred) 205 | sc.tl.tsne(pred) 206 | kmeans = KMeans(n_clusters=cls, init="k-means++", random_state=0).fit(pred.obsm['X_pca']) 207 | pred.obs['kmeans'] = kmeans.labels_.astype(str) 208 | p=pred.obs['kmeans'].to_numpy() 209 | return p 210 | 211 | pred,extra,h = self(patch, center, adj.squeeze(0)) 212 | if self.label is not None: 213 | adata=ann.AnnData(pred.squeeze().cpu().numpy()) 214 | idx=self.label!='undetermined' 215 | cls=len(set(self.label)) 216 | x=adata[idx] 217 | l=self.label[idx] 218 | predlbl=cluster(x,cls-1) 219 | self.log('nmi',nmi_score(predlbl,l)) 220 | self.log('ari',ari_score(predlbl,l)) 221 | 222 | loss = F.mse_loss(pred.squeeze(0), exp.squeeze(0)) 223 | self.log('valid_loss', loss,on_epoch=True, prog_bar=True, logger=True) 224 | 225 | pred=pred.squeeze(0).cpu().numpy().T 226 | exp=exp.squeeze(0).cpu().numpy().T 227 | r=[] 228 | for g in range(self.n_genes): 229 | r.append(pearsonr(pred[g],exp[g])[0]) 230 | R=torch.Tensor(r).mean() 231 | self.log('R', R, on_epoch=True, prog_bar=True, logger=True) 232 | return loss 233 | 234 | def configure_optimizers(self): 235 | # self.hparams available because we called self.save_hyperparameters() 236 | optim=torch.optim.Adam(self.parameters(), lr=self.learning_rate) 237 | StepLR = torch.optim.lr_scheduler.StepLR(optim, step_size=50, gamma=0.9) 238 | optim_dict = {'optimizer': optim, 'lr_scheduler': StepLR} 239 | return optim_dict 240 | -------------------------------------------------------------------------------- /HIST2ST_train.py: -------------------------------------------------------------------------------- 1 | import os 2 | import torch 3 | import random 4 | import argparse 5 | import pickle as pk 6 | import pytorch_lightning as pl 7 | from utils import * 8 | from HIST2ST import * 9 | from predict import * 10 | from torch.utils.data import DataLoader 11 | from pytorch_lightning.loggers import TensorBoardLogger 12 | 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument('--gpu', type=int, default=2, help='the id of gpu.') 15 | parser.add_argument('--fold', type=int, default=5, help='dataset fold.') 16 | parser.add_argument('--seed', type=int, default=12000, help='random seed.') 17 | parser.add_argument('--epochs', type=int, default=350, help='number of epochs.') 18 | parser.add_argument('--name', type=str, default='hist2ST', help='prefix name.') 19 | parser.add_argument('--data', type=str, default='her2st', help='dataset name:{"her2st","cscc"}.') 20 | parser.add_argument('--logger', type=str, default='../logs/my_logs', help='logger path.') 21 | parser.add_argument('--lr', type=float, default=1e-5, help='learning rate.') 22 | parser.add_argument('--dropout', type=float, default=0.2, help='dropout.') 23 | 24 | parser.add_argument('--bake', type=int, default=5, help='the number of augmented images.') 25 | parser.add_argument('--lamb', type=float, default=0.5, help='the loss coef of self-distillation.') 26 | 27 | 28 | parser.add_argument('--nb', type=str, default='F', help='zinb or nb loss.') 29 | parser.add_argument('--zinb', type=float, default=0.25, help='the loss coef of zinb.') 30 | 31 | parser.add_argument('--prune', type=str, default='Grid', help='how to prune the edge:{"Grid","NA"}') 32 | parser.add_argument('--policy', type=str, default='mean', help='the aggregation way in the GNN .') 33 | parser.add_argument('--neighbor', type=int, default=4, help='the number of neighbors in the GNN.') 34 | 35 | parser.add_argument('--tag', type=str, default='5-7-2-8-4-16-32', 36 | help='hyper params: kernel-patch-depth1-depth2-depth3-heads-channel,' 37 | 'depth1-depth2-depth3 are the depth of Convmixer, Multi-head layer in Transformer, and GNN, respectively' 38 | 'patch is the value of kernel_size and stride in the path embedding layer of Convmixer' 39 | 'kernel is the kernel_size in the depthwise of Convmixer module' 40 | 'heads are the number of attention heads in the Multi-head layer' 41 | 'channel is the value of the input and output channel of depthwise and pointwise. ') 42 | 43 | args = parser.parse_args() 44 | random.seed(args.seed) 45 | np.random.seed(args.seed) 46 | torch.manual_seed(args.seed) 47 | torch.cuda.manual_seed(args.seed) 48 | torch.cuda.manual_seed_all(args.seed) 49 | torch.backends.cudnn.benchmark = False 50 | torch.backends.cudnn.deterministic = True 51 | kernel,patch,depth1,depth2,depth3,heads,channel=map(lambda x:int(x),args.tag.split('-')) 52 | 53 | trainset = pk_load(args.fold,'train',False,args.data,neighs=args.neighbor, prune=args.prune) 54 | train_loader = DataLoader(trainset, batch_size=1, num_workers=0, shuffle=True) 55 | testset = pk_load(args.fold,'test',False,args.data,neighs=args.neighbor, prune=args.prune) 56 | test_loader = DataLoader(testset, batch_size=1, num_workers=0, shuffle=False) 57 | label=None 58 | if args.fold in [5,11,17,23,26,30] and args.data=='her2st': 59 | label=testset.label[testset.names[0]] 60 | 61 | genes=785 62 | if args.data=='cscc': 63 | args.name+='_cscc' 64 | genes=171 65 | 66 | log_name='' 67 | if args.zinb>0: 68 | if args.nb=='T': 69 | args.name+='_nb' 70 | else: 71 | args.name+='_zinb' 72 | log_name+=f'-{args.zinb}' 73 | if args.bake>0: 74 | args.name+='_bake' 75 | log_name+=f'-{args.bake}-{args.lamb}' 76 | log_name=f'{args.fold}-{args.name}-{args.tag}'+log_name+f'-{args.policy}-{args.neighbor}' 77 | logger = TensorBoardLogger( 78 | args.logger, 79 | name=log_name 80 | ) 81 | print(log_name) 82 | 83 | model = Hist2ST( 84 | depth1=depth1, depth2=depth2, depth3=depth3, 85 | n_genes=genes, learning_rate=args.lr, label=label, 86 | kernel_size=kernel, patch_size=patch, 87 | heads=heads, channel=channel, dropout=args.dropout, 88 | zinb=args.zinb, nb=args.nb=='T', 89 | bake=args.bake, lamb=args.lamb, 90 | policy=args.policy, 91 | ) 92 | trainer = pl.Trainer( 93 | gpus=[args.gpu], max_epochs=args.epochs, 94 | logger=logger,check_val_every_n_epoch=2, 95 | ) 96 | 97 | trainer.fit(model, train_loader, test_loader) 98 | torch.save(model.state_dict(),f"./model/{args.fold}-Hist2ST{'_cscc' if args.data=='cscc' else ''}.ckpt") 99 | # model.load_state_dict(torch.load(f"./model/{args.fold}-Hist2ST{'_cscc' if args.data=='cscc' else ''}.ckpt"),) 100 | pred, gt = test(model, test_loader,'cuda') 101 | R=get_R(pred,gt)[0] 102 | print('Pearson Correlation:',np.nanmean(R)) 103 | clus,ARI=cluster(pred,label) 104 | print('ARI:',ARI) 105 | -------------------------------------------------------------------------------- /NB_module.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | import torch.nn as nn 4 | import torch.nn.functional as F 5 | 6 | class MeanAct(nn.Module): 7 | def __init__(self): 8 | super(MeanAct, self).__init__() 9 | def forward(self, x): 10 | return torch.clamp(torch.exp(x), min=1e-5, max=1e6) 11 | 12 | class DispAct(nn.Module): 13 | def __init__(self): 14 | super(DispAct, self).__init__() 15 | def forward(self, x): 16 | return torch.clamp(F.softplus(x), min=1e-4, max=1e4) 17 | 18 | def NB_loss(x, h_r, h_p): 19 | 20 | ll = torch.lgamma(torch.exp(h_r) + x) - torch.lgamma(torch.exp(h_r)) 21 | ll += h_p * x - torch.log(torch.exp(h_p) + 1) * (x + torch.exp(h_r)) 22 | 23 | loss = -torch.mean(torch.sum(ll, axis=-1)) 24 | return loss 25 | 26 | def ZINB_loss(x, mean, disp, pi, scale_factor=1.0, ridge_lambda=0.0): 27 | eps = 1e-10 28 | if isinstance(scale_factor,float): 29 | scale_factor=np.full((len(mean),),scale_factor) 30 | scale_factor = scale_factor[:, None] 31 | mean = mean * scale_factor 32 | 33 | t1 = torch.lgamma(disp+eps) + torch.lgamma(x+1.0) - torch.lgamma(x+disp+eps) 34 | t2 = (disp+x) * torch.log(1.0 + (mean/(disp+eps))) + (x * (torch.log(disp+eps) - torch.log(mean+eps))) 35 | nb_final = t1 + t2 36 | 37 | nb_case = nb_final - torch.log(1.0-pi+eps) 38 | zero_nb = torch.pow(disp/(disp+mean+eps), disp) 39 | zero_case = -torch.log(pi + ((1.0-pi)*zero_nb)+eps) 40 | result = torch.where(torch.le(x, 1e-8), zero_case, nb_case) 41 | 42 | if ridge_lambda > 0: 43 | ridge = ridge_lambda*torch.square(pi) 44 | result += ridge 45 | result = torch.mean(result) 46 | return result -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spatial Transcriptomics Prediction from Histology jointly through Transformer and Graph Neural Networks 2 | ### Yuansong Zeng, Zhuoyi Wei, Weijiang Yu, Rui Yin, Bingling Li, Zhonghui Tang, Yutong Lu, Yuedong Yang* 3 | 4 | 5 | Here, we have developed Hist2ST, a deep learning-based model using histology images to predict RNA-seq expression. 6 | At each sequenced spot, the corre-sponding histology image is cropped into an image patch, from which 2D vision 7 | features are learned through convolutional operations. Meanwhile, the spatial relations with the whole image and 8 | neighbored patches are captured through Transformer and graph neural network modules, respectively. These learned 9 | features are then used to predict the gene expression by following the zero-inflated negative binomial (ZINB) distribution. 10 | To alleviate the impact by the small spatial transcriptomics data, a self-distillation mechanism is employed for efficient 11 | learning of the model. Hist2ST was tested on the HER2-positive breast cancer and the cutaneous squamous cell carcinoma datasets, 12 | and shown to outperform existing methods in terms of both gene expression prediction and following spatial region identification. 13 | 14 | 15 | 16 | ![(Variational) gcn](Workflow.png) 17 | 18 | 19 | 20 | # Usage 21 | ```python 22 | import torch 23 | from HIST2ST import Hist2ST 24 | 25 | model = Hist2ST( 26 | depth1=2, depth2=8, depth3=4, 27 | n_genes=785, learning_rate=1e-5, 28 | kernel_size=5, patch_size=7, fig_size=112, 29 | heads=16, channel=32, dropout=0.2, 30 | zinb=0.25, nb=False, 31 | bake=5, lamb=0.5, 32 | policy='mean', 33 | ) 34 | 35 | # patches: [N, 3, W, H] 36 | # coordinates: [N, 2] 37 | # adjacency: [N, N] 38 | pred_expression = model(patches, coordinates,adjacency) # [N, n_genes] 39 | 40 | ``` 41 | 42 | Note: the detailed parameters instructions please see [HIST2ST_train](https://github.com/biomed-AI/Hist2ST/blob/main/HIST2ST_train.py) 43 | 44 | 45 | ## System environment 46 | Required package: 47 | - PyTorch >= 1.10 48 | - pytorch-lightning >= 1.4 49 | - scanpy >= 1.8 50 | - python >=3.7 51 | - tensorboard 52 | 53 | 54 | # Hist2ST pipeline 55 | 56 | See [tutorial.ipynb](tutorial.ipynb) 57 | 58 | 59 | NOTE: Run the following command if you want to run the script tutorial.ipynb 60 | 61 | 1. Please run the script `download.sh` in the folder [data](https://github.com/biomed-AI/Hist2ST/tree/main/data) 62 | 63 | or 64 | 65 | Run the command line `git clone https://github.com/almaan/her2st.git` in the dir [data](https://github.com/biomed-AI/Hist2ST/tree/main/data) 66 | 67 | 2. Run `gunzip *.gz` in the dir `Hist2ST/data/her2st/data/ST-cnts/` to unzip the gz files 68 | 69 | 70 | # Datasets 71 | 72 | - human HER2-positive breast tumor ST data https://github.com/almaan/her2st/. 73 | - human cutaneous squamous cell carcinoma 10x Visium data (GSE144240). 74 | - you can also download all datasets from [here](https://www.synapse.org/#!Synapse:syn29738084/files/) 75 | 76 | 77 | # Trained models 78 | All Trained models of our method on HER2+ and cSCC datasets can be found at [synapse](https://www.synapse.org/#!Synapse:syn29738084/files/) 79 | 80 | 81 | # Citation 82 | 83 | Please cite our paper: 84 | 85 | ``` 86 | 87 | @article{zengys, 88 | title={Spatial Transcriptomics Prediction from Histology jointly through Transformer and Graph Neural Networks}, 89 | author={ Yuansong Zeng, Zhuoyi Wei, Weijiang Yu, Rui Yin, Bingling Li, Zhonghui Tang, Yutong Lu, Yuedong Yang}, 90 | journal={biorxiv}, 91 | year={2021} 92 | publisher={Cold Spring Harbor Laboratory} 93 | } 94 | 95 | ``` 96 | -------------------------------------------------------------------------------- /Workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biomed-AI/Hist2ST/7480e5d1f7712282e6c96d8430684e7830517769/Workflow.png -------------------------------------------------------------------------------- /data/download.sh: -------------------------------------------------------------------------------- 1 | git clone https://github.com/almaan/her2st.git -------------------------------------------------------------------------------- /data/her_hvg_cut_1000.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biomed-AI/Hist2ST/7480e5d1f7712282e6c96d8430684e7830517769/data/her_hvg_cut_1000.npy -------------------------------------------------------------------------------- /data/skin_hvg_cut_1000.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biomed-AI/Hist2ST/7480e5d1f7712282e6c96d8430684e7830517769/data/skin_hvg_cut_1000.npy -------------------------------------------------------------------------------- /dataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import torch 4 | import torchvision 5 | import numpy as np 6 | import scanpy as sc 7 | import pandas as pd 8 | import scprep as scp 9 | import anndata as ad 10 | import seaborn as sns 11 | import torch.nn as nn 12 | import torch.optim as optim 13 | import matplotlib.pyplot as plt 14 | import torch.nn.functional as F 15 | import torchvision.transforms as transforms 16 | from PIL import ImageFile, Image 17 | from utils import read_tiff, get_data 18 | from graph_construction import calcADJ 19 | from collections import defaultdict as dfd 20 | ImageFile.LOAD_TRUNCATED_IMAGES = True 21 | Image.MAX_IMAGE_PIXELS = None 22 | 23 | class ViT_HER2ST(torch.utils.data.Dataset): 24 | """Some Information about HER2ST""" 25 | def __init__(self,train=True,fold=0,r=4,flatten=True,ori=False,adj=False,prune='Grid',neighs=4): 26 | super(ViT_HER2ST, self).__init__() 27 | 28 | self.cnt_dir = 'data/her2st/data/ST-cnts' 29 | self.img_dir = 'data/her2st/data/ST-imgs' 30 | self.pos_dir = 'data/her2st/data/ST-spotfiles' 31 | self.lbl_dir = 'data/her2st/data/ST-pat/lbl' 32 | self.r = 224//r 33 | 34 | # gene_list = list(np.load('data/her_hvg.npy',allow_pickle=True)) 35 | gene_list = list(np.load('data/her_hvg_cut_1000.npy',allow_pickle=True)) 36 | self.gene_list = gene_list 37 | names = os.listdir(self.cnt_dir) 38 | names.sort() 39 | names = [i[:2] for i in names] 40 | self.train = train 41 | self.ori = ori 42 | self.adj = adj 43 | # samples = ['A1','B1','C1','D1','E1','F1','G2','H1'] 44 | samples = names[1:33] 45 | 46 | te_names = [samples[fold]] 47 | print(te_names) 48 | tr_names = list(set(samples)-set(te_names)) 49 | 50 | if train: 51 | self.names = tr_names 52 | else: 53 | self.names = te_names 54 | 55 | print('Loading imgs...') 56 | self.img_dict = {i:torch.Tensor(np.array(self.get_img(i))) for i in self.names} 57 | print('Loading metadata...') 58 | self.meta_dict = {i:self.get_meta(i) for i in self.names} 59 | self.label={i:None for i in self.names} 60 | self.lbl2id={ 61 | 'invasive cancer':0, 'breast glands':1, 'immune infiltrate':2, 62 | 'cancer in situ':3, 'connective tissue':4, 'adipose tissue':5, 'undetermined':-1 63 | } 64 | if not train and self.names[0] in ['A1','B1','C1','D1','E1','F1','G2','H1','J1']: 65 | self.lbl_dict={i:self.get_lbl(i) for i in self.names} 66 | # self.label={i:m['label'].values for i,m in self.lbl_dict.items()} 67 | idx=self.meta_dict[self.names[0]].index 68 | lbl=self.lbl_dict[self.names[0]] 69 | lbl=lbl.loc[idx,:]['label'].values 70 | # lbl=torch.Tensor(list(map(lambda i:self.lbl2id[i],lbl))) 71 | self.label[self.names[0]]=lbl 72 | elif train: 73 | for i in self.names: 74 | idx=self.meta_dict[i].index 75 | if i in ['A1','B1','C1','D1','E1','F1','G2','H1','J1']: 76 | lbl=self.get_lbl(i) 77 | lbl=lbl.loc[idx,:]['label'].values 78 | lbl=torch.Tensor(list(map(lambda i:self.lbl2id[i],lbl))) 79 | self.label[i]=lbl 80 | else: 81 | self.label[i]=torch.full((len(idx),),-1) 82 | self.gene_set = list(gene_list) 83 | self.exp_dict = { 84 | i:scp.transform.log(scp.normalize.library_size_normalize(m[self.gene_set].values)) 85 | for i,m in self.meta_dict.items() 86 | } 87 | if self.ori: 88 | self.ori_dict = {i:m[self.gene_set].values for i,m in self.meta_dict.items()} 89 | self.counts_dict={} 90 | for i,m in self.ori_dict.items(): 91 | n_counts=m.sum(1) 92 | sf = n_counts / np.median(n_counts) 93 | self.counts_dict[i]=sf 94 | self.center_dict = { 95 | i:np.floor(m[['pixel_x','pixel_y']].values).astype(int) 96 | for i,m in self.meta_dict.items() 97 | } 98 | self.loc_dict = {i:m[['x','y']].values for i,m in self.meta_dict.items()} 99 | self.adj_dict = { 100 | i:calcADJ(m,neighs,pruneTag=prune) 101 | for i,m in self.loc_dict.items() 102 | } 103 | self.patch_dict=dfd(lambda :None) 104 | self.lengths = [len(i) for i in self.meta_dict.values()] 105 | self.cumlen = np.cumsum(self.lengths) 106 | self.id2name = dict(enumerate(self.names)) 107 | self.flatten=flatten 108 | def __getitem__(self, index): 109 | ID=self.id2name[index] 110 | im = self.img_dict[ID] 111 | im = im.permute(1,0,2) 112 | # im = torch.Tensor(np.array(self.im)) 113 | exps = self.exp_dict[ID] 114 | if self.ori: 115 | oris = self.ori_dict[ID] 116 | sfs = self.counts_dict[ID] 117 | centers = self.center_dict[ID] 118 | loc = self.loc_dict[ID] 119 | adj = self.adj_dict[ID] 120 | patches = self.patch_dict[ID] 121 | positions = torch.LongTensor(loc) 122 | patch_dim = 3 * self.r * self.r * 4 123 | label=self.label[ID] 124 | exps = torch.Tensor(exps) 125 | if patches is None: 126 | n_patches = len(centers) 127 | if self.flatten: 128 | patches = torch.zeros((n_patches,patch_dim)) 129 | else: 130 | patches = torch.zeros((n_patches,3,2*self.r,2*self.r)) 131 | for i in range(n_patches): 132 | center = centers[i] 133 | x, y = center 134 | patch = im[(x-self.r):(x+self.r),(y-self.r):(y+self.r),:] 135 | if self.flatten: 136 | patches[i] = patch.flatten() 137 | else: 138 | patches[i]=patch.permute(2,0,1) 139 | self.patch_dict[ID]=patches 140 | data=[patches, positions, exps] 141 | if self.adj: 142 | data.append(adj) 143 | if self.ori: 144 | data+=[torch.Tensor(oris),torch.Tensor(sfs)] 145 | data.append(torch.Tensor(centers)) 146 | return data 147 | 148 | def __len__(self): 149 | return len(self.exp_dict) 150 | 151 | def get_img(self,name): 152 | pre = self.img_dir+'/'+name[0]+'/'+name 153 | fig_name = os.listdir(pre)[0] 154 | path = pre+'/'+fig_name 155 | im = Image.open(path) 156 | return im 157 | 158 | def get_cnt(self,name): 159 | path = self.cnt_dir+'/'+name+'.tsv' 160 | df = pd.read_csv(path,sep='\t',index_col=0) 161 | 162 | return df 163 | 164 | def get_pos(self,name): 165 | path = self.pos_dir+'/'+name+'_selection.tsv' 166 | # path = self.pos_dir+'/'+name+'_labeled_coordinates.tsv' 167 | df = pd.read_csv(path,sep='\t') 168 | 169 | x = df['x'].values 170 | y = df['y'].values 171 | x = np.around(x).astype(int) 172 | y = np.around(y).astype(int) 173 | id = [] 174 | for i in range(len(x)): 175 | id.append(str(x[i])+'x'+str(y[i])) 176 | df['id'] = id 177 | 178 | return df 179 | 180 | def get_meta(self,name,gene_list=None): 181 | cnt = self.get_cnt(name) 182 | pos = self.get_pos(name) 183 | meta = cnt.join((pos.set_index('id'))) 184 | 185 | return meta 186 | 187 | def get_lbl(self,name): 188 | # path = self.pos_dir+'/'+name+'_selection.tsv' 189 | path = self.lbl_dir+'/'+name+'_labeled_coordinates.tsv' 190 | df = pd.read_csv(path,sep='\t') 191 | 192 | x = df['x'].values 193 | y = df['y'].values 194 | x = np.around(x).astype(int) 195 | y = np.around(y).astype(int) 196 | id = [] 197 | for i in range(len(x)): 198 | id.append(str(x[i])+'x'+str(y[i])) 199 | df['id'] = id 200 | df.drop('pixel_x', inplace=True, axis=1) 201 | df.drop('pixel_y', inplace=True, axis=1) 202 | df.drop('x', inplace=True, axis=1) 203 | df.drop('y', inplace=True, axis=1) 204 | df.set_index('id',inplace=True) 205 | return df 206 | 207 | class ViT_SKIN(torch.utils.data.Dataset): 208 | """Some Information about ViT_SKIN""" 209 | def __init__(self,train=True,r=4,norm=False,fold=0,flatten=True,ori=False,adj=False,prune='NA',neighs=4): 210 | super(ViT_SKIN, self).__init__() 211 | 212 | self.dir = './data/GSE144240_RAW/' 213 | self.r = 224//r 214 | 215 | patients = ['P2', 'P5', 'P9', 'P10'] 216 | reps = ['rep1', 'rep2', 'rep3'] 217 | names = [] 218 | for i in patients: 219 | for j in reps: 220 | names.append(i+'_ST_'+j) 221 | gene_list = list(np.load('data/skin_hvg_cut_1000.npy',allow_pickle=True)) 222 | 223 | self.ori = ori 224 | self.adj = adj 225 | self.norm = norm 226 | self.train = train 227 | self.flatten = flatten 228 | self.gene_list = gene_list 229 | samples = names 230 | te_names = [samples[fold]] 231 | tr_names = list(set(samples)-set(te_names)) 232 | 233 | if train: 234 | self.names = tr_names 235 | else: 236 | self.names = te_names 237 | 238 | print(te_names) 239 | print('Loading imgs...') 240 | self.img_dict = {i:torch.Tensor(np.array(self.get_img(i))) for i in self.names} 241 | print('Loading metadata...') 242 | self.meta_dict = {i:self.get_meta(i) for i in self.names} 243 | 244 | self.gene_set = list(gene_list) 245 | if self.norm: 246 | self.exp_dict = { 247 | i:sc.pp.scale(scp.transform.log(scp.normalize.library_size_normalize(m[self.gene_set].values))) 248 | for i,m in self.meta_dict.items() 249 | } 250 | else: 251 | self.exp_dict = { 252 | i:scp.transform.log(scp.normalize.library_size_normalize(m[self.gene_set].values)) 253 | for i,m in self.meta_dict.items() 254 | } 255 | if self.ori: 256 | self.ori_dict = {i:m[self.gene_set].values for i,m in self.meta_dict.items()} 257 | self.counts_dict={} 258 | for i,m in self.ori_dict.items(): 259 | n_counts=m.sum(1) 260 | sf = n_counts / np.median(n_counts) 261 | self.counts_dict[i]=sf 262 | self.center_dict = { 263 | i:np.floor(m[['pixel_x','pixel_y']].values).astype(int) 264 | for i,m in self.meta_dict.items() 265 | } 266 | self.loc_dict = {i:m[['x','y']].values for i,m in self.meta_dict.items()} 267 | self.adj_dict = { 268 | i:calcADJ(m,neighs,pruneTag=prune) 269 | for i,m in self.loc_dict.items() 270 | } 271 | self.patch_dict=dfd(lambda :None) 272 | self.lengths = [len(i) for i in self.meta_dict.values()] 273 | self.cumlen = np.cumsum(self.lengths) 274 | self.id2name = dict(enumerate(self.names)) 275 | 276 | 277 | def filter_helper(self): 278 | a = np.zeros(len(self.gene_list)) 279 | n = 0 280 | for i,exp in self.exp_dict.items(): 281 | n += exp.shape[0] 282 | exp[exp>0] = 1 283 | for j in range((len(self.gene_list))): 284 | a[j] += np.sum(exp[:,j]) 285 | 286 | 287 | def __getitem__(self, index): 288 | ID=self.id2name[index] 289 | im = self.img_dict[ID].permute(1,0,2) 290 | 291 | exps = self.exp_dict[ID] 292 | if self.ori: 293 | oris = self.ori_dict[ID] 294 | sfs = self.counts_dict[ID] 295 | adj=self.adj_dict[ID] 296 | centers = self.center_dict[ID] 297 | loc = self.loc_dict[ID] 298 | patches = self.patch_dict[ID] 299 | positions = torch.LongTensor(loc) 300 | patch_dim = 3 * self.r * self.r * 4 301 | exps = torch.Tensor(exps) 302 | if patches is None: 303 | n_patches = len(centers) 304 | if self.flatten: 305 | patches = torch.zeros((n_patches,patch_dim)) 306 | else: 307 | patches = torch.zeros((n_patches,3,2*self.r,2*self.r)) 308 | 309 | for i in range(n_patches): 310 | center = centers[i] 311 | x, y = center 312 | patch = im[(x-self.r):(x+self.r),(y-self.r):(y+self.r),:] 313 | if self.flatten: 314 | patches[i] = patch.flatten() 315 | else: 316 | patches[i]=patch.permute(2,0,1) 317 | self.patch_dict[ID]=patches 318 | data=[patches, positions, exps] 319 | if self.adj: 320 | data.append(adj) 321 | if self.ori: 322 | data+=[torch.Tensor(oris),torch.Tensor(sfs)] 323 | data.append(torch.Tensor(centers)) 324 | return data 325 | 326 | def __len__(self): 327 | return len(self.exp_dict) 328 | 329 | def get_img(self,name): 330 | path = glob.glob(self.dir+'*'+name+'.jpg')[0] 331 | im = Image.open(path) 332 | return im 333 | 334 | def get_cnt(self,name): 335 | path = glob.glob(self.dir+'*'+name+'_stdata.tsv')[0] 336 | df = pd.read_csv(path,sep='\t',index_col=0) 337 | return df 338 | 339 | def get_pos(self,name): 340 | path = glob.glob(self.dir+'*spot*'+name+'.tsv')[0] 341 | df = pd.read_csv(path,sep='\t') 342 | 343 | x = df['x'].values 344 | y = df['y'].values 345 | x = np.around(x).astype(int) 346 | y = np.around(y).astype(int) 347 | id = [] 348 | for i in range(len(x)): 349 | id.append(str(x[i])+'x'+str(y[i])) 350 | df['id'] = id 351 | 352 | return df 353 | 354 | def get_meta(self,name,gene_list=None): 355 | cnt = self.get_cnt(name) 356 | pos = self.get_pos(name) 357 | meta = cnt.join(pos.set_index('id'),how='inner') 358 | 359 | return meta 360 | 361 | def get_overlap(self,meta_dict,gene_list): 362 | gene_set = set(gene_list) 363 | for i in meta_dict.values(): 364 | gene_set = gene_set&set(i.columns) 365 | return list(gene_set) -------------------------------------------------------------------------------- /gcn.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | from torch.nn import init 5 | from torch.autograd import Variable 6 | 7 | import time 8 | import random 9 | import numpy as np 10 | from collections import defaultdict 11 | 12 | class gs_block(nn.Module): 13 | def __init__( 14 | self, feature_dim, embed_dim, 15 | policy='mean', gcn=False, num_sample=10 16 | ): 17 | super().__init__() 18 | self.gcn = gcn 19 | self.policy=policy 20 | self.embed_dim = embed_dim 21 | self.feat_dim = feature_dim 22 | self.num_sample = num_sample 23 | self.weight = nn.Parameter(torch.FloatTensor( 24 | embed_dim, 25 | self.feat_dim if self.gcn else 2*self.feat_dim 26 | )) 27 | init.xavier_uniform_(self.weight) 28 | 29 | def forward(self, x, Adj): 30 | neigh_feats = self.aggregate(x, Adj) 31 | if not self.gcn: 32 | combined = torch.cat([x, neigh_feats], dim=1) 33 | else: 34 | combined = neigh_feats 35 | combined = F.relu(self.weight.mm(combined.T)).T 36 | combined = F.normalize(combined,2,1) 37 | return combined 38 | def aggregate(self,x, Adj): 39 | adj=Variable(Adj).to(Adj.device) 40 | if not self.gcn: 41 | n=len(adj) 42 | adj = adj-torch.eye(n).to(adj.device) 43 | if self.policy=='mean': 44 | num_neigh = adj.sum(1, keepdim=True) 45 | mask = adj.div(num_neigh) 46 | to_feats = mask.mm(x) 47 | elif self.policy=='max': 48 | indexs = [i.nonzero() for i in adj==1] 49 | to_feats = [] 50 | for feat in [x[i.squeeze()] for i in indexs]: 51 | if len(feat.size()) == 1: 52 | to_feats.append(feat.view(1, -1)) 53 | else: 54 | to_feats.append(torch.max(feat,0)[0].view(1, -1)) 55 | to_feats = torch.cat(to_feats, 0) 56 | return to_feats -------------------------------------------------------------------------------- /graph_construction.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | from scipy.spatial import distance_matrix, minkowski_distance, distance 4 | def calcADJ(coord, k=8, distanceType='euclidean', pruneTag='NA'): 5 | r""" 6 | Calculate spatial Matrix directly use X/Y coordinates 7 | """ 8 | spatialMatrix=coord#.cpu().numpy() 9 | nodes=spatialMatrix.shape[0] 10 | Adj=torch.zeros((nodes,nodes)) 11 | for i in np.arange(spatialMatrix.shape[0]): 12 | tmp=spatialMatrix[i,:].reshape(1,-1) 13 | distMat = distance.cdist(tmp,spatialMatrix, distanceType) 14 | if k == 0: 15 | k = spatialMatrix.shape[0]-1 16 | res = distMat.argsort()[:k+1] 17 | tmpdist = distMat[0,res[0][1:k+1]] 18 | boundary = np.mean(tmpdist)+np.std(tmpdist) #optional 19 | for j in np.arange(1,k+1): 20 | # No prune 21 | if pruneTag == 'NA': 22 | Adj[i][res[0][j]]=1.0 23 | elif pruneTag == 'STD': 24 | if distMat[0,res[0][j]]<=boundary: 25 | Adj[i][res[0][j]]=1.0 26 | # Prune: only use nearest neighbor as exact grid: 6 in cityblock, 8 in euclidean 27 | elif pruneTag == 'Grid': 28 | if distMat[0,res[0][j]]<=2.0: 29 | Adj[i][res[0][j]]=1.0 30 | return Adj -------------------------------------------------------------------------------- /predict.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | import scanpy as sc 4 | import anndata as ad 5 | from tqdm import tqdm 6 | from dataset import ViT_HER2ST, ViT_SKIN 7 | from scipy.stats import pearsonr,spearmanr 8 | from sklearn.cluster import KMeans 9 | from sklearn.metrics import adjusted_rand_score as ari_score 10 | from sklearn.metrics.cluster import normalized_mutual_info_score as nmi_score 11 | def pk_load(fold,mode='train',flatten=False,dataset='her2st',r=4,ori=True,adj=True,prune='Grid',neighs=4): 12 | assert dataset in ['her2st','cscc'] 13 | if dataset=='her2st': 14 | dataset = ViT_HER2ST( 15 | train=(mode=='train'),fold=fold,flatten=flatten, 16 | ori=ori,neighs=neighs,adj=adj,prune=prune,r=r 17 | ) 18 | elif dataset=='cscc': 19 | dataset = ViT_SKIN( 20 | train=(mode=='train'),fold=fold,flatten=flatten, 21 | ori=ori,neighs=neighs,adj=adj,prune=prune,r=r 22 | ) 23 | return dataset 24 | def test(model,test,device='cuda'): 25 | model=model.to(device) 26 | model.eval() 27 | preds=None 28 | ct=None 29 | gt=None 30 | loss=0 31 | with torch.no_grad(): 32 | for patch, position, exp, adj, *_, center in tqdm(test): 33 | patch, position, adj = patch.to(device), position.to(device), adj.to(device).squeeze(0) 34 | pred = model(patch, position, adj)[0] 35 | preds = pred.squeeze().cpu().numpy() 36 | ct = center.squeeze().cpu().numpy() 37 | gt = exp.squeeze().cpu().numpy() 38 | adata = ad.AnnData(preds) 39 | adata.obsm['spatial'] = ct 40 | adata_gt = ad.AnnData(gt) 41 | adata_gt.obsm['spatial'] = ct 42 | return adata,adata_gt 43 | def cluster(adata,label): 44 | idx=label!='undetermined' 45 | tmp=adata[idx] 46 | l=label[idx] 47 | sc.pp.pca(tmp) 48 | sc.tl.tsne(tmp) 49 | kmeans = KMeans(n_clusters=len(set(l)), init="k-means++", random_state=0).fit(tmp.obsm['X_pca']) 50 | p=kmeans.labels_.astype(str) 51 | lbl=np.full(len(adata),str(len(set(l)))) 52 | lbl[idx]=p 53 | adata.obs['kmeans']=lbl 54 | return p,round(ari_score(p,l),3) 55 | def get_R(data1,data2,dim=1,func=pearsonr): 56 | adata1=data1.X 57 | adata2=data2.X 58 | r1,p1=[],[] 59 | for g in range(data1.shape[dim]): 60 | if dim==1: 61 | r,pv=func(adata1[:,g],adata2[:,g]) 62 | elif dim==0: 63 | r,pv=func(adata1[g,:],adata2[g,:]) 64 | r1.append(r) 65 | p1.append(pv) 66 | r1=np.array(r1) 67 | p1=np.array(p1) 68 | return r1,p1 69 | -------------------------------------------------------------------------------- /run_trained_models.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "053309c2-cb6d-4623-83b7-7b0d947998e6", 7 | "metadata": {}, 8 | "outputs": [ 9 | { 10 | "name": "stdout", 11 | "output_type": "stream", 12 | "text": [ 13 | "[easydl] tensorflow not available!\n" 14 | ] 15 | } 16 | ], 17 | "source": [ 18 | "import torch\n", 19 | "import numpy as np\n", 20 | "import pytorch_lightning as pl\n", 21 | "import torchvision.transforms as tf\n", 22 | "from tqdm import tqdm\n", 23 | "from predict import *\n", 24 | "from HIST2ST import *\n", 25 | "from dataset import ViT_HER2ST, ViT_SKIN\n", 26 | "from scipy.stats import pearsonr,spearmanr\n", 27 | "from torch.utils.data import DataLoader\n", 28 | "from pytorch_lightning.loggers import TensorBoardLogger\n", 29 | "from copy import deepcopy as dcp\n", 30 | "from collections import defaultdict as dfd\n", 31 | "from sklearn.metrics import adjusted_rand_score as ari_score\n", 32 | "from sklearn.metrics.cluster import normalized_mutual_info_score as nmi_score" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "id": "b132bfb4-bd29-44fa-bbca-3790b0848fd4", 38 | "metadata": {}, 39 | "source": [ 40 | "# Data Loading" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 2, 46 | "id": "6281d8f2-e5f7-4c26-88bf-5256197b1418", 47 | "metadata": { 48 | "tags": [] 49 | }, 50 | "outputs": [], 51 | "source": [ 52 | "name=[*[f'A{i}' for i in range(2,7)],*[f'B{i}' for i in range(1,7)],\n", 53 | " *[f'C{i}' for i in range(1,7)],*[f'D{i}' for i in range(1,7)],\n", 54 | " *[f'E{i}' for i in range(1,4)],*[f'F{i}' for i in range(1,4)],*[f'G{i}' for i in range(1,4)]]\n", 55 | "patients = ['P2', 'P5', 'P9', 'P10']\n", 56 | "reps = ['rep1', 'rep2', 'rep3']\n", 57 | "skinname = []\n", 58 | "for i in patients:\n", 59 | " for j in reps:\n", 60 | " skinname.append(i+'_ST_'+j)\n", 61 | "device='cuda'\n", 62 | "tag='5-7-2-8-4-16-32'\n", 63 | "k,p,d1,d2,d3,h,c=map(lambda x:int(x),tag.split('-'))\n", 64 | "dropout=0.2\n", 65 | "random.seed(12000)\n", 66 | "np.random.seed(12000)\n", 67 | "torch.manual_seed(12000)\n", 68 | "torch.cuda.manual_seed(12000)\n", 69 | "torch.cuda.manual_seed_all(12000) \n", 70 | "torch.backends.cudnn.benchmark = False\n", 71 | "torch.backends.cudnn.deterministic = True" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "source": [ 77 | "\n", 78 | "# Hist2ST Prediction\n", 79 | "\n", 80 | "### To run the trained model, please select the trained model and replace the value of the variable fold with the number in the name of the selected trained model." 81 | ], 82 | "metadata": { 83 | "collapsed": false 84 | } 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 6, 89 | "id": "0f19d5d8-6510-493b-9287-ba7215155dc1", 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [ 93 | "fold=5\n", 94 | "data='her2st'\n", 95 | "prune='Grid' if data=='her2st' else 'NA'\n", 96 | "genes=171 if data=='cscc' else 785" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 7, 102 | "id": "76cfcfb9-3169-46da-9288-2e2be7942d84", 103 | "metadata": {}, 104 | "outputs": [ 105 | { 106 | "name": "stdout", 107 | "output_type": "stream", 108 | "text": [ 109 | "['B1']\n", 110 | "Loading imgs...\n", 111 | "Loading metadata...\n" 112 | ] 113 | }, 114 | { 115 | "name": "stderr", 116 | "output_type": "stream", 117 | "text": [ 118 | "100%|██████████████████████████████████| 1/1 [00:02<00:00, 2.85s/it]\n" 119 | ] 120 | } 121 | ], 122 | "source": [ 123 | "testset = pk_load(fold,'test',dataset=data,flatten=False,adj=True,ori=True,prune=prune)\n", 124 | "test_loader = DataLoader(testset, batch_size=1, num_workers=0, shuffle=False)\n", 125 | "label=testset.label[testset.names[0]]\n", 126 | "genes=785\n", 127 | "model=Hist2ST(\n", 128 | " depth1=d1, depth2=d2,depth3=d3,n_genes=genes, \n", 129 | " kernel_size=k, patch_size=p,\n", 130 | " heads=h, channel=c, dropout=0.2,\n", 131 | " zinb=0.25, nb=False,\n", 132 | " bake=5, lamb=0.5, \n", 133 | ")\n", 134 | "model.load_state_dict(torch.load(f'./model/{fold}-Hist2ST.ckpt'))\n", 135 | "pred, gt = test(model, test_loader,'cuda')" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": 7, 141 | "id": "4266cce3-6bc7-4f5b-81d5-d03d3ae089c4", 142 | "metadata": { 143 | "pycharm": { 144 | "name": "#%%\n" 145 | } 146 | }, 147 | "outputs": [ 148 | { 149 | "name": "stdout", 150 | "output_type": "stream", 151 | "text": [ 152 | "Pearson Correlation: 0.2887870599966082\n", 153 | "ARI: 0.431\n" 154 | ] 155 | } 156 | ], 157 | "source": [ 158 | "R=get_R(pred,gt)[0]\n", 159 | "print('Pearson Correlation:',np.nanmean(R))\n", 160 | "\n", 161 | "\n", 162 | "# clus,ARI=cluster(pred,label)\n", 163 | "# print('ARI:',ARI)\n" 164 | ] 165 | } 166 | ], 167 | "metadata": { 168 | "kernelspec": { 169 | "display_name": "Python [conda env:task]", 170 | "language": "python", 171 | "name": "conda-env-task-py" 172 | }, 173 | "language_info": { 174 | "codemirror_mode": { 175 | "name": "ipython", 176 | "version": 3 177 | }, 178 | "file_extension": ".py", 179 | "mimetype": "text/x-python", 180 | "name": "python", 181 | "nbconvert_exporter": "python", 182 | "pygments_lexer": "ipython3", 183 | "version": "3.7.0" 184 | }, 185 | "pycharm": { 186 | "stem_cell": { 187 | "cell_type": "raw", 188 | "source": [], 189 | "metadata": { 190 | "collapsed": false 191 | } 192 | } 193 | } 194 | }, 195 | "nbformat": 4, 196 | "nbformat_minor": 5 197 | } -------------------------------------------------------------------------------- /transformer.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | import pandas as pd 4 | import scanpy as sc 5 | import torch.nn.functional as F 6 | from easydl import * 7 | from anndata import AnnData 8 | from torch import nn, einsum 9 | from scipy.stats import pearsonr 10 | from torch.autograd import Function 11 | from torch.autograd.variable import * 12 | from einops import rearrange, repeat 13 | from einops.layers.torch import Rearrange 14 | class SelectItem(nn.Module): 15 | def __init__(self, item_index): 16 | super(SelectItem, self).__init__() 17 | self.item_index = item_index 18 | 19 | def forward(self, inputs): 20 | return inputs[self.item_index] 21 | class PreNorm(nn.Module): 22 | def __init__(self, dim, fn): 23 | super().__init__() 24 | self.norm = nn.LayerNorm(dim) 25 | self.fn = fn 26 | def forward(self, x, **kwargs): 27 | return self.fn(self.norm(x), **kwargs) 28 | 29 | class FeedForward(nn.Module): 30 | def __init__(self, dim, hidden_dim, dropout = 0.): 31 | super().__init__() 32 | self.net = nn.Sequential( 33 | nn.Linear(dim, hidden_dim), 34 | nn.GELU(), 35 | nn.Dropout(dropout), 36 | nn.Linear(hidden_dim, dim), 37 | nn.Dropout(dropout) 38 | ) 39 | def forward(self, x): 40 | return self.net(x) 41 | 42 | class Attention(nn.Module): 43 | def __init__(self, dim, heads = 8, dim_head = 64, dropout = 0.): 44 | super().__init__() 45 | inner_dim = dim_head * heads 46 | project_out = not (heads == 1 and dim_head == dim) 47 | 48 | self.heads = heads 49 | self.scale = dim_head ** -0.5 50 | 51 | self.attend = nn.Softmax(dim = -1) 52 | self.to_qkv = nn.Linear(dim, inner_dim * 3, bias = False) 53 | 54 | self.to_out = nn.Sequential( 55 | nn.Linear(inner_dim, dim), 56 | nn.Dropout(dropout) 57 | ) if project_out else nn.Identity() 58 | 59 | # @get_local('attn') 60 | def forward(self, x): 61 | b, n, _, h = *x.shape, self.heads 62 | qkv = self.to_qkv(x).chunk(3, dim = -1) 63 | q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h = h), qkv) 64 | dots = einsum('b h i d, b h j d -> b h i j', q, k) * self.scale 65 | attn = self.attend(dots) 66 | out = einsum('b h i j, b h j d -> b h i d', attn, v) 67 | out = rearrange(out, 'b h n d -> b n (h d)') 68 | return self.to_out(out) 69 | 70 | class attn_block(nn.Module): 71 | def __init__(self, dim, heads, dim_head, mlp_dim, dropout = 0.): 72 | super().__init__() 73 | self.attn=PreNorm(dim, Attention(dim, heads = heads, dim_head = dim_head, dropout = dropout)) 74 | self.ff=PreNorm(dim, FeedForward(dim, mlp_dim, dropout = dropout)) 75 | def forward(self, x): 76 | x = self.attn(x) + x 77 | x = self.ff(x) + x 78 | return x 79 | -------------------------------------------------------------------------------- /tutorial.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "053309c2-cb6d-4623-83b7-7b0d947998e6", 7 | "metadata": {}, 8 | "outputs": [ 9 | { 10 | "name": "stdout", 11 | "output_type": "stream", 12 | "text": [ 13 | "[easydl] tensorflow not available!\n" 14 | ] 15 | } 16 | ], 17 | "source": [ 18 | "import torch\n", 19 | "import numpy as np\n", 20 | "import pytorch_lightning as pl\n", 21 | "import torchvision.transforms as tf\n", 22 | "from tqdm import tqdm\n", 23 | "from predict import *\n", 24 | "from HIST2ST import *\n", 25 | "from dataset import ViT_HER2ST, ViT_SKIN\n", 26 | "from scipy.stats import pearsonr,spearmanr\n", 27 | "from torch.utils.data import DataLoader\n", 28 | "from pytorch_lightning.loggers import TensorBoardLogger\n", 29 | "from copy import deepcopy as dcp\n", 30 | "from collections import defaultdict as dfd\n", 31 | "from sklearn.metrics import adjusted_rand_score as ari_score\n", 32 | "from sklearn.metrics.cluster import normalized_mutual_info_score as nmi_score" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "id": "b132bfb4-bd29-44fa-bbca-3790b0848fd4", 38 | "metadata": {}, 39 | "source": [ 40 | "# Data Loading" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 2, 46 | "id": "6281d8f2-e5f7-4c26-88bf-5256197b1418", 47 | "metadata": { 48 | "tags": [] 49 | }, 50 | "outputs": [], 51 | "source": [ 52 | "name=[*[f'A{i}' for i in range(2,7)],*[f'B{i}' for i in range(1,7)],\n", 53 | " *[f'C{i}' for i in range(1,7)],*[f'D{i}' for i in range(1,7)],\n", 54 | " *[f'E{i}' for i in range(1,4)],*[f'F{i}' for i in range(1,4)],*[f'G{i}' for i in range(1,4)]]\n", 55 | "patients = ['P2', 'P5', 'P9', 'P10']\n", 56 | "reps = ['rep1', 'rep2', 'rep3']\n", 57 | "skinname = []\n", 58 | "for i in patients:\n", 59 | " for j in reps:\n", 60 | " skinname.append(i+'_ST_'+j)\n", 61 | "device='cuda'\n", 62 | "tag='5-7-2-8-4-16-32'\n", 63 | "k,p,d1,d2,d3,h,c=map(lambda x:int(x),tag.split('-'))\n", 64 | "dropout=0.2\n", 65 | "random.seed(12000)\n", 66 | "np.random.seed(12000)\n", 67 | "torch.manual_seed(12000)\n", 68 | "torch.cuda.manual_seed(12000)\n", 69 | "torch.cuda.manual_seed_all(12000) \n", 70 | "torch.backends.cudnn.benchmark = False\n", 71 | "torch.backends.cudnn.deterministic = True" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 3, 77 | "id": "0030c0ad-e445-42a5-97b1-135a222f5e82", 78 | "metadata": {}, 79 | "outputs": [ 80 | { 81 | "name": "stdout", 82 | "output_type": "stream", 83 | "text": [ 84 | "['B1']\n", 85 | "Loading imgs...\n", 86 | "Loading metadata...\n" 87 | ] 88 | } 89 | ], 90 | "source": [ 91 | "fold=5\n", 92 | "data='her2st'\n", 93 | "prune='Grid' if data=='her2st' else 'NA'\n", 94 | "genes=171 if data=='cscc' else 785\n", 95 | "trainset = pk_load(fold,'train',dataset=data,flatten=False,adj=True,ori=True,prune=prune)\n", 96 | "train_loader = DataLoader(trainset, batch_size=1, num_workers=0, shuffle=True)" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "id": "2febd49c-f377-4211-9133-39da1ba5909e", 102 | "metadata": {}, 103 | "source": [ 104 | "# Hist2ST training" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 4, 110 | "id": "82bc6cee-8b9a-4c43-80b5-1c68feb3c550", 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [ 114 | "model=Hist2ST(\n", 115 | " depth1=d1, depth2=d2,depth3=d3,n_genes=genes,\n", 116 | " kernel_size=k, patch_size=p,\n", 117 | " heads=h, channel=c, dropout=0.2,\n", 118 | " zinb=0.25, nb=False,\n", 119 | " bake=5, lamb=0.5, \n", 120 | ")" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": 6, 126 | "id": "3309e1d3-528b-46cd-bcba-2f2cc4020e53", 127 | "metadata": { 128 | "tags": [] 129 | }, 130 | "outputs": [ 131 | { 132 | "name": "stderr", 133 | "output_type": "stream", 134 | "text": [ 135 | "GPU available: True, used: True\n", 136 | "TPU available: False, using: 0 TPU cores\n", 137 | "IPU available: False, using: 0 IPUs\n", 138 | "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1,2,3]\n", 139 | "\n", 140 | " | Name | Type | Params\n", 141 | "-----------------------------------------------\n", 142 | "0 | patch_embedding | Conv2d | 4.7 K \n", 143 | "1 | x_embed | Embedding | 65.5 K\n", 144 | "2 | y_embed | Embedding | 65.5 K\n", 145 | "3 | vit | ViT | 71.4 M\n", 146 | "4 | mean | Sequential | 804 K \n", 147 | "5 | disp | Sequential | 804 K \n", 148 | "6 | pi | Sequential | 804 K \n", 149 | "7 | coef | Sequential | 1.1 M \n", 150 | "8 | gene_head | Sequential | 806 K \n", 151 | "-----------------------------------------------\n", 152 | "75.8 M Trainable params\n", 153 | "0 Non-trainable params\n", 154 | "75.8 M Total params\n", 155 | "303.159 Total estimated model params size (MB)\n" 156 | ] 157 | }, 158 | { 159 | "data": { 160 | "application/vnd.jupyter.widget-view+json": { 161 | "model_id": "3a07cb1dd99e45c88839b3c19b0285de", 162 | "version_major": 2, 163 | "version_minor": 0 164 | }, 165 | "text/plain": [ 166 | "Training: 0it [00:00, ?it/s]" 167 | ] 168 | }, 169 | "metadata": {}, 170 | "output_type": "display_data" 171 | } 172 | ], 173 | "source": [ 174 | "logger=None\n", 175 | "trainer = pl.Trainer(\n", 176 | " gpus=[0], max_epochs=350,\n", 177 | " logger=logger,\n", 178 | ")\n", 179 | "trainer.fit(model, train_loader)\n", 180 | "\n", 181 | "import os\n", 182 | "if not os.path.isdir(\"./model/\"):\n", 183 | " os.mkdir(\"./model/\")\n", 184 | "\n", 185 | "torch.save(model.state_dict(),f\"./model/{fold}-Hist2ST.ckpt\")" 186 | ] 187 | }, 188 | { 189 | "cell_type": "markdown", 190 | "id": "45afd1c5-2495-4240-b3b6-92b415507582", 191 | "metadata": {}, 192 | "source": [ 193 | "# Hist2ST Prediction" 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": 6, 199 | "id": "0f19d5d8-6510-493b-9287-ba7215155dc1", 200 | "metadata": {}, 201 | "outputs": [], 202 | "source": [ 203 | "fold=5\n", 204 | "data='her2st'\n", 205 | "prune='Grid' if data=='her2st' else 'NA'\n", 206 | "genes=171 if data=='cscc' else 785" 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": 7, 212 | "id": "76cfcfb9-3169-46da-9288-2e2be7942d84", 213 | "metadata": {}, 214 | "outputs": [ 215 | { 216 | "name": "stdout", 217 | "output_type": "stream", 218 | "text": [ 219 | "['B1']\n", 220 | "Loading imgs...\n", 221 | "Loading metadata...\n" 222 | ] 223 | }, 224 | { 225 | "name": "stderr", 226 | "output_type": "stream", 227 | "text": [ 228 | "100%|██████████████████████████████████| 1/1 [00:02<00:00, 2.85s/it]\n" 229 | ] 230 | } 231 | ], 232 | "source": [ 233 | "testset = pk_load(fold,'test',dataset=data,flatten=False,adj=True,ori=True,prune=prune)\n", 234 | "test_loader = DataLoader(testset, batch_size=1, num_workers=0, shuffle=False)\n", 235 | "label=testset.label[testset.names[0]]\n", 236 | "genes=785\n", 237 | "model=Hist2ST(\n", 238 | " depth1=d1, depth2=d2,depth3=d3,n_genes=genes, \n", 239 | " kernel_size=k, patch_size=p,\n", 240 | " heads=h, channel=c, dropout=0.2,\n", 241 | " zinb=0.25, nb=False,\n", 242 | " bake=5, lamb=0.5, \n", 243 | ")\n", 244 | "model.load_state_dict(torch.load(f'./model/{fold}-Hist2ST.ckpt'))\n", 245 | "pred, gt = test(model, test_loader,'cuda')" 246 | ] 247 | }, 248 | { 249 | "cell_type": "code", 250 | "execution_count": 7, 251 | "id": "4266cce3-6bc7-4f5b-81d5-d03d3ae089c4", 252 | "metadata": {}, 253 | "outputs": [ 254 | { 255 | "name": "stdout", 256 | "output_type": "stream", 257 | "text": [ 258 | "Pearson Correlation: 0.2887870599966082\n", 259 | "ARI: 0.431\n" 260 | ] 261 | } 262 | ], 263 | "source": [ 264 | "R=get_R(pred,gt)[0]\n", 265 | "print('Pearson Correlation:',np.nanmean(R))\n", 266 | "clus,ARI=cluster(pred,label)\n", 267 | "print('ARI:',ARI)" 268 | ] 269 | }, 270 | { 271 | "cell_type": "code", 272 | "execution_count": 10, 273 | "id": "6f613201-90c3-4cbf-be06-008b63155c29", 274 | "metadata": {}, 275 | "outputs": [ 276 | { 277 | "name": "stderr", 278 | "output_type": "stream", 279 | "text": [ 280 | "... storing 'kmeans' as categorical\n" 281 | ] 282 | }, 283 | { 284 | "data": { 285 | "image/png": "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\n", 286 | "text/plain": [ 287 | "
" 288 | ] 289 | }, 290 | "metadata": {}, 291 | "output_type": "display_data" 292 | } 293 | ], 294 | "source": [ 295 | "sc.pl.spatial(pred, img=None, color='kmeans', spot_size=112)" 296 | ] 297 | } 298 | ], 299 | "metadata": { 300 | "kernelspec": { 301 | "display_name": "Python [conda env:task]", 302 | "language": "python", 303 | "name": "conda-env-task-py" 304 | }, 305 | "language_info": { 306 | "codemirror_mode": { 307 | "name": "ipython", 308 | "version": 3 309 | }, 310 | "file_extension": ".py", 311 | "mimetype": "text/x-python", 312 | "name": "python", 313 | "nbconvert_exporter": "python", 314 | "pygments_lexer": "ipython3", 315 | "version": "3.7.0" 316 | }, 317 | "pycharm": { 318 | "stem_cell": { 319 | "cell_type": "raw", 320 | "source": [], 321 | "metadata": { 322 | "collapsed": false 323 | } 324 | } 325 | } 326 | }, 327 | "nbformat": 4, 328 | "nbformat_minor": 5 329 | } -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import torch 4 | import pickle 5 | import numpy as np 6 | import pandas as pd 7 | import scprep as scp 8 | import anndata as ann 9 | import seaborn as sns 10 | import matplotlib.pyplot as plt 11 | import scanpy as sc, anndata as ad 12 | from os import name 13 | from PIL import Image 14 | from sklearn import preprocessing 15 | from sklearn.cluster import KMeans 16 | Image.MAX_IMAGE_PIXELS = 933120000 17 | # from dataset import MARKERS 18 | BCELL = ['CD19', 'CD79A', 'CD79B', 'MS4A1'] 19 | TUMOR = ['FASN'] 20 | CD4T = ['CD4'] 21 | CD8T = ['CD8A', 'CD8B'] 22 | DC = ['CLIC2', 'CLEC10A', 'CD1B', 'CD1A', 'CD1E'] 23 | MDC = ['LAMP3'] 24 | CMM = ['BRAF', 'KRAS'] 25 | IG = {'B_cell':BCELL, 'Tumor':TUMOR, 'CD4+T_cell':CD4T, 'CD8+T_cell':CD8T, 'Dendritic_cells':DC, 26 | 'Mature_dendritic_cells':MDC, 'Cutaneous_Malignant_Melanoma':CMM} 27 | MARKERS = [] 28 | for i in IG.values(): 29 | MARKERS+=i 30 | LYM = {'B_cell':BCELL, 'CD4+T_cell':CD4T, 'CD8+T_cell':CD8T} 31 | 32 | def read_tiff(path): 33 | Image.MAX_IMAGE_PIXELS = 933120000 34 | im = Image.open(path) 35 | imarray = np.array(im) 36 | # I = plt.imread(path) 37 | return im 38 | 39 | def preprocess(adata, n_keep=1000, include=LYM, g=True): 40 | adata.var_names_make_unique() 41 | sc.pp.normalize_total(adata) 42 | sc.pp.log1p(adata) 43 | if g: 44 | # with open("data/gene_list.txt", "rb") as fp: 45 | # b = pickle.load(fp) 46 | b = list(np.load('data/skin_a.npy',allow_pickle=True)) 47 | adata = adata[:,b] 48 | elif include: 49 | # b = adata.copy() 50 | # sc.pp.highly_variable_genes(b, n_top_genes=n_keep,subset=True) 51 | # hvgs = b.var_names 52 | # n_union = len(hvgs&include) 53 | # n_include = len(include) 54 | # hvgs = list(set(hvgs)-set(include))[n_include-n_union:] 55 | # g = include 56 | # adata = adata[:,g] 57 | exp = np.zeros((adata.X.shape[0],len(include))) 58 | for n,(i,v) in enumerate(include.items()): 59 | tmp = adata[:,v].X 60 | tmp = np.mean(tmp,1).flatten() 61 | exp[:,n] = tmp 62 | adata = adata[:,:len(include)] 63 | adata.X = exp 64 | adata.var_names = list(include.keys()) 65 | 66 | else: 67 | sc.pp.highly_variable_genes(adata, n_top_genes=n_keep,subset=True) 68 | c = adata.obsm['spatial'] 69 | scaler = preprocessing.StandardScaler().fit(c) 70 | c = scaler.transform(c) 71 | adata.obsm['position_norm'] = c 72 | # with open("data/gene_list.txt", "wb") as fp: 73 | # pickle.dump(g, fp) 74 | return adata 75 | 76 | def comp_umap(adata): 77 | sc.pp.pca(adata) 78 | sc.pp.neighbors(adata) 79 | sc.tl.umap(adata) 80 | sc.tl.leiden(adata, key_added="clusters") 81 | return adata 82 | 83 | def comp_tsne_km(adata,k=10): 84 | sc.pp.pca(adata) 85 | sc.tl.tsne(adata) 86 | kmeans = KMeans(n_clusters=k, init="k-means++", random_state=0).fit(adata.obsm['X_pca']) 87 | adata.obs['kmeans'] = kmeans.labels_.astype(str) 88 | return adata 89 | 90 | def co_embed(a,b,k=10): 91 | a.obs['tag'] = 'Truth' 92 | b.obs['tag'] = 'Pred' 93 | adata = ad.concat([a,b]) 94 | sc.pp.pca(adata) 95 | sc.tl.tsne(adata) 96 | kmeans = KMeans(n_clusters=k, init="k-means++", random_state=0).fit(adata.obsm['X_pca']) 97 | adata.obs['kmeans'] = kmeans.labels_.astype(str) 98 | return adata 99 | 100 | def build_adata(name='H1'): 101 | cnt_dir = 'data/her2st/data/ST-cnts' 102 | img_dir = 'data/her2st/data/ST-imgs' 103 | pos_dir = 'data/her2st/data/ST-spotfiles' 104 | 105 | pre = img_dir+'/'+name[0]+'/'+name 106 | fig_name = os.listdir(pre)[0] 107 | path = pre+'/'+fig_name 108 | im = Image.open(path) 109 | 110 | path = cnt_dir+'/'+name+'.tsv' 111 | cnt = pd.read_csv(path,sep='\t',index_col=0) 112 | 113 | path = pos_dir+'/'+name+'_selection.tsv' 114 | df = pd.read_csv(path,sep='\t') 115 | 116 | x = df['x'].values 117 | y = df['y'].values 118 | id = [] 119 | for i in range(len(x)): 120 | id.append(str(x[i])+'x'+str(y[i])) 121 | df['id'] = id 122 | 123 | meta = cnt.join((df.set_index('id'))) 124 | 125 | gene_list = list(np.load('data/her_g_list.npy')) 126 | adata = ann.AnnData(scp.transform.log(scp.normalize.library_size_normalize(meta[gene_list].values))) 127 | adata.var_names = gene_list 128 | adata.obsm['spatial'] = np.floor(meta[['pixel_x','pixel_y']].values).astype(int) 129 | 130 | return adata, im 131 | 132 | 133 | def get_data(dataset='bc1', n_keep=1000, include=LYM, g=True): 134 | if dataset == 'bc1': 135 | adata = sc.datasets.visium_sge(sample_id='V1_Breast_Cancer_Block_A_Section_1', include_hires_tiff=True) 136 | adata = preprocess(adata, n_keep, include, g) 137 | img_path = adata.uns["spatial"]['V1_Breast_Cancer_Block_A_Section_1']["metadata"]["source_image_path"] 138 | elif dataset == 'bc2': 139 | adata = sc.datasets.visium_sge(sample_id='V1_Breast_Cancer_Block_A_Section_2', include_hires_tiff=True) 140 | adata = preprocess(adata, n_keep, include, g) 141 | img_path = adata.uns["spatial"]['V1_Breast_Cancer_Block_A_Section_2']["metadata"]["source_image_path"] 142 | else: 143 | adata = sc.datasets.visium_sge(sample_id=dataset, include_hires_tiff=True) 144 | adata = preprocess(adata, n_keep, include, g) 145 | img_path = adata.uns["spatial"][dataset]["metadata"]["source_image_path"] 146 | 147 | return adata, img_path 148 | def find_resolution(adata_, n_clusters, random=666): 149 | obtained_clusters = -1 150 | iteration = 0 151 | resolutions = [0., 1000.] 152 | while obtained_clusters != n_clusters and iteration < 50: 153 | current_res = sum(resolutions) / 2 154 | adata = sc.tl.louvain(adata_, resolution=current_res, random_state=random, copy=True) 155 | labels = adata.obs['louvain'] 156 | obtained_clusters = len(np.unique(labels)) 157 | 158 | if obtained_clusters < n_clusters: 159 | resolutions[0] = current_res 160 | else: 161 | resolutions[1] = current_res 162 | 163 | iteration = iteration + 1 164 | 165 | return current_res 166 | def get_center_labels(features, resolution=0.1): 167 | n_cells = features.shape[0] 168 | adata0 = ad.AnnData(features) 169 | sc.pp.neighbors(adata0, n_neighbors=15, use_rep="X") 170 | adata0 = sc.tl.louvain(adata0, resolution=resolution, random_state=0, copy=True) 171 | y_pred = adata0.obs['louvain'] 172 | y_pred = np.asarray(y_pred, dtype=int) 173 | 174 | features = pd.DataFrame(adata0.X, index=np.arange(0, adata0.shape[0])) 175 | Group = pd.Series(y_pred, index=np.arange(0, adata0.shape[0]), name="Group") 176 | Mergefeature = pd.concat([features, Group], axis=1) 177 | 178 | init_centroid = np.asarray(Mergefeature.groupby("Group").mean()) 179 | n_clusters = init_centroid.shape[0] 180 | 181 | return init_centroid, y_pred 182 | def lvcluster(adata1,label): 183 | adata=adata1.copy() 184 | n_clusters = len(set(label)) 185 | sc.pp.neighbors(adata, n_neighbors=45, use_rep="X") 186 | resolution = find_resolution(adata, n_clusters) 187 | init_centers, cluster_labels_cpu = get_center_labels(adata.X, resolution=resolution) 188 | return cluster_labels_cpu 189 | def normalize( 190 | adata, filter_min_counts=False, size_factors=True, 191 | normalize_input=True, logtrans_input=True, hvg=True 192 | ): 193 | 194 | if filter_min_counts: 195 | sc.pp.filter_genes(adata, min_counts=1) 196 | sc.pp.filter_cells(adata, min_counts=1) 197 | # sc.pp.filter_genes(adata, min_genes=2000) 198 | # sc.pp.filter_cells(adata, min_cells=3) 199 | 200 | if size_factors or normalize_input or logtrans_input: 201 | adata.raw = adata.copy() 202 | else: 203 | adata.raw = adata 204 | 205 | if size_factors: 206 | sc.pp.normalize_per_cell(adata) 207 | adata.obs['size_factors'] = adata.obs.n_counts / np.median(adata.obs.n_counts) 208 | else: 209 | adata.obs['size_factors'] = 1.0 210 | 211 | if logtrans_input: 212 | sc.pp.log1p(adata) 213 | 214 | if normalize_input: 215 | sc.pp.scale(adata) 216 | 217 | if hvg: 218 | sc.pp.highly_variable_genes(adata, n_top_genes=2000) # min_mean=0.0125, max_mean=3, min_disp=0.5 219 | adata = adata[:, adata.var.highly_variable] 220 | 221 | return adata 222 | if __name__ == '__main__': 223 | 224 | adata, img_path = get_data() 225 | print(adata.X.toarray()) 226 | --------------------------------------------------------------------------------