# MedVision-Classification
MedVision-Classification 是一个基于 PyTorch Lightning 的医学影像分类框架,提供了训练和推理的简单接口。
## 特点
- 基于 PyTorch Lightning 的高级接口
- 支持常见的医学影像格式(NIfTI、DICOM 等)
- 内置多种分类模型架构(ResNet、DenseNet、EfficientNet 等)
- 灵活的数据加载和预处理管道
- 模块化设计,易于扩展
- 命令行界面用于训练和推理
- 支持二分类和多分类任务
## 安装
### 系统要求
- Python 3.8+
- PyTorch 2.0+
- CUDA (可选,用于GPU加速)
### 基本安装
最简单的安装方式:
```bash
pip install -e .
```
### 从源码安装
```bash
git clone https://github.com/yourusername/medvision-classification.git
cd medvision-classification
pip install -e .
```
### 使用requirements文件
```bash
# 基本环境
pip install -r requirements.txt
# 开发环境
pip install -r requirements-dev.txt
```
### 使用conda环境
推荐使用 conda 创建独立的虚拟环境:
```bash
# 创建并激活环境
conda env create -f environment.yml
conda activate medvision-cls
# 安装项目本身
pip install -e .
```
## 快速入门
### 训练2D模型
```bash
MedVision-cls train configs/train_config.yml
```
### 训练3D模型
```bash
MedVision-cls train configs/train_3d_resnet_config.yml
```
### 测试模型
```bash
MedVision-cls test configs/test_config.yml
```
### 推理
```bash
MedVision-cls predict configs/inference_config.yml --input /path/to/image --output /path/to/output
```
## 配置格式
### 2D分类训练配置示例
```yaml
# 2D ResNet Training Configuration
seed: 42
task_dim: 2d
# Model configuration
model:
type: "classification"
network:
name: "resnet50"
pretrained: true
num_classes: 2
# Metrics to compute
metrics:
accuracy:
type: "accuracy"
f1:
type: "f1"
precision:
type: "precision"
recall:
type: "recall"
auc:
type: "auroc"
# Loss configuration
loss:
type: "cross_entropy"
weight: null
label_smoothing: 0.0
# Optimizer configuration
optimizer:
type: "adam"
lr: 0.001
weight_decay: 0.0001
# Scheduler configuration
scheduler:
type: "cosine"
T_max: 100
eta_min: 0.00001
# Data configuration
data:
type: "medical"
batch_size: 4
num_workers: 4
data_dir: "data/classification"
image_format: "*.png"
# Transform configuration for 2D data
transforms:
image_size: [224, 224]
normalize: true
augment: true
# Data split configuration
train_val_split: [0.8, 0.2]
seed: 42
# Training configuration
training:
max_epochs: 10
accelerator: "gpu"
devices: [0,1,2,3] # Multi-GPU training
precision: 16
save_metrics: true
# Callbacks
model_checkpoint:
monitor: "val/accuracy"
mode: "max"
save_top_k: 3
filename: "epoch_{epoch:02d}-val_acc_{val/accuracy:.3f}"
# Validation configuration
validation:
check_val_every_n_epoch: 1
# Class names
class_names:
- "Class_0"
- "Class_1"
# Output paths
outputs:
output_dir: "outputs"
checkpoint_dir: "outputs/checkpoints"
log_dir: "outputs/logs"
# Logging
logging:
log_every_n_steps: 10
wandb:
enabled: false
project: "medvision-2d-classification"
entity: null
```
### 3D分类训练配置示例
```yaml
# 3D ResNet Training Configuration
seed: 42
task_dim: 3D
# Model configuration
model:
type: "classification"
network:
name: "resnet3d_18" # Options: resnet3d_18, resnet3d_34, resnet3d_50
pretrained: false # No pretrained weights for 3D models
in_channels: 3 # Input channels (typically 1 for medical images)
dropout: 0.1
num_classes: 2
# Metrics to compute
metrics:
accuracy:
type: "accuracy"
f1:
type: "f1"
precision:
type: "precision"
recall:
type: "recall"
auc:
type: "auroc"
# Loss configuration
loss:
type: "cross_entropy"
weight: null
label_smoothing: 0.0
# Optimizer configuration
optimizer:
type: "adam"
lr: 0.001
weight_decay: 0.0001
# Scheduler configuration
scheduler:
type: "cosine"
T_max: 100
eta_min: 0.00001
# Data configuration
data:
type: "medical"
batch_size: 4 # Smaller batch size for 3D data
num_workers: 4
data_dir: "data/3D"
image_format: "*.nii.gz" # 3D medical image format
# Transform configuration for 3D data
transforms:
image_size: [64, 64, 64] # [D, H, W] for 3D volumes
normalize: true
augment: true
# Data split configuration
train_val_split: [0.8, 0.2]
seed: 42
# Training configuration
training:
max_epochs: 5
accelerator: "gpu"
devices: 1 # Single GPU for 3D (memory intensive)
precision: 16 # Use mixed precision to save memory
# Callbacks
early_stopping:
monitor: "val/loss"
patience: 10
mode: "min"
model_checkpoint:
monitor: "val/accuracy"
mode: "max"
save_top_k: 3
filename: "epoch_{epoch:02d}-val_acc_{val/accuracy:.3f}"
# Validation configuration
validation:
check_val_every_n_epoch: 1
# Output paths
outputs:
output_dir: "outputs"
checkpoint_dir: "outputs/checkpoints"
log_dir: "outputs/logs"
# Logging
logging:
log_every_n_steps: 10
wandb:
enabled: false
project: "medvision-3d-classification"
entity: null
```
### 推理配置示例
```yaml
# Model configuration
model:
type: "classification"
network:
name: "resnet50"
pretrained: false
num_classes: 2
checkpoint_path: "outputs/checkpoints/best_model.ckpt"
# Inference settings
inference:
batch_size: 1
device: "cuda:0" # 或 "cpu"
return_probabilities: true
class_names: ["class0", "class1"]
confidence_threshold: 0.5
# Preprocessing
preprocessing:
image_size: [224, 224]
normalize: true
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
# Output settings
output:
save_predictions: true
include_probabilities: true
format: "json" # 或 "csv"
```
## 数据格式
### 文件夹结构
```
data/
├── classification/
│ ├── train/
│ │ ├── class1/
│ │ │ ├── image1.png
│ │ │ └── image2.png
│ │ └── class2/
│ │ ├── image3.png
│ │ └── image4.png
│ ├── val/
│ │ ├── class1/
│ │ └── class2/
│ └── test/
│ ├── class1/
│ └── class2/
```
### CSV格式
```csv
image_path,label
/path/to/image1.png,0
/path/to/image2.png,1
/path/to/image3.png,0
```
## 支持的模型
- **ResNet系列**: ResNet18, ResNet34, ResNet50, ResNet101, ResNet152
- **DenseNet系列**: DenseNet121, DenseNet161, DenseNet169, DenseNet201
- **EfficientNet系列**: EfficientNet-B0 到 EfficientNet-B7
- **Vision Transformer**: ViT-Base, ViT-Large
- **ConvNeXt**: ConvNeXt-Tiny, ConvNeXt-Small, ConvNeXt-Base
- **Medical专用**: MedNet, RadImageNet预训练模型
## 许可证
本项目基于 MIT 许可证开源。
## 贡献
欢迎贡献代码!请查看 [CONTRIBUTING.md](CONTRIBUTING.md) 了解详情。
## 引用
如果您在研究中使用了本框架,请引用:
```bibtex
@software{medvision_classification,
title={MedVision-Classification: A PyTorch Lightning Framework for Medical Image Classification},
author={Your Name},
year={2025},
url={https://github.com/Hi-Zhipeng/MedVision-classification}
}
```
"""
PyTorch Lightning module for medical image classification
"""
import torch
import torch.nn as nn
import pytorch_lightning as pl
from torch.optim import Adam, SGD, AdamW
from torch.optim.lr_scheduler import CosineAnnealingLR, ReduceLROnPlateau, StepLR
from typing import Dict, Any
from torchmetrics import MetricCollection, Accuracy, F1Score, Precision, Recall, AUROC
from .model_factory import create_model
from ..losses import create_loss
class ClassificationLightningModule(pl.LightningModule):
"""PyTorch Lightning module for medical image classification"""
def __init__(
self,
model_name: str = "resnet50",
num_classes: int = 2,
pretrained: bool = True,
loss_config: Dict[str, Any] = None,
optimizer_config: Dict[str, Any] = None,
scheduler_config: Dict[str, Any] = None,
metrics_config: Dict[str, Any] = None,
**model_kwargs
):
super().__init__()
self.save_hyperparameters()
# Create model
self.model = create_model(
model_name=model_name,
num_classes=num_classes,
pretrained=pretrained,
**model_kwargs
)
# Setup loss
loss_config = loss_config or {"type": "cross_entropy"}
self.loss_fn = create_loss(loss_config)
# Setup metrics
metrics_config = metrics_config or {}
self.setup_metrics(metrics_config)
# Store configs
self.optimizer_config = optimizer_config or {"type": "adam", "lr": 1e-3}
self.scheduler_config = scheduler_config or {"type": "cosine", "T_max": 100}
# For logging
self.train_step_outputs = []
self.val_step_outputs = []
self.test_step_outputs = []
def setup_metrics(self, metrics_config: Dict[str, Any]):
"""Setup metrics for training, validation, and testing"""
print(f"Setting up metrics with config: {metrics_config}")
# Create metric collections using torchmetrics directly
train_metrics = {}
val_metrics = {}
test_metrics = {}
# Only create metrics specified in config
for metric_name, metric_config in metrics_config.items():
print(f"Creating metric: {metric_name} with config: {metric_config}")
try:
metric_type = metric_config.get("type", "accuracy").lower()
task = "binary" if self.hparams.num_classes == 2 else "multiclass"
if metric_type == "accuracy":
metric = Accuracy(task=task, num_classes=self.hparams.num_classes)
elif metric_type == "f1":
metric = F1Score(task=task, num_classes=self.hparams.num_classes, average="macro")
elif metric_type == "precision":
metric = Precision(task=task, num_classes=self.hparams.num_classes, average="macro")
elif metric_type == "recall":
metric = Recall(task=task, num_classes=self.hparams.num_classes, average="macro")
elif metric_type == "auroc":
metric = AUROC(task=task, num_classes=self.hparams.num_classes)
else:
print(f"⚠️ Unknown metric type: {metric_type}, skipping")
continue
train_metrics[metric_name] = metric.clone()
val_metrics[metric_name] = metric.clone()
test_metrics[metric_name] = metric.clone()
print(f"✅ Created and cloned {metric_name}")
except Exception as e:
print(f"❌ Could not create metric {metric_name}: {e}")
import traceback
traceback.print_exc()
continue
# Create ModuleDict collections
self.train_metrics = nn.ModuleDict(train_metrics)
self.val_metrics = nn.ModuleDict(val_metrics)
self.test_metrics = nn.ModuleDict(test_metrics)
print(f"Final train_metrics: {list(self.train_metrics.keys())}")
print(f"Final val_metrics: {list(self.val_metrics.keys())}")
print(f"Final test_metrics: {list(self.test_metrics.keys())}")
def _update_metrics(self, metrics: nn.ModuleDict, logits: torch.Tensor, labels: torch.Tensor):
"""Update metrics with predictions and labels"""
preds = torch.softmax(logits, dim=1)
pred_classes = torch.argmax(preds, dim=1)
for metric_name, metric in metrics.items():
if metric_name == "auc":
# AUC needs probabilities - for binary: preds[:, 1], for multiclass: preds
if self.hparams.num_classes == 2:
metric.update(preds[:, 1], labels)
else:
metric.update(preds, labels)
else:
# Other metrics need predicted class indices
metric.update(pred_classes, labels)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.model(x)
# def training_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> torch.Tensor:
# images, labels = batch["image"], batch["label"]
# logits = self(images)
# loss = self.loss_fn(logits, labels)
# # Update metrics
# self._update_metrics(self.train_metrics, logits, labels)
# # Log loss
# self.log("train/loss", loss, prog_bar=True, on_step=True, on_epoch=True)
# self.train_step_outputs.append(loss)
# return loss
# def on_train_epoch_end(self):
# # Compute and log metrics
# for metric_name, metric in self.train_metrics.items():
# value = metric.compute()
# self.log(f"train/{metric_name}", value, prog_bar=True)
# metric.reset()
# self.train_step_outputs.clear()
# def validation_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> torch.Tensor:
# images, labels = batch["image"], batch["label"]
# logits = self(images)
# loss = self.loss_fn(logits, labels)
# # Update metrics
# self._update_metrics(self.val_metrics, logits, labels)
# # Log loss
# self.log("val/loss", loss, prog_bar=True, on_step=False, on_epoch=True)
# self.val_step_outputs.append(loss)
# return loss
# def on_validation_epoch_end(self):
# # Compute and log metrics
# for metric_name, metric in self.val_metrics.items():
# value = metric.compute()
# self.log(f"val/{metric_name}", value, prog_bar=True, on_epoch=True)
# metric.reset()
# self.val_step_outputs.clear()
# def test_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> torch.Tensor:
# images, labels = batch["image"], batch["label"]
# logits = self(images)
# loss = self.loss_fn(logits, labels)
# # Update metrics
# self._update_metrics(self.test_metrics, logits, labels)
# # Log loss
# self.log("test/loss", loss, on_step=False, on_epoch=True)
# self.test_step_outputs.append({
# "loss": loss,
# "preds": torch.softmax(logits, dim=1),
# "labels": labels
# })
# return loss
# def on_test_epoch_end(self):
# # Compute metrics without logging to avoid deadlock
# for metric_name, metric in self.test_metrics.items():
# value = metric.compute()
# # Store metric values for later use if needed
# # self.log(f"test/{metric_name}", value) # Removed to avoid deadlock
# metric.reset()
# self.test_step_outputs.clear()
# ...existing code...
def training_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> torch.Tensor:
images, labels = batch["image"], batch["label"]
logits = self(images)
loss = self.loss_fn(logits, labels)
# Update metrics
self._update_metrics(self.train_metrics, logits, labels)
# Log loss
self.log("train/loss", loss, prog_bar=True, on_step=True, on_epoch=True, sync_dist=True)
# Log metrics in the step itself to avoid deadlock
for metric_name, metric in self.train_metrics.items():
self.log(f"train/{metric_name}", metric, prog_bar=True, on_step=False, on_epoch=True, sync_dist=True)
self.train_step_outputs.append(loss)
return loss
def on_train_epoch_end(self):
# Only reset metrics, don't log here to avoid deadlock
for metric in self.train_metrics.values():
metric.reset()
self.train_step_outputs.clear()
def validation_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> torch.Tensor:
images, labels = batch["image"], batch["label"]
logits = self(images)
loss = self.loss_fn(logits, labels)
# Update metrics
self._update_metrics(self.val_metrics, logits, labels)
# Log loss
self.log("val/loss", loss, prog_bar=True, on_step=False, on_epoch=True, sync_dist=True)
# Log metrics in the step itself to avoid deadlock
for metric_name, metric in self.val_metrics.items():
self.log(f"val/{metric_name}", metric, prog_bar=True, on_step=False, on_epoch=True, sync_dist=True)
self.val_step_outputs.append(loss)
return loss
def on_validation_epoch_end(self):
# Only reset metrics, don't log here to avoid deadlock
for metric in self.val_metrics.values():
metric.reset()
self.val_step_outputs.clear()
def test_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> torch.Tensor:
images, labels = batch["image"], batch["label"]
logits = self(images)
loss = self.loss_fn(logits, labels)
# Update metrics
self._update_metrics(self.test_metrics, logits, labels)
# Log loss
self.log("test/loss", loss, on_step=False, on_epoch=True, sync_dist=True)
# Log metrics in the step itself to avoid deadlock
for metric_name, metric in self.test_metrics.items():
self.log(f"test/{metric_name}", metric, on_step=False, on_epoch=True, sync_dist=True)
self.test_step_outputs.append({
"loss": loss,
"preds": torch.softmax(logits, dim=1),
"labels": labels
})
return loss
def on_test_epoch_end(self):
# Only reset metrics, don't log here to avoid deadlock
for metric in self.test_metrics.values():
metric.reset()
self.test_step_outputs.clear()
def predict_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> Dict[str, torch.Tensor]:
images = batch["image"]
logits = self(images)
probs = torch.softmax(logits, dim=1)
preds = torch.argmax(probs, dim=1)
return {
"predictions": preds,
"probabilities": probs,
"logits": logits
}
def configure_optimizers(self):
# Setup optimizer
optimizer_type = self.optimizer_config.get("type", "adam").lower()
lr = self.optimizer_config.get("lr", 1e-3)
weight_decay = self.optimizer_config.get("weight_decay", 0)
if optimizer_type == "adam":
optimizer = Adam(self.parameters(), lr=lr, weight_decay=weight_decay)
elif optimizer_type == "adamw":
optimizer = AdamW(self.parameters(), lr=lr, weight_decay=weight_decay)
elif optimizer_type == "sgd":
momentum = self.optimizer_config.get("momentum", 0.9)
optimizer = SGD(self.parameters(), lr=lr, weight_decay=weight_decay, momentum=momentum)
else:
raise ValueError(f"Unsupported optimizer: {optimizer_type}")
# Setup scheduler
scheduler_type = self.scheduler_config.get("type", "cosine").lower()
if scheduler_type == "cosine":
T_max = self.scheduler_config.get("T_max", 100)
eta_min = self.scheduler_config.get("eta_min", 0)
scheduler = CosineAnnealingLR(optimizer, T_max=T_max, eta_min=eta_min)
return [optimizer], [scheduler]
elif scheduler_type == "plateau":
patience = self.scheduler_config.get("patience", 10)
factor = self.scheduler_config.get("factor", 0.5)
monitor = self.scheduler_config.get("monitor", "val/loss")
scheduler = ReduceLROnPlateau(optimizer, patience=patience, factor=factor)
return {
"optimizer": optimizer,
"lr_scheduler": {
"scheduler": scheduler,
"monitor": monitor,
"interval": "epoch",
"frequency": 1
}
}
elif scheduler_type == "step":
step_size = self.scheduler_config.get("step_size", 30)
gamma = self.scheduler_config.get("gamma", 0.1)
scheduler = StepLR(optimizer, step_size=step_size, gamma=gamma)
return [optimizer], [scheduler]
else:
return optimizer
Raw data
{
"_id": null,
"home_page": null,
"name": "medvision-classification",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": "medical imaging, classification, pytorch, lightning, deep learning",
"author": null,
"author_email": "weizhipeng <weizhipeng@shu.edu.cn>",
"download_url": "https://files.pythonhosted.org/packages/a0/55/e4b48c8b98e5e6f07ce2dd4f8da868b62afd4a455e1376f28e30e6f7abb1/medvision_classification-0.1.0.tar.gz",
"platform": null,
"description": "# MedVision-Classification\n\nMedVision-Classification \u662f\u4e00\u4e2a\u57fa\u4e8e PyTorch Lightning \u7684\u533b\u5b66\u5f71\u50cf\u5206\u7c7b\u6846\u67b6\uff0c\u63d0\u4f9b\u4e86\u8bad\u7ec3\u548c\u63a8\u7406\u7684\u7b80\u5355\u63a5\u53e3\u3002\n\n## \u7279\u70b9\n\n- \u57fa\u4e8e PyTorch Lightning \u7684\u9ad8\u7ea7\u63a5\u53e3\n- \u652f\u6301\u5e38\u89c1\u7684\u533b\u5b66\u5f71\u50cf\u683c\u5f0f\uff08NIfTI\u3001DICOM \u7b49\uff09\n- \u5185\u7f6e\u591a\u79cd\u5206\u7c7b\u6a21\u578b\u67b6\u6784\uff08ResNet\u3001DenseNet\u3001EfficientNet \u7b49\uff09\n- \u7075\u6d3b\u7684\u6570\u636e\u52a0\u8f7d\u548c\u9884\u5904\u7406\u7ba1\u9053\n- \u6a21\u5757\u5316\u8bbe\u8ba1\uff0c\u6613\u4e8e\u6269\u5c55\n- \u547d\u4ee4\u884c\u754c\u9762\u7528\u4e8e\u8bad\u7ec3\u548c\u63a8\u7406\n- \u652f\u6301\u4e8c\u5206\u7c7b\u548c\u591a\u5206\u7c7b\u4efb\u52a1\n\n## \u5b89\u88c5\n\n### \u7cfb\u7edf\u8981\u6c42\n\n- Python 3.8+\n- PyTorch 2.0+\n- CUDA (\u53ef\u9009\uff0c\u7528\u4e8eGPU\u52a0\u901f)\n\n### \u57fa\u672c\u5b89\u88c5\n\n\u6700\u7b80\u5355\u7684\u5b89\u88c5\u65b9\u5f0f\uff1a\n\n```bash\npip install -e .\n```\n\n### \u4ece\u6e90\u7801\u5b89\u88c5\n\n```bash\ngit clone https://github.com/yourusername/medvision-classification.git\ncd medvision-classification\npip install -e .\n```\n\n### \u4f7f\u7528requirements\u6587\u4ef6\n\n```bash\n# \u57fa\u672c\u73af\u5883\npip install -r requirements.txt\n\n# \u5f00\u53d1\u73af\u5883\npip install -r requirements-dev.txt\n```\n\n### \u4f7f\u7528conda\u73af\u5883\n\n\u63a8\u8350\u4f7f\u7528 conda \u521b\u5efa\u72ec\u7acb\u7684\u865a\u62df\u73af\u5883\uff1a\n\n```bash\n# \u521b\u5efa\u5e76\u6fc0\u6d3b\u73af\u5883\nconda env create -f environment.yml\nconda activate medvision-cls\n\n# \u5b89\u88c5\u9879\u76ee\u672c\u8eab\npip install -e .\n```\n\n## \u5feb\u901f\u5165\u95e8\n\n### \u8bad\u7ec32D\u6a21\u578b\n\n```bash\nMedVision-cls train configs/train_config.yml\n```\n\n### \u8bad\u7ec33D\u6a21\u578b\n\n```bash\nMedVision-cls train configs/train_3d_resnet_config.yml\n```\n\n### \u6d4b\u8bd5\u6a21\u578b\n\n```bash\nMedVision-cls test configs/test_config.yml\n```\n\n### \u63a8\u7406\n\n```bash\nMedVision-cls predict configs/inference_config.yml --input /path/to/image --output /path/to/output\n```\n\n## \u914d\u7f6e\u683c\u5f0f\n\n### 2D\u5206\u7c7b\u8bad\u7ec3\u914d\u7f6e\u793a\u4f8b\n\n```yaml\n# 2D ResNet Training Configuration\nseed: 42\n\ntask_dim: 2d\n\n# Model configuration\nmodel:\n type: \"classification\"\n network:\n name: \"resnet50\"\n pretrained: true\n num_classes: 2\n\n # Metrics to compute\n metrics:\n accuracy:\n type: \"accuracy\"\n f1:\n type: \"f1\"\n precision:\n type: \"precision\"\n recall:\n type: \"recall\"\n auc:\n type: \"auroc\"\n \n # Loss configuration\n loss:\n type: \"cross_entropy\"\n weight: null\n label_smoothing: 0.0\n \n # Optimizer configuration\n optimizer:\n type: \"adam\"\n lr: 0.001\n weight_decay: 0.0001\n \n # Scheduler configuration\n scheduler:\n type: \"cosine\"\n T_max: 100\n eta_min: 0.00001\n\n# Data configuration\ndata:\n type: \"medical\"\n batch_size: 4\n num_workers: 4\n data_dir: \"data/classification\"\n image_format: \"*.png\"\n \n # Transform configuration for 2D data\n transforms:\n image_size: [224, 224]\n normalize: true\n augment: true\n \n # Data split configuration\n train_val_split: [0.8, 0.2]\n seed: 42\n\n# Training configuration\ntraining:\n max_epochs: 10\n accelerator: \"gpu\"\n devices: [0,1,2,3] # Multi-GPU training\n precision: 16\n save_metrics: true\n \n # Callbacks\n model_checkpoint:\n monitor: \"val/accuracy\"\n mode: \"max\"\n save_top_k: 3\n filename: \"epoch_{epoch:02d}-val_acc_{val/accuracy:.3f}\"\n\n# Validation configuration\nvalidation:\n check_val_every_n_epoch: 1\n\n# Class names\nclass_names:\n - \"Class_0\"\n - \"Class_1\"\n\n# Output paths\noutputs:\n output_dir: \"outputs\"\n checkpoint_dir: \"outputs/checkpoints\"\n log_dir: \"outputs/logs\"\n\n# Logging\nlogging:\n log_every_n_steps: 10\n wandb:\n enabled: false\n project: \"medvision-2d-classification\"\n entity: null\n```\n\n### 3D\u5206\u7c7b\u8bad\u7ec3\u914d\u7f6e\u793a\u4f8b\n\n```yaml\n# 3D ResNet Training Configuration\nseed: 42\n\ntask_dim: 3D\n\n# Model configuration\nmodel:\n type: \"classification\"\n network:\n name: \"resnet3d_18\" # Options: resnet3d_18, resnet3d_34, resnet3d_50\n pretrained: false # No pretrained weights for 3D models\n in_channels: 3 # Input channels (typically 1 for medical images)\n dropout: 0.1\n num_classes: 2\n\n # Metrics to compute\n metrics:\n accuracy:\n type: \"accuracy\"\n f1:\n type: \"f1\"\n precision:\n type: \"precision\"\n recall:\n type: \"recall\"\n auc:\n type: \"auroc\"\n\n # Loss configuration\n loss:\n type: \"cross_entropy\"\n weight: null\n label_smoothing: 0.0\n \n # Optimizer configuration\n optimizer:\n type: \"adam\"\n lr: 0.001\n weight_decay: 0.0001\n \n # Scheduler configuration\n scheduler:\n type: \"cosine\"\n T_max: 100\n eta_min: 0.00001\n\n# Data configuration\ndata:\n type: \"medical\"\n batch_size: 4 # Smaller batch size for 3D data\n num_workers: 4\n data_dir: \"data/3D\"\n image_format: \"*.nii.gz\" # 3D medical image format\n \n # Transform configuration for 3D data\n transforms:\n image_size: [64, 64, 64] # [D, H, W] for 3D volumes\n normalize: true\n augment: true\n \n # Data split configuration\n train_val_split: [0.8, 0.2]\n seed: 42\n\n# Training configuration\ntraining:\n max_epochs: 5\n accelerator: \"gpu\"\n devices: 1 # Single GPU for 3D (memory intensive)\n precision: 16 # Use mixed precision to save memory\n \n # Callbacks\n early_stopping:\n monitor: \"val/loss\"\n patience: 10\n mode: \"min\"\n \n model_checkpoint:\n monitor: \"val/accuracy\"\n mode: \"max\"\n save_top_k: 3\n filename: \"epoch_{epoch:02d}-val_acc_{val/accuracy:.3f}\"\n\n# Validation configuration\nvalidation:\n check_val_every_n_epoch: 1\n\n# Output paths\noutputs:\n output_dir: \"outputs\"\n checkpoint_dir: \"outputs/checkpoints\"\n log_dir: \"outputs/logs\"\n\n# Logging\nlogging:\n log_every_n_steps: 10\n wandb:\n enabled: false\n project: \"medvision-3d-classification\"\n entity: null\n```\n\n### \u63a8\u7406\u914d\u7f6e\u793a\u4f8b\n\n```yaml\n# Model configuration\nmodel:\n type: \"classification\"\n network:\n name: \"resnet50\"\n pretrained: false\n num_classes: 2\n checkpoint_path: \"outputs/checkpoints/best_model.ckpt\"\n\n# Inference settings\ninference:\n batch_size: 1\n device: \"cuda:0\" # \u6216 \"cpu\"\n return_probabilities: true\n class_names: [\"class0\", \"class1\"]\n confidence_threshold: 0.5\n\n# Preprocessing\npreprocessing:\n image_size: [224, 224]\n normalize: true\n mean: [0.485, 0.456, 0.406]\n std: [0.229, 0.224, 0.225]\n\n# Output settings\noutput:\n save_predictions: true\n include_probabilities: true\n format: \"json\" # \u6216 \"csv\"\n```\n\n## \u6570\u636e\u683c\u5f0f\n\n### \u6587\u4ef6\u5939\u7ed3\u6784\n\n```\ndata/\n\u251c\u2500\u2500 classification/\n\u2502 \u251c\u2500\u2500 train/\n\u2502 \u2502 \u251c\u2500\u2500 class1/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 image1.png\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 image2.png\n\u2502 \u2502 \u2514\u2500\u2500 class2/\n\u2502 \u2502 \u251c\u2500\u2500 image3.png\n\u2502 \u2502 \u2514\u2500\u2500 image4.png\n\u2502 \u251c\u2500\u2500 val/\n\u2502 \u2502 \u251c\u2500\u2500 class1/\n\u2502 \u2502 \u2514\u2500\u2500 class2/\n\u2502 \u2514\u2500\u2500 test/\n\u2502 \u251c\u2500\u2500 class1/\n\u2502 \u2514\u2500\u2500 class2/\n```\n\n### CSV\u683c\u5f0f\n\n```csv\nimage_path,label\n/path/to/image1.png,0\n/path/to/image2.png,1\n/path/to/image3.png,0\n```\n\n## \u652f\u6301\u7684\u6a21\u578b\n\n- **ResNet\u7cfb\u5217**: ResNet18, ResNet34, ResNet50, ResNet101, ResNet152\n- **DenseNet\u7cfb\u5217**: DenseNet121, DenseNet161, DenseNet169, DenseNet201\n- **EfficientNet\u7cfb\u5217**: EfficientNet-B0 \u5230 EfficientNet-B7\n- **Vision Transformer**: ViT-Base, ViT-Large\n- **ConvNeXt**: ConvNeXt-Tiny, ConvNeXt-Small, ConvNeXt-Base\n- **Medical\u4e13\u7528**: MedNet, RadImageNet\u9884\u8bad\u7ec3\u6a21\u578b\n\n## \u8bb8\u53ef\u8bc1\n\n\u672c\u9879\u76ee\u57fa\u4e8e MIT \u8bb8\u53ef\u8bc1\u5f00\u6e90\u3002\n\n## \u8d21\u732e\n\n\u6b22\u8fce\u8d21\u732e\u4ee3\u7801\uff01\u8bf7\u67e5\u770b [CONTRIBUTING.md](CONTRIBUTING.md) \u4e86\u89e3\u8be6\u60c5\u3002\n\n## \u5f15\u7528\n\n\u5982\u679c\u60a8\u5728\u7814\u7a76\u4e2d\u4f7f\u7528\u4e86\u672c\u6846\u67b6\uff0c\u8bf7\u5f15\u7528\uff1a\n\n```bibtex\n@software{medvision_classification,\n title={MedVision-Classification: A PyTorch Lightning Framework for Medical Image Classification},\n author={Your Name},\n year={2025},\n url={https://github.com/Hi-Zhipeng/MedVision-classification}\n}\n```\n\n\n\"\"\"\nPyTorch Lightning module for medical image classification\n\"\"\"\n\nimport torch\nimport torch.nn as nn\nimport pytorch_lightning as pl\nfrom torch.optim import Adam, SGD, AdamW\nfrom torch.optim.lr_scheduler import CosineAnnealingLR, ReduceLROnPlateau, StepLR\nfrom typing import Dict, Any\nfrom torchmetrics import MetricCollection, Accuracy, F1Score, Precision, Recall, AUROC\n\nfrom .model_factory import create_model\nfrom ..losses import create_loss\n\n\nclass ClassificationLightningModule(pl.LightningModule):\n \"\"\"PyTorch Lightning module for medical image classification\"\"\"\n \n def __init__(\n self,\n model_name: str = \"resnet50\",\n num_classes: int = 2,\n pretrained: bool = True,\n loss_config: Dict[str, Any] = None,\n optimizer_config: Dict[str, Any] = None,\n scheduler_config: Dict[str, Any] = None,\n metrics_config: Dict[str, Any] = None,\n **model_kwargs\n ):\n super().__init__()\n self.save_hyperparameters()\n \n # Create model\n self.model = create_model(\n model_name=model_name,\n num_classes=num_classes,\n pretrained=pretrained,\n **model_kwargs\n )\n \n # Setup loss\n loss_config = loss_config or {\"type\": \"cross_entropy\"}\n self.loss_fn = create_loss(loss_config)\n \n # Setup metrics\n metrics_config = metrics_config or {}\n \n self.setup_metrics(metrics_config)\n \n # Store configs\n self.optimizer_config = optimizer_config or {\"type\": \"adam\", \"lr\": 1e-3}\n self.scheduler_config = scheduler_config or {\"type\": \"cosine\", \"T_max\": 100}\n \n # For logging\n self.train_step_outputs = []\n self.val_step_outputs = []\n self.test_step_outputs = []\n \n def setup_metrics(self, metrics_config: Dict[str, Any]):\n \"\"\"Setup metrics for training, validation, and testing\"\"\"\n \n print(f\"Setting up metrics with config: {metrics_config}\")\n \n # Create metric collections using torchmetrics directly\n train_metrics = {}\n val_metrics = {}\n test_metrics = {}\n \n # Only create metrics specified in config\n for metric_name, metric_config in metrics_config.items():\n print(f\"Creating metric: {metric_name} with config: {metric_config}\")\n try:\n metric_type = metric_config.get(\"type\", \"accuracy\").lower()\n task = \"binary\" if self.hparams.num_classes == 2 else \"multiclass\"\n \n if metric_type == \"accuracy\":\n metric = Accuracy(task=task, num_classes=self.hparams.num_classes)\n elif metric_type == \"f1\":\n metric = F1Score(task=task, num_classes=self.hparams.num_classes, average=\"macro\")\n elif metric_type == \"precision\":\n metric = Precision(task=task, num_classes=self.hparams.num_classes, average=\"macro\")\n elif metric_type == \"recall\":\n metric = Recall(task=task, num_classes=self.hparams.num_classes, average=\"macro\")\n elif metric_type == \"auroc\":\n metric = AUROC(task=task, num_classes=self.hparams.num_classes)\n else:\n print(f\"\u26a0\ufe0f Unknown metric type: {metric_type}, skipping\")\n continue\n \n train_metrics[metric_name] = metric.clone()\n val_metrics[metric_name] = metric.clone()\n test_metrics[metric_name] = metric.clone()\n print(f\"\u2705 Created and cloned {metric_name}\")\n \n except Exception as e:\n print(f\"\u274c Could not create metric {metric_name}: {e}\")\n import traceback\n traceback.print_exc()\n continue\n \n # Create ModuleDict collections\n self.train_metrics = nn.ModuleDict(train_metrics)\n self.val_metrics = nn.ModuleDict(val_metrics)\n self.test_metrics = nn.ModuleDict(test_metrics)\n \n print(f\"Final train_metrics: {list(self.train_metrics.keys())}\")\n print(f\"Final val_metrics: {list(self.val_metrics.keys())}\")\n print(f\"Final test_metrics: {list(self.test_metrics.keys())}\")\n \n def _update_metrics(self, metrics: nn.ModuleDict, logits: torch.Tensor, labels: torch.Tensor):\n \"\"\"Update metrics with predictions and labels\"\"\"\n preds = torch.softmax(logits, dim=1)\n pred_classes = torch.argmax(preds, dim=1)\n \n for metric_name, metric in metrics.items():\n if metric_name == \"auc\":\n # AUC needs probabilities - for binary: preds[:, 1], for multiclass: preds\n if self.hparams.num_classes == 2:\n metric.update(preds[:, 1], labels)\n else:\n metric.update(preds, labels)\n else:\n # Other metrics need predicted class indices\n metric.update(pred_classes, labels)\n \n def forward(self, x: torch.Tensor) -> torch.Tensor:\n return self.model(x)\n \n # def training_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> torch.Tensor:\n # images, labels = batch[\"image\"], batch[\"label\"]\n # logits = self(images)\n # loss = self.loss_fn(logits, labels)\n \n # # Update metrics\n # self._update_metrics(self.train_metrics, logits, labels)\n \n # # Log loss\n # self.log(\"train/loss\", loss, prog_bar=True, on_step=True, on_epoch=True)\n \n # self.train_step_outputs.append(loss)\n # return loss\n \n # def on_train_epoch_end(self):\n # # Compute and log metrics\n # for metric_name, metric in self.train_metrics.items():\n # value = metric.compute()\n # self.log(f\"train/{metric_name}\", value, prog_bar=True)\n # metric.reset()\n \n # self.train_step_outputs.clear()\n \n # def validation_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> torch.Tensor:\n # images, labels = batch[\"image\"], batch[\"label\"]\n\n # logits = self(images)\n # loss = self.loss_fn(logits, labels)\n \n # # Update metrics\n # self._update_metrics(self.val_metrics, logits, labels)\n \n # # Log loss\n # self.log(\"val/loss\", loss, prog_bar=True, on_step=False, on_epoch=True)\n \n # self.val_step_outputs.append(loss)\n # return loss\n \n # def on_validation_epoch_end(self):\n # # Compute and log metrics\n # for metric_name, metric in self.val_metrics.items():\n # value = metric.compute()\n # self.log(f\"val/{metric_name}\", value, prog_bar=True, on_epoch=True)\n # metric.reset()\n \n # self.val_step_outputs.clear()\n \n # def test_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> torch.Tensor:\n # images, labels = batch[\"image\"], batch[\"label\"]\n # logits = self(images)\n # loss = self.loss_fn(logits, labels)\n \n # # Update metrics\n # self._update_metrics(self.test_metrics, logits, labels)\n \n # # Log loss\n # self.log(\"test/loss\", loss, on_step=False, on_epoch=True)\n \n # self.test_step_outputs.append({\n # \"loss\": loss,\n # \"preds\": torch.softmax(logits, dim=1),\n # \"labels\": labels\n # })\n # return loss\n \n # def on_test_epoch_end(self):\n # # Compute metrics without logging to avoid deadlock\n # for metric_name, metric in self.test_metrics.items():\n # value = metric.compute()\n # # Store metric values for later use if needed\n # # self.log(f\"test/{metric_name}\", value) # Removed to avoid deadlock\n # metric.reset()\n \n # self.test_step_outputs.clear()\n\n# ...existing code...\n\n def training_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> torch.Tensor:\n images, labels = batch[\"image\"], batch[\"label\"]\n logits = self(images)\n loss = self.loss_fn(logits, labels)\n \n # Update metrics\n self._update_metrics(self.train_metrics, logits, labels)\n \n # Log loss\n self.log(\"train/loss\", loss, prog_bar=True, on_step=True, on_epoch=True, sync_dist=True)\n \n # Log metrics in the step itself to avoid deadlock\n for metric_name, metric in self.train_metrics.items():\n self.log(f\"train/{metric_name}\", metric, prog_bar=True, on_step=False, on_epoch=True, sync_dist=True)\n \n self.train_step_outputs.append(loss)\n return loss\n\n def on_train_epoch_end(self):\n # Only reset metrics, don't log here to avoid deadlock\n for metric in self.train_metrics.values():\n metric.reset()\n \n self.train_step_outputs.clear()\n\n def validation_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> torch.Tensor:\n images, labels = batch[\"image\"], batch[\"label\"]\n logits = self(images)\n loss = self.loss_fn(logits, labels)\n \n # Update metrics\n self._update_metrics(self.val_metrics, logits, labels)\n \n # Log loss\n self.log(\"val/loss\", loss, prog_bar=True, on_step=False, on_epoch=True, sync_dist=True)\n \n # Log metrics in the step itself to avoid deadlock\n for metric_name, metric in self.val_metrics.items():\n self.log(f\"val/{metric_name}\", metric, prog_bar=True, on_step=False, on_epoch=True, sync_dist=True)\n \n self.val_step_outputs.append(loss)\n return loss\n\n def on_validation_epoch_end(self):\n # Only reset metrics, don't log here to avoid deadlock\n for metric in self.val_metrics.values():\n metric.reset()\n \n self.val_step_outputs.clear()\n\n def test_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> torch.Tensor:\n images, labels = batch[\"image\"], batch[\"label\"]\n logits = self(images)\n loss = self.loss_fn(logits, labels)\n \n # Update metrics\n self._update_metrics(self.test_metrics, logits, labels)\n \n # Log loss\n self.log(\"test/loss\", loss, on_step=False, on_epoch=True, sync_dist=True)\n \n # Log metrics in the step itself to avoid deadlock\n for metric_name, metric in self.test_metrics.items():\n self.log(f\"test/{metric_name}\", metric, on_step=False, on_epoch=True, sync_dist=True)\n \n self.test_step_outputs.append({\n \"loss\": loss,\n \"preds\": torch.softmax(logits, dim=1),\n \"labels\": labels\n })\n return loss\n\n def on_test_epoch_end(self):\n # Only reset metrics, don't log here to avoid deadlock\n \n for metric in self.test_metrics.values():\n metric.reset()\n\n self.test_step_outputs.clear()\n\n def predict_step(self, batch: Dict[str, torch.Tensor], batch_idx: int) -> Dict[str, torch.Tensor]:\n images = batch[\"image\"]\n logits = self(images)\n probs = torch.softmax(logits, dim=1)\n preds = torch.argmax(probs, dim=1)\n \n return {\n \"predictions\": preds,\n \"probabilities\": probs,\n \"logits\": logits\n }\n \n def configure_optimizers(self):\n # Setup optimizer\n optimizer_type = self.optimizer_config.get(\"type\", \"adam\").lower()\n lr = self.optimizer_config.get(\"lr\", 1e-3)\n weight_decay = self.optimizer_config.get(\"weight_decay\", 0)\n \n if optimizer_type == \"adam\":\n optimizer = Adam(self.parameters(), lr=lr, weight_decay=weight_decay)\n elif optimizer_type == \"adamw\":\n optimizer = AdamW(self.parameters(), lr=lr, weight_decay=weight_decay)\n elif optimizer_type == \"sgd\":\n momentum = self.optimizer_config.get(\"momentum\", 0.9)\n optimizer = SGD(self.parameters(), lr=lr, weight_decay=weight_decay, momentum=momentum)\n else:\n raise ValueError(f\"Unsupported optimizer: {optimizer_type}\")\n \n # Setup scheduler\n scheduler_type = self.scheduler_config.get(\"type\", \"cosine\").lower()\n \n if scheduler_type == \"cosine\":\n T_max = self.scheduler_config.get(\"T_max\", 100)\n eta_min = self.scheduler_config.get(\"eta_min\", 0)\n scheduler = CosineAnnealingLR(optimizer, T_max=T_max, eta_min=eta_min)\n return [optimizer], [scheduler]\n elif scheduler_type == \"plateau\":\n patience = self.scheduler_config.get(\"patience\", 10)\n factor = self.scheduler_config.get(\"factor\", 0.5)\n monitor = self.scheduler_config.get(\"monitor\", \"val/loss\")\n scheduler = ReduceLROnPlateau(optimizer, patience=patience, factor=factor)\n return {\n \"optimizer\": optimizer,\n \"lr_scheduler\": {\n \"scheduler\": scheduler,\n \"monitor\": monitor,\n \"interval\": \"epoch\",\n \"frequency\": 1\n }\n }\n elif scheduler_type == \"step\":\n step_size = self.scheduler_config.get(\"step_size\", 30)\n gamma = self.scheduler_config.get(\"gamma\", 0.1)\n scheduler = StepLR(optimizer, step_size=step_size, gamma=gamma)\n return [optimizer], [scheduler]\n else:\n return optimizer\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A comprehensive PyTorch Lightning framework for medical image classification with support for 2D/3D images",
"version": "0.1.0",
"project_urls": {
"Bug Tracker": "https://github.com/Hi-Zhipeng/MedVision-classification/issues",
"Documentation": "https://github.com/Hi-Zhipeng/MedVision-classification#readme",
"Homepage": "https://github.com/Hi-Zhipeng/MedVision-classification",
"Repository": "https://github.com/Hi-Zhipeng/MedVision-classification"
},
"split_keywords": [
"medical imaging",
" classification",
" pytorch",
" lightning",
" deep learning"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "25e76a9292eff1ad6a4801ffa0290fea61e6b07f88186f616d1c6eb564baa72b",
"md5": "2230764f1564f60a034173b60d11641d",
"sha256": "edfb9248f21cfb9c490c0b05c627352163f43f69453c1edc3c49611ff989b0a7"
},
"downloads": -1,
"filename": "medvision_classification-0.1.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "2230764f1564f60a034173b60d11641d",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 54485,
"upload_time": "2025-07-15T05:21:57",
"upload_time_iso_8601": "2025-07-15T05:21:57.841051Z",
"url": "https://files.pythonhosted.org/packages/25/e7/6a9292eff1ad6a4801ffa0290fea61e6b07f88186f616d1c6eb564baa72b/medvision_classification-0.1.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "a055e4b48c8b98e5e6f07ce2dd4f8da868b62afd4a455e1376f28e30e6f7abb1",
"md5": "c7134a2d454e926c640eddd5f5230438",
"sha256": "7ea941740eb64754892108ecf6966e040084ca5e97866f29a48589ddcc14c15e"
},
"downloads": -1,
"filename": "medvision_classification-0.1.0.tar.gz",
"has_sig": false,
"md5_digest": "c7134a2d454e926c640eddd5f5230438",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 47624,
"upload_time": "2025-07-15T05:22:00",
"upload_time_iso_8601": "2025-07-15T05:22:00.065046Z",
"url": "https://files.pythonhosted.org/packages/a0/55/e4b48c8b98e5e6f07ce2dd4f8da868b62afd4a455e1376f28e30e6f7abb1/medvision_classification-0.1.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-07-15 05:22:00",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "Hi-Zhipeng",
"github_project": "MedVision-classification",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"requirements": [
{
"name": "torch",
"specs": [
[
">=",
"2.0.0"
]
]
},
{
"name": "torchvision",
"specs": [
[
">=",
"0.15.0"
]
]
},
{
"name": "pytorch-lightning",
"specs": [
[
">=",
"2.0.0"
]
]
},
{
"name": "lightning",
"specs": [
[
">=",
"2.0.0"
]
]
},
{
"name": "numpy",
"specs": [
[
">=",
"1.21.0"
]
]
},
{
"name": "pyyaml",
"specs": [
[
">=",
"6.0"
]
]
},
{
"name": "tqdm",
"specs": [
[
">=",
"4.64.0"
]
]
},
{
"name": "click",
"specs": [
[
">=",
"8.0.0"
]
]
},
{
"name": "pillow",
"specs": [
[
">=",
"9.0.0"
]
]
},
{
"name": "scikit-image",
"specs": [
[
">=",
"0.19.0"
]
]
},
{
"name": "pandas",
"specs": [
[
">=",
"1.4.0"
]
]
},
{
"name": "nibabel",
"specs": [
[
">=",
"3.2.0"
]
]
},
{
"name": "SimpleITK",
"specs": [
[
">=",
"2.2.0"
]
]
},
{
"name": "pydicom",
"specs": [
[
">=",
"2.3.0"
]
]
},
{
"name": "monai",
"specs": [
[
">=",
"1.3.0"
]
]
},
{
"name": "opencv-python",
"specs": [
[
">=",
"4.7.0"
]
]
},
{
"name": "albumentations",
"specs": [
[
">=",
"1.3.0"
]
]
},
{
"name": "matplotlib",
"specs": [
[
">=",
"3.6.0"
]
]
},
{
"name": "seaborn",
"specs": [
[
">=",
"0.12.0"
]
]
},
{
"name": "tensorboard",
"specs": [
[
">=",
"2.10.0"
]
]
},
{
"name": "wandb",
"specs": [
[
">=",
"0.15.0"
]
]
},
{
"name": "scikit-learn",
"specs": [
[
">=",
"1.1.0"
]
]
},
{
"name": "scipy",
"specs": [
[
">=",
"1.9.0"
]
]
},
{
"name": "torchmetrics",
"specs": [
[
">=",
"0.11.0"
]
]
},
{
"name": "timm",
"specs": [
[
">=",
"0.9.0"
]
]
},
{
"name": "efficientnet-pytorch",
"specs": [
[
">=",
"0.7.0"
]
]
},
{
"name": "hydra-core",
"specs": [
[
">=",
"1.3.0"
]
]
},
{
"name": "omegaconf",
"specs": [
[
">=",
"2.3.0"
]
]
},
{
"name": "rich",
"specs": [
[
">=",
"13.0.0"
]
]
}
],
"lcname": "medvision-classification"
}