Skip to content

Commit a5781b4

Browse files
authored
[Enhance] Separate installation for each tasks on release task (#1869)
* separte_import * align with pre commit * update unit test code * add separate task env pre merge test & aplly it to github action * add multiprocess to requirement
1 parent c5b41d9 commit a5781b4

File tree

17 files changed

+376
-189
lines changed

17 files changed

+376
-189
lines changed

.github/workflows/pre_merge.yml

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,69 @@ jobs:
6262
curl -Os https://uploader.codecov.io/latest/linux/codecov
6363
chmod +x codecov
6464
./codecov -t ${{ secrets.CODECOV_TOKEN }} --sha $COMMIT_ID -Z true -U $HTTP_PROXY -f .tox/coverage.xml
65-
Pre-Merge-Integration-Test:
65+
Pre-Merge-Integration-Common-Test:
6666
runs-on: [self-hosted, linux, x64, dev]
6767
needs: Pre-Merge-Unit-Test
68-
timeout-minutes: 360
68+
timeout-minutes: 120
69+
steps:
70+
- name: Checkout repository
71+
uses: actions/checkout@v3
72+
- name: Install dependencies
73+
run: python -m pip install -r requirements/dev.txt
74+
- name: Integration-testing
75+
run: tox -e pre-merge -- tests/integration/cli/test_cli.py
76+
Pre-Merge-Integration-Cls-Test:
77+
runs-on: [self-hosted, linux, x64, dev]
78+
needs: Pre-Merge-Unit-Test
79+
timeout-minutes: 120
80+
steps:
81+
- name: Checkout repository
82+
uses: actions/checkout@v3
83+
- name: Install dependencies
84+
run: python -m pip install -r requirements/dev.txt
85+
- name: Integration-testing
86+
run: tox -e pre-merge-cls
87+
Pre-Merge-Integration-Det-Test:
88+
runs-on: [self-hosted, linux, x64, dev]
89+
needs: Pre-Merge-Unit-Test
90+
timeout-minutes: 120
91+
steps:
92+
- name: Checkout repository
93+
uses: actions/checkout@v3
94+
- name: Install dependencies
95+
run: python -m pip install -r requirements/dev.txt
96+
- name: Integration-testing
97+
run: tox -e pre-merge-det
98+
Pre-Merge-Integration-Seg-Test:
99+
runs-on: [self-hosted, linux, x64, dev]
100+
needs: Pre-Merge-Unit-Test
101+
timeout-minutes: 120
102+
steps:
103+
- name: Checkout repository
104+
uses: actions/checkout@v3
105+
- name: Install dependencies
106+
run: python -m pip install -r requirements/dev.txt
107+
- name: Integration-testing
108+
run: tox -e pre-merge-seg
109+
Pre-Merge-Integration-Action-Test:
110+
runs-on: [self-hosted, linux, x64, dev]
111+
needs: Pre-Merge-Unit-Test
112+
timeout-minutes: 120
113+
steps:
114+
- name: Checkout repository
115+
uses: actions/checkout@v3
116+
- name: Install dependencies
117+
run: python -m pip install -r requirements/dev.txt
118+
- name: Integration-testing
119+
run: tox -e pre-merge-action
120+
Pre-Merge-Integration-Anomaly-Test:
121+
runs-on: [self-hosted, linux, x64, dev]
122+
needs: Pre-Merge-Unit-Test
123+
timeout-minutes: 120
69124
steps:
70125
- name: Checkout repository
71126
uses: actions/checkout@v3
72127
- name: Install dependencies
73128
run: python -m pip install -r requirements/dev.txt
74129
- name: Integration-testing
75-
run: tox -e pre-merge -- tests/integration/cli
130+
run: tox -e pre-merge-anomaly

otx/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,28 @@
55

66
__version__ = "1.0.0"
77
# NOTE: Sync w/ otx/api/usecases/exportable_code/demo/requirements.txt
8+
9+
MMCLS_AVAILABLE = True
10+
MMDET_AVAILABLE = True
11+
MMSEG_AVAILABLE = True
12+
MMACTION_AVAILABLE = True
13+
14+
try:
15+
import mmcls # noqa: F401
16+
except ImportError:
17+
MMCLS_AVAILABLE = False
18+
19+
try:
20+
import mmdet # noqa: F401
21+
except ImportError:
22+
MMDET_AVAILABLE = False
23+
24+
try:
25+
import mmseg # noqa: F401
26+
except ImportError:
27+
MMSEG_AVAILABLE = False
28+
29+
try:
30+
import mmaction # noqa: F401
31+
except ImportError:
32+
MMACTION_AVAILABLE = False

otx/cli/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55

66
import os
77

8-
os.environ["FEATURE_FLAGS_OTX_ACTION_TASKS"] = "1"
8+
from otx import MMACTION_AVAILABLE
9+
10+
if MMACTION_AVAILABLE:
11+
os.environ["FEATURE_FLAGS_OTX_ACTION_TASKS"] = "1"

otx/mpa/det/explainer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
build_dataset,
1414
)
1515
from otx.algorithms.detection.adapters.mmdet.data import ImageTilingDataset
16+
from otx.mpa.modules.hooks.det_saliency_map_hook import DetSaliencyMapHook
1617
from otx.mpa.modules.hooks.recording_forward_hooks import (
1718
ActivationMapHook,
18-
DetSaliencyMapHook,
1919
EigenCamHook,
2020
)
2121
from otx.mpa.registry import STAGES

