# -*- coding: utf-8 -*-
# Copyright (C) 2016-2019 by Brendt Wohlberg <brendt@ieee.org>
# Cristina Garcia-Cardona <cgarciac@lanl.gov>
# All rights reserved. BSD 3-clause License.
# This file is part of the SPORCO package. Details of the copyright
# and user license can be found in the 'LICENSE.txt' file distributed
# with the package.
"""Classes for FISTA algorithm for the Convolutional BPDN problem"""
from __future__ import division, absolute_import, print_function
import copy
import numpy as np
from sporco.util import u
from sporco.cnvrep import CSC_ConvRepIndexing, mskWshape
from sporco.linalg import inner
from sporco.fft import (rfftn, irfftn, empty_aligned, rfftn_empty_aligned,
rfl2norm2)
from sporco.prox import prox_l1
from sporco.fista import fista
__author__ = """Cristina Garcia-Cardona <cgarciac@lanl.gov>"""
[docs]class ConvBPDN(fista.FISTADFT):
r"""
Base class for FISTA algorithm for the Convolutional BPDN (CBPDN)
:cite:`garcia-2018-convolutional1` problem.
|
.. inheritance-diagram:: ConvBPDN
:parts: 2
|
The generic problem form is
.. math::
\mathrm{argmin}_\mathbf{x} \;
f( \{ \mathbf{x}_m \} ) + \lambda g( \{ \mathbf{x}_m \} )
where :math:`f = (1/2) \left\| \sum_m \mathbf{d}_m * \mathbf{x}_m -
\mathbf{s} \right\|_2^2`, and :math:`g(\cdot)` is a penalty
term or the indicator function of a constraint; with input
image :math:`\mathbf{s}`, dictionary filters :math:`\mathbf{d}_m`,
and coefficient maps :math:`\mathbf{x}_m`. It is solved via the
FISTA formulation
Proximal step
.. math::
\mathbf{x}_k = \mathrm{prox}_{t_k}(g) (\mathbf{y}_k - 1/L \nabla
f(\mathbf{y}_k) ) \;\;.
Combination step
.. math::
\mathbf{y}_{k+1} = \mathbf{x}_k + \left( \frac{t_k - 1}{t_{k+1}}
\right) (\mathbf{x}_k - \mathbf{x}_{k-1}) \;\;,
with :math:`t_{k+1} = \frac{1 + \sqrt{1 + 4 t_k^2}}{2}`.
After termination of the :meth:`solve` method, attribute
:attr:`itstat` is a list of tuples representing statistics of each
iteration. The fields of the named tuple ``IterationStats`` are:
``Iter`` : Iteration number
``ObjFun`` : Objective function value
``DFid`` : Value of data fidelity term :math:`(1/2) \| \sum_m
\mathbf{d}_m * \mathbf{x}_m - \mathbf{s} \|_2^2`
``RegL1`` : Value of regularisation term :math:`\sum_m \|
\mathbf{x}_m \|_1`
``Rsdl`` : Residual
``L`` : Inverse of gradient step parameter
``Time`` : Cumulative run time
"""
[docs] class Options(fista.FISTADFT.Options):
r"""ConvBPDN algorithm options
Options include all of those defined in
:class:`.fista.FISTADFT.Options`, together with
additional options:
``NonNegCoef`` : Flag indicating whether to force solution to
be non-negative.
``NoBndryCross`` : Flag indicating whether all solution
coefficients corresponding to filters crossing the image
boundary should be forced to zero.
``L1Weight`` : An array of weights for the :math:`\ell_1`
norm. The array shape must be such that the array is
compatible for multiplication with the X/Y variables. If this
option is defined, the regularization term is :math:`\lambda
\sum_m \| \mathbf{w}_m \odot \mathbf{x}_m \|_1` where
:math:`\mathbf{w}_m` denotes slices of the weighting array on
the filter index axis.
"""
defaults = copy.deepcopy(fista.FISTADFT.Options.defaults)
defaults.update({'NonNegCoef': False, 'NoBndryCross': False})
defaults.update({'L1Weight': 1.0})
defaults.update({'L': 500.0})
def __init__(self, opt=None):
"""
Parameters
----------
opt : dict or None, optional (default None)
ConvBPDN algorithm options
"""
if opt is None:
opt = {}
fista.FISTADFT.Options.__init__(self, opt)
def __setitem__(self, key, value):
"""Set options."""
fista.FISTADFT.Options.__setitem__(self, key, value)
itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1')
hdrtxt_objfn = ('Fnc', 'DFid', u('Regℓ1'))
hdrval_objfun = {'Fnc': 'ObjFun', 'DFid': 'DFid', u('Regℓ1'): 'RegL1'}
def __init__(self, D, S, lmbda=None, opt=None, dimK=None, dimN=2):
"""
This class supports an arbitrary number of spatial dimensions,
`dimN`, with a default of 2. The input dictionary `D` is either
`dimN` + 1 dimensional, in which case each spatial component
(image in the default case) is assumed to consist of a single
channel, or `dimN` + 2 dimensional, in which case the final
dimension is assumed to contain the channels (e.g. colour
channels in the case of images). The input signal set `S` is
either `dimN` dimensional (no channels, only one signal),
`dimN` + 1 dimensional (either multiple channels or multiple
signals), or `dimN` + 2 dimensional (multiple channels and
multiple signals). Determination of problem dimensions is
handled by :class:`.cnvrep.CSC_ConvRepIndexing`.
Parameters
----------
D : array_like
Dictionary array
S : array_like
Signal array
lmbda : float
Regularisation parameter
opt : :class:`ConvBPDN.Options` object
Algorithm options
dimK : 0, 1, or None, optional (default None)
Number of dimensions in input signal corresponding to multiple
independent signals
dimN : int, optional (default 2)
Number of spatial/temporal dimensions
"""
# Set default options if none specified
if opt is None:
opt = ConvBPDN.Options()
# Infer problem dimensions and set relevant attributes of self
if not hasattr(self, 'cri'):
self.cri = CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN)
# Set dtype attribute based on S.dtype and opt['DataType']
self.set_dtype(opt, S.dtype)
# Set default lambda value if not specified
if lmbda is None:
cri = CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN)
Df = rfftn(D.reshape(cri.shpD), cri.Nv, axes=cri.axisN)
Sf = rfftn(S.reshape(cri.shpS), axes=cri.axisN)
b = np.conj(Df) * Sf
lmbda = 0.1 * abs(b).max()
# Set l1 term scaling and weight array
self.lmbda = self.dtype.type(lmbda)
self.wl1 = np.asarray(opt['L1Weight'], dtype=self.dtype)
# Call parent class __init__
self.Xf = None
xshape = self.cri.shpX
super(ConvBPDN, self).__init__(xshape, S.dtype, opt)
# Reshape D and S to standard layout
self.D = np.asarray(D.reshape(self.cri.shpD), dtype=self.dtype)
self.S = np.asarray(S.reshape(self.cri.shpS), dtype=self.dtype)
# Compute signal in DFT domain
self.Sf = rfftn(self.S, None, self.cri.axisN)
# Create byte aligned arrays for FFT calls
self.Y = self.X.copy()
self.X = empty_aligned(self.Y.shape, dtype=self.dtype)
self.X[:] = self.Y
# Initialise auxiliary variable Vf: Create byte aligned arrays
# for FFT calls
self.Vf = rfftn_empty_aligned(self.X.shape, self.cri.axisN,
self.dtype)
self.Xf = rfftn(self.X, None, self.cri.axisN)
self.Yf = self.Xf.copy()
self.store_prev()
self.Yfprv = self.Yf.copy() + 1e5
self.setdict()
# Initialization needed for back tracking (if selected)
self.postinitialization_backtracking_DFT()
[docs] def setdict(self, D=None):
"""Set dictionary array."""
if D is not None:
self.D = np.asarray(D, dtype=self.dtype)
self.Df = rfftn(self.D, self.cri.Nv, self.cri.axisN)
[docs] def getcoef(self):
"""Get final coefficient array."""
return self.X
[docs] def eval_grad(self):
"""Compute gradient in Fourier domain."""
# Compute D X - S
Ryf = self.eval_Rf(self.Yf)
# Compute D^H Ryf
gradf = np.conj(self.Df) * Ryf
# Multiple channel signal, multiple channel dictionary
if self.cri.Cd > 1:
gradf = np.sum(gradf, axis=self.cri.axisC, keepdims=True)
return gradf
[docs] def eval_Rf(self, Vf):
"""Evaluate smooth term in Vf."""
return inner(self.Df, Vf, axis=self.cri.axisM) - self.Sf
[docs] def eval_proxop(self, V):
"""Compute proximal operator of :math:`g`."""
return prox_l1(V, (self.lmbda / self.L) * self.wl1)
[docs] def rsdl(self):
"""Compute fixed point residual in Fourier domain."""
diff = self.Xf - self.Yfprv
return rfl2norm2(diff, self.X.shape, axis=self.cri.axisN)
[docs] def eval_objfn(self):
"""Compute components of objective function as well as total
contribution to objective function.
"""
dfd = self.obfn_dfd()
reg = self.obfn_reg()
obj = dfd + reg[0]
return (obj, dfd) + reg[1:]
[docs] def obfn_dfd(self):
r"""Compute data fidelity term :math:`(1/2) \| \sum_m
\mathbf{d}_m * \mathbf{x}_m - \mathbf{s} \|_2^2`.
This function takes into account the unnormalised DFT scaling,
i.e. given that the variables are the DFT of multi-dimensional
arrays computed via :func:`.rfftn`, this returns the data
fidelity term in the original (spatial) domain.
"""
Ef = self.eval_Rf(self.Xf)
return rfl2norm2(Ef, self.S.shape, axis=self.cri.axisN) / 2.0
[docs] def obfn_reg(self):
"""Compute regularisation term and contribution to objective
function.
"""
rl1 = np.linalg.norm((self.wl1 * self.X).ravel(), 1)
return (self.lmbda * rl1, rl1)
[docs] def obfn_f(self, Xf=None):
r"""Compute data fidelity term :math:`(1/2) \| \sum_m
\mathbf{d}_m * \mathbf{x}_m - \mathbf{s} \|_2^2`
This is used for backtracking. Since the backtracking is
computed in the DFT, it is important to preserve the
DFT scaling.
"""
if Xf is None:
Xf = self.Xf
Rf = self.eval_Rf(Xf)
return 0.5 * np.linalg.norm(Rf.flatten(), 2)**2
[docs] def reconstruct(self, X=None):
"""Reconstruct representation."""
if X is None:
X = self.X
Xf = rfftn(X, None, self.cri.axisN)
Sf = np.sum(self.Df * Xf, axis=self.cri.axisM)
return irfftn(Sf, self.cri.Nv, self.cri.axisN)
[docs]class ConvBPDNMask(ConvBPDN):
r"""
FISTA algorithm for Convolutional BPDN with a spatial mask.
|
.. inheritance-diagram:: ConvBPDNMask
:parts: 2
|
Solve the optimisation problem
.. math::
\mathrm{argmin}_\mathbf{x} \;
(1/2) \left\| W \left(\sum_m \mathbf{d}_m * \mathbf{x}_m -
\mathbf{s}\right) \right\|_2^2 + \lambda \sum_m
\| \mathbf{x}_m \|_1 \;\;,
where :math:`W` is a mask array.
See :class:`ConvBPDN` for interface details.
"""
def __init__(self, D, S, lmbda, W=None, opt=None, dimK=None, dimN=2):
"""
|
Parameters
----------
D : array_like
Dictionary matrix
S : array_like
Signal vector or matrix
lmbda : float
Regularisation parameter
W : array_like
Mask array. The array shape must be such that the array is
compatible for multiplication with input array S (see
:func:`.cnvrep.mskWshape` for more details).
opt : :class:`ConvBPDNMask.Options` object
Algorithm options
dimK : 0, 1, or None, optional (default None)
Number of dimensions in input signal corresponding to multiple
independent signals
dimN : int, optional (default 2)
Number of spatial dimensions
"""
super(ConvBPDNMask, self).__init__(D, S, lmbda, opt, dimK=dimK,
dimN=dimN)
if W is None:
W = np.array([1.0], dtype=self.dtype)
self.W = np.asarray(W.reshape(mskWshape(W, self.cri)),
dtype=self.dtype)
# Create byte aligned arrays for FFT calls
self.WRy = empty_aligned(self.S.shape, dtype=self.dtype)
self.Ryf = rfftn_empty_aligned(self.S.shape, self.cri.axisN,
self.dtype)
[docs] def eval_grad(self):
"""Compute gradient in Fourier domain."""
# Compute D X - S
self.Ryf[:] = self.eval_Rf(self.Yf)
# Map to spatial domain to multiply by mask
Ry = irfftn(self.Ryf, self.cri.Nv, self.cri.axisN)
# Multiply by mask
self.WRy[:] = (self.W**2) * Ry
# Map back to frequency domain
WRyf = rfftn(self.WRy, self.cri.Nv, self.cri.axisN)
gradf = np.conj(self.Df) * WRyf
# Multiple channel signal, multiple channel dictionary
if self.cri.Cd > 1:
gradf = np.sum(gradf, axis=self.cri.axisC, keepdims=True)
return gradf
[docs] def obfn_dfd(self):
r"""Compute data fidelity term :math:`(1/2) \| W (\sum_m
\mathbf{d}_m * \mathbf{x}_{m} - \mathbf{s}) \|_2^2`
"""
Ef = self.eval_Rf(self.Xf)
E = irfftn(Ef, self.cri.Nv, self.cri.axisN)
return (np.linalg.norm(self.W * E)**2) / 2.0
[docs] def obfn_f(self, Xf=None):
r"""Compute data fidelity term :math:`(1/2) \| W (\sum_m
\mathbf{d}_m * \mathbf{x}_{m} - \mathbf{s}) \|_2^2`.
This is used for backtracking. Since the backtracking is
computed in the DFT, it is important to preserve the
DFT scaling.
"""
if Xf is None:
Xf = self.Xf
Rf = self.eval_Rf(Xf)
R = irfftn(Rf, self.cri.Nv, self.cri.axisN)
WRf = rfftn(self.W * R, self.cri.Nv, self.cri.axisN)
return 0.5 * np.linalg.norm(WRf.flatten(), 2)**2