commit 91ecf157d62f68a65ab75fff648bc3d7ce47c246 Author: Steffen Illium Date: Thu Feb 13 20:28:20 2020 +0100 initial diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..e7e9d11 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml diff --git a/.idea/hom_traj_gen.iml b/.idea/hom_traj_gen.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/hom_traj_gen.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8656114 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..0b3a4df --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/build_data.py b/build_data.py new file mode 100644 index 0000000..1528552 --- /dev/null +++ b/build_data.py @@ -0,0 +1,11 @@ +from pathlib import Path + +from lib.objects.map import Map +from preprocessing.generator import Generator + +if __name__ == '__main__': + data_root = Path() / 'data' + maps_root = Path() / 'res' / 'maps' + map_object = Map('Tate').from_image(maps_root / 'tate_sw.bmp') + generator = Generator(data_root, map_object) + generator.generate_n_trajectories_m_alternatives(100, 10, 'test') diff --git a/dataset/__pycache__/dataset.cpython-37.pyc b/dataset/__pycache__/dataset.cpython-37.pyc new file mode 100644 index 0000000..06e264d Binary files /dev/null and b/dataset/__pycache__/dataset.cpython-37.pyc differ diff --git a/dataset/dataset.py b/dataset/dataset.py new file mode 100644 index 0000000..fb78e2f --- /dev/null +++ b/dataset/dataset.py @@ -0,0 +1,96 @@ +import shelve +from pathlib import Path + +import torch +from torch.utils.data import ConcatDataset, Dataset + +from lib.objects.map import Map +from preprocessing.generator import Generator + + +class TrajDataset(Dataset): + def __init__(self, data): + super(TrajDataset, self).__init__() + self.alternatives = data['alternatives'] + self.trajectory = data['trajectory'] + self.labels = data['labels'] + + def __len__(self): + return len(self.alternatives) + + def __getitem__(self, item): + return self.trajectory.vertices, self.alternatives[item].vertices, self.labels[item] + + +class DataSetMapping(Dataset): + def __init__(self, dataset, mapping): + self._dataset = dataset + self._mapping = mapping + + def __len__(self): + return self._mapping.shape[0] + + def __getitem__(self, item): + return self._dataset[self._mapping[item]] + + +class TrajData(object): + @property + def name(self): + return self.__class__.__name__ + + def __init__(self, data_root, mapname='tate_sw', trajectories=1000, alternatives=10, + train_val_test_split=(0.6, 0.2, 0.2), rebuild=False, equal_samples=True, **_): + + self.rebuild = rebuild + self.equal_samples = equal_samples + self._alternatives = alternatives + self._trajectories = trajectories + self.mapname = mapname + self.train_split, self.val_split, self.test_split = train_val_test_split + self.data_root = Path(data_root) + self._dataset = None + self._dataset, self._train_map, self._val_map, self._test_map = self._load_dataset() + + def _build_data_on_demand(self): + maps_root = Path() / 'res' / 'maps' + map_object = Map(self.mapname).from_image(maps_root / f'{self.mapname}.bmp') + assert maps_root.exists() + dataset_file = Path(self.data_root) / f'{self.mapname}.pik' + if dataset_file.exists() and self.rebuild: + dataset_file.unlink() + if not dataset_file.exists(): + generator = Generator(self.data_root, map_object) + generator.generate_n_trajectories_m_alternatives(self._trajectories, self._alternatives, + self.mapname, equal_samples=self.equal_samples) + return True + + def _load_dataset(self): + assert self._build_data_on_demand() + with shelve.open(str(self.data_root / f'{self.mapname}.pik')) as d: + dataset = ConcatDataset([TrajDataset(d[key]) for key in d.keys() if key != 'map']) + indices = torch.randperm(len(dataset)) + + train_size = int(len(dataset) * self.train_split) + val_size = int(len(dataset) * self.val_split) + test_size = int(len(dataset) * self.test_split) + + train_map = indices[:train_size] + val_map = indices[train_size:val_size] + test_map = indices[test_size:] + return dataset, train_map, val_map, test_map + + @property + def train_dataset(self): + return DataSetMapping(self._dataset, self._train_map) + + @property + def val_dataset(self): + return DataSetMapping(self._dataset, self._val_map) + + @property + def test_dataset(self): + return DataSetMapping(self._dataset, self._test_map) + + def get_datasets(self): + return self.train_dataset, self.val_dataset, self.test_dataset diff --git a/evaluation/classification.py b/evaluation/classification.py new file mode 100644 index 0000000..4e56c10 --- /dev/null +++ b/evaluation/classification.py @@ -0,0 +1,37 @@ +import matplotlib.pyplot as plt + +from sklearn.metrics import roc_curve, auc + + +class ROCEvaluation(object): + + BINARY_PROBLEM = 2 + linewidth = 2 + + def __init__(self, save_fig=True): + self.epoch = 0 + pass + + def __call__(self, prediction, label, prepare_fig=True): + + # Compute ROC curve and ROC area + fpr, tpr, _ = roc_curve(prediction, label) + roc_auc = auc(fpr, tpr) + + if prepare_fig: + fig = self._prepare_fig() + fig.plot(fpr, tpr, color='darkorange', + lw=2, label=f'ROC curve (area = {roc_auc})') + self._prepare_fig() + return roc_auc + + def _prepare_fig(self): + fig = plt.gcf() + fig.plot([0, 1], [0, 1], color='navy', lw=self.linewidth, linestyle='--') + fig.xlim([0.0, 1.0]) + fig.ylim([0.0, 1.05]) + fig.xlabel('False Positive Rate') + fig.ylabel('True Positive Rate') + + fig.legend(loc="lower right") + return fig diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/__pycache__/__init__.cpython-37.pyc b/lib/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..b3e442d Binary files /dev/null and b/lib/__pycache__/__init__.cpython-37.pyc differ diff --git a/lib/models/__init__.py b/lib/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/models/blocks.py b/lib/models/blocks.py new file mode 100644 index 0000000..f1697b3 --- /dev/null +++ b/lib/models/blocks.py @@ -0,0 +1,468 @@ +from abc import ABC +from pathlib import Path +from typing import Union + +import torch +from torch import nn +import torch.nn.functional as F +import pytorch_lightning as pl + + +# Utility - Modules +################### +from torch.utils.data import DataLoader + +from dataset.dataset import TrajData + + +class Flatten(nn.Module): + def __init__(self, to=(-1, )): + super(Flatten, self).__init__() + self.to = to + + def forward(self, x): + return x.view(x.size(0), *self.to) + + +class Interpolate(nn.Module): + def __init__(self, size=None, scale_factor=None, mode='nearest', align_corners=None): + super(Interpolate, self).__init__() + self.interp = nn.functional.interpolate + self.size = size + self.scale_factor = scale_factor + self.align_corners = align_corners + self.mode = mode + + def forward(self, x): + x = self.interp(x, size=self.size, scale_factor=self.scale_factor, + mode=self.mode, align_corners=self.align_corners) + return x + + +class AutoPad(nn.Module): + + def __init__(self, interpolations=3, base=2): + super(AutoPad, self).__init__() + self.fct = base ** interpolations + + def forward(self, x): + x = F.pad(x, + [0, + (x.shape[-1] // self.fct + 1) * self.fct - x.shape[-1] if x.shape[-1] % self.fct != 0 else 0, + (x.shape[-2] // self.fct + 1) * self.fct - x.shape[-2] if x.shape[-2] % self.fct != 0 else 0, + 0]) + return x + + +class LightningBaseModule(pl.LightningModule, ABC): + + @classmethod + def name(cls): + raise NotImplementedError('Give your model a name!') + + @property + def shape(self): + try: + x = torch.randn(self.in_shape).unsqueeze(0) + output = self(x) + return output.shape[1:] + except Exception as e: + print(e) + return -1 + + def __init__(self, params): + super(LightningBaseModule, self).__init__() + self.hparams = params + + # Data loading + # ============================================================================= + # Dataset + self.dataset = TrajData('data') + + def size(self): + return self.shape + + def _move_to_model_device(self, x): + return x.cuda() if next(self.parameters()).is_cuda else x.cpu() + + def save_to_disk(self, model_path): + Path(model_path, exist_ok=True).mkdir(parents=True, exist_ok=True) + if not (model_path / 'model_class.obj').exists(): + with (model_path / 'model_class.obj').open('wb') as f: + torch.save(self.__class__, f) + return True + + @pl.data_loader + def train_dataloader(self): + return DataLoader(dataset=self.dataset.train_dataset, shuffle=True, + batch_size=self.hparams.data_param.batchsize, + num_workers=self.hparams.data_param.worker) + + @pl.data_loader + def test_dataloader(self): + return DataLoader(dataset=self.dataset.test_dataset, shuffle=True, + batch_size=self.hparams.data_param.batchsize, + num_workers=self.hparams.data_param.worker) + + @pl.data_loader + def val_dataloader(self): + return DataLoader(dataset=self.dataset.val_dataset, shuffle=True, + batch_size=self.hparams.data_param.batchsize, + num_workers=self.hparams.data_param.worker) + + def configure_optimizers(self): + raise NotImplementedError + + def forward(self, *args, **kwargs): + raise NotImplementedError + + def validation_step(self, *args, **kwargs): + raise NotImplementedError + + def validation_end(self, outputs): + raise NotImplementedError + + def training_step(self, batch_xy, batch_nb, *args, **kwargs): + raise NotImplementedError + + def test_step(self, *args, **kwargs): + raise NotImplementedError + + def test_end(self, outputs): + from sklearn.metrics import roc_auc_score + + y_scores, y_true = [], [] + for output in outputs: + y_scores.append(output['y_pred']) + y_true.append(output['y_true']) + + y_true = torch.cat(y_true, dim=0) + # FIXME: What did this do do i need it? + # y_true = (y_true != V.HOMOTOPIC).long() + y_scores = torch.cat(y_scores, dim=0) + + roc_auc_scores = roc_auc_score(y_true.cpu().numpy(), y_scores.cpu().numpy()) + print(f'AUC Score: {roc_auc_scores}') + return {'roc_auc_scores': roc_auc_scores} + + def init_weights(self): + def _weight_init(m): + if hasattr(m, 'weight'): + if isinstance(m.weight, torch.Tensor): + torch.nn.init.xavier_uniform_(m.weight) + if hasattr(m, 'bias'): + if isinstance(m.bias, torch.Tensor): + m.bias.data.fill_(0.01) + self.apply(_weight_init) + + +# +# Sub - Modules +################### +class ConvModule(nn.Module): + + @property + def shape(self): + x = torch.randn(self.in_shape).unsqueeze(0) + output = self(x) + return output.shape[1:] + + def __init__(self, in_shape, activation: nn.Module = nn.ELU, pooling_size=None, use_bias=True, use_norm=True, + dropout: Union[int, float] = 0, + conv_filters=64, conv_kernel=5, conv_stride=1, conv_padding=0): + super(ConvModule, self).__init__() + + # Module Paramters + self.in_shape = in_shape + in_channels, height, width = in_shape[0], in_shape[1], in_shape[2] + self.activation = activation() + + # Convolution Paramters + self.padding = conv_padding + self.stride = conv_stride + + # Modules + self.dropout = nn.Dropout2d(dropout) if dropout else False + self.pooling = nn.MaxPool2d(pooling_size) if pooling_size else False + self.norm = nn.BatchNorm2d(in_channels, eps=1e-04, affine=False) if use_norm else False + self.conv = nn.Conv2d(in_channels, conv_filters, conv_kernel, bias=use_bias, + padding=self.padding, stride=self.stride + ) + + def forward(self, x): + x = self.norm(x) if self.norm else x + + tensor = self.conv(x) + tensor = self.dropout(tensor) if self.dropout else tensor + tensor = self.pooling(tensor) if self.pooling else tensor + tensor = self.activation(tensor) + return tensor + + +class DeConvModule(nn.Module): + + @property + def shape(self): + x = torch.randn(self.in_shape).unsqueeze(0) + output = self(x) + return output.shape[1:] + + def __init__(self, in_shape, conv_filters=3, conv_kernel=5, conv_stride=1, conv_padding=0, + dropout: Union[int, float] = 0, autopad=False, + activation: Union[None, nn.Module] = nn.ReLU, interpolation_scale=None, + use_bias=True, normalize=False): + super(DeConvModule, self).__init__() + in_channels, height, width = in_shape[0], in_shape[1], in_shape[2] + self.padding = conv_padding + self.stride = conv_stride + self.in_shape = in_shape + self.conv_filters = conv_filters + + self.autopad = AutoPad() if autopad else False + self.interpolation = Interpolate(scale_factor=interpolation_scale) if interpolation_scale else False + self.norm = nn.BatchNorm2d(in_channels, eps=1e-04, affine=False) if normalize else False + self.dropout = nn.Dropout2d(dropout) if dropout else False + self.de_conv = nn.ConvTranspose2d(in_channels, self.conv_filters, conv_kernel, bias=use_bias, + padding=self.padding, stride=self.stride) + + self.activation = activation() if activation else None + + def forward(self, x): + x = self.norm(x) if self.norm else x + x = self.dropout(x) if self.dropout else x + x = self.autopad(x) if self.autopad else x + x = self.interpolation(x) if self.interpolation else x + + tensor = self.de_conv(x) + tensor = self.activation(tensor) if self.activation else tensor + return tensor + + def size(self): + return self.shape + + +# +# Full Model Parts +################### +class Generator(nn.Module): + @property + def shape(self): + x = torch.randn(self.lat_dim).unsqueeze(0) + output = self(x) + return output.shape[1:] + + # noinspection PyUnresolvedReferences + def __init__(self, out_channels, re_shape, lat_dim, use_norm=False, use_bias=True, dropout: Union[int, float] = 0, + filters: List[int] = None, activation=nn.ReLU): + super(Generator, self).__init__() + assert filters, '"Filters" has to be a list of int len 3' + self.filters = filters + self.activation = activation + self.inner_activation = activation() + self.out_activation = None + self.lat_dim = lat_dim + self.dropout = dropout + self.l1 = nn.Linear(self.lat_dim, reduce(mul, re_shape), bias=use_bias) + # re_shape = (self.lat_dim // reduce(mul, re_shape[1:]), ) + tuple(re_shape[1:]) + + self.flat = Flatten(to=re_shape) + + self.deconv1 = DeConvModule(re_shape, conv_filters=self.filters[0], + conv_kernel=5, + conv_padding=2, + conv_stride=1, + normalize=use_norm, + activation=self.activation, + interpolation_scale=2, + dropout=self.dropout + ) + + self.deconv2 = DeConvModule(self.deconv1.shape, conv_filters=self.filters[1], + conv_kernel=3, + conv_padding=1, + conv_stride=1, + normalize=use_norm, + activation=self.activation, + interpolation_scale=2, + dropout=self.dropout + ) + + self.deconv3 = DeConvModule(self.deconv2.shape, conv_filters=self.filters[2], + conv_kernel=3, + conv_padding=1, + conv_stride=1, + normalize=use_norm, + activation=self.activation, + interpolation_scale=2, + dropout=self.dropout + ) + + self.deconv4 = DeConvModule(self.deconv3.shape, conv_filters=out_channels, + conv_kernel=3, + conv_padding=1, + # normalize=use_norm, + activation=self.out_activation + ) + + def forward(self, z): + tensor = self.l1(z) + tensor = self.inner_activation(tensor) + tensor = self.flat(tensor) + tensor = self.deconv1(tensor) + tensor = self.deconv2(tensor) + tensor = self.deconv3(tensor) + tensor = self.deconv4(tensor) + return tensor + + def size(self): + return self.shape + + +class UnitGenerator(Generator): + + def __init__(self, *args, **kwargs): + kwargs.update(use_norm=True) + super(UnitGenerator, self).__init__(*args, **kwargs) + self.norm_f = nn.BatchNorm1d(self.l1.out_features, eps=1e-04, affine=False) + self.norm1 = nn.BatchNorm2d(self.deconv1.conv_filters, eps=1e-04, affine=False) + self.norm2 = nn.BatchNorm2d(self.deconv2.conv_filters, eps=1e-04, affine=False) + self.norm3 = nn.BatchNorm2d(self.deconv3.conv_filters, eps=1e-04, affine=False) + + def forward(self, z_c1_c2_c3): + z, c1, c2, c3 = z_c1_c2_c3 + tensor = self.l1(z) + tensor = self.inner_activation(tensor) + tensor = self.norm(tensor) + tensor = self.flat(tensor) + + tensor = self.deconv1(tensor) + c3 + tensor = self.inner_activation(tensor) + tensor = self.norm1(tensor) + + tensor = self.deconv2(tensor) + c2 + tensor = self.inner_activation(tensor) + tensor = self.norm2(tensor) + + tensor = self.deconv3(tensor) + c1 + tensor = self.inner_activation(tensor) + tensor = self.norm3(tensor) + + tensor = self.deconv4(tensor) + return tensor + + +class BaseEncoder(nn.Module): + @property + def shape(self): + x = torch.randn(self.in_shape).unsqueeze(0) + output = self(x) + return output.shape[1:] + + # noinspection PyUnresolvedReferences + def __init__(self, in_shape, lat_dim=256, use_bias=True, use_norm=False, dropout: Union[int, float] = 0, + latent_activation: Union[nn.Module, None] = None, activation: nn.Module = nn.ELU, + filters: List[int] = None): + super(BaseEncoder, self).__init__() + assert filters, '"Filters" has to be a list of int len 3' + + # Optional Padding for odd image-sizes + # Obsolet, already Done by autopadding module on incoming tensors + # in_shape = [x+1 if x % 2 != 0 and idx else x for idx, x in enumerate(in_shape)] + + # Parameters + self.lat_dim = lat_dim + self.in_shape = in_shape + self.use_bias = use_bias + self.latent_activation = latent_activation() if latent_activation else None + + # Modules + self.conv1 = ConvModule(self.in_shape, conv_filters=filters[0], + conv_kernel=3, + conv_padding=1, + conv_stride=1, + pooling_size=2, + use_norm=use_norm, + dropout=dropout, + activation=activation + ) + + self.conv2 = ConvModule(self.conv1.shape, conv_filters=filters[1], + conv_kernel=3, + conv_padding=1, + conv_stride=1, + pooling_size=2, + use_norm=use_norm, + dropout=dropout, + activation=activation + ) + + self.conv3 = ConvModule(self.conv2.shape, conv_filters=filters[2], + conv_kernel=5, + conv_padding=2, + conv_stride=1, + pooling_size=2, + use_norm=use_norm, + dropout=dropout, + activation=activation + ) + + self.flat = Flatten() + + def forward(self, x): + tensor = self.conv1(x) + tensor = self.conv2(tensor) + tensor = self.conv3(tensor) + tensor = self.flat(tensor) + return tensor + + +class UnitEncoder(BaseEncoder): + # noinspection PyUnresolvedReferences + def __init__(self, *args, **kwargs): + kwargs.update(use_norm=True) + super(UnitEncoder, self).__init__(*args, **kwargs) + self.l1 = nn.Linear(reduce(mul, self.conv3.shape), self.lat_dim, bias=self.use_bias) + + def forward(self, x): + c1 = self.conv1(x) + c2 = self.conv2(c1) + c3 = self.conv3(c2) + tensor = self.flat(c3) + l1 = self.l1(tensor) + return c1, c2, c3, l1 + + +class VariationalEncoder(BaseEncoder): + # noinspection PyUnresolvedReferences + def __init__(self, *args, **kwargs): + super(VariationalEncoder, self).__init__(*args, **kwargs) + + self.logvar = nn.Linear(reduce(mul, self.conv3.shape), self.lat_dim, bias=self.use_bias) + self.mu = nn.Linear(reduce(mul, self.conv3.shape), self.lat_dim, bias=self.use_bias) + + @staticmethod + def reparameterize(mu, logvar): + std = torch.exp(0.5*logvar) + eps = torch.randn_like(std) + return mu + eps*std + + def forward(self, x): + tensor = super(VariationalEncoder, self).forward(x) + mu = self.mu(tensor) + logvar = self.logvar(tensor) + z = self.reparameterize(mu, logvar) + return mu, logvar, z + + +class Encoder(BaseEncoder): + # noinspection PyUnresolvedReferences + def __init__(self, *args, **kwargs): + super(Encoder, self).__init__(*args, **kwargs) + + self.l1 = nn.Linear(reduce(mul, self.conv3.shape), self.lat_dim, bias=self.use_bias) + + def forward(self, x): + tensor = super(Encoder, self).forward(x) + tensor = self.l1(tensor) + tensor = self.latent_activation(tensor) if self.latent_activation else tensor + return tensor diff --git a/lib/objects/__init__.py b/lib/objects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/objects/__pycache__/__init__.cpython-37.pyc b/lib/objects/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..27d6e98 Binary files /dev/null and b/lib/objects/__pycache__/__init__.cpython-37.pyc differ diff --git a/lib/objects/__pycache__/map.cpython-37.pyc b/lib/objects/__pycache__/map.cpython-37.pyc new file mode 100644 index 0000000..73bd914 Binary files /dev/null and b/lib/objects/__pycache__/map.cpython-37.pyc differ diff --git a/lib/objects/__pycache__/trajectory.cpython-37.pyc b/lib/objects/__pycache__/trajectory.cpython-37.pyc new file mode 100644 index 0000000..a38c7db Binary files /dev/null and b/lib/objects/__pycache__/trajectory.cpython-37.pyc differ diff --git a/lib/objects/__pycache__/variables.cpython-37.pyc b/lib/objects/__pycache__/variables.cpython-37.pyc new file mode 100644 index 0000000..c7faada Binary files /dev/null and b/lib/objects/__pycache__/variables.cpython-37.pyc differ diff --git a/lib/objects/map.py b/lib/objects/map.py new file mode 100644 index 0000000..fd3d6af --- /dev/null +++ b/lib/objects/map.py @@ -0,0 +1,125 @@ +from pathlib import Path + +import copy +from math import sqrt +import numpy as np + +from PIL import Image, ImageDraw +import networkx as nx +from matplotlib import pyplot as plt + +from lib.objects.trajectory import Trajectory + + +class Map(object): + + white = [1, 255] + black = [0] + + def __copy__(self): + return copy.deepcopy(self) + + @property + def shape(self): + return self.map_array.shape + + @property + def width(self): + return self.shape[0] + + @property + def height(self): + return self.shape[1] + + @property + def as_graph(self): + return self._G + + @property + def as_array(self): + return self.map_array + + def __init__(self, name='', array_like_map_representation=None): + self.map_array: np.ndarray = array_like_map_representation + self.name = name + pass + + def __setattr__(self, key, value): + super(Map, self).__setattr__(key, value) + if key == 'map_array' and self.map_array is not None: + self._G = self._build_graph() + + def _build_graph(self, full_neighbors=True): + graph = nx.Graph() + # Do checks in order: up - left - upperLeft - lowerLeft + neighbors = [(0, -1, 1), (-1, 0, 1), (-1, -1, sqrt(2)), (-1, 1, sqrt(2))] + + # Check pixels for their color (determine if walkable) + for idx, value in np.ndenumerate(self.map_array): + if value in self.white: + y, x = idx + # IF walkable, add node + graph.add_node((y, x), count=0) + # Fully connect to all surrounding neighbors + for n, (xdif, ydif, weight) in enumerate(neighbors): + # Differentiate between 8 and 4 neighbors + if not full_neighbors and n >= 2: + break + + query_node = (y + ydif, x + xdif) + if graph.has_node(query_node): + graph.add_edge(idx, query_node, weight=weight) + return graph + + @classmethod + def from_image(cls, imagepath: Path): + with Image.open(imagepath) as image: + return cls(name=imagepath.name, array_like_map_representation=np.array(image)) + + def simple_trajectory_between(self, start, dest): + vertices = list(nx.shortest_path(self._G, start, dest)) + trajectory = Trajectory(vertices) + return trajectory + + def get_valid_position(self): + not_found, valid_position = True, (-9999, -9999) + while not_found: + valid_position = int(np.random.choice(self.height, 1)), int(np.random.choice(self.width, 1)) + if self._G.has_node(valid_position): + not_found = False + pass + return valid_position + + def get_trajectory_from_vertices(self, *args): + coords = list() + for start, dest in zip(args[:-1], args[1:]): + coords.extend(nx.shortest_path(self._G, start, dest)) + return Trajectory(coords) + + def get_random_trajectory(self): + start = self.get_valid_position() + dest = self.get_valid_position() + return self.simple_trajectory_between(start, dest) + + def are_homotopic(self, trajectory, other_trajectory): + if not all(isinstance(x, Trajectory) for x in [trajectory, other_trajectory]): + raise TypeError + polyline = trajectory.vertices.copy() + polyline.extend(reversed(other_trajectory.vertices)) + + img = Image.new('L', (self.height, self.width), 0) + ImageDraw.Draw(img).polygon(polyline, outline=1, fill=1) + + a = (np.array(img) * self.map_array).sum() + if a >= 1: + return False + else: + return True + + def draw(self): + fig, ax = plt.gcf(), plt.gca() + # The standard colormaps also all have reversed versions. + # They have the same names with _r tacked on to the end. + # https: // matplotlib.org / api / pyplot_summary.html?highlight = colormaps + img = ax.imshow(self.as_array, cmap='Greys_r') + return dict(img=img, fig=fig, ax=ax) diff --git a/lib/objects/trajectory.py b/lib/objects/trajectory.py new file mode 100644 index 0000000..a1cbe3a --- /dev/null +++ b/lib/objects/trajectory.py @@ -0,0 +1,65 @@ +from math import atan2 +from typing import List, Tuple, Union + +from matplotlib import pyplot as plt +from lib.objects import variables as V + + +class Trajectory(object): + + @property + def endpoints(self): + return self.start, self.dest + + @property + def start(self): + return self.vertices[0] + + @property + def dest(self): + return self.vertices[-1] + + @property + def xs(self): + return [x[1] for x in self.vertices] + + @property + def ys(self): + return [x[0] for x in self.vertices] + + @property + def as_paired_list(self): + return list(zip(self.vertices[:-1], self.vertices[1:])) + + def __init__(self, vertices: Union[List[Tuple[int]], None] = None): + assert any((isinstance(vertices, list), vertices is None)) + if vertices is not None: + self.vertices = vertices + pass + + def is_equal_to(self, other_trajectory): + # ToDo: do further equality Checks here + return self.vertices == other_trajectory.vertices + + def draw(self, highlights=True, label=None, **kwargs): + if label is not None: + kwargs.update(color='red' if label == V.HOMOTOPIC else 'green', + label='Homotopic' if label == V.HOMOTOPIC else 'Alternative') + if highlights: + kwargs.update(marker='bo') + fig, ax = plt.gcf(), plt.gca() + img = plt.plot(self.xs, self.ys, **kwargs) + return dict(img=img, fig=fig, ax=ax) + + def min_vertices(self, vertices): + vertices, last_angle = [self.start], 0 + for (x1, y1), (x2, y2) in self.as_paired_list: + current_angle = atan2(x1-x2, y1-y2) + if current_angle != last_angle: + vertices.append((x2, y2)) + last_angle = current_angle + else: + continue + if vertices[-1] != self.dest: + vertices.append(self.dest) + return self.__class__(vertices=vertices) diff --git a/lib/objects/variables.py b/lib/objects/variables.py new file mode 100644 index 0000000..c931827 --- /dev/null +++ b/lib/objects/variables.py @@ -0,0 +1,9 @@ +from pathlib import Path +_ROOT = Path('..') + +HOMOTOPIC = 0 +ALTERNATIVE = 1 + +_key_1 = 'eyJhcGlfYWRkcmVzcyI6Imh0dHBzOi8vdWkubmVwdHVuZS5haSIsImFwaV91cmwiOiJodHRwczovL3VpLm' +_key_2 = '5lcHR1bmUuYWkiLCJhcGlfa2V5IjoiZmI0OGMzNzUtOTg1NS00Yzg2LThjMzYtMWFiYjUwMDUyMjVlIn0=' +NEPTUNE_KEY = _key_1 + _key_2 diff --git a/lib/utils/__init__.py b/lib/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/utils/__pycache__/__init__.cpython-37.pyc b/lib/utils/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..8d17a88 Binary files /dev/null and b/lib/utils/__pycache__/__init__.cpython-37.pyc differ diff --git a/lib/utils/__pycache__/config.cpython-37.pyc b/lib/utils/__pycache__/config.cpython-37.pyc new file mode 100644 index 0000000..c5a35d5 Binary files /dev/null and b/lib/utils/__pycache__/config.cpython-37.pyc differ diff --git a/lib/utils/__pycache__/logging.cpython-37.pyc b/lib/utils/__pycache__/logging.cpython-37.pyc new file mode 100644 index 0000000..c3a4e2c Binary files /dev/null and b/lib/utils/__pycache__/logging.cpython-37.pyc differ diff --git a/lib/utils/config.py b/lib/utils/config.py new file mode 100644 index 0000000..bef0f76 --- /dev/null +++ b/lib/utils/config.py @@ -0,0 +1,96 @@ +import ast + +from argparse import Namespace +from collections import defaultdict +from configparser import ConfigParser +from pathlib import Path + + +def is_jsonable(x): + import json + try: + json.dumps(x) + return True + except TypeError: + return False + + +class Config(ConfigParser): + + # TODO: Do this programmatically; This did not work: + # Initialize Default Sections + # for section in self.default_sections: + # self.__setattr__(section, property(lambda x :x._get_namespace_for_section(section)) + + @property + def main(self): + return self._get_namespace_for_section('main') + + @property + def model(self): + return self._get_namespace_for_section('model') + + @property + def train(self): + return self._get_namespace_for_section('train') + + @property + def data(self): + return self._get_namespace_for_section('data') + + @property + def project(self): + return self._get_namespace_for_section('project') + ################################################### + + @property + def tags(self, ): + return [f'{key}: {val}' for key, val in self.serializable.items()] + + @property + def serializable(self): + return {f'{section}_{key}': val for section, params in self._sections.items() + for key, val in params.items() if is_jsonable(val)} + + @property + def as_dict(self): + return self._sections + + def _get_namespace_for_section(self, item): + return Namespace(**{key: self.get(item, key) for key in self[item]}) + + def __init__(self, **kwargs): + super(Config, self).__init__(**kwargs) + pass + + @classmethod + def read_namespace(cls, namespace: Namespace): + + space_dict = defaultdict(dict) + for key in namespace.__dict__: + section, *attr_name = key.split('_') + attr_name = '_'.join(attr_name) + value = str(namespace.__getattribute__(key)) + + space_dict[section][attr_name] = value + new_config = cls() + new_config.read_dict(space_dict) + return new_config + + def get(self, *args, **kwargs): + item = super(Config, self).get(*args, **kwargs) + try: + return ast.literal_eval(item) + except SyntaxError: + return item + except ValueError: + return item + + def write(self, filepath, **kwargs): + path = Path(filepath, exist_ok=True) + path.parent.mkdir(parents=True, exist_ok=True) + + with path.open('w') as configfile: + super().write(configfile) + + return True diff --git a/lib/utils/logging.py b/lib/utils/logging.py new file mode 100644 index 0000000..5c13116 --- /dev/null +++ b/lib/utils/logging.py @@ -0,0 +1,69 @@ +from pathlib import Path + +from pytorch_lightning.logging.base import LightningLoggerBase +from pytorch_lightning.logging.neptune import NeptuneLogger +from pytorch_lightning.logging.test_tube import TestTubeLogger + +from lib.utils.config import Config + + +class Logger(LightningLoggerBase): + + @property + def experiment(self): + if self.debug: + return self.testtubelogger.experiment + else: + return self.neptunelogger.experiment + + @property + def name(self): + return self.config.model.type + + @property + def project_name(self): + return f"{self.config.project.owner}/{self.config.project.name}" + + @property + def version(self): + return f"version_{self.config.get('main', 'seed')}" + + @property + def outpath(self): + # ToDo: Add further path modification such as dataset config etc. + return Path(self.config.train.outpath) + + def __init__(self, config: Config, debug=False): + """ + params (dict|None): Optional. Parameters of the experiment. After experiment creation params are read-only. + Parameters are displayed in the experiment’s Parameters section and each key-value pair can be + viewed in experiments view as a column. + properties (dict|None): Optional default is {}. Properties of the experiment. + They are editable after experiment is created. Properties are displayed in the experiment’s Details and + each key-value pair can be viewed in experiments view as a column. + tags (list|None): Optional default []. Must be list of str. Tags of the experiment. + They are editable after experiment is created (see: append_tag() and remove_tag()). + Tags are displayed in the experiment’s Details and can be viewed in experiments view as a column. + """ + super(Logger, self).__init__() + + self.debug = debug + self.config = config + self._testtube_kwargs = dict(save_dir=self.outpath, version=self.version, name=self.name) + self._neptune_kwargs = dict(offline_mode=not self.debug, + api_key=self.config.project.neptune_key, + project_name=self.project_name, + name=self.name, + upload_source_files=list()) + self.neptunelogger = NeptuneLogger(**self._neptune_kwargs) + self.testtubelogger = TestTubeLogger(**self._testtube_kwargs) + + def log_hyperparams(self, params): + self.neptunelogger.log_hyperparams(params) + self.testtubelogger.log_hyperparams(params) + pass + + def log_metrics(self, metrics, step_num): + self.neptunelogger.log_metrics(metrics, step_num) + self.testtubelogger.log_metrics(metrics, step_num) + pass diff --git a/lib/utils/model_io.py b/lib/utils/model_io.py new file mode 100644 index 0000000..cc83e59 --- /dev/null +++ b/lib/utils/model_io.py @@ -0,0 +1,76 @@ +from argparse import Namespace +from pathlib import Path +from natsort import natsorted +from torch import nn + + +# Hyperparamter Object +class ModelParameters(Namespace): + + _activations = dict( + leaky_relu=nn.LeakyReLU, + relu=nn.ReLU, + sigmoid=nn.Sigmoid, + tanh=nn.Tanh + ) + + @property + def model_param(self): + return self._model_param + + @property + def train_param(self): + return self._train_param + + @property + def data_param(self): + return self._data_param + + def __init__(self, model_param, train_param, data_param): + self._model_param = model_param + self._train_param = train_param + self._data_param = data_param + kwargs = vars(model_param) + kwargs.update(vars(train_param)) + kwargs.update(vars(data_param)) + super(ModelParameters, self).__init__(**kwargs) + + def __getattribute__(self, item): + if item == 'activation': + try: + return self._activations[item] + except KeyError: + return nn.ReLU + return super(ModelParameters, self).__getattribute__(item) + + +class SavedLightningModels(object): + + @classmethod + def load_checkpoint(cls, models_root_path, model, n=-1, tags_file_path=''): + assert models_root_path.exists(), f'The path {models_root_path.absolute()} does not exist!' + found_checkpoints = list(Path(models_root_path).rglob('*.ckpt')) + + found_checkpoints = natsorted(found_checkpoints, key=lambda y: y.name) + + if not tags_file_path: + tag_files = models_root_path.rglob('meta_tags.csv') + tags_file_path = list(tag_files)[0] + + return cls(weights=found_checkpoints[n], model=model, tags=tags_file_path) + + def __init__(self, **kwargs): + self.weights: str = kwargs.get('weights', '') + self.tags: str = kwargs.get('tags', '') + + self.model = kwargs.get('model', None) + assert self.model is not None + + def restore(self): + pretrained_model = self.model.load_from_metrics( + weights_path=self.weights, + tags_csv=self.tags + ) + pretrained_model.eval() + pretrained_model.freeze() + return pretrained_model \ No newline at end of file diff --git a/lib/utils/transforms.py b/lib/utils/transforms.py new file mode 100644 index 0000000..e373aaf --- /dev/null +++ b/lib/utils/transforms.py @@ -0,0 +1,12 @@ +import numpy as np + + +class AsArray(object): + def __init__(self, width, height): + self.width = width + self.height = height + + def __call__(self, x): + array = np.zeros((self.width, self.height)) + + return array diff --git a/main.py b/main.py new file mode 100644 index 0000000..63da624 --- /dev/null +++ b/main.py @@ -0,0 +1,71 @@ +# Imports +# ============================================================================= +import os +from distutils.util import strtobool +from pathlib import Path +from argparse import ArgumentParser + +import warnings + +from pytorch_lightning import Trainer +from torch.utils.data import DataLoader + +from dataset.dataset import TrajData +from lib.utils.config import Config +from lib.utils.logging import Logger + +warnings.filterwarnings('ignore', category=FutureWarning) +warnings.filterwarnings('ignore', category=UserWarning) + +_ROOT = Path(__file__).parent + +# Paramter Configuration +# ============================================================================= +# Argument Parser +main_arg_parser = ArgumentParser(description="parser for fast-neural-style") + +# Main Parameters +main_arg_parser.add_argument("--main_debug", type=strtobool, default=False, help="") +main_arg_parser.add_argument("--main_eval", type=strtobool, default=False, help="") +main_arg_parser.add_argument("--main_seed", type=int, default=69, help="") + +# Data Parameters +main_arg_parser.add_argument("--data_worker", type=int, default=10, help="") +main_arg_parser.add_argument("--data_batchsize", type=int, default=100, help="") +main_arg_parser.add_argument("--data_root", type=str, default='../data/rpoot', help="") + +# Transformations +main_arg_parser.add_argument("--transformations_to_tensor", type=strtobool, default=False, help="") + +# Transformations +main_arg_parser.add_argument("--train_outpath", type=str, default="output", help="") +main_arg_parser.add_argument("--train_version", type=strtobool, required=False, help="") +main_arg_parser.add_argument("--train_epochs", type=int, default=10, help="") +main_arg_parser.add_argument("--train_batch_size", type=int, default=512, help="") +main_arg_parser.add_argument("--train_lr", type=float, default=0.002, help="") + +# Model +main_arg_parser.add_argument("--model_type", type=str, default="LeNetAE", help="") +main_arg_parser.add_argument("--model_activation", type=str, default="relu", help="") +main_arg_parser.add_argument("--model_filters", type=str, default="[32, 16, 4]", help="") +main_arg_parser.add_argument("--model_use_bias", type=strtobool, default=True, help="") +main_arg_parser.add_argument("--model_use_norm", type=strtobool, default=True, help="") +main_arg_parser.add_argument("--model_dropout", type=float, default=0.00, help="") + +# Project +main_arg_parser.add_argument("--project_name", type=str, default='traj-gen', help="") +main_arg_parser.add_argument("--project_owner", type=str, default='si11ium', help="") +main_arg_parser.add_argument("--project_neptune_key", type=str, default=os.getenv('NEPTUNE_KEY'), help="") + +# Parse it +args = main_arg_parser.parse_args() +config = Config.read_namespace(args) + +# Trainer loading +# ============================================================================= +trainer = Trainer(logger=Logger(config, debug=True)) + + +if __name__ == '__main__': + print(next(iter(train_dataloader))) + pass diff --git a/preprocessing/__pycache__/generator.cpython-37.pyc b/preprocessing/__pycache__/generator.cpython-37.pyc new file mode 100644 index 0000000..4bcc2eb Binary files /dev/null and b/preprocessing/__pycache__/generator.cpython-37.pyc differ diff --git a/preprocessing/generator.py b/preprocessing/generator.py new file mode 100644 index 0000000..ef63829 --- /dev/null +++ b/preprocessing/generator.py @@ -0,0 +1,121 @@ +import multiprocessing as mp +import pickle +import shelve +from collections import defaultdict + +from pathlib import Path +from typing import Union + +from tqdm import trange + +from lib.objects.map import Map + + +class Generator: + + possible_modes = ['one_patching'] + + def __init__(self, data_root, map_obj, binary=True): + self.binary: bool = binary + self.map: Map = map_obj + + self.data_root = Path(data_root) + + def generate_n_trajectories_m_alternatives(self, n, m, dataset_name='', **kwargs): + trajectories_with_alternatives = list() + for _ in trange(n, desc='Processing Trajectories'): + trajectory = self.map.get_random_trajectory() + alternatives, labels = self.generate_n_alternatives(trajectory, m, dataset_name=dataset_name, **kwargs) + trajectories_with_alternatives.append(dict(trajectory=trajectory, alternatives=alternatives, labels=labels)) + return trajectories_with_alternatives + + def generate_alternatives(self, trajectory, output: Union[mp. + Queue, None] = None, mode='one_patching'): + start, dest = trajectory.endpoints + if mode == 'one_patching': + patch = self.map.get_valid_position() + alternative = self.map.get_trajectory_from_vertices(start, patch, dest) + else: + raise RuntimeError(f'mode checking went wrong...') + + if output: + output.put(alternative) + return alternative + + def generate_n_alternatives(self, trajectory, n, dataset_name: Union[str, Path] = '', + mode='one_patching', equal_samples=True): + assert mode in self.possible_modes, f'Parameter "mode" must be either {self.possible_modes}, but was {mode}.' + # Define an output queue + output = mp.Queue() + # Setup a list of processes that we want to run + processes = [mp.Process(target=self.generate_alternatives, + kwargs=dict(trajectory=trajectory, output=output, mode=mode)) + for _ in range(n)] + # Run processes + for p in processes: + p.start() + # Exit the completed processes + for p in processes: + p.join() + # Get process results from the output queue + results = [output.get() for _ in processes] + + # label per homotopic class + homotopy_classes = defaultdict(list) + homotopy_classes[0].append(trajectory) + for i in range(len(results)): + alternative = results[i] + class_not_found, label = True, None + # check for homotopy class + for label in homotopy_classes.keys(): + if self.map.are_homotopic(homotopy_classes[label][0], alternative): + homotopy_classes[label].append(alternative) + class_not_found = False + break + if class_not_found: + label = len(homotopy_classes) + homotopy_classes[label].append(alternative) + + # There should be as much homotopic samples as non-homotopic samples + if equal_samples: + homotopy_classes = self._remove_unequal(homotopy_classes) + + # Compose lists of alternatives with labels + alternatives, labels = list(), list() + for key in homotopy_classes.keys(): + alternatives.extend([homotopy_classes[key]]) + labels.extend([key] * len(homotopy_classes[key])) + + # Write to disk + if dataset_name: + self.write_to_disk(dataset_name, trajectory, alternatives, labels) + + # Return + return alternatives, labels + + def write_to_disk(self, filepath, trajectory, alternatives, labels): + dataset_name = filepath if filepath.endswith('.pik') else f'{filepath}.pik' + self.data_root.mkdir(exist_ok=True, parents=True) + with shelve.open(str(self.data_root / dataset_name), protocol=pickle.HIGHEST_PROTOCOL) as f: + new_key = len(f) + f[f'trajectory_{new_key}'] = dict(alternatives=alternatives, + trajectory=trajectory, + labels=labels) + if 'map' not in f: + f['map'] = dict(map=self.map, name=f'map_{self.map.name}') + + @staticmethod + def _remove_unequal(hom_dict): + hom_dict = hom_dict.copy() + + counter = len(hom_dict) + while sum([len(hom_dict[class_id]) for class_id in range(len(hom_dict))]) > len(hom_dict[0]): + if counter > len(hom_dict): + counter = len(hom_dict) + if counter in hom_dict: + if len(hom_dict[counter]) == 0: + del hom_dict[counter] + else: + del hom_dict[counter][-1] + counter -= 1 + return hom_dict diff --git a/res/maps/Map.bmp b/res/maps/Map.bmp new file mode 100644 index 0000000..cae2af8 Binary files /dev/null and b/res/maps/Map.bmp differ diff --git a/res/maps/doom.bmp b/res/maps/doom.bmp new file mode 100644 index 0000000..bfbbb54 Binary files /dev/null and b/res/maps/doom.bmp differ diff --git a/res/maps/home.bmp b/res/maps/home.bmp new file mode 100644 index 0000000..b196a05 Binary files /dev/null and b/res/maps/home.bmp differ diff --git a/res/maps/maze.bmp b/res/maps/maze.bmp new file mode 100644 index 0000000..b6e4fe8 Binary files /dev/null and b/res/maps/maze.bmp differ diff --git a/res/maps/oet.bmp b/res/maps/oet.bmp new file mode 100644 index 0000000..c93c030 Binary files /dev/null and b/res/maps/oet.bmp differ diff --git a/res/maps/priz.bmp b/res/maps/priz.bmp new file mode 100644 index 0000000..99fb858 Binary files /dev/null and b/res/maps/priz.bmp differ diff --git a/res/maps/tate.bmp b/res/maps/tate.bmp new file mode 100644 index 0000000..2224ef6 Binary files /dev/null and b/res/maps/tate.bmp differ diff --git a/res/maps/tate_sw.bmp b/res/maps/tate_sw.bmp new file mode 100644 index 0000000..d55d73c Binary files /dev/null and b/res/maps/tate_sw.bmp differ diff --git a/res/maps/tum.bmp b/res/maps/tum.bmp new file mode 100644 index 0000000..5effbf6 Binary files /dev/null and b/res/maps/tum.bmp differ diff --git a/visualization/bars.py b/visualization/bars.py new file mode 100644 index 0000000..e69de29 diff --git a/visualization/tools.py b/visualization/tools.py new file mode 100644 index 0000000..8ba6651 --- /dev/null +++ b/visualization/tools.py @@ -0,0 +1,26 @@ +from pathlib import Path +import matplotlib.pyplot as plt + + +class Plotter(object): + def __init__(self, root_path=''): + self.root_path = Path(root_path) + + def save_current_figure(self, path, extention='.png'): + fig, _ = plt.gcf(), plt.gca() + # Prepare save location and check img file extention + path = self.root_path / Path(path if str(path).endswith(extention) else f'{str(path)}{extention}') + path.parent.mkdir(exist_ok=True, parents=True) + fig.savefig(path) + fig.clf() + + def show_current_figure(self): + fig, _ = plt.gcf(), plt.gca() + fig.show() + fig.clf() + + +if __name__ == '__main__': + output_root = Path('..') / 'output' + p = Plotter(output_root) + p.save_current_figure('test.png')