otx/mpa/det/inferrer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
build_dataset,
1919
)
2020
from otx.algorithms.detection.adapters.mmdet.data import ImageTilingDataset
21+
from otx.mpa.modules.hooks.det_saliency_map_hook import DetSaliencyMapHook
2122
from otx.mpa.modules.hooks.recording_forward_hooks import (
2223
ActivationMapHook,
23-
DetSaliencyMapHook,
2424
FeatureVectorHook,
2525
)
2626
from otx.mpa.registry import STAGES

otx/mpa/modules/hooks/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#
44

55
# flake8: noqa
6+
from otx import MMDET_AVAILABLE
7+
68
from . import (
79
adaptive_training_hooks,
810
checkpoint_hook,
@@ -22,3 +24,6 @@
2224
unbiased_teacher_hook,
2325
workflow_hooks,
2426
)
27+
28+
if MMDET_AVAILABLE:
29+
from . import det_saliency_map_hook
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from typing import List, Tuple, Union
2+
3+
import torch
4+
import torch.nn.functional as F
5+
6+
from otx.mpa.modules.models.heads.custom_atss_head import CustomATSSHead
7+
from otx.mpa.modules.models.heads.custom_ssd_head import CustomSSDHead
8+
from otx.mpa.modules.models.heads.custom_vfnet_head import CustomVFNetHead
9+
from otx.mpa.modules.models.heads.custom_yolox_head import CustomYOLOXHead
10+
11+
from .recording_forward_hooks import BaseRecordingForwardHook
12+
13+
14+
class DetSaliencyMapHook(BaseRecordingForwardHook):
15+
"""Saliency map hook for object detection models."""
16+
17+
def __init__(self, module: torch.nn.Module) -> None:
18+
super().__init__(module)
19+
self._neck = module.neck if module.with_neck else None
20+
self._bbox_head = module.bbox_head
21+
self._num_cls_out_channels = module.bbox_head.cls_out_channels # SSD-like heads also have background class
22+
if hasattr(module.bbox_head, "anchor_generator"):
23+
self._num_anchors = module.bbox_head.anchor_generator.num_base_anchors
24+
else:
25+
self._num_anchors = [1] * 10
26+
27+
def func(
28+
self,
29+
x: Union[torch.Tensor, List[torch.Tensor], Tuple[torch.Tensor]],
30+
_: int = -1,
31+
cls_scores_provided: bool = False,
32+
) -> torch.Tensor:
33+
"""
34+
Generate the saliency map from raw classification head output, then normalizing to (0, 255).
35+
36+
:param x: Feature maps from backbone/FPN or classification scores from cls_head
37+
:param cls_scores_provided: If True - use 'x' as is, otherwise forward 'x' through the classification head
38+
:return: Class-wise Saliency Maps. One saliency map per each class - [batch, class_id, H, W]
39+
"""
40+
if cls_scores_provided:
41+
cls_scores = x
42+
else:
43+
cls_scores = self._get_cls_scores_from_feature_map(x)
44+
45+
bs, _, h, w = cls_scores[-1].size()
46+
saliency_maps = torch.empty(bs, self._num_cls_out_channels, h, w)
47+
for batch_idx in range(bs):
48+
cls_scores_anchorless = []
49+
for scale_idx, cls_scores_per_scale in enumerate(cls_scores):
50+
cls_scores_anchor_grouped = cls_scores_per_scale[batch_idx].reshape(
51+
self._num_anchors[scale_idx], (self._num_cls_out_channels), *cls_scores_per_scale.shape[-2:]
52+
)
53+
cls_scores_out, _ = cls_scores_anchor_grouped.max(dim=0)
54+
cls_scores_anchorless.append(cls_scores_out.unsqueeze(0))
55+
cls_scores_anchorless_resized = []
56+
for cls_scores_anchorless_per_level in cls_scores_anchorless:
57+
cls_scores_anchorless_resized.append(
58+
F.interpolate(cls_scores_anchorless_per_level, (h, w), mode="bilinear")
59+
)
60+
saliency_maps[batch_idx] = torch.cat(cls_scores_anchorless_resized, dim=0).mean(dim=0)
61+
62+
saliency_maps = saliency_maps.reshape((bs, self._num_cls_out_channels, -1))
63+
max_values, _ = torch.max(saliency_maps, -1)
64+
min_values, _ = torch.min(saliency_maps, -1)
65+
saliency_maps = 255 * (saliency_maps - min_values[:, :, None]) / (max_values - min_values + 1e-12)[:, :, None]
66+
saliency_maps = saliency_maps.reshape((bs, self._num_cls_out_channels, h, w))
67+
saliency_maps = saliency_maps.to(torch.uint8)
68+
return saliency_maps
69+
70+
def _get_cls_scores_from_feature_map(self, x: torch.Tensor) -> List:
71+
"""Forward features through the classification head of the detector."""
72+
with torch.no_grad():
73+
if self._neck is not None:
74+
x = self._neck(x)
75+
76+
if isinstance(self._bbox_head, CustomSSDHead):
77+
cls_scores = []
78+
for feat, cls_conv in zip(x, self._bbox_head.cls_convs):
79+
cls_scores.append(cls_conv(feat))
80+
elif isinstance(self._bbox_head, CustomATSSHead):
81+
cls_scores = []
82+
for cls_feat in x:
83+
for cls_conv in self._bbox_head.cls_convs:
84+
cls_feat = cls_conv(cls_feat)
85+
cls_score = self._bbox_head.atss_cls(cls_feat)
86+
cls_scores.append(cls_score)
87+
elif isinstance(self._bbox_head, CustomVFNetHead):
88+
# Not clear how to separate cls_scores from bbox_preds
89+
cls_scores, _, _ = self._bbox_head(x)
90+
elif isinstance(self._bbox_head, CustomYOLOXHead):
91+
92+
def forward_single(x, cls_convs, conv_cls):
93+
"""Forward feature of a single scale level."""
94+
cls_feat = cls_convs(x)
95+
cls_score = conv_cls(cls_feat)
96+
return cls_score
97+
98+
map_results = map(
99+
forward_single, x, self._bbox_head.multi_level_cls_convs, self._bbox_head.multi_level_conv_cls
100+
)
101+
cls_scores = list(map_results)
102+
else:
103+
raise NotImplementedError(
104+
"Not supported detection head provided. "
105+
"DetSaliencyMapHook supports only the following single stage detectors: "
106+
"YOLOXHead, ATSSHead, SSDHead, VFNetHead."
107+
)
108+
return cls_scores

