Source code for mindarmour.utils.util

# Copyright 2019 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" Util for MindArmour. """
import numpy as np
from scipy.ndimage import uniform_filter

from mindspore import Tensor
from mindspore.nn import Cell
from mindspore.ops.composite import GradOperation

from mindarmour.utils._check_param import check_numpy_param, check_param_multi_types, check_equal_shape

from .logger import LogUtil

LOGGER = LogUtil.get_instance()
TAG = 'util'


def jacobian_matrix(grad_wrap_net, inputs, num_classes):
    """
    Calculate the Jacobian matrix for inputs.

    Args:
        grad_wrap_net (Cell): A network wrapped by GradWrap.
        inputs (numpy.ndarray): Input samples.
        num_classes (int): Number of labels of model output.

    Returns:
        numpy.ndarray, the Jacobian matrix of inputs. (labels, batch_size, ...)

    Raises:
        ValueError: If grad_wrap_net is not a instance of class `GradWrap`.
    """
    if not isinstance(grad_wrap_net, GradWrap):
        msg = 'grad_wrap_net be and instance of class `GradWrap`.'
        LOGGER.error(TAG, msg)
        raise ValueError(msg)
    grad_wrap_net.set_train()
    grads_matrix = []
    for idx in range(num_classes):
        sens = np.zeros((inputs.shape[0], num_classes)).astype(np.float32)
        sens[:, idx] = 1.0
        grads = grad_wrap_net(Tensor(inputs), Tensor(sens))
        grads_matrix.append(grads.asnumpy())
    return np.asarray(grads_matrix)


def jacobian_matrix_for_detection(grad_wrap_net, inputs, num_boxes, num_classes):
    """
    Calculate the Jacobian matrix for inputs, specifically for object detection model.

    Args:
        grad_wrap_net (Cell): A network wrapped by GradWrap.
        inputs (numpy.ndarray): Input samples.
        num_boxes (int): Number of boxes inferred by each image.
        num_classes (int): Number of labels of model output.

    Returns:
        numpy.ndarray, the Jacobian matrix of inputs. (labels, batch_size, ...)

    Raises:
        ValueError: If grad_wrap_net is not a instance of class `GradWrap`.
    """
    if not isinstance(grad_wrap_net, GradWrap):
        msg = 'grad_wrap_net be and instance of class `GradWrap`.'
        LOGGER.error(TAG, msg)
        raise ValueError(msg)
    grad_wrap_net.set_train()
    grads_matrix = []
    inputs_tensor = tuple()
    if isinstance(inputs, tuple):
        for item in inputs:
            inputs_tensor += (Tensor(item),)
    else:
        inputs_tensor += (Tensor(inputs),)
    for idx in range(num_classes):
        batch_size = inputs[0].shape[0] if isinstance(inputs, tuple) else inputs.shape[0]
        sens = np.zeros((batch_size, num_boxes, num_classes)).astype(np.float32)
        sens[:, :, idx] = 1.0

        grads = grad_wrap_net(*(inputs_tensor), Tensor(sens))
        grads_matrix.append(grads.asnumpy())
    return np.asarray(grads_matrix)


class WithLossCell(Cell):
    """
    Wrap the network with loss function.

    Args:
        network (Cell): The target network to wrap.
        loss_fn (Function): The loss function is used for computing loss.

    Examples:
        >>> data = Tensor(np.ones([1, 1, 32, 32]).astype(np.float32)*0.01)
        >>> label = Tensor(np.ones([1, 10]).astype(np.float32))
        >>> net = NET()
        >>> loss_fn = nn.SoftmaxCrossEntropyWithLogits()
        >>> loss_net = WithLossCell(net, loss_fn)
        >>> loss_out = loss_net(data, label)
    """
    def __init__(self, network, loss_fn):
        super(WithLossCell, self).__init__()
        self._network = network
        self._loss_fn = loss_fn

    def construct(self, data, label):
        """
        Compute loss based on the wrapped loss cell.

        Args:
            data (Tensor): Tensor data to train.
            label (Tensor): Tensor label data.

        Returns:
            Tensor, compute result.
        """
        out = self._network(data)
        return self._loss_fn(out, label)


