Repository: ivanpanshin/SupCon-Framework
Branch: main
Commit: 4a934d4a19b7
Files: 25
Total size: 97.5 KB
Directory structure:
gitextract_cf8_gd7q/
├── .gitattributes
├── LICENSE.md
├── README.md
├── configs/
│ └── train/
│ ├── lr_finder_supcon_resnet18_cifar100_stage2.yml
│ ├── lr_finder_supcon_resnet18_cifar10_stage2.yml
│ ├── swa_supcon_resnet18_cifar100_stage1.yml
│ ├── swa_supcon_resnet18_cifar100_stage2.yml
│ ├── swa_supcon_resnet18_cifar10_stage1.yml
│ ├── swa_supcon_resnet18_cifar10_stage2.yml
│ ├── train_supcon_resnet18_cifar100_stage1.yml
│ ├── train_supcon_resnet18_cifar100_stage2.yml
│ ├── train_supcon_resnet18_cifar10_stage1.yml
│ └── train_supcon_resnet18_cifar10_stage2.yml
├── learning_rate_finder.py
├── requirements.txt
├── swa.py
├── t-SNE.ipynb
├── tools/
│ ├── backbones.py
│ ├── datasets.py
│ ├── losses.py
│ ├── models.py
│ ├── optimizers.py
│ ├── schedulers.py
│ └── utils.py
└── train.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
*.ipynb linguist-vendored
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) 2022 Ivan Panshin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# SupCon-Framework
The repo is an implementation of [Supervised Contrastive Learning](https://arxiv.org/abs/2004.11362). It's based on another [implementation](https://github.com/HobbitLong/SupContrast), but with several differencies:
- Fixed bugs (incorrect ResNet implementations, which leads to a very small max batch size),
- Offers a lot of additional functionality (first of all, rich validation).
To be more precise, in this implementations you will find:
- Augmentations with [albumentations](https://github.com/albumentations-team/albumentations)
- Hyperparameters are moved to .yml configs
- [t-SNE](https://github.com/DmitryUlyanov/Multicore-TSNE) visualizations
- 2-step validation (for features before and after the projection head) using metrics like AMI, NMI, mAP, precision_at_1, etc with [PyTorch Metric Learning](https://github.com/KevinMusgrave/pytorch-metric-learning).
- [Exponential Moving Average](https://github.com/fadel/pytorch_ema) for a more stable training, and Stochastic Moving Average for a better generalization and just overall performance.
- Automatic Mixed Precision (torch version) training in order to be able to train with a bigger batch size (roughly by a factor of 2).
- LabelSmoothing loss, and [LRFinder](https://github.com/davidtvs/pytorch-lr-finder) for the second stage of the training (FC).
- TensorBoard logs, checkpoints
- Support of [timm models](https://github.com/rwightman/pytorch-image-models), and [pytorch-optimizer](https://github.com/jettify/pytorch-optimizer)
## Install
1. Clone the repo:
```
git clone https://github.com/ivanpanshin/SupCon-Framework && cd SupCon-Framework/
```
2. Create a clean virtual environment
```
python3 -m venv venv
source venv/bin/activate
```
3. Install dependencies
````
python -m pip install --upgrade pip
pip install -r requirements.txt
````
## Training
In order to execute Cifar10 training run:
```
python train.py --config_name configs/train/train_supcon_resnet18_cifar10_stage1.yml
python swa.py --config_name configs/train/swa_supcon_resnet18_cifar10_stage1.yml
python train.py --config_name configs/train/train_supcon_resnet18_cifar10_stage2.yml
python swa.py --config_name configs/train/swa_supcon_resnet18_cifar10_stage2.yml
```
In order to run LRFinder on the second stage of the training, run:
```
python learning_rate_finder.py --config_name configs/train/lr_finder_supcon_resnet18_cifar10_stage2.yml
```
The process of training Cifar100 is exactly the same, just change config names from cifar10 to cifar100.
After that you can check the results of the training either in `logs` or `runs` directory. For example, in order to check tensorboard logs for the first stage of Cifar10 training, run:
```
tensorboard --logdir runs/supcon_first_stage_cifar10
```
## Visualizations
This repo is supplied with t-SNE visualizations so that you can check embeddings you get after the training. Check `t-SNE.ipynb` for details.
Those are t-SNE visualizations for Cifar10 for validation and train with SupCon (top), and validation and train with CE (bottom).
Those are t-SNE visualizations for Cifar100 for validation and train with SupCon (top), and validation and train with CE (bottom).
## Results
| Model | Stage | Dataset | Accuracy |
| ------------- | ------------- | ------------- | ------------- |
| ResNet18 | Frist | CIFAR10 | 95.9 |
| ResNet18 | Second | CIFAR10 | 94.9 |
| ResNet18 | Frist | CIFAR100 | 79.0 |
| ResNet18 | Second | CIFAR100 | 77.9 |
Note that even though the accuracy on the second stage is lower, it's not always the case. In my experience, the difference between stages is usually around 1 percent, including the difference that favors the second stage.
Training time for the whole pipeline (without any early stopping) on CIFAR10 or CIFAR100 is around 4 hours (single 2080Ti with AMP). However, with reasonable early stopping that value goes down to around 2.5-3 hours.
## Custom datasets
It's fairly easy to adapt this pipeline to custom datasets. First, you need to check `tools/datasets.py` for that. Second, add a new class for your dataset. The only guideline here is to follow the same augmentation logic, that is
```
if self.second_stage:
image = self.transform(image=image)['image']
else:
image = self.transform(image)
```
Third, add your dataset to `DATASETS` dict still inside `tools/datasets.py`, and you're good to go.
## FAQ
- Q: What hyperparameters should I try to change?
A: First of all, learning rate. Second of all, try to change the augmentation policy. SupCon is built around "cropping + color jittering" scheme, so you can try changing the cropping size or the intensity of jittering. Check `tools.utils.build_transforms` for that.
- Q: What backbone and batch size should I use?
A: This is quite simple. Take the biggest backbone that you can, and after that take the highest batch size your GPU can offer. The reason for that: SupCon is more prone (than regular classification training with CE/LabelSmoothing/etc) to improving with stronger backbones. Moverover, it has a property of explicit hard positive and negative mining. It means that the higher the batch size - the more difficult and helpful samples you supply to the model.
- Q: Do I need the second stage of the training?
A: Not necessarily. You can do classification based only on embeddings. In order to do that compute embeddings for the train set, and at inference time do the following: take a sample, compute its embedding, take the closest one from the training, take its class. To make this fast and efficient, you something like [faiss](https://github.com/facebookresearch/faiss) for similarity search. Note that this is actually how validation is done in this repo. Moveover, during training you will see a metric `precision_at_1`. This is actually just an accuracy based solely on embeddings.
- Q: Should I use AMP?
A: If your GPU has tensor cores (like 2080Ti) - yes. If it doesn't (like 1080Ti) - check the speed with AMP and without. If the speed dropped slightly (or even increased by a bit) - use it, since SupCon works better with bigger batch sizes.
- Q: How should I use EMA?
A: You only need to choose the `ema_decay_per_epoch` parameter in the config. The heuristic is fairly simple. If your dataset is big, then something as small as 0.3 will do just fine. And as your dataset gets smaller, you can increase `ema_decay_per_epoch`. Thanks to bonlime for this idea. I advice you to check his great [pytorch tools](https://github.com/bonlime/pytorch-tools) repo, it's a hidden gem.
- Q: Is it better than training with Cross Entropy/Label Smoothing/etc?
A: Unfortunately, in my experience, it's much easier to get better results with something like CE. It's more stable, faster to train, and simply produces better or the same results. For instance, in case on CIFAR10/100 it's trivial to train ResNet18 up tp 96/81 percent respectively. Of cource, I've seen cases where SupCon performs better, but it takes quite a bit of work to make it outperform CE.
- Q: How long should I train with SupCon?
A: The answer is tricky. On one hand, authors of the original paper claim that the longer you train with SupCon, the better it gets. However, I did not observe such a behavior in my tests. So the only recommendation I can give is the following: start with 100 epochs for easy datasets (like CIFAR10/100), and 1000 for more industrial ones. Then - monitor the training process. If the validaton metric (such as `precision_at_1`) doesn't impove for several dozens of epochs - you can stop the training. You might incorporate early stopping for this reason into the pipeline.
================================================
FILE: configs/train/lr_finder_supcon_resnet18_cifar100_stage2.yml
================================================
model:
backbone: resnet18
ckpt_pretrained:
num_classes: 100
dataset: data/cifar100
dataloaders:
train_batch_size: 200 # the higher - the better
valid_batch_size: 200
num_workers: 16 # set this to num of threads in your CPU
optimizer:
name: SGD
params:
lr: 0.001
criterion:
name: 'CrossEntropy'
================================================
FILE: configs/train/lr_finder_supcon_resnet18_cifar10_stage2.yml
================================================
model:
backbone: resnet18
ckpt_pretrained:
num_classes: 10
dataset: data/cifar10
dataloaders:
train_batch_size: 200 # the higher - the better
valid_batch_size: 200
num_workers: 16 # set this to num of threads in your CPU
optimizer:
name: SGD
params:
lr: 0.001
criterion:
name: 'CrossEntropy'
================================================
FILE: configs/train/swa_supcon_resnet18_cifar100_stage1.yml
================================================
model:
backbone: resnet18
num_classes:
top_k_checkoints: 3
train:
amp: True # set this to True, if your GPU supports FP16. 2080Ti - okay, 1080Ti - not okay
weights_dir: weights/supcon_first_stage_cifar100
stage: first
dataset: data/cifar100
dataloaders:
train_batch_size: 200 # the higher - the better
valid_batch_size: 200
num_workers: 12 # set this to num of threads in your CPU
================================================
FILE: configs/train/swa_supcon_resnet18_cifar100_stage2.yml
================================================
model:
backbone: resnet18
num_classes: 100
top_k_checkoints: 3
train:
amp: True # set this to True, if your GPU supports FP16. 2080Ti - okay, 1080Ti - not okay
weights_dir: weights/supcon_second_stage_cifar100
stage: second
dataset: data/cifar100
dataloaders:
train_batch_size: 200 # the higher - the better
valid_batch_size: 200
num_workers: 12 # set this to num of threads in your CPU
================================================
FILE: configs/train/swa_supcon_resnet18_cifar10_stage1.yml
================================================
model:
backbone: resnet18
num_classes:
top_k_checkoints: 3
train:
amp: True # set this to True, if your GPU supports FP16. 2080Ti - okay, 1080Ti - not okay
weights_dir: weights/supcon_first_stage_cifar10
stage: first
dataset: data/cifar10
dataloaders:
train_batch_size: 200 # the higher - the better
valid_batch_size: 200
num_workers: 12 # set this to num of threads in your CPU
================================================
FILE: configs/train/swa_supcon_resnet18_cifar10_stage2.yml
================================================
model:
backbone: resnet18
num_classes: 10
top_k_checkoints: 3
train:
amp: True # set this to True, if your GPU supports FP16. 2080Ti - okay, 1080Ti - not okay
weights_dir: weights/supcon_second_stage_cifar10
stage: second
dataset: data/cifar10
dataloaders:
train_batch_size: 200 # the higher - the better
valid_batch_size: 200
num_workers: 12 # set this to num of threads in your CPU
================================================
FILE: configs/train/train_supcon_resnet18_cifar100_stage1.yml
================================================
model:
backbone: resnet18
ckpt_pretrained:
num_classes: # in the first stage of training we don't need num_classes, since we don't have a FC head
train:
n_epochs: 100
amp: True # set this to True, if your GPU supports FP16. 2080Ti - okay, 1080Ti - not okay
ema: True # optional, but I recommend it, since the training might get unstable otherwise
ema_decay_per_epoch: 0.3 # 0.3 for middle/big datasets. Increase, if you have low amount of samples
logging_name: supcon_first_stage_cifar100
target_metric: precision_at_1
stage: first # first = Supcon, second = FC finetuning for classification
dataset: data/cifar100
dataloaders:
train_batch_size: 200 # the higher - the better
valid_batch_size: 200
num_workers: 12 # set this to num of threads in your CPU
optimizer:
name: SGD
params:
lr: 0.1
scheduler:
name: CosineAnnealingLR
params:
T_max: 100
eta_min: 0.01
criterion:
name: 'SupCon'
params:
temperature: 0.1
================================================
FILE: configs/train/train_supcon_resnet18_cifar100_stage2.yml
================================================
model:
backbone: resnet18
ckpt_pretrained: weights/supcon_first_stage_cifar100/swa
num_classes: 100
train:
n_epochs: 20
amp: True # set this to True, if your GPU supports FP16. 2080Ti - okay, 1080Ti - not okay
ema: True # optional, but I recommend it, since the training might get unstable otherwise
ema_decay_per_epoch: 0.3 # for middle/big datasets. Increase, if you have low amount of samples
logging_name: supcon_second_stage_cifar100
target_metric: accuracy
stage: second # first = Supcon, second = FC finetuning for classification
dataset: data/cifar100
dataloaders:
train_batch_size: 20 # the higher - the better
valid_batch_size: 20
num_workers: 12 # set this to num of threads in your CPU
optimizer:
name: SGD
params:
lr: 0.01
scheduler:
name: CosineAnnealingLR
params:
T_max: 20
eta_min: 0.001
criterion:
name: 'LabelSmoothing'
params:
classes: 100
smoothing: 0.01
================================================
FILE: configs/train/train_supcon_resnet18_cifar10_stage1.yml
================================================
model:
backbone: resnet18
ckpt_pretrained:
num_classes: # in the first stage of training we don't need num_classes, since we don't have a FC head
train:
n_epochs: 100
amp: True # set this to True, if your GPU supports FP16. 2080Ti - okay, 1080Ti - not okay
ema: True # optional, but I recommend it, since the training might get unstable otherwise
ema_decay_per_epoch: 0.3 # 0.3 for middle/big datasets. Increase, if you have low amount of samples
logging_name: supcon_first_stage_cifar10
target_metric: precision_at_1
stage: first # first = Supcon, second = FC finetuning for classification
dataset: data/cifar10
dataloaders:
train_batch_size: 200 # the higher - the better
valid_batch_size: 200
num_workers: 12 # set this to num of threads in your CPU
optimizer:
name: SGD
params:
lr: 0.1
scheduler:
name: CosineAnnealingLR
params:
T_max: 100
eta_min: 0.01
criterion:
name: 'SupCon'
params:
temperature: 0.1
================================================
FILE: configs/train/train_supcon_resnet18_cifar10_stage2.yml
================================================
model:
backbone: resnet18
ckpt_pretrained: weights/supcon_first_stage_cifar10/swa
num_classes: 10
train:
n_epochs: 20
amp: True # set this to True, if your GPU supports FP16. 2080Ti - okay, 1080Ti - not okay
ema: True # optional, but I recommend it, since the training might get unstable otherwise
ema_decay_per_epoch: 0.3 # for middle/big datasets. Increase, if you have low amount of samples
logging_name: supcon_second_stage_cifar10
target_metric: accuracy
stage: second # first = Supcon, second = FC finetuning for classification
dataset: data/cifar10
dataloaders:
train_batch_size: 20 # the higher - the better
valid_batch_size: 20
num_workers: 12 # set this to num of threads in your CPU
optimizer:
name: SGD
params:
lr: 0.01
scheduler:
name: CosineAnnealingLR
params:
T_max: 20
eta_min: 0.001
criterion:
name: 'LabelSmoothing'
params:
classes: 10
smoothing: 0.01
================================================
FILE: learning_rate_finder.py
================================================
import argparse
import os
import matplotlib.pyplot as plt
import yaml
from torch_lr_finder import LRFinder
from tools import utils
def parse_config():
parser = argparse.ArgumentParser()
parser.add_argument(
"--config_name",
type=str,
default="configs/train/lr_finder_supcon_resnet18_cifar10_stage2.yml",
)
parser_args = parser.parse_args()
with open(vars(parser_args)["config_name"], "r") as config_file:
hyperparams = yaml.full_load(config_file)
return hyperparams
if __name__ == "__main__":
os.makedirs("lr_finder_plots", exist_ok=True)
hyperparams = parse_config()
backbone = hyperparams["model"]["backbone"]
ckpt_pretrained = hyperparams["model"]["ckpt_pretrained"]
num_classes = hyperparams['model']['num_classes']
optimizer_params = hyperparams["optimizer"]
scheduler_params = None
criterion_params = hyperparams["criterion"]
data_dir = hyperparams["dataset"]
batch_sizes = {
"train_batch_size": hyperparams["dataloaders"]["train_batch_size"],
"valid_batch_size": hyperparams["dataloaders"]["valid_batch_size"]
}
num_workers = hyperparams["dataloaders"]["num_workers"]
transforms = utils.build_transforms(second_stage=True)
loaders = utils.build_loaders(data_dir, transforms, batch_sizes, num_workers, second_stage=True)
model = utils.build_model(backbone, second_stage=True, num_classes=num_classes,
ckpt_pretrained=ckpt_pretrained).cuda()
optim = utils.build_optim(model, optimizer_params, scheduler_params, criterion_params)
criterion, optimizer, scheduler = (
optim["criterion"],
optim["optimizer"],
optim["scheduler"],
)
lr_finder = LRFinder(model, optimizer, criterion, device="cuda")
lr_finder.range_test(loaders["train_features_loader"], end_lr=1, num_iter=300)
fig, ax = plt.subplots()
lr_finder.plot(ax=ax)
fig.savefig(
"lr_finder_plots/supcon_{}_{}_bs_{}_stage_{}_lr_finder.png".format(
optimizer_params["name"],
data_dir.split("/")[-1],
batch_sizes["train_batch_size"],
'second'
)
)
================================================
FILE: requirements.txt
================================================
absl-py==0.11.0
albumentations==0.5.2
argon2-cffi==20.1.0
async-generator==1.10
attrs==20.3.0
backcall==0.2.0
bleach==3.2.1
cachetools==4.2.0
certifi==2020.12.5
cffi==1.14.4
chardet==4.0.0
cycler==0.10.0
dataclasses==0.8
decorator==4.4.2
defusedxml==0.6.0
entrypoints==0.3
faiss-gpu==1.6.5
google-auth==1.24.0
google-auth-oauthlib==0.4.2
grpcio==1.34.0
idna==2.10
imageio==2.9.0
imgaug==0.4.0
importlib-metadata==3.3.0
ipykernel==5.4.3
ipython==7.16.1
ipython-genutils==0.2.0
ipywidgets==7.6.3
jedi==0.18.0
Jinja2==2.11.2
joblib==1.0.0
jsonschema==3.2.0
jupyter==1.0.0
jupyter-client==6.1.11
jupyter-console==6.2.0
jupyter-core==4.7.0
jupyterlab-pygments==0.1.2
jupyterlab-widgets==1.0.0
kiwisolver==1.3.1
Markdown==3.3.3
MarkupSafe==1.1.1
matplotlib==3.3.3
mistune==0.8.4
MulticoreTSNE==0.1
nbclient==0.5.1
nbconvert==6.0.7
nbformat==5.0.8
nest-asyncio==1.4.3
networkx==2.5
notebook==6.1.6
numpy==1.19.4
oauthlib==3.1.0
opencv-python==4.4.0.46
opencv-python-headless==4.4.0.46
packaging==20.8
pandas==1.1.5
pandocfilters==1.4.3
parso==0.8.1
pexpect==4.8.0
pickleshare==0.7.5
Pillow==8.0.1
prometheus-client==0.9.0
prompt-toolkit==3.0.10
protobuf==3.14.0
ptyprocess==0.7.0
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycparser==2.20
Pygments==2.7.4
pyparsing==2.4.7
pyrsistent==0.17.3
python-dateutil==2.8.1
pytorch-metric-learning==0.9.95
pytorch-ranger==0.1.1
pytz==2020.4
PyWavelets==1.1.1
PyYAML==5.3.1
pyzmq==20.0.0
qtconsole==5.0.1
QtPy==1.9.0
requests==2.25.1
requests-oauthlib==1.3.0
rsa==4.6
scikit-image==0.17.2
scikit-learn==0.23.2
scipy==1.5.4
Send2Trash==1.5.0
Shapely==1.7.1
six==1.15.0
tensorboard==2.4.0
tensorboard-plugin-wit==1.7.0
terminado==0.9.2
testpath==0.4.4
threadpoolctl==2.1.0
tifffile==2020.9.3
timm==0.3.4
torch==1.7.1
torch-ema @ git+https://github.com/fadel/pytorch_ema@27afe25b9fb9f0d05a87ae94e4e4ad9e92d70a85
torch-lr-finder==0.2.1
torch-optimizer==0.0.1a17
torchvision==0.8.2
tornado==6.1
tqdm==4.54.1
traitlets==4.3.3
typing-extensions==3.7.4.3
urllib3==1.26.2
wcwidth==0.2.5
webencodings==0.5.1
Werkzeug==1.0.1
widgetsnbextension==3.5.1
zipp==3.4.0
================================================
FILE: swa.py
================================================
import os
from collections import OrderedDict
import torch
import argparse
import yaml
from tools import utils
scaler = torch.cuda.amp.GradScaler()
def swa(paths):
state_dicts = []
for path in paths:
state_dicts.append(torch.load(path)["model_state_dict"])
average_dict = OrderedDict()
for k in state_dicts[0].keys():
average_dict[k] = sum([state_dict[k] for state_dict in state_dicts]) / len(state_dicts)
return average_dict
def parse_config():
parser = argparse.ArgumentParser()
parser.add_argument(
"--config_name",
type=str,
default="configs/train/swa_supcon_resnet18_cifar10_stage2.yml"
)
parser_args = parser.parse_args()
with open(vars(parser_args)["config_name"], "r") as config_file:
hyperparams = yaml.full_load(config_file)
return hyperparams
if __name__ == "__main__":
hyperparams = parse_config()
backbone = hyperparams["model"]["backbone"]
num_classes = hyperparams['model']['num_classes']
top_k_checkoints = hyperparams['model']['top_k_checkoints']
amp = hyperparams['train']['amp']
weights_dir = hyperparams['train']['weights_dir']
stage = hyperparams['train']['stage']
data_dir = hyperparams["dataset"]
batch_sizes = {
"train_batch_size": hyperparams["dataloaders"]["train_batch_size"],
'valid_batch_size': hyperparams['dataloaders']['valid_batch_size']
}
num_workers = hyperparams["dataloaders"]["num_workers"]
if not amp: scaler = None
utils.seed_everything()
if os.path.exists(os.path.join(weights_dir, "swa")):
os.remove(os.path.join(weights_dir, "swa"))
transforms = utils.build_transforms(second_stage=(stage == 'second'))
loaders = utils.build_loaders(data_dir, transforms, batch_sizes, num_workers, second_stage=(stage == 'second'))
model = utils.build_model(backbone, second_stage=(stage == 'second'), num_classes=num_classes, ckpt_pretrained=None).cuda()
list_of_epochs = sorted([int(x.split('epoch')[1]) for x in os.listdir(weights_dir)])
best_epochs = list_of_epochs[-top_k_checkoints::]
model_prefix = "epoch"
checkpoints_paths = ["{}/{}{}".format(weights_dir, model_prefix, epoch) for epoch in best_epochs]
average_dict = swa(checkpoints_paths)
torch.save({"model_state_dict": average_dict}, os.path.join(weights_dir, "swa"))
model.load_state_dict(torch.load(os.path.join(weights_dir, "swa"))['model_state_dict'])
if stage == 'first':
valid_metrics = utils.validation_constructive(loaders['valid_loader'], loaders['train_features_loader'], model,
scaler)
else:
valid_metrics = utils.validation_ce(model, None, loaders['valid_loader'], scaler)
print('swa stage {} validation metrics: {}'.format(stage, valid_metrics))
================================================
FILE: t-SNE.ipynb
================================================
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from MulticoreTSNE import MulticoreTSNE as TSNE\n",
"from matplotlib import pyplot as plt\n",
"from tools import utils\n",
"import torch\n",
"import torch.functional as F\n",
"\n",
"scaler = torch.cuda.amp.GradScaler()"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Files already downloaded and verified\n",
"Files already downloaded and verified\n",
"Files already downloaded and verified\n"
]
}
],
"source": [
"ckpt_pretrained = 'weights/supcon_first_stage_cifar10/swa'\n",
"data_dir = 'data/cifar10'\n",
"num_classes = 10\n",
"batch_sizes = {\n",
" \"train_batch_size\": 20,\n",
" 'valid_batch_size': 20\n",
"}\n",
"num_workers = 16\n",
"backbone = 'resnet18'\n",
"stage = 'first'\n",
"\n",
"transforms = utils.build_transforms(second_stage=(stage == 'second'))\n",
"loaders = utils.build_loaders(data_dir, transforms, batch_sizes, num_workers, second_stage=(stage == 'second'))\n",
" "
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
""
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"model = utils.build_model(backbone, second_stage=(stage == 'second'), num_classes=num_classes, ckpt_pretrained=ckpt_pretrained).cuda()\n",
"model.use_projection_head(False)\n",
"model.eval()\n",
"\n",
"embeddings, labels = utils.compute_embeddings(loaders['valid_loader'], model, scaler)\n",
"embeddings_tsne = TSNE(n_jobs=num_workers).fit_transform(embeddings)\n",
"vis_x = embeddings_tsne[:, 0]\n",
"vis_y = embeddings_tsne[:, 1]\n",
"plt.scatter(vis_x, vis_y, c=labels, cmap=plt.cm.get_cmap(\"jet\", num_classes), marker='.')\n",
"plt.colorbar(ticks=range(num_classes))\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"embeddings, labels = utils.compute_embeddings(loaders['train_features_loader'], model, scaler)\n",
"embeddings_tsne = TSNE(n_jobs=num_workers).fit_transform(embeddings)\n",
"vis_x = embeddings_tsne[:, 0]\n",
"vis_y = embeddings_tsne[:, 1]\n",
"plt.scatter(vis_x, vis_y, c=labels, cmap=plt.cm.get_cmap(\"jet\", num_classes), marker='.')\n",
"plt.colorbar(ticks=range(num_classes))\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.9"
},
"pycharm": {
"stem_cell": {
"cell_type": "raw",
"metadata": {
"collapsed": false
},
"source": []
}
}
},
"nbformat": 4,
"nbformat_minor": 4
}
================================================
FILE: tools/backbones.py
================================================
import torchvision.models as models
# for timm models we don't have such files, since it provides a simple wrapper timm.create_model. Check tools.models.py
BACKBONES = {
"alexnet": models.alexnet,
"resnet18": models.resnet18,
"resnet34": models.resnet34,
"resnet50": models.resnet50,
"resnet101": models.resnet101,
"resnet152": models.resnet152,
"mobilenet_v2": models.mobilenet_v2,
"vgg11": models.vgg11,
"vgg11_bn": models.vgg11_bn,
"vgg13": models.vgg13,
"vgg13_bn": models.vgg13_bn,
"vgg16": models.vgg16,
"vgg16_bn": models.vgg16_bn,
"vgg19": models.vgg19,
"vgg19_bn": models.vgg19_bn,
"densenet121": models.densenet121,
"densenet169": models.densenet169,
"densenet161": models.densenet161,
"densenet201": models.densenet201,
"inception_v3": models.inception_v3,
"resnext50_32x4d": models.resnext50_32x4d,
"resnext101_32x8d": models.resnext101_32x8d,
"wide_resnet50": models.wide_resnet50_2,
"wide_resnet101": models.wide_resnet101_2,
}
================================================
FILE: tools/datasets.py
================================================
import torchvision
class SupConDatasetCifar10(torchvision.datasets.CIFAR10):
def __init__(self, data_dir, train, transform, second_stage):
super().__init__(root=data_dir, train=train, download=True, transform=transform)
self.second_stage = second_stage
self.transform = transform
def __getitem__(self, idx):
image, label = self.data[idx], self.targets[idx]
# leave this part unchanged. The reason for this implementation - in the first stage of training
# you have TwoCropTransform(actual transforms), so you have to call it by self.transform(img)
# on the other hard, in the second stage of training there is no wrapper, so it's a regular
# albumentation trans block, so it's called by self.transform(image=img)['image']
if self.second_stage:
image = self.transform(image=image)['image']
else:
image = self.transform(image)
return image, label
class SupConDatasetCifar100(torchvision.datasets.CIFAR100):
def __init__(self, data_dir, train, transform, second_stage):
super().__init__(root=data_dir, train=train, download=True, transform=transform)
self.second_stage = second_stage
self.transform = transform
def __getitem__(self, idx):
image, label = self.data[idx], self.targets[idx]
# leave this part unchanged. The reason for this implementation - in the first stage of training
# you have TwoCropTransform(actual transforms), so you have to call it by self.transform(img)
# on the other hard, in the second stage of training there is no wrapper, so it's a regular
# albumentation trans block, so it's called by self.transform(image=img)['image']
if self.second_stage:
image = self.transform(image=image)['image']
else:
image = self.transform(image)
return image, label
DATASETS = {'cifar10': SupConDatasetCifar10,
'cifar100': SupConDatasetCifar100}
def create_supcon_dataset(dataset_name, data_dir, train, transform, second_stage):#, csv, second_stage):
try:
return DATASETS[dataset_name](data_dir, train, transform, second_stage)#, csv, second_stage)
except KeyError:
Exception('Can\'t find such a dataset. Either use cifar10 or cifar100, or write your own one in tools.datasets')
================================================
FILE: tools/losses.py
================================================
import torch.nn as nn
import torch
class SupConLoss(nn.Module):
"""Supervised Contrastive Learning: https://arxiv.org/pdf/2004.11362.pdf.
It also supports the unsupervised contrastive loss in SimCLR"""
def __init__(self, temperature=0.07, contrast_mode='all',
base_temperature=0.07):
super(SupConLoss, self).__init__()
self.temperature = temperature
self.contrast_mode = contrast_mode
self.base_temperature = base_temperature
def forward(self, features, labels=None, mask=None):
"""Compute loss for model. If both `labels` and `mask` are None,
it degenerates to SimCLR unsupervised loss:
https://arxiv.org/pdf/2002.05709.pdf
Args:
features: hidden vector of shape [bsz, n_views, ...].
labels: ground truth of shape [bsz].
mask: contrastive mask of shape [bsz, bsz], mask_{i,j}=1 if sample j
has the same class as sample i. Can be asymmetric.
Returns:
A loss scalar.
"""
device = (torch.device('cuda')
if features.is_cuda
else torch.device('cpu'))
if len(features.shape) < 3:
raise ValueError('`features` needs to be [bsz, n_views, ...],'
'at least 3 dimensions are required')
if len(features.shape) > 3:
features = features.view(features.shape[0], features.shape[1], -1)
batch_size = features.shape[0]
if labels is not None and mask is not None:
raise ValueError('Cannot define both `labels` and `mask`')
elif labels is None and mask is None:
mask = torch.eye(batch_size, dtype=torch.float32).to(device)
elif labels is not None:
labels = labels.contiguous().view(-1, 1)
if labels.shape[0] != batch_size:
raise ValueError('Num of labels does not match num of features')
mask = torch.eq(labels, labels.T).float().to(device)
else:
mask = mask.float().to(device)
contrast_count = features.shape[1]
contrast_feature = torch.cat(torch.unbind(features, dim=1), dim=0)
if self.contrast_mode == 'one':
anchor_feature = features[:, 0]
anchor_count = 1
elif self.contrast_mode == 'all':
anchor_feature = contrast_feature
anchor_count = contrast_count
else:
raise ValueError('Unknown mode: {}'.format(self.contrast_mode))
# compute logits
anchor_dot_contrast = torch.div(
torch.matmul(anchor_feature, contrast_feature.T),
self.temperature)
# for numerical stability
logits_max, _ = torch.max(anchor_dot_contrast, dim=1, keepdim=True)
logits = anchor_dot_contrast - logits_max.detach()
# tile mask
mask = mask.repeat(anchor_count, contrast_count)
# mask-out self-contrast cases
logits_mask = torch.scatter(
torch.ones_like(mask),
1,
torch.arange(batch_size * anchor_count).view(-1, 1).to(device),
0
)
mask = mask * logits_mask
# compute log_prob
exp_logits = torch.exp(logits) * logits_mask
log_prob = logits - torch.log(exp_logits.sum(1, keepdim=True))
# compute mean of log-likelihood over positive
mean_log_prob_pos = (mask * log_prob).sum(1) / mask.sum(1)
# loss
loss = - (self.temperature / self.base_temperature) * mean_log_prob_pos
loss = loss.view(anchor_count, batch_size).mean()
return loss
class LabelSmoothingLoss(nn.Module):
def __init__(self, classes, smoothing=0, dim=-1):
super(LabelSmoothingLoss, self).__init__()
self.confidence = 1.0 - smoothing
self.smoothing = smoothing
self.cls = classes
self.dim = dim
def forward(self, pred, target):
pred = pred.log_softmax(dim=self.dim)
with torch.no_grad():
# true_dist = pred.data.clone()
true_dist = torch.zeros_like(pred)
true_dist.fill_(self.smoothing / (self.cls - 1))
true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)
return torch.mean(torch.sum(-true_dist * pred, dim=self.dim))
LOSSES = {'SupCon': SupConLoss,
'LabelSmoothing': LabelSmoothingLoss,
'CrossEntropy': nn.CrossEntropyLoss}
================================================
FILE: tools/models.py
================================================
import torch
import torch.nn as nn
import torch.nn.functional as F
import timm
from .backbones import BACKBONES
def create_encoder(backbone):
try:
if 'timm_' in backbone:
backbone = backbone.split('_')[-1]
timm.create_model(model_name=backbone, pretrained=True)
else:
model = BACKBONES[backbone](pretrained=True)
except RuntimeError or KeyError:
raise RuntimeError('Specify the correct backbone name. Either one of torchvision backbones, or a timm backbone.'
'For timm - add prefix \'timm_\'. For instance, timm_resnet18')
layers = torch.nn.Sequential(*list(model.children()))
try:
potential_last_layer = layers[-1]
while not isinstance(potential_last_layer, nn.Linear):
potential_last_layer = potential_last_layer[-1]
except TypeError:
raise TypeError('Can\'t find the linear layer of the model')
features_dim = potential_last_layer.in_features
model = torch.nn.Sequential(*list(model.children())[:-1])
return model, features_dim
class SupConModel(nn.Module):
def __init__(self, backbone='resnet50', projection_dim=128, second_stage=False, num_classes=None):
super(SupConModel, self).__init__()
self.encoder, self.features_dim = create_encoder(backbone)
self.second_stage = second_stage
self.projection_head = True
self.projection_dim = projection_dim
self.embed_dim = projection_dim
if self.second_stage:
for param in self.encoder.parameters():
param.requires_grad = False
self.classifier = nn.Linear(self.features_dim, num_classes)
else:
self.head = nn.Sequential(
nn.Linear(self.features_dim, self.features_dim),
nn.ReLU(inplace=True),
nn.Linear(self.features_dim, self.projection_dim))
def use_projection_head(self, mode):
self.projection_head = mode
if mode:
self.embed_dim = self.projection_dim
else:
self.embed_dim = self.features_dim
def forward(self, x):
if self.second_stage:
feat = self.encoder(x).squeeze()
return self.classifier(feat)
else:
feat = self.encoder(x).squeeze()
if self.projection_head:
return F.normalize(self.head(feat), dim=1)
else:
return F.normalize(feat, dim=1)
================================================
FILE: tools/optimizers.py
================================================
import torch.optim as optim
import torch_optimizer as jettify_optim
OPTIMIZERS = {
"Adam": optim.Adam,
'AdamW': optim.AdamW,
"SGD": optim.SGD,
'LookAhead': jettify_optim.Lookahead,
'Ranger': jettify_optim.Ranger,
'RAdam': jettify_optim.RAdam,
}
================================================
FILE: tools/schedulers.py
================================================
import torch.optim as optim
SCHEDULERS = {
"ReduceLROnPlateau": optim.lr_scheduler.ReduceLROnPlateau,
"CosineAnnealingLR": optim.lr_scheduler.CosineAnnealingLR,
}
================================================
FILE: tools/utils.py
================================================
import torch
import albumentations as A
from albumentations import pytorch as AT
from torch.utils.data import DataLoader
import random
import os
import numpy as np
import torch.backends.cudnn as cudnn
from sklearn.metrics import f1_score
from pytorch_metric_learning.utils.accuracy_calculator import AccuracyCalculator
from .losses import LOSSES
from .optimizers import OPTIMIZERS
from .schedulers import SCHEDULERS
from .models import SupConModel
from .datasets import create_supcon_dataset
def seed_everything(seed=42):
random.seed(seed)
os.environ["PYHTONHASHSEED"] = str(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.backends.cudnn.deterministic = True
def add_to_logs(logging, message):
logging.info(message)
def add_to_tensorboard_logs(writer, message, tag, index):
writer.add_scalar(tag, message, index)
class TwoCropTransform:
"""Create two crops of the same image"""
def __init__(self, crop_transform):
self.crop_transform = crop_transform
def __call__(self, x):
return [self.crop_transform(image=x), self.crop_transform(image=x)]
def build_transforms(second_stage):
if second_stage:
train_transforms = A.Compose([
#A.Flip(),
#A.Rotate(),
A.Resize(224, 224),
A.Normalize(),
AT.ToTensorV2()
])
valid_transforms = A.Compose([A.Resize(224, 224), A.Normalize(), AT.ToTensorV2()])
transforms_dict = {
"train_transforms": train_transforms,
"valid_transforms": valid_transforms,
}
else:
train_transforms = A.Compose([
A.RandomResizedCrop(height=224, width=224, scale=(0.15, 1.)),
A.Rotate(),
A.ColorJitter(0.4, 0.4, 0.4, 0.1, p=0.9),
A.ToGray(p=0.2),
A.Normalize(),
AT.ToTensorV2(),
])
valid_transforms = A.Compose([A.Resize(224, 224), A.Normalize(), AT.ToTensorV2()])
transforms_dict = {
"train_transforms": train_transforms,
'valid_transforms': valid_transforms,
}
return transforms_dict
def build_loaders(data_dir, transforms, batch_sizes, num_workers, second_stage=False):
dataset_name = data_dir.split('/')[-1]
if second_stage:
train_features_dataset = create_supcon_dataset(dataset_name, data_dir=data_dir, train=True,
transform=transforms['train_transforms'], second_stage=True)
else:
# train_features_dataset is used for evaluation -> hence, we don't need TwoCropTransform
train_features_dataset = create_supcon_dataset(dataset_name, data_dir=data_dir, train=True,
transform=transforms['valid_transforms'], second_stage=True)
train_supcon_dataset = create_supcon_dataset(dataset_name, data_dir=data_dir, train=True,
transform=TwoCropTransform(transforms['train_transforms']), second_stage=False)
valid_dataset = create_supcon_dataset(dataset_name, data_dir=data_dir, train=False,
transform=transforms['valid_transforms'], second_stage=True)
if not second_stage:
train_supcon_loader = DataLoader(
train_supcon_dataset, batch_size=batch_sizes['train_batch_size'], shuffle=True,
num_workers=num_workers, pin_memory=True)
train_features_loader = DataLoader(
train_features_dataset, batch_size=batch_sizes['train_batch_size'], shuffle=True,
num_workers=num_workers, pin_memory=True, drop_last=True)
valid_loader = DataLoader(
valid_dataset, batch_size=batch_sizes['valid_batch_size'], shuffle=True,
num_workers=num_workers, pin_memory=True, drop_last=True)
if second_stage:
return {'train_features_loader': train_features_loader, 'valid_loader': valid_loader}
return {'train_supcon_loader': train_supcon_loader, 'train_features_loader': train_features_loader, 'valid_loader': valid_loader}
def build_model(backbone, second_stage=False, num_classes=None, ckpt_pretrained=None):
model = SupConModel(backbone=backbone, second_stage=second_stage, num_classes=num_classes)
if ckpt_pretrained:
model.load_state_dict(torch.load(ckpt_pretrained)['model_state_dict'], strict=False)
return model
def build_optim(model, optimizer_params, scheduler_params, loss_params):
if 'params' in loss_params:
criterion = LOSSES[loss_params['name']](**loss_params['params'])
else:
criterion = LOSSES[loss_params['name']]()
optimizer = OPTIMIZERS[optimizer_params["name"]](model.parameters(), **optimizer_params["params"])
if scheduler_params:
scheduler = SCHEDULERS[scheduler_params["name"]](optimizer, **scheduler_params["params"])
else:
scheduler = None
return {"criterion": criterion, "optimizer": optimizer, "scheduler": scheduler}
def compute_embeddings(loader, model, scaler):
# note that it's okay to do len(loader) * bs, since drop_last=True is enabled
total_embeddings = np.zeros((len(loader)*loader.batch_size, model.embed_dim))
total_labels = np.zeros(len(loader)*loader.batch_size)
for idx, (images, labels) in enumerate(loader):
images = images.cuda()
bsz = labels.shape[0]
if scaler:
with torch.cuda.amp.autocast():
embed = model(images)
total_embeddings[idx * bsz: (idx + 1) * bsz] = embed.detach().cpu().numpy()
total_labels[idx * bsz: (idx + 1) * bsz] = labels.detach().numpy()
else:
embed = model(images)
total_embeddings[idx * bsz: (idx + 1) * bsz] = embed.detach().cpu().numpy()
total_labels[idx * bsz: (idx + 1) * bsz] = labels.detach().numpy()
del images, labels, embed
torch.cuda.empty_cache()
return np.float32(total_embeddings), total_labels.astype(int)
def train_epoch_constructive(train_loader, model, criterion, optimizer, scaler, ema):
model.train()
train_loss = []
for idx, (images, labels) in enumerate(train_loader):
images = torch.cat([images[0]['image'], images[1]['image']], dim=0)
images = images.cuda()
labels = labels.cuda()
bsz = labels.shape[0]
if scaler:
with torch.cuda.amp.autocast():
embed = model(images)
f1, f2 = torch.split(embed, [bsz, bsz], dim=0)
embed = torch.cat([f1.unsqueeze(1), f2.unsqueeze(1)], dim=1)
loss = criterion(embed, labels)
else:
embed = model(images)
f1, f2 = torch.split(embed, [bsz, bsz], dim=0)
embed = torch.cat([f1.unsqueeze(1), f2.unsqueeze(1)], dim=1)
loss = criterion(embed, labels)
del images, labels, embed
torch.cuda.empty_cache()
train_loss.append(loss.item())
optimizer.zero_grad()
if scaler:
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
else:
loss.backward()
optimizer.step()
if ema:
ema.update(model.parameters())
return {'loss': np.mean(train_loss)}
def validation_constructive(valid_loader, train_loader, model, scaler):
calculator = AccuracyCalculator(k=1)
model.eval()
query_embeddings, query_labels = compute_embeddings(valid_loader, model, scaler)
reference_embeddings, reference_labels = compute_embeddings(train_loader, model, scaler)
acc_dict = calculator.get_accuracy(
query_embeddings,
reference_embeddings,
query_labels,
reference_labels,
embeddings_come_from_same_source=False
)
del query_embeddings, query_labels, reference_embeddings, reference_labels
torch.cuda.empty_cache()
return acc_dict
def train_epoch_ce(train_loader, model, criterion, optimizer, scaler, ema):
model.train()
train_loss = []
for batch_i, (data, target) in enumerate(train_loader):
data, target = data.cuda(), target.cuda()
optimizer.zero_grad()
if scaler:
with torch.cuda.amp.autocast():
output = model(data)
loss = criterion(output, target)
train_loss.append(loss.item())
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
else:
output = model(data)
loss = criterion(output, target)
train_loss.append(loss.item())
loss.backward()
optimizer.step()
if ema:
ema.update(model.parameters())
del data, target, output
torch.cuda.empty_cache()
return {"loss": np.mean(train_loss)}
def validation_ce(model, criterion, valid_loader, scaler):
model.eval()
val_loss = []
valid_bs = valid_loader.batch_size
# note that it's okay to do len(loader) * bs, since drop_last=True is enabled
y_pred, y_true = np.zeros(len(valid_loader)*valid_bs), np.zeros(len(valid_loader)*valid_bs)
correct_samples = 0
for batch_i, (data, target) in enumerate(valid_loader):
with torch.no_grad():
data, target = data.cuda(), target.cuda()
if scaler:
with torch.cuda.amp.autocast():
output = model(data)
if criterion:
loss = criterion(output, target)
val_loss.append(loss.item())
else:
output = model(data)
if criterion:
loss = criterion(output, target)
val_loss.append(loss.item())
correct_samples += (
target.detach().cpu().numpy() == np.argmax(output.detach().cpu().numpy(), axis=1)
).sum()
y_pred[batch_i * valid_bs : (batch_i + 1) * valid_bs] = np.argmax(output.detach().cpu().numpy(), axis=1)
y_true[batch_i * valid_bs : (batch_i + 1) * valid_bs] = target.detach().cpu().numpy()
del data, target, output
torch.cuda.empty_cache()
valid_loss = np.mean(val_loss)
f1_scores = f1_score(y_true, y_pred, average=None)
f1_score_macro = f1_score(y_true, y_pred, average='macro')
accuracy_score = correct_samples / (len(valid_loader)*valid_bs)
metrics = {"loss": valid_loss, "accuracy": accuracy_score, "f1_scores": f1_scores, 'f1_score_macro': f1_score_macro}
return metrics
def copy_parameters_from_model(model):
copy_of_model_parameters = [p.clone().detach() for p in model.parameters() if p.requires_grad]
return copy_of_model_parameters
def copy_parameters_to_model(copy_of_model_parameters, model):
for s_param, param in zip(copy_of_model_parameters, model.parameters()):
if param.requires_grad:
param.data.copy_(s_param.data)
================================================
FILE: train.py
================================================
import argparse
import logging
import os
import time
import torch
import yaml
import shutil
from torch.utils.tensorboard import SummaryWriter
from torch_ema import ExponentialMovingAverage
from tools import utils
scaler = torch.cuda.amp.GradScaler()
def parse_config():
parser = argparse.ArgumentParser()
parser.add_argument(
"--config_name",
type=str,
default="configs/train/train_supcon_resnet18_cifar10_stage2.yml",
)
parser_args = parser.parse_args()
with open(vars(parser_args)["config_name"], "r") as config_file:
hyperparams = yaml.full_load(config_file)
return hyperparams
if __name__ == "__main__":
# parse hyperparameters
hyperparams = parse_config()
print(hyperparams)
backbone = hyperparams["model"]["backbone"]
ckpt_pretrained = hyperparams['model']['ckpt_pretrained']
num_classes = hyperparams['model']['num_classes']
amp = hyperparams['train']['amp']
ema = hyperparams['train']['ema']
ema_decay_per_epoch = hyperparams['train']['ema_decay_per_epoch']
n_epochs = hyperparams["train"]["n_epochs"]
logging_name = hyperparams['train']['logging_name']
target_metric = hyperparams['train']['target_metric']
stage = hyperparams['train']['stage']
data_dir = hyperparams["dataset"]
optimizer_params = hyperparams["optimizer"]
scheduler_params = hyperparams["scheduler"]
criterion_params = hyperparams["criterion"]
batch_sizes = {
"train_batch_size": hyperparams["dataloaders"]["train_batch_size"],
'valid_batch_size': hyperparams['dataloaders']['valid_batch_size']
}
num_workers = hyperparams["dataloaders"]["num_workers"]
if not amp: scaler = None
utils.seed_everything()
# create model, loaders, optimizer, etc
transforms = utils.build_transforms(second_stage=(stage == 'second'))
loaders = utils.build_loaders(data_dir, transforms, batch_sizes, num_workers, second_stage=(stage == 'second'))
model = utils.build_model(backbone, second_stage=(stage == 'second'), num_classes=num_classes, ckpt_pretrained=ckpt_pretrained).cuda()
if ema:
iters = len(loaders['train_features_loader'])
ema_decay = ema_decay_per_epoch**(1/iters)
ema = ExponentialMovingAverage(model.parameters(), decay=ema_decay)
optim = utils.build_optim(model, optimizer_params, scheduler_params, criterion_params)
criterion, optimizer, scheduler = (
optim["criterion"],
optim["optimizer"],
optim["scheduler"],
)
# handle logging (regular logs, tensorboard, and weights)
if logging_name is None:
logging_name = "stage_{}_model_{}_dataset_{}".format(stage, backbone, data_dir.split("/")[-1])
shutil.rmtree("weights/{}".format(logging_name), ignore_errors=True)
shutil.rmtree(
"runs/{}".format(logging_name),
ignore_errors=True,
)
shutil.rmtree(
"logs/{}".format(logging_name),
ignore_errors=True,
)
os.makedirs(
"logs/{}".format(logging_name),
exist_ok=True,
)
writer = SummaryWriter("runs/{}".format(logging_name))
logging_dir = "logs/{}".format(logging_name)
logging_path = os.path.join(logging_dir, "train.log")
logging.basicConfig(filename=logging_path, level=logging.INFO, filemode="w+")
# epoch loop
metric_best = 0
for epoch in range(n_epochs):
utils.add_to_logs(logging, "{}, epoch {}".format(time.ctime(), epoch))
start_training_time = time.time()
if stage == 'first':
train_metrics = utils.train_epoch_constructive(loaders['train_supcon_loader'], model, criterion, optimizer, scaler, ema)
else:
train_metrics = utils.train_epoch_ce(loaders['train_features_loader'], model, criterion, optimizer, scaler, ema)
end_training_time = time.time()
if ema:
copy_of_model_parameters = utils.copy_parameters_from_model(model)
ema.copy_to(model.parameters())
start_validation_time = time.time()
if stage == 'first':
valid_metrics_projection_head = utils.validation_constructive(loaders['valid_loader'], loaders['train_features_loader'], model, scaler)
model.use_projection_head(False)
valid_metrics_encoder = utils.validation_constructive(loaders['valid_loader'], loaders['train_features_loader'], model, scaler)
model.use_projection_head(True)
print(
'epoch {}, train time {:.2f} valid time {:.2f} train loss {:.2f}\nvalid acc dict projection head {}\nvalid acc dict encoder {}'.format(
epoch,
end_training_time - start_training_time,
time.time() - start_validation_time,
train_metrics['loss'], valid_metrics_projection_head, valid_metrics_encoder))
valid_metrics = valid_metrics_projection_head
else:
valid_metrics = utils.validation_ce(model, criterion, loaders['valid_loader'], scaler)
print(
'epoch {}, train time {:.2f} valid time {:.2f} train loss {:.2f}\n valid acc dict {}\n'.format(
epoch,
end_training_time - start_training_time,
time.time() - start_validation_time,
train_metrics['loss'], valid_metrics))
# write train and valid metrics to the logs
utils.add_to_tensorboard_logs(writer, train_metrics['loss'], "Loss/train", epoch)
for valid_metric in valid_metrics:
try:
utils.add_to_tensorboard_logs(writer, valid_metrics[valid_metric],
'{}/validation'.format(valid_metric), epoch)
except AssertionError:
# in case valid metric is a list
pass
if stage == 'first':
utils.add_to_logs(
logging,
"Epoch {}, train loss: {:.4f}\nvalid metrics projection head: {}\nvalid metric encoder: {}".format(
epoch,
train_metrics['loss'],
valid_metrics_projection_head,
valid_metrics_encoder
),
)
else:
utils.add_to_logs(
logging,
"Epoch {}, train loss: {:.4f} valid metrics: {}".format(
epoch,
train_metrics['loss'],
valid_metrics
),
)
# check if the best value of metric changed. If so -> save the model
if valid_metrics[target_metric] > metric_best:
utils.add_to_logs(
logging,
"{} increased ({:.6f} --> {:.6f}). Saving model ...".format(target_metric,
metric_best, valid_metrics[target_metric]
),
)
os.makedirs(
"weights/{}".format(logging_name),
exist_ok=True,
)
torch.save(
{
"epoch": epoch,
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
},
"weights/{}/epoch{}".format(
logging_name, epoch
),
)
metric_best = valid_metrics[target_metric]
# if ema is used, go back to regular weights without ema
if ema:
utils.copy_parameters_to_model(copy_of_model_parameters, model)
scheduler.step()
writer.close()