otx/mpa/modules/hooks/recording_forward_hooks.py

Lines changed: 6 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,14 @@
1515
from __future__ import annotations
1616

1717
from abc import ABC, abstractmethod
18-
from typing import List, Sequence, Tuple, Union
18+
from typing import Sequence, Union
1919

2020
import torch
21-
import torch.nn.functional as F
22-
from mmcls.models.necks.gap import GlobalAveragePooling
2321

24-
from otx.mpa.modules.models.heads.custom_atss_head import CustomATSSHead
25-
from otx.mpa.modules.models.heads.custom_ssd_head import CustomSSDHead
26-
from otx.mpa.modules.models.heads.custom_vfnet_head import CustomVFNetHead
27-
from otx.mpa.modules.models.heads.custom_yolox_head import CustomYOLOXHead
22+
from otx import MMCLS_AVAILABLE
23+
24+
if MMCLS_AVAILABLE:
25+
from mmcls.models.necks.gap import GlobalAveragePooling
2826

2927

3028
class BaseRecordingForwardHook(ABC):
@@ -130,103 +128,6 @@ def func(feature_map: Union[torch.Tensor, Sequence[torch.Tensor]]) -> torch.Tens
130128
return feature_vector
131129

132130

133-
class DetSaliencyMapHook(BaseRecordingForwardHook):
134-
"""Saliency map hook for object detection models."""
135-
136-
def __init__(self, module: torch.nn.Module) -> None:
137-
super().__init__(module)
138-
self._neck = module.neck if module.with_neck else None
139-
self._bbox_head = module.bbox_head
140-
self._num_cls_out_channels = module.bbox_head.cls_out_channels # SSD-like heads also have background class
141-
if hasattr(module.bbox_head, "anchor_generator"):
142-
self._num_anchors = module.bbox_head.anchor_generator.num_base_anchors
143-
else:
144-
self._num_anchors = [1] * 10
145-
146-
def func(
147-
self,
148-
x: Union[torch.Tensor, List[torch.Tensor], Tuple[torch.Tensor]],
149-
_: int = -1,
150-
cls_scores_provided: bool = False,
151-
) -> torch.Tensor:
152-
"""
153-
Generate the saliency map from raw classification head output, then normalizing to (0, 255).
154-
155-
:param x: Feature maps from backbone/FPN or classification scores from cls_head
156-
:param cls_scores_provided: If True - use 'x' as is, otherwise forward 'x' through the classification head
157-
:return: Class-wise Saliency Maps. One saliency map per each class - [batch, class_id, H, W]
158-
"""
159-
if cls_scores_provided:
160-
cls_scores = x
161-
else:
162-
cls_scores = self._get_cls_scores_from_feature_map(x)
163-
164-
bs, _, h, w = cls_scores[-1].size()
165-
saliency_maps = torch.empty(bs, self._num_cls_out_channels, h, w)
166-
for batch_idx in range(bs):
167-
cls_scores_anchorless = []
168-
for scale_idx, cls_scores_per_scale in enumerate(cls_scores):
169-
cls_scores_anchor_grouped = cls_scores_per_scale[batch_idx].reshape(
170-
self._num_anchors[scale_idx], (self._num_cls_out_channels), *cls_scores_per_scale.shape[-2:]
171-
)
172-
cls_scores_out, _ = cls_scores_anchor_grouped.max(dim=0)
173-
cls_scores_anchorless.append(cls_scores_out.unsqueeze(0))
174-
cls_scores_anchorless_resized = []
175-
for cls_scores_anchorless_per_level in cls_scores_anchorless:
176-
cls_scores_anchorless_resized.append(
177-
F.interpolate(cls_scores_anchorless_per_level, (h, w), mode="bilinear")
178-
)
179-
saliency_maps[batch_idx] = torch.cat(cls_scores_anchorless_resized, dim=0).mean(dim=0)
180-
181-
saliency_maps = saliency_maps.reshape((bs, self._num_cls_out_channels, -1))
182-
max_values, _ = torch.max(saliency_maps, -1)
183-
min_values, _ = torch.min(saliency_maps, -1)
184-
saliency_maps = 255 * (saliency_maps - min_values[:, :, None]) / (max_values - min_values + 1e-12)[:, :, None]
185-
saliency_maps = saliency_maps.reshape((bs, self._num_cls_out_channels, h, w))
186-
saliency_maps = saliency_maps.to(torch.uint8)
187-
return saliency_maps
188-
189-
def _get_cls_scores_from_feature_map(self, x: torch.Tensor) -> List:
190-
"""Forward features through the classification head of the detector."""
191-
with torch.no_grad():
192-
if self._neck is not None:
193-
x = self._neck(x)
194-
195-
if isinstance(self._bbox_head, CustomSSDHead):
196-
cls_scores = []
197-
for feat, cls_conv in zip(x, self._bbox_head.cls_convs):
198-
cls_scores.append(cls_conv(feat))
199-
elif isinstance(self._bbox_head, CustomATSSHead):
200-
cls_scores = []
201-
for cls_feat in x:
202-
for cls_conv in self._bbox_head.cls_convs:
203-
cls_feat = cls_conv(cls_feat)
204-
cls_score = self._bbox_head.atss_cls(cls_feat)
205-
cls_scores.append(cls_score)
206-
elif isinstance(self._bbox_head, CustomVFNetHead):
207-
# Not clear how to separate cls_scores from bbox_preds
208-
cls_scores, _, _ = self._bbox_head(x)
209-
elif isinstance(self._bbox_head, CustomYOLOXHead):
210-
211-
def forward_single(x, cls_convs, conv_cls):
212-
"""Forward feature of a single scale level."""
213-
cls_feat = cls_convs(x)
214-
cls_score = conv_cls(cls_feat)
215-
return cls_score
216-
217-
map_results = map(
218-
forward_single, x, self._bbox_head.multi_level_cls_convs, self._bbox_head.multi_level_conv_cls
219-
)
220-
cls_scores = list(map_results)
221-
else:
222-
raise NotImplementedError(
223-
"Not supported detection head provided. "
224-
"DetSaliencyMapHook supports only the following single stage detectors: "
225-
"YOLOXHead, ATSSHead, SSDHead, VFNetHead."
226-
)
227-
return cls_scores
228-
229-
230131
class ReciproCAMHook(BaseRecordingForwardHook):
231132
"""
232133
Implementation of recipro-cam for class-wise saliency map
@@ -280,7 +181,7 @@ def _predict_from_feature_map(self, x: torch.Tensor) -> torch.Tensor:
280181
return logits
281182

282183
def _get_mosaic_feature_map(self, feature_map: torch.Tensor, c: int, h: int, w: int) -> torch.Tensor:
283-
if self._neck is not None and isinstance(self._neck, GlobalAveragePooling):
184+
if MMCLS_AVAILABLE and self._neck is not None and isinstance(self._neck, GlobalAveragePooling):
284185
"""
285186
Optimization workaround for the GAP case (simulate GAP with more simple compute graph)
286187
Possible due to static sparsity of mosaic_feature_map

0 commit comments

Comments
 (0)