BIMStruct3D-segmentation / pointcept /models /masked_scene_contrast /masked_scene_contrast_v1m1_base.py
Download pointcept/models/masked_scene_contrast/masked_scene_contrast_v1m1_base.py from dfki-av/BIMStruct3D-segmentation: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/dfki-av/BIMStruct3D-segmentation/resolve/7ab05dd198d38ea08e60bb1e4f3f04aa369975c2/pointcept/models/masked_scene_contrast/masked_scene_contrast_v1m1_base.py
- Command line
-
hf download hf://dfki-av/BIMStruct3D-segmentation@7ab05dd198d38ea08e60bb1e4f3f04aa369975c2/pointcept/models/masked_scene_contrast/masked_scene_contrast_v1m1_base.py
-
curl -L -o masked_scene_contrast_v1m1_base.py https://huggingface.co/dfki-av/BIMStruct3D-segmentation/resolve/7ab05dd198d38ea08e60bb1e4f3f04aa369975c2/pointcept/models/masked_scene_contrast/masked_scene_contrast_v1m1_base.py
12.3 kB
| """ | |
| Masked Scene Contrast | |
| https://arxiv.org/abs/2303.14191 | |
| Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com) | |
| Please cite our work if the code is helpful to you. | |
| """ | |
| import random | |
| from itertools import chain | |
| import torch | |
| import torch.nn as nn | |
| import torch.distributed as dist | |
| from torch_geometric.nn.pool import voxel_grid | |
| from timm.layers import trunc_normal_ | |
| import pointops | |
| from pointcept.models.builder import MODELS, build_model | |
| from pointcept.models.utils import offset2batch | |
| from pointcept.utils.comm import get_world_size | |
| class MaskedSceneContrast(nn.Module): | |
| def __init__( | |
| self, | |
| backbone, | |
| backbone_in_channels, | |
| backbone_out_channels, | |
| mask_grid_size=0.1, | |
| mask_rate=0.4, | |
| view1_mix_prob=0, | |
| view2_mix_prob=0, | |
| matching_max_k=8, | |
| matching_max_radius=0.03, | |
| matching_max_pair=8192, | |
| nce_t=0.4, | |
| contrast_weight=1, | |
| reconstruct_weight=1, | |
| reconstruct_color=True, | |
| reconstruct_normal=True, | |
| ): | |
| super().__init__() | |
| self.backbone = build_model(backbone) | |
| self.mask_grid_size = mask_grid_size | |
| self.mask_rate = mask_rate | |
| self.view1_mix_prob = view1_mix_prob | |
| self.view2_mix_prob = view2_mix_prob | |
| self.matching_max_k = matching_max_k | |
| self.matching_max_radius = matching_max_radius | |
| self.matching_max_pair = matching_max_pair | |
| self.nce_t = nce_t | |
| self.contrast_weight = contrast_weight | |
| self.reconstruct_weight = reconstruct_weight | |
| self.reconstruct_color = reconstruct_color | |
| self.reconstruct_normal = reconstruct_normal | |
| self.mask_token = nn.Parameter(torch.zeros(1, backbone_in_channels)) | |
| trunc_normal_(self.mask_token, mean=0.0, std=0.02) | |
| self.color_head = ( | |
| nn.Linear(backbone_out_channels, 3) if reconstruct_color else None | |
| ) | |
| self.normal_head = ( | |
| nn.Linear(backbone_out_channels, 3) if reconstruct_normal else None | |
| ) | |
| self.nce_criteria = torch.nn.CrossEntropyLoss(reduction="mean") | |
| def generate_cross_masks( | |
| self, view1_origin_coord, view1_offset, view2_origin_coord, view2_offset | |
| ): | |
| # union origin coord | |
| view1_batch = offset2batch(view1_offset) | |
| view2_batch = offset2batch(view2_offset) | |
| view1_batch_count = view1_batch.bincount() | |
| view2_batch_count = view2_batch.bincount() | |
| view1_origin_coord_split = view1_origin_coord.split(list(view1_batch_count)) | |
| view2_origin_coord_split = view2_origin_coord.split(list(view2_batch_count)) | |
| union_origin_coord = torch.cat( | |
| list( | |
| chain.from_iterable( | |
| zip(view1_origin_coord_split, view2_origin_coord_split) | |
| ) | |
| ) | |
| ) | |
| union_offset = torch.cat( | |
| [view1_offset.unsqueeze(-1), view2_offset.unsqueeze(-1)], dim=-1 | |
| ).sum(-1) | |
| union_batch = offset2batch(union_offset) | |
| # grid partition | |
| mask_patch_coord = union_origin_coord.div(self.mask_grid_size) | |
| mask_patch_grid_coord = torch.floor(mask_patch_coord) | |
| mask_patch_cluster = voxel_grid( | |
| pos=mask_patch_grid_coord, size=1, batch=union_batch, start=0 | |
| ) | |
| unique, cluster, counts = torch.unique( | |
| mask_patch_cluster, sorted=True, return_inverse=True, return_counts=True | |
| ) | |
| patch_num = unique.shape[0] | |
| patch_max_point = counts.max().item() | |
| patch2point_map = cluster.new_zeros(patch_num, patch_max_point) | |
| patch2point_mask = torch.lt( | |
| torch.arange(patch_max_point).cuda().unsqueeze(0), counts.unsqueeze(-1) | |
| ) | |
| sorted_cluster_value, sorted_cluster_indices = torch.sort(cluster) | |
| patch2point_map[patch2point_mask] = sorted_cluster_indices | |
| # generate cross masks | |
| assert self.mask_rate <= 0.5 | |
| patch_mask = torch.zeros(patch_num, device=union_origin_coord.device).int() | |
| rand_perm = torch.randperm(patch_num) | |
| mask_patch_num = int(patch_num * self.mask_rate) | |
| # mask1 tag with 1, mask2 tag with 2 | |
| patch_mask[rand_perm[0:mask_patch_num]] = 1 | |
| patch_mask[rand_perm[mask_patch_num : mask_patch_num * 2]] = 2 | |
| point_mask = torch.zeros( | |
| union_origin_coord.shape[0], device=union_origin_coord.device | |
| ).int() | |
| point_mask[ | |
| patch2point_map[patch_mask == 1][patch2point_mask[patch_mask == 1]] | |
| ] = 1 | |
| point_mask[ | |
| patch2point_map[patch_mask == 2][patch2point_mask[patch_mask == 2]] | |
| ] = 2 | |
| # separate mask to view1 and view2 | |
| point_mask_split = point_mask.split( | |
| list( | |
| torch.cat( | |
| [view1_batch_count.unsqueeze(-1), view2_batch_count.unsqueeze(-1)], | |
| dim=-1, | |
| ).flatten() | |
| ) | |
| ) | |
| view1_point_mask = torch.cat(point_mask_split[0::2]) == 1 | |
| view2_point_mask = torch.cat(point_mask_split[1::2]) == 2 | |
| return view1_point_mask, view2_point_mask | |
| def match_contrastive_pair( | |
| self, view1_coord, view1_offset, view2_coord, view2_offset, max_k, max_radius | |
| ): | |
| index, distance = pointops.knn_query( | |
| max_k, | |
| view2_coord.float(), | |
| view2_offset.int(), | |
| view1_coord.float(), | |
| view1_offset.int(), | |
| ) | |
| index = torch.cat( | |
| [ | |
| torch.arange(index.shape[0], device=index.device, dtype=torch.long) | |
| .view(-1, 1, 1) | |
| .expand(-1, max_k, 1), | |
| index.view(-1, max_k, 1), | |
| ], | |
| dim=-1, | |
| )[distance.squeeze(-1) < max_radius] | |
| unique, count = index[:, 0].unique(return_counts=True) | |
| select = ( | |
| torch.cumsum(count, dim=0) | |
| - torch.randint(count.max(), count.shape, device=count.device) % count | |
| - 1 | |
| ) | |
| index = index[select] | |
| if index.shape[0] > self.matching_max_pair: | |
| index = index[torch.randperm(index.shape[0])[: self.matching_max_pair]] | |
| return index | |
| def compute_contrastive_loss( | |
| self, view1_feat, view1_offset, view2_feat, view2_offset, match_index | |
| ): | |
| assert view1_offset.shape == view2_offset.shape | |
| view1_feat = view1_feat[match_index[:, 0]] | |
| view2_feat = view2_feat[match_index[:, 1]] | |
| view1_feat = view1_feat / ( | |
| torch.norm(view1_feat, p=2, dim=1, keepdim=True) + 1e-7 | |
| ) | |
| view2_feat = view2_feat / ( | |
| torch.norm(view2_feat, p=2, dim=1, keepdim=True) + 1e-7 | |
| ) | |
| sim = torch.mm(view1_feat, view2_feat.transpose(1, 0)) | |
| with torch.no_grad(): | |
| pos_sim = torch.diagonal(sim).mean() | |
| neg_sim = sim.mean(dim=-1).mean() - pos_sim / match_index.shape[0] | |
| labels = torch.arange(sim.shape[0], device=view1_feat.device).long() | |
| loss = self.nce_criteria(torch.div(sim, self.nce_t), labels) | |
| if get_world_size() > 1: | |
| dist.all_reduce(loss) | |
| dist.all_reduce(pos_sim) | |
| dist.all_reduce(neg_sim) | |
| return ( | |
| loss / get_world_size(), | |
| pos_sim / get_world_size(), | |
| neg_sim / get_world_size(), | |
| ) | |
| def forward(self, data_dict): | |
| view1_origin_coord = data_dict["view1_origin_coord"] | |
| view1_coord = data_dict["view1_coord"] | |
| view1_feat = data_dict["view1_feat"] | |
| view1_offset = data_dict["view1_offset"].int() | |
| view2_origin_coord = data_dict["view2_origin_coord"] | |
| view2_coord = data_dict["view2_coord"] | |
| view2_feat = data_dict["view2_feat"] | |
| view2_offset = data_dict["view2_offset"].int() | |
| # mask generation by union original coord (without spatial augmentation) | |
| view1_point_mask, view2_point_mask = self.generate_cross_masks( | |
| view1_origin_coord, view1_offset, view2_origin_coord, view2_offset | |
| ) | |
| view1_mask_tokens = self.mask_token.expand(view1_coord.shape[0], -1) | |
| view1_weight = view1_point_mask.unsqueeze(-1).type_as(view1_mask_tokens) | |
| view1_feat = view1_feat * (1 - view1_weight) + view1_mask_tokens * view1_weight | |
| view2_mask_tokens = self.mask_token.expand(view2_coord.shape[0], -1) | |
| view2_weight = view2_point_mask.unsqueeze(-1).type_as(view2_mask_tokens) | |
| view2_feat = view2_feat * (1 - view2_weight) + view2_mask_tokens * view2_weight | |
| view1_data_dict = dict( | |
| origin_coord=view1_origin_coord, | |
| coord=view1_coord, | |
| feat=view1_feat, | |
| offset=view1_offset, | |
| ) | |
| view2_data_dict = dict( | |
| origin_coord=view2_origin_coord, | |
| coord=view2_coord, | |
| feat=view2_feat, | |
| offset=view2_offset, | |
| ) | |
| # SparseConv based method need grid coord | |
| if "view1_grid_coord" in data_dict.keys(): | |
| view1_data_dict["grid_coord"] = data_dict["view1_grid_coord"] | |
| if "view2_grid_coord" in data_dict.keys(): | |
| view2_data_dict["grid_coord"] = data_dict["view2_grid_coord"] | |
| # view mixing strategy | |
| if random.random() < self.view1_mix_prob: | |
| view1_data_dict["offset"] = torch.cat( | |
| [view1_offset[1:-1:2], view1_offset[-1].unsqueeze(0)], dim=0 | |
| ) | |
| if random.random() < self.view2_mix_prob: | |
| view2_data_dict["offset"] = torch.cat( | |
| [view2_offset[1:-1:2], view2_offset[-1].unsqueeze(0)], dim=0 | |
| ) | |
| view1_feat = self.backbone(view1_data_dict) | |
| view2_feat = self.backbone(view2_data_dict) | |
| match_index = self.match_contrastive_pair( | |
| view1_origin_coord, | |
| view1_offset, | |
| view2_origin_coord, | |
| view2_offset, | |
| max_k=self.matching_max_k, | |
| max_radius=self.matching_max_radius, | |
| ) | |
| nce_loss, pos_sim, neg_sim = self.compute_contrastive_loss( | |
| view1_feat, view1_offset, view2_feat, view2_offset, match_index | |
| ) | |
| loss = nce_loss * self.contrast_weight | |
| result_dict = dict(nce_loss=nce_loss, pos_sim=pos_sim, neg_sim=neg_sim) | |
| if self.color_head is not None: | |
| assert "view1_color" in data_dict.keys() | |
| assert "view2_color" in data_dict.keys() | |
| view1_color = data_dict["view1_color"] | |
| view2_color = data_dict["view2_color"] | |
| view1_color_pred = self.color_head(view1_feat[view1_point_mask]) | |
| view2_color_pred = self.color_head(view2_feat[view2_point_mask]) | |
| color_loss = ( | |
| torch.sum((view1_color_pred - view1_color[view1_point_mask]) ** 2) | |
| + torch.sum((view2_color_pred - view2_color[view2_point_mask]) ** 2) | |
| ) / (view1_color_pred.shape[0] + view2_color_pred.shape[0]) | |
| loss = loss + color_loss * self.reconstruct_weight | |
| result_dict["color_loss"] = color_loss | |
| if self.normal_head is not None: | |
| assert "view1_normal" in data_dict.keys() | |
| assert "view2_normal" in data_dict.keys() | |
| view1_normal = data_dict["view1_normal"] | |
| view2_normal = data_dict["view2_normal"] | |
| view1_normal_pred = self.normal_head(view1_feat[view1_point_mask]) | |
| view2_normal_pred = self.normal_head(view2_feat[view2_point_mask]) | |
| view1_normal_pred = view1_normal_pred / ( | |
| torch.norm(view1_normal_pred, p=2, dim=1, keepdim=True) + 1e-10 | |
| ) | |
| view2_normal_pred = view2_normal_pred / ( | |
| torch.norm(view2_normal_pred, p=2, dim=1, keepdim=True) + 1e-10 | |
| ) | |
| normal_loss = ( | |
| torch.sum(view1_normal_pred * view1_normal[view1_point_mask]) | |
| + torch.sum(view2_normal_pred * view2_normal[view2_point_mask]) | |
| ) / (view1_normal_pred.shape[0] + view2_normal_pred.shape[0]) | |
| loss = loss + normal_loss * self.reconstruct_weight | |
| result_dict["normal_loss"] = normal_loss | |
| result_dict["loss"] = loss | |
| return result_dict | |