[docs]class GradWrapWithLoss(Cell): """ Construct a network to compute the gradient of loss function in input space and weighted by `weight`. Args: network (Cell): The target network to wrap. Examples: >>> data = Tensor(np.ones([1, 1, 32, 32]).astype(np.float32)*0.01) >>> labels = Tensor(np.ones([1, 10]).astype(np.float32)) >>> net = NET() >>> loss_fn = nn.SoftmaxCrossEntropyWithLogits() >>> loss_net = WithLossCell(net, loss_fn) >>> grad_all = GradWrapWithLoss(loss_net) >>> out_grad = grad_all(data, labels) """ def __init__(self, network): super(GradWrapWithLoss, self).__init__() self._grad_all = GradOperation(get_all=True, sens_param=False) self._network = network
[docs] def construct(self, inputs, labels): """ Compute gradient of `inputs` with labels and weight. Args: inputs (Tensor): Inputs of network. labels (Tensor): Labels of inputs. Returns: Tensor, gradient matrix. """ gout = self._grad_all(self._network)(inputs, labels) return gout[0]
[docs]class GradWrap(Cell): """ Construct a network to compute the gradient of network outputs in input space and weighted by `weight`, expressed as a jacobian matrix. Args: network (Cell): The target network to wrap. Examples: >>> data = Tensor(np.ones([1, 1, 32, 32]).astype(np.float32)*0.01) >>> label = Tensor(np.ones([1, 10]).astype(np.float32)) >>> num_classes = 10 >>> sens = np.zeros((data.shape[0], num_classes)).astype(np.float32) >>> sens[:, 1] = 1.0 >>> net = NET() >>> wrap_net = GradWrap(net) >>> wrap_net(data, Tensor(sens)) """ def __init__(self, network): super(GradWrap, self).__init__() self.grad = GradOperation(get_all=False, sens_param=True) self.network = network
[docs] def construct(self, *data): """ Compute jacobian matrix. Args: data (Tensor): Data consists of inputs and weight. - inputs: Inputs of network. - weight: Weight of each gradient, 'weight' has the same shape with labels. Returns: Tensor, Jacobian matrix. """ gout = self.grad(self.network)(*data) return gout
def calculate_iou(box_i, box_j): """ Calculate the intersection over union (iou) of two boxes. Args: box_i (numpy.ndarray): Coordinates of the first box, with the format as (x1, y1, x2, y2). (x1, y1) and (x2, y2) are coordinates of the lower left corner and the upper right corner, respectively. box_j (numpy.ndarray): Coordinates of the second box, with the format as (x1, y1, x2, y2). Returns: float, iou of two input boxes. """ check_numpy_param('box_i', box_i) check_numpy_param('box_j', box_j) if box_i.shape[-1] != 4 or box_j.shape[-1] != 4: msg = 'The length of both coordinate arrays should be 4, bug got {} and {}.'.format(box_i.shape, box_j.shape) LOGGER.error(TAG, msg) raise ValueError(msg) i_x1, i_y1, i_x2, i_y2 = box_i j_x1, j_y1, j_x2, j_y2 = box_j s_i = (i_x2 - i_x1)*(i_y2 - i_y1) s_j = (j_x2 - j_x1)*(j_y2 - j_y1) inner_left_line = max(i_x1, j_x1) inner_right_line = min(i_x2, j_x2) inner_top_line = min(i_y2, j_y2) inner_bottom_line = max(i_y1, j_y1) if inner_left_line >= inner_right_line or inner_top_line <= inner_bottom_line: return 0 inner_area = (inner_right_line - inner_left_line)*(inner_top_line - inner_bottom_line) return inner_area / (s_i + s_j - inner_area) def to_tensor_tuple(inputs_ori): """Transfer inputs data into tensor type.""" inputs_ori = check_param_multi_types('inputs_ori', inputs_ori, [np.ndarray, tuple]) if isinstance(inputs_ori, tuple): inputs_tensor = tuple() for item in inputs_ori: inputs_tensor += (Tensor(item),) else: inputs_tensor = (Tensor(inputs_ori),) return inputs_tensor def calculate_lp_distance(original_image, compared_image): """ Calculate l0, l2 and linf distance for two images with the same shape. Args: original_image (np.ndarray): Original image. compared_image (np.ndarray): Another image for comparison. Returns: tuple, (l0, l2 and linf) distances between two images. Raises: TypeError: If type of original_image or type of compared_image is not numpy.ndarray. ValueError: If the shape of original_image and compared_image are not the same. """ check_numpy_param('original_image', original_image) check_numpy_param('compared_image', compared_image) check_equal_shape('original_image', original_image, 'compared_image', compared_image) avoid_zero_div = 1e-14 diff = (original_image - compared_image).flatten() data = original_image.flatten() l0_dist = np.linalg.norm(diff, ord=0) \ / (np.linalg.norm(data, ord=0) + avoid_zero_div) l2_dist = np.linalg.norm(diff, ord=2) \ / (np.linalg.norm(data, ord=2) + avoid_zero_div) linf_dist = np.linalg.norm(diff, ord=np.inf) \ / (np.linalg.norm(data, ord=np.inf) + avoid_zero_div) return l0_dist, l2_dist, linf_dist def _crop(arr, crop_width): """Crop arr by crop_width along each dimension.""" arr = np.array(arr, copy=False) if isinstance(crop_width, int): crops = [[crop_width, crop_width]]*arr.ndim elif isinstance(crop_width[0], int): if len(crop_width) == 1: crops = [[crop_width[0], crop_width[0]]]*arr.ndim else: crops = [crop_width]*arr.ndim elif len(crop_width) == 1: crops = [crop_width[0]]*arr.ndim elif len(crop_width) == arr.ndim: crops = crop_width else: msg = 'crop_width should be a sequence of N pairs, ' \ 'a single pair, or a single integer' LOGGER.error(TAG, msg) raise ValueError(msg) slices = tuple(slice(a, arr.shape[i] - b) for i, (a, b) in enumerate(crops)) cropped = arr[slices] return cropped def compute_ssim(image1, image2): """ compute structural similarity between two images. Args: image1 (numpy.ndarray): The first image to be compared. image2 (numpy.ndarray): The second image to be compared. channels). Returns: float, structural similarity. """ if not image1.shape == image2.shape: msg = 'Input images must have the same dimensions, but got ' \ 'image1.shape: {} and image2.shape: {}' \ .format(image1.shape, image2.shape) LOGGER.error(TAG, msg) raise ValueError() if len(image1.shape) == 3: # rgb mode if image1.shape[0] in [1, 3]: # from nhw to hwn image1 = np.array(image1).transpose(1, 2, 0) image2 = np.array(image2).transpose(1, 2, 0) # loop over channels n_channels = image1.shape[-1] total_ssim = np.empty(n_channels) for ch in range(n_channels): ch_result = compute_ssim(image1[..., ch], image2[..., ch]) total_ssim[..., ch] = ch_result return total_ssim.mean() k1 = 0.01 k2 = 0.03 win_size = 7 if np.any((np.asarray(image1.shape) - win_size) < 0): msg = 'Size of each dimension must be larger win_size:7, ' \ 'but got image.shape:{}.' \ .format(image1.shape) LOGGER.error(TAG, msg) raise ValueError(msg) image1 = image1.astype(np.float64) image2 = image2.astype(np.float64) ndim = image1.ndim tmp = win_size ** ndim cov_norm = tmp / (tmp - 1) # compute means ux = uniform_filter(image1, size=win_size) uy = uniform_filter(image2, size=win_size) # compute variances and covariances uxx = uniform_filter(image1*image1, size=win_size) uyy = uniform_filter(image2*image2, size=win_size) uxy = uniform_filter(image1*image2, size=win_size) vx = cov_norm*(uxx - ux*ux) vy = cov_norm*(uyy - uy*uy) vxy = cov_norm*(uxy - ux*uy) data_range = 2 c1 = (k1*data_range)**2 c2 = (k2*data_range)**2 a1 = 2*ux*uy + c1 a2 = 2*vxy + c2 b1 = ux**2 + uy**2 + c1 b2 = vx + vy + c2 d = b1*b2 s = (a1*a2) / d # padding pad = (win_size - 1) // 2 mean_ssim = _crop(s, pad).mean() return mean_ssim