✅ severity 분류로 하실 경우 알려주시면 label_dict도 바로 만들어 드리겠습니다. 필요하면 새 h5ad 생성 코드까지 작성해 드릴 수 있습니다! 🚀 어떤 task로 진행할지 결정하시면 말씀해 주세요.
2️⃣ScRAT 코드 수정
👉 dataloader.py
# line 178 수정
id_dict = {
'normal': 0,
'hypertrophic cardiomyopathy': 1,
'dilated cardiomyopathy': 2
}
# line 185, 187, 189 수정
patient_id = data.obs['patient']
label = data.obs['disease__ontology_label']
cell_type = data.obs['cell_type_annotation']
👉 main.py
# line 83 추가
elif args.task == 'custom':
label_dict = {0: 'normal', 1: 'hypertrophic cardiomyopathy', 2: 'dilated cardiomyopathy'}
* 현재 코드들은 이진 분류(binary classification)를 기본으로 하고 있기 때문에, 다중 클래스(multi-class) 분류로 확장하기 위해
아래의 과정들 수정해주자.
- output_class를 3으로 수정
# line 107 수정
# 현재 코드들은 이진 분류(binary classification)를 기본으로 하고 있기 때문에, 다중 클래스(multi-class) 분류로 확장하기 위해
# 현재는 고정적으로 1로 설정되어 있습니다
# output_class를 3으로 수정
output_class = len(set(labels_)) # 또는 3
- BCELoss → CrossEntropyLoss로 변경
# line 167~ 수정
# 현재 코드들은 이진 분류(binary classification)를 기본으로 하고 있기 때문에, 다중 클래스(multi-class) 분류로 확장하기 위해
if output_class == 1:
loss = nn.BCELoss()(sigmoid(out), y_)
elif output_class == 3:
loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(out, y_.long().view(-1))
- valid loop에서 sigmoid + majority voting 관련 처리 제거 및 변경
with torch.no_grad():
for batch in valid_loader:
x_ = torch.from_numpy(data[batch[0]]).float().to(device).squeeze(0)
y_ = batch[1].int().to(device)
out = model(x_)
if output_class == 1:
out = sigmoid(out)
loss = nn.BCELoss()(out, y_ * torch.ones_like(out))
valid_loss.append(loss.item())
out = out.detach().cpu().numpy()
# majority voting for binary classification
f = lambda x: 1 if x > 0.5 else 0
func = np.vectorize(f)
voted = func(out).reshape(-1)
out = np.argmax(np.bincount(voted))
pred.append(out)
true.append(y_.item())
else:
loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(out, y_.long().view(-1))
valid_loss.append(loss.item())
out = out.detach().cpu().numpy()
# average prediction across cells → then argmax
avg_logits = out.mean(axis=0) # shape: (n_class,)
pred_label = np.argmax(avg_logits)
pred.append(pred_label)
true.append(y_.item())
- test loop에서 sigmoid + majority voting 관련 처리 제거 및 변경
best_model.eval()
pred = []
test_id = []
true = []
wrong = []
prob = []
with torch.no_grad():
for batch in test_loader:
x_ = torch.from_numpy(data[batch[0]]).float().to(device).squeeze(0)
y_ = batch[1].int().numpy()
id_ = batch[2][0]
out = best_model(x_)
if output_class == 1:
# === Binary Classification ===
out = sigmoid(out)
out = out.detach().cpu().numpy().reshape(-1)
y_label = y_[0][0]
true.append(y_label)
if args.model != 'Transformer':
prob.append(out[0])
else:
prob.append(out.mean())
# majority voting
f = lambda x: 1 if x > 0.5 else 0
func = np.vectorize(f)
out = np.argmax(np.bincount(func(out).reshape(-1))).reshape(-1)[0]
pred.append(out)
test_id.append(patient_id[batch[2][0][0][0]])
if out != y_label:
wrong.append(patient_id[batch[2][0][0][0]])
else:
# === Multi-class Classification ===
out = out.detach().cpu().numpy() # shape: (n_cells, n_classes)
avg_logits = out.mean(axis=0) # shape: (n_classes,)
pred_label = np.argmax(avg_logits)
prob.append(avg_logits[pred_label])
y_label = y_[0][0]
true.append(y_label)
pred.append(pred_label)
test_id.append(patient_id[batch[2][0][0][0]])
if pred_label != y_label:
wrong.append(patient_id[batch[2][0][0][0]])
- 평가 부분도 수정 필요
if len(wrongs) == 0:
wrongs = set(wrong)
else:
wrongs = wrongs.intersection(set(wrong))
# AUC 계산 방식 분기
try:
if output_class == 1:
test_auc = metrics.roc_auc_score(true, prob)
else:
test_auc = metrics.roc_auc_score(true, prob, multi_class='ovr')
except:
test_auc = 0.0
test_acc = accuracy_score(true, pred)
# 출력
for idx in range(len(pred)):
true_label = int(true[idx])
pred_label = int(pred[idx])
print(f"{test_id[idx]} -- true: {label_dict[true_label]} -- pred: {label_dict[pred_label]}")
test_accs.append(test_acc)
# Confusion Matrix 및 지표 분기
if output_class == 1:
cm = confusion_matrix(true, pred).ravel()
recall = cm[3] / (cm[3] + cm[2])
precision = cm[3] / (cm[3] + cm[1]) if (cm[3] + cm[1]) != 0 else 0
print("Confusion Matrix: " + str(cm))
else:
cm = confusion_matrix(true, pred)
recall = metrics.recall_score(true, pred, average='macro')
precision = metrics.precision_score(true, pred, average='macro')
print("Confusion Matrix:\n", cm)
print("Best performance: Epoch %d, Loss %f, Test ACC %f, Test AUC %f, Test Recall %f, Test Precision %f" % (
max_epoch, max_loss, test_acc, test_auc, recall, precision))
for w in wrongs:
v = patient_summary.get(w, 0)
patient_summary[w] = v + 1
return test_auc, test_acc, cm, recall, precision
기타 체크 사항:
TransformerPredictor 클래스가 num_classes=output_class를 받을 수 있어야 함 → 확인 필요
MyDataset 클래스가 다중 클래스 라벨을 제대로 반환하고 있는지 확인 필요 (y_ 반환값의 shape 확인)
평가 지표(정확도, confusion matrix 등)도 multi-class에 맞게 처리되었는지 확인
동일한 Python 프로세스 (PID 1190569) 가 GPU 0,1,2,3 메모리 모두 잡고 있습니다.
즉, 현재 코드가 멀티 GPU 사용 중입니다 (아마 DataParallel).
현재 코드만으로 더 학습 가능한가?
👉 지금 상황
이미 GPU 메모리의 대부분을 사용하고 있습니다.
batch size, 모델 크기 (emb_dim, heads 등) 더 키우면 OOM 날 가능성이 높습니다.
👉 추가 학습 가능성
✅ 지금 세팅으로 가능한 최대 규모로 학습 중입니다.
✅ 더 큰 데이터, 더 큰 모델 학습은 현재 구조(예: DataParallel)로는 더 이상 확장 어렵습니다.
👉 왜?
DataParallel은 GPU마다 동일 모델의 복사본을 메모리에 올리고 forward/backward pass를 나눔
그래서 GPU 개수 늘려도 메모리 이득은 거의 없고, 단지 연산 속도만 소폭 증가
그럼 더 학습하려면?
💡 현 구조 한계
DataParallel 방식은 메모리를 효율적으로 나누지 못합니다.
💡 대안 1️⃣ DistributedDataParallel (DDP) → GPU간 메모리 효율적 분산 학습 (각 GPU가 데이터 subset만 처리) → torchrun + DDP로 코드 변경 필요
2️⃣ Gradient Accumulation → 작은 batch 여러 번 forward-backward 후 한 번 update → batch_size 늘린 효과, 메모리 footprint 유지 가능
3️⃣ 모델 경량화 → emb_dim, heads, layers 줄이고 학습 반복 횟수 (repeat) 늘려 통계적 의미 확보
📌 결론
✅ 현재 코드와 세팅으로는 GPU를 최대로 사용 중이고, 추가 학습 효과를 보기는 어렵습니다. ✅ DDP + gradient accumulation 으로 넘어가야 더 큰 데이터, 더 큰 모델 학습이 가능해요
< n_splits > 전체 데이터(여기선 환자)를 몇 개의 fold로 나눌 것인지 결정 n_splits=2이면: 한 번 나눌 때 50%는 훈련(train), 50%는 테스트(test)로 분할됨 n_splits=5이면: 80% train / 20% test로 나눔 (5개의 조합이 돌아가며 test set이 됨)
< repeat > KFold 분할을 몇 번 반복할지 설정 예: repeat=1이면 한 번만 나누고 끝 예: repeat=5이면 서로 다른 seed로 5번 반복해서 더 다양한 조합을 실험에 반영 여러 번 반복해서 실험의 신뢰성과 일반화 성능을 더 잘 평가할 수 있음 * main.py의 309번째 줄에 'args.repeat * 100'으로 되어 있어 ; --repeat 1이면 실제로 100회 반복
< 기본값> n_splits = 5 repeat = 3
그리고 ScRAT 코드에서 이 값이 이렇게 쓰입니다: rkf = RepeatedKFold(n_splits=n_splits, n_repeats=repeat * 100, ...) 즉: rkf = RepeatedKFold(n_splits=5, n_repeats=3 * 100 = 300) 이 말은 최대 300 × 5 = 1500번의 split 시도가 가능하다는 의미입니다.
하지만 아래 조건이 있어서: iter_count = 0 ... if iter_count == n_splits * repeat: break → 최종적으로 정상적인 학습이 완료된 split만 15개 수집됩니다:
✅ 최종 valid split 수 = n_splits × repeat = 5 × 3 = 15개
test set에 클래스가 하나만 있으면 skip되고,skip되더라도 다음 split을 계속 시도해서 총 15개의 valid split을 확보합니다.최종적으로 ✅ Total valid splits used: 15가 출력되면, 정상적으로 15개 실험이 완료된 것입니다.
--train_sample_cells 500 # 학습 시 각 환자 샘플에서 500개 세포를 랜덤 선택 --test_sample_cells 500 # 테스트 시에도 동일하게 500개 세포 선택
--train_num_sample 20 # 한 명의 환자에서 500개의 세포를 20번 샘플링하여 20개의 bag 생성 --test_num_sample 100 # 테스트도 같은 방식으로 100개의 bag 생성
--inter_only False) # mixup된 샘플만 학습에 사용할지 여부 --same_pheno 0 # 같은 클래스끼리 mixup할지, 다른 클래스끼리 할지
--augment_num 0 # Mixup된 새로운 가짜 샘플을 몇 개 생성할지 --alpha 1.0 # mixup의 비율 (Beta 분포 파라미터)
Confusion Matrix: [[3 0 0] [4 0 0] [2 0 0]] Best performance: Epoch 1, Loss 1.063645, Test ACC 0.333333, Test AUC 0.000000, Test Recall 0.333333, Test Precision 0.111111 ✅ Total valid splits used: 1
* 기본 설정으로 실행해본 것 (Test ACC 0.35, Test AUC 0.09 -> 성능 안 나옴)
CUDA_VISIBLE_DEVICES=0,1,2,3 PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 python main.py --dataset /data/project/kim89/cardio_minimal.h5ad --task custom_cardio --batch_size 2 --heads 2
# 어떤 split은 9명 test, 33명 train
# 어떤 split은 8명 test, 34명 train
=================================
=== Final Evaluation (average across all splits) ===
=================================
Best performance: Test ACC 0.354630, Test AUC 0.092063, Test Recall 0.354815, Test Precision 0.169982
=================================
=== 저희 논문용 Final Evaluation (average across all splits) ===
=================================
Best performance: Test ACC 0.354630+-0.229447, Test AUC 0.092063+-0.209896, Test Recall 0.354815+-0.177892, Test Precision 0.169982+-0.154554
Confusion Matrix: [[0 3 0] [0 4 0] [0 2 0]] Best performance: Epoch 1, Loss 0.944177, Test ACC 0.444444, Test AUC 0.000000, Test Recall 0.333333, Test Precision 0.148148 ✅ Total valid splits used: 1
Confusion Matrix: [[0 4 0] [0 2 0] [1 2 0]] Best performance: Epoch 1, Loss 1.268232, Test ACC 0.222222, Test AUC 0.000000, Test Recall 0.333333, Test Precision 0.083333 ✅ Total valid splits used: 2
Confusion Matrix: [[0 4 0] [0 1 0] [0 3 0]] Best performance: Epoch 1, Loss 0.798926, Test ACC 0.125000, Test AUC 0.000000, Test Recall 0.333333, Test Precision 0.041667 ✅ Total valid splits used: 3
Confusion Matrix: [[1 0 0] [2 2 0] [0 3 0]] Best performance: Epoch 2, Loss 0.730647, Test ACC 0.375000, Test AUC 0.000000, Test Recall 0.500000, Test Precision 0.244444 ✅ Total valid splits used: 4
Confusion Matrix: [[3 0 0] [4 0 0] [2 0 0]] Best performance: Epoch 2, Loss 0.755023, Test ACC 0.333333, Test AUC 0.000000, Test Recall 0.333333, Test Precision 0.111111 ✅ Total valid splits used: 1
Confusion Matrix: [[0 4 0] [0 2 0] [0 3 0]] Best performance: Epoch 1, Loss 0.866745, Test ACC 0.222222, Test AUC 0.000000, Test Recall 0.333333, Test Precision 0.074074 ✅ Total valid splits used: 2
Confusion Matrix: [[0 4 0] [0 1 0] [0 3 0]] Best performance: Epoch 1, Loss 0.888302, Test ACC 0.125000, Test AUC 0.000000, Test Recall 0.333333, Test Precision 0.041667 ✅ Total valid splits used: 3
Confusion Matrix: [[0 1 0] [0 4 0] [0 3 0]] Best performance: Epoch 1, Loss 0.957637, Test ACC 0.500000, Test AUC 0.000000, Test Recall 0.333333, Test Precision 0.166667 ✅ Total valid splits used: 4
[[2 0 2] [0 0 4] [0 0 0]] Best performance: Epoch 1, Loss 0.892665, Test ACC 0.250000, Test AUC 0.500000, Test Recall 0.166667, Test Precision 0.333333 ✅ Total valid splits used: 5
를 시도하는데, 당신의 cardio.h5ad 파일에는 obsm['X_pca']가 존재하지 않기 때문에 발생한 에러입니다.
🔍 왜 이런 일이 생겼을까요?
obsm['X_pca']는 보통 PCA 차원 축소 결과를 저장하는 필드
ScRAT은 이걸 전제하고 데이터에서 PCA로 축소된 feature를 로드하려고 함
하지만 현재 h5ad는 PCA가 사전에 계산되어 저장되어 있지 않음
🚀 해결 방법
✅ 1️⃣ PCA를 사전에 계산하여 h5ad에 추가
아래와 같은 스크립트로 PCA를 추가하고 새 파일로 저장하세요:
import scanpy as sc
adata = sc.read_h5ad('/data/project/kim89/cardio.h5ad')
# PCA 실행
sc.pp.pca(adata, n_comps=50) # 필요에 따라 n_comps 조절
# 결과 저장
adata.write('/data/project/kim89/cardio_pca.h5ad')
RuntimeError: CUDA error: no kernel image is available for execution on the device
👉 현재 설치된 PyTorch 버전이 RTX 4090 GPU를 지원하지 않아서 발생한 에러입니다. 경고에서도 이미 말해주고 있죠:
NVIDIA GeForce RTX 4090 with CUDA capability sm_89 is not compatible with the current PyTorch installation.
The current PyTorch install supports CUDA capabilities sm_37 sm_50 sm_60 sm_70.
즉, RTX 4090 (sm_89)에서 실행할 수 있는 CUDA 커널 코드가 PyTorch 빌드에 포함되어 있지 않다는 뜻입니다.
✅ 왜 발생했나?
현재 설치된 PyTorch (1.10.0)는 RTX 4090이 출시되기 전 버전이라 RTX 4090 아키텍처 (Ada Lovelace, sm_89)를 지원하지 않습니다.
PyTorch 1.10은 sm_70 (Volta) 정도까지 지원합니다.
🚀 해결 방법
🔹 최신 PyTorch로 업그레이드
👉 RTX 4090을 지원하려면 PyTorch 1.13 이상, 권장 버전은 1.13.1, 2.x 이상입니다.
🔑 예시 (CUDA 11.8, RTX 4090 호환):
pip install torch==2.0.1+cu118 torchvision==0.15.2+cu118 torchaudio==2.0.2 --extra-index-url https://download.pytorch.org/whl/cu118
# 디스크 공간 부족으로 다운로드/설치 실패시,
# 설치를 임시 공간(/data/project/kim89/pip_cache)에 지정
mkdir -p /data/project/kim89/pip_cache
mkdir -p /data/project/kim89/tmp
TMPDIR=/data/project/kim89/tmp \
pip install --cache-dir=/data/project/kim89/pip_cache \
torch==2.0.1+cu118 torchvision==0.15.2+cu118 torchaudio==2.0.2 \
--extra-index-url https://download.pytorch.org/whl/cu118
# 설치 끝나면 /data/project/kim89/pip_cache와 /data/project/kim89/tmp를 지워서 공간을 확보하세요:
# rm -rf /data/project/kim89/pip_cache /data/project/kim89/tmp
훈련 데이터(train_index)에 클래스가 2개 이상 존재해야 학습을 진행하는 조건이 있지만,
test data에는 없음.
test_index 클래스 확인 추가하자.
label_stat = [] # train set에 포함된 환자들의 라벨 목록
for idx in train_index:
label_stat.append(labels_[p_idx[idx][0]])
unique, cts = np.unique(label_stat, return_counts=True)
# 훈련 데이터(train_index)에 클래스가 2개 이상 존재해야 학습을 진행한다.
if len(unique) < 2 or (1 in cts):
# 클래스가 하나밖에 없음 → 불균형 → 스킵
# or
# 등장한 클래스 중 한 클래스의 환자 수가 1명밖에 안 됨 → 학습이 불안정해질 가능성이 매우 높기 때문에 skip
continue
# print(dict(zip(unique, cts)))
# 원래 코드에는 test set의 클래스 불균형은 체크하지 않음
### ✅ test_index 클래스 확인 추가
test_labels = [labels_[p_idx[i][0]] for i in test_index]
if len(set(test_labels)) < 2:
print(f"⚠️ Skipping split: test set has only one class -> {set(test_labels)}")
continue
< 두번째 수정 해야할 것 >
p_idx는 다음 조건에 따라 필터링되기 때문에 실제 유효 환자 수가 지정해준 값보다 적을 수 있습니다:
if len(tt_idx) < 500: # 셀 수 500개 미만인 환자 제외
continue
또한, label별 환자 수 균형이 맞지 않다면 일부 split에서 클래스가 하나만 포함되는 테스트셋이 만들어질 수 있습니다.
예를 들어 보면:
환자 ID
Label
1~20
0
21~58
1
만약 KFold로 잘못 나눠서 test에 21~50만 들어가면 → label 1만 존재 → ROC AUC 계산 불가 → split skip됨
코드를 수정해주자.
# if len(tt_idx) < 500: # exclude the sample with the number of cells fewer than 500
if len(tt_idx) < max(args.train_sample_cells, args.test_sample_cells):
* 참고로, 그럼 train_sample_cells와 test_sample_cells 가 사용되는 곳은 :
sampling() 단계에서 환자 셀 중 무작위로 train_sample_cells만큼 샘플링해서 모델에 넘깁니다.
test_sample_cells도 마찬가지.
하지만 문제는?
Custom_data()에서 하드코딩된 500 셀 이상 필터 때문에 환자 자체가 걸러지는 것이 우선 발생하고, 그 다음에야 train_sample_cells만큼 샘플링이 이뤄짐.
그래서 코드를 수정해주었으니 이제 내가 원하는 수의 환자가 다 활용된다!
< 내가 가진 데이터 >
<Cardio>
전체 셀 수: 592,689개
셀 수 ≥ 500인 환자 수: 42명
500개 미만 셀을 가진 환자 수: 0 이므로 전부 500개 이상 셀을 가진 환자지만, label이 유효하지 않거나 누락된 환자가 일부 있어 최종적으로 유효 환자는 42명
# 실행 방법은 아래와 같습니다.
# 1. Setup
git clone https://github.com/yuzhenmao/ScRAT
cd ScRAT
python -m venv scrat
source scrat/bin/activate
pip install -r requirements.txt
# 2. PCA 추가
# 데이터에 PCA를 추가하는 Python 코드를 실행합니다.
# ※ 데이터 경로는 직접 수정해주셔야 합니다.
pip install scanpy
python data_check.py
# 3.실행 코드 교체
# dataloader.py와 main.py는 보내드린 파일로 교체해주세요.
# 4-1. cardio 데이터 실행
# ※ --dataset 경로는 수정해주셔야 합니다.
python main.py --dataset /data/project/kim89/cardio_pca.h5ad --task custom_cardio
# 4-2. covid 데이터 실행
# ※ --dataset 경로는 수정해주셔야 합니다.
python main.py --dataset /data/project/kim89/covid_pca.h5ad --task custom_covid
### 에러 발생 시 참고
# 'torchmetrics.utilities.imports' 에러 발생 시
pip install torchmetrics==0.10.3
# 'RuntimeError: CUDA error: no kernel image is available for execution on the device' 에러 발생 시
# 현재 설치된 PyTorch 버전이 RTX 4090 GPU를 지원하지 않아서 발생한 에러
pip install torch==2.0.1+cu118 torchvision==0.15.2+cu118 torchaudio==2.0.2 --extra-index-url https://download.pytorch.org/whl/cu118
# pytorch_lightning==1.7.7이 최신 torch와 호환되는지 확인해야 합니다. 보통 1.13+ torch는 lightning 1.8 이상과 잘 맞습니다.
pip install pytorch_lightning==1.9.0