Skip to content
Open

WPL #37

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Vision And Security Technology (VAST) Lab.
1. OpenMax
2. Multimodal OpenMax
3. Extreme Value Machine (EVM)
4. Weibull Prototype Learning (WPL)

### Reimplementation of libMR
This repo contains a torch based reimplementation of the [libMR repo](https://github.com/Vastlab/libMR)
Expand Down
172 changes: 172 additions & 0 deletions vast/opensetAlgos/WPL.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@

"""
Motivated from: https://github.com/Vastlab/vast/blob/main/vast/opensetAlgos/openmax.py

Weibull Prototype Learning (WPL)
Comment thread
akshay-raj-dhamija marked this conversation as resolved.


WPL fits a Weibull distribution for each dimension of the feature of each class.
If we have 'n' classes and the dimension of the feature is 'd', then it fit 'n x d' Weibull.
It computes and saves the center (mean) of each class, which is equivalent to Prototype in the literature.
So, each Prototype has 'd' dimensions. Therefore, we fit 'd' Weibulls for each center (Prototype).
Distance of a point (an input) is defined as the absolute value of the difference between
the point (input) and the center (Prototype). So, distance is a vector with 'd' dimension.
Distance is not a scalar. The probability of each class is the minimum Weibull probability of all dimensions.

In the training time, if a Weibll (class, dimension) has 5 or more unique values, maximum likelihood
estimator (MLE) or maximum a posterior (MAP) can be used to estimate the Weibull parameter.
If a Weibll (class, dimension) has less than 5 unique values, instead of estimating, it uses
the default parameter for creating Weibull. The default parameters are the default scale and default shape.


WPL has 4 arguments: tail size, distance multiplier, default_scale, default shape

The main difference between WPL and OpenMax is how they compute the distance.
Therefore, the number of Weibull is different between WPL and OpenMax. Another difference
is how they are training Weibull when the number of unique values is less than 5.


"""

import torch
import itertools
from ..tools import pairwisedistances
from ..DistributionModels import weibull
from typing import Iterator, Tuple, List, Dict, OrderedDict


def WPL_Params(parser):
WPL_params = parser.add_argument_group("WPL params")
WPL_params.add_argument(
"--tailsize",
nargs="+",
type=float,
default=[1.0],
help="tail size to use default: %(default)s",
)
WPL_params.add_argument(
"--distance_multiplier",
nargs="+",
type=float,
default=[1.0],
help="distance multiplier to use default: %(default)s",
)
WPL_params.add_argument(
"--default_scale",
nargs="+",
type=float,
default=[1.0],
help="Weibull scale to use when the number of uniqe element is less than 5 default: %(default)s",
)
WPL_params.add_argument(
"--default_shape",
nargs="+",
type=float,
default=[0.1],
help="Weibull shape to use when the number of uniqe element is less than 5 default: %(default)s",
)
Comment on lines +54 to +67

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these parameters are not supposed to be grid searched, they should not be a list rather floats. And if you are planning to grid search them, it should be noted that these do not impact the training in any way, since these are just the default values that need to be returned in a corner case. Possibly, it would be better if this was handled by whatever process is calling the inference function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default_scale and default_shape are similar to tail_size, cover_threshold, and distance_multiplier of EVM. Please check the EVM.py in the main repo.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Algo Breakdown: Please try to simplify or break down your algorithm. Your WPL algo has two parts one is per dimension weibull and the other is how you address the case where weibull is needed for less than 5 samples/values. I am asking to separate this part from your core algorithm.
If tomorrow someone comes up with a new way to estimate default_scale and default_shape rather than fixing default values (let's say a mean from other class values, which can't be found until the algorithm has been run on the entire dataset) would you create a new WPL.py and call it a new algorithm even though the per dimension weibull idea is the same?

Reducing Computation: default_scale and default_shape are not similar to tail_size, cover_threshold, and distance_multiplier of EVM. These EVM parameters were being used as a list to reduce redundant computation. In your case, you will be recalling the mr.FitHigh function using the same parameters when your default_scale and default_shape change, even though they do not impact the result in this case. That is you are adding computational overhead rather than reducing it.
default_scale and default_shape are simply being used to replace scale and shape values where none could be found, not reduce computation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of the comments Algo Breakdown and Reducing Computation are correct. @scruz13 please make required changes.

return parser, dict(
group_parser=WPL_params,
param_names=("tailsize", "distance_multiplier", "default_scale", "default_shape"),
param_id_string="TS_{}_DM_{:.4f}_SC_{:.4f}_SH_{:.4f}",
)


def fit_high(distances, distance_multiplier, tailsize, default_shape, default_scale):
distances = torch.unique(distances)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

distances is always supposed to be a 2D tensor i.e.

no_of_samples x no_of_samples_to_which_distance_was_found

so the dim should be used when using unique
Also unique by default returns the sorted tensor, which might be redundant work taken care of in weibull.py

This seems to be trying to address a bigger problem, should it not be addressed in weibull.py?
@tboult: If we remove multiple occurrences of the same values, won't it result in different weibull models.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the WPL, we have a prototype. i.e., no_of_samples_to_which_distance_was_found == 1 .

It is better to do not change weibull.py because it affects EVM and our papers

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even in openmax no_of_samples_to_which_distance_was_found=1 but still its fit_high function supports working on a 2D tensor absence of dim in your code above removes that support.

I will let you and @tboult decide if you want to incorporate this in weibull.py it is very much possible to incorporate this there without changing the default behaviour for any of the other algorithms. At least changing it there helps to quickly test the same idea for other algorithms as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: I made an incorrect suggestion above, dim with unique doesn't do what I expected it to. So using unique in fit_high will by default remove the support for computation on a 2D tensor.

if tailsize <= 1:
tailsize = min(tailsize * distances.shape[1], distances.shape[1])
tailsize = int(min(tailsize, distances.shape[1]))
mr = weibull.weibull()
if distances.shape[1] < 5:
pass

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to simply create a weibull object where scale and shape tensors are nan and return it. The end-user can simply replace the nan values with the defaults you have in args.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

args contains scale and shape of Weibull. So, why do not using it here? Why do you want to end-user replace it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check my comment above #37 (comment)

mr.sign = 1
mr.wbFits = torch.zeros(1, 2)
mr.wbFits[0, 1] = default_scale
mr.wbFits[0, 0] = default_shape
mr._ = torch.Tensor(0.0) # translate Amount
mr.smallScoreTensor = torch.Tensor(0.0)# small Score
Comment on lines +83 to +88

@akshay-raj-dhamija akshay-raj-dhamija Oct 29, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could just create this as done at

mr = weibull.weibull(

It might make it more readable

else:
mr.FitHigh(distances.double() * distance_multiplier, tailsize, isSorted=False)
mr.tocpu()
return mr


def WPL_Training(
pos_classes_to_process: List[str],
features_all_classes: OrderedDict[str, torch.Tensor],
args,
gpu: int,
models=None,
) -> Iterator[Tuple[str, Tuple[str, dict]]]:
"""
:param pos_classes_to_process: List of class names to be processed by this function in the current process class.
:param features_all_classes: features of all classes, note the classes in pos_classes_to_process can be a subset of the keys for this ordered dictionary
:param args: A named tuple or an argument parser object containing the arguments mentioned in the WPL_Params function above.
:param gpu: An integer corresponding to the gpu number to use by the current process.
:param models: Not used during training, input ignored.
:return: Iterator(Tuple(parameter combination identifier, Tuple(class name, its evm model)))
"""
dimension = None
for pos_cls_name in pos_classes_to_process:
features = features_all_classes[pos_cls_name].clone().to(f"cuda:{gpu}")
if dimension == None:
dimension = features.shape[1]
else:
assert dimension == features.shape[1]
Comment on lines +110 to +116

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dimension will always be None so will never reach the else statement.


center = torch.mean(features, dim=0).view(1,dimension).to(f"cuda:{gpu}")
distances = torch.abs(features - center.repeat(features.shape[0], 1))
for tailsize, distance_multiplier, default_shape, default_scale in itertools.product(
args.tailsize, args.distance_multiplier, args.default_shape, args.default_scale
):
weibull_list = list()
for k in range(dimension):
weibull_model = fit_high(distances[:,k].T, distance_multiplier, tailsize, default_shape, default_scale)
weibull_list.append(weibull_model)

yield (
f"TS_{tailsize}_DM_{distance_multiplier:.4f}_SC_{default_scale:.4f}_SH_{default_shape:.4f}",
(pos_cls_name, {'center':center, 'weibull_list': weibull_list})
)




def WPL_Inference(
pos_classes_to_process: List[str],
features_all_classes: Dict[str, torch.Tensor],
args,
gpu: int,
models: Dict = None,
) -> Iterator[Tuple[str, Tuple[str, torch.Tensor]]]:
"""
:param pos_classes_to_process: List of batches to be processed by this function in the current process.
:param features_all_classes: features of all classes, note the classes in pos_classes_to_process can be a subset of
the keys for this dictionary
:param args: Can be a named tuple or an argument parser object containing the arguments mentioned in the WPL_Params
function above. Only the distance_metric argument is actually used during inferencing.
:param gpu: An integer corresponding to the gpu number to use by the current process.
:param models: The collated model created for a single hyper parameter combination.
:return: Iterator(Tuple(str, Tuple(batch_identifier, torch.Tensor)))
"""
for batch_to_process in pos_classes_to_process:
test_cls_feature = features_all_classes[batch_to_process].to(f"cuda:{gpu}")
assert test_cls_feature.shape[0] != 0
probs = []
for cls_no, cls_name in enumerate(models.keys())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you expect the models variable to be an ordered dict if a user instead passes a dict rather than letting the user know this will simply provide them with incorrect results. This can be avoided by simply using enumerate(sorted(models.keys())) instead of enumerate(models.keys())

center = model[cls_name]["center"].to(f"cuda:{gpu}")
dimension = center.shape[1]
distances = torch.abs(test_cls_feature - center.repeat(test_cls_feature.shape[0], 1))
weibull_list = models[class_name]["weibull_list"]
p = torch.empty(dimension)
for k in range(dimension):
weibull = weibull_list[k]
p[k] = 1 - weibull.wscore(distances[:,k].cpu()) )
probs.append( torch.min(p) )

probs = torch.cat(probs, dim=1)
yield ("probs", (batch_to_process, probs))



4 changes: 4 additions & 0 deletions vast/opensetAlgos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"EVM_Training",
"EVM_Inference",
"EVM_Inference_cpu_max_knowness_prob",
"WPL_Params",
"WPL_Training",
"WPL_Inference",
]
from .openmax import *
from .EVM import *
from .multimodal_openmax import *
from .WPL import *