Source code for sporco.dictlrn.cbpdndlmd

# -*- coding: utf-8 -*-
# Copyright (C) 2015-2020 by Brendt Wohlberg <brendt@ieee.org>
# 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.

"""Dictionary learning based on CBPDN sparse coding with a spatial mask in
the data fidelity term
"""

from __future__ import print_function, absolute_import

import copy
import numpy as np

import sporco.cnvrep as cr
import sporco.admm.cbpdn as admm_cbpdn
import sporco.admm.ccmodmd as admm_ccmod
import sporco.pgm.cbpdn as pgm_cbpdn
import sporco.pgm.ccmod as pgm_ccmod
from sporco.dictlrn import dictlrn
import sporco.dictlrn.common as dc
from sporco.common import _fix_dynamic_class_lookup
from sporco.fft import rfftn, irfftn
from sporco.linalg import inner


__author__ = """Brendt Wohlberg <brendt@ieee.org>"""



[docs] def cbpdnmsk_class_label_lookup(label): """Get a ConvBPDNMask class from a label string.""" clsmod = {'admm': admm_cbpdn.ConvBPDNMaskDcpl, 'pgm': pgm_cbpdn.ConvBPDNMask} if label in clsmod: return clsmod[label] else: raise ValueError('Unknown ConvBPDNMask solver method %s' % label)
[docs] def ConvBPDNMaskOptionsDefaults(method='admm'): """Get defaults dict for the ConvBPDNMask class specified by the ``method`` parameter. """ dflt = copy.deepcopy(cbpdnmsk_class_label_lookup(method).Options.defaults) if method == 'admm': dflt.update({'MaxMainIter': 1, 'AutoRho': {'Period': 10, 'AutoScaling': False, 'RsdlRatio': 10.0, 'Scaling': 2.0, 'RsdlTarget': 1.0}}) else: dflt.update({'MaxMainIter': 1}) return dflt
[docs] def ConvBPDNMaskOptions(opt=None, method='admm'): """A wrapper function that dynamically defines a class derived from the Options class associated with one of the implementations of the Convolutional BPDN problem, and returns an object instantiated with the provided parameters. The wrapper is designed to allow the appropriate object to be created by calling this function using the same syntax as would be used if it were a class. The specific implementation is selected by use of an additional keyword argument 'method'. Valid values are as specified in the documentation for :func:`ConvBPDN`. """ # Assign base class depending on method selection argument base = cbpdnmsk_class_label_lookup(method).Options # Nested class with dynamically determined inheritance class ConvBPDNMaskOptions(base): def __init__(self, opt): super(ConvBPDNMaskOptions, self).__init__(opt) # Allow pickling of objects of type ConvBPDNOptions _fix_dynamic_class_lookup(ConvBPDNMaskOptions, method) # Return object of the nested class type return ConvBPDNMaskOptions(opt)
[docs] def ConvBPDNMask(*args, **kwargs): """A wrapper function that dynamically defines a class derived from one of the implementations of the Convolutional Constrained MOD problems, and returns an object instantiated with the provided parameters. The wrapper is designed to allow the appropriate object to be created by calling this function using the same syntax as would be used if it were a class. The specific implementation is selected by use of an additional keyword argument 'method'. Valid values are: - ``'admm'`` : Use the implementation defined in :class:`.admm.cbpdn.ConvBPDNMaskDcpl`. - ``'pgm'`` : Use the implementation defined in :class:`.pgm.cbpdn.ConvBPDNMask`. The default value is ``'admm'``. """ # Extract method selection argument or set default method = kwargs.pop('method', 'admm') # Assign base class depending on method selection argument base = cbpdnmsk_class_label_lookup(method) # Nested class with dynamically determined inheritance class ConvBPDNMask(base): def __init__(self, *args, **kwargs): super(ConvBPDNMask, self).__init__(*args, **kwargs) # Allow pickling of objects of type ConvBPDNMask _fix_dynamic_class_lookup(ConvBPDNMask, method) # Return object of the nested class type return ConvBPDNMask(*args, **kwargs)
[docs] def ccmodmsk_class_label_lookup(label): """Get a ConvCnstrMODMask class from a label string.""" clsmod = {'ism': admm_ccmod.ConvCnstrMODMaskDcpl_IterSM, 'cg': admm_ccmod.ConvCnstrMODMaskDcpl_CG, 'cns': admm_ccmod.ConvCnstrMODMaskDcpl_Consensus, 'pgm': pgm_ccmod.ConvCnstrMODMask} if label in clsmod: return clsmod[label] else: raise ValueError('Unknown ConvCnstrMODMask solver method %s' % label)
[docs] def ConvCnstrMODMaskOptionsDefaults(method='pgm'): """Get defaults dict for the ConvCnstrMODMask class specified by the ``method`` parameter. """ dflt = copy.deepcopy(ccmodmsk_class_label_lookup(method).Options.defaults) if method == 'pgm': dflt.update({'MaxMainIter': 1}) else: dflt.update({'MaxMainIter': 1, 'AutoRho': {'Period': 10, 'AutoScaling': False, 'RsdlRatio': 10.0, 'Scaling': 2.0, 'RsdlTarget': 1.0}}) return dflt
[docs] def ConvCnstrMODMaskOptions(opt=None, method='pgm'): """A wrapper function that dynamically defines a class derived from the Options class associated with one of the implementations of the Convolutional Constrained MOD problem, and returns an object instantiated with the provided parameters. The wrapper is designed to allow the appropriate object to be created by calling this function using the same syntax as would be used if it were a class. The specific implementation is selected by use of an additional keyword argument 'method'. Valid values are as specified in the documentation for :func:`ConvCnstrMODMask`. """ # Assign base class depending on method selection argument base = ccmodmsk_class_label_lookup(method).Options # Nested class with dynamically determined inheritance class ConvCnstrMODMaskOptions(base): def __init__(self, opt): super(ConvCnstrMODMaskOptions, self).__init__(opt) # Allow pickling of objects of type ConvCnstrMODMaskOptions _fix_dynamic_class_lookup(ConvCnstrMODMaskOptions, method) # Return object of the nested class type return ConvCnstrMODMaskOptions(opt)
[docs] def ConvCnstrMODMask(*args, **kwargs): """A wrapper function that dynamically defines a class derived from one of the implementations of the Convolutional Constrained MOD problems, and returns an object instantiated with the provided parameters. The wrapper is designed to allow the appropriate object to be created by calling this function using the same syntax as would be used if it were a class. The specific implementation is selected by use of an additional keyword argument 'method'. Valid values are: - ``'ism'`` : Use the implementation defined in :class:`.ConvCnstrMODMaskDcpl_IterSM`. This method works well for a small number of training images, but is very slow for larger training sets. - ``'cg'`` : Use the implementation defined in :class:`.ConvCnstrMODMaskDcpl_CG`. This method is slower than ``'ism'`` for small training sets, but has better run time scaling as the training set grows. - ``'cns'`` : Use the implementation defined in :class:`.ConvCnstrMODMaskDcpl_Consensus`. This method is a good choice for large training sets. - ``'pgm'`` : Use the implementation defined in :class:`.pgm.ccmod.ConvCnstrMODMask`. This method is the best choice for large training sets. The default value is ``'pgm'``. """ # Extract method selection argument or set default method = kwargs.pop('method', 'pgm') # Assign base class depending on method selection argument base = ccmodmsk_class_label_lookup(method) # Nested class with dynamically determined inheritance class ConvCnstrMODMask(base): def __init__(self, *args, **kwargs): super(ConvCnstrMODMask, self).__init__(*args, **kwargs) # Allow pickling of objects of type ConvCnstrMODMask _fix_dynamic_class_lookup(ConvCnstrMODMask, method) # Return object of the nested class type return ConvCnstrMODMask(*args, **kwargs)
[docs] class ConvBPDNMaskDictLearn(dictlrn.DictLearn): r""" Dictionary learning by alternating between sparse coding and dictionary update stages. | .. inheritance-diagram:: ConvBPDNMaskDictLearn :parts: 2 | The sparse coding is performed using :class:`.admm.cbpdn.ConvBPDNMaskDcpl` (see :cite:`heide-2015-fast`) or :class:`.pgm.cbpdn.ConvBPDNMask` (see :cite:`chalasani-2013-fast` and :cite:`wohlberg-2016-efficient`), and the dictionary update is computed using :class:`.pgm.ccmod.ConvCnstrMODMask` (see :cite:`garcia-2018-convolutional1`) or one of the solver classes in :mod:`.admm.ccmodmd` (see :cite:`wohlberg-2016-efficient` and :cite:`garcia-2018-convolutional1`). The coupling between sparse coding and dictionary update stages is as in :cite:`garcia-2017-subproblem`. Solve the optimisation problem .. math:: \mathrm{argmin}_{\mathbf{d}, \mathbf{x}} \; (1/2) \sum_k \left \| W (\sum_m \mathbf{d}_m * \mathbf{x}_{k,m} - \mathbf{s}_k ) \right \|_2^2 + \lambda \sum_k \sum_m \| \mathbf{x}_{k,m} \|_1 \quad \text{such that} \quad \mathbf{d}_m \in C \;\; \forall m \;, where :math:`C` is the feasible set consisting of filters with unit norm and constrained support, via interleaved alternation between the ADMM steps of the :class:`.ConvBPDNMaskDcpl` and :func:`.ConvCnstrMODMaskDcpl` problems. The multi-channel variants :cite:`wohlberg-2016-convolutional` supported by :class:`.ConvBPDNMaskDcpl` and :func:`.ConvCnstrMODMaskDcpl` are also supported. 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_k \| W (\sum_m \mathbf{d}_m * \mathbf{x}_{k,m} - \mathbf{s}_k) \|_2^2` ``RegL1`` : Value of regularisation term :math:`\sum_k \sum_m \| \mathbf{x}_{k,m} \|_1` ``Cnstr`` : Constraint violation measure *If the ADMM solver is selected for sparse coding:* ``XPrRsdl`` : Norm of X primal residual ``XDlRsdl`` : Norm of X dual residual ``XRho`` : X penalty parameter *If the PGM solver is selected for sparse coding:* ``X_F_Btrack`` : Value of objective function for CSC problem ``X_Q_Btrack`` : Value of quadratic approximation for CSC problem ``X_ItBt`` : Number of iterations in backtracking for CSC problem ``X_L`` : Inverse of gradient step parameter for CSC problem *If an ADMM solver is selected for the dictionary update:* ``DPrRsdl`` : Norm of D primal residual ``DDlRsdl`` : Norm of D dual residual ``DRho`` : D penalty parameter *If the PGM solver is selected for the dictionary update:* ``D_F_Btrack`` : Value of objective function for CDU problem ``D_Q_Btrack`` : Value of wuadratic approximation for CDU problem ``D_ItBt`` : Number of iterations in backtracking for CDU problem ``D_L`` : Inverse of gradient step parameter for CDU problem ``Time`` : Cumulative run time """
[docs] class Options(dictlrn.DictLearn.Options): """CBPDN dictionary learning algorithm options. Options include all of those defined in :class:`.dictlrn.DictLearn.Options`, together with additional options: ``AccurateDFid`` : Flag determining whether data fidelity term is estimated from the value computed in the X update (``False``) or is computed after every outer iteration over an X update and a D update (``True``), which is slower but more accurate. ``DictSize`` : Dictionary size vector. ``CBPDN`` : An options class appropriate for the selected sparse coding solver class ``CCMOD`` : An options class appropriate for the selected dictionary update solver class """ defaults = copy.deepcopy(dictlrn.DictLearn.Options.defaults) defaults.update({'DictSize': None, 'AccurateDFid': False}) def __init__(self, opt=None, xmethod=None, dmethod=None): """ Valid values for parameters ``xmethod`` and ``dmethod`` are documented in functions :func:`.ConvBPDNMask` and :func:`.ConvCnstrMODMask` respectively. """ if xmethod is None: xmethod = 'admm' if dmethod is None: dmethod = 'pgm' self.xmethod = xmethod self.dmethod = dmethod self.defaults.update( {'CBPDN': ConvBPDNMaskOptionsDefaults(xmethod), 'CCMOD': ConvCnstrMODMaskOptionsDefaults(dmethod)}) # Initialisation of CBPDN and CCMOD keys here is required to # ensure that the corresponding options have types appropriate # for classes in the cbpdn and ccmod modules, and are not just # standard entries in the parent option tree dictlrn.DictLearn.Options.__init__(self, { 'CBPDN': ConvBPDNMaskOptions(self.defaults['CBPDN'], method=xmethod), 'CCMOD': ConvCnstrMODMaskOptions(self.defaults['CCMOD'], method=dmethod)}) if opt is None: opt = {} self.update(opt)
def __init__(self, D0, S, lmbda, W, opt=None, xmethod=None, dmethod=None, dimK=1, dimN=2): """ | **Call graph** .. image:: ../_static/jonga/cbpdnmddl_init.svg :width: 20% :target: ../_static/jonga/cbpdnmddl_init.svg | Parameters ---------- D0 : array_like Initial dictionary array S : array_like Signal array lmbda : float Regularisation parameter W : array_like Mask array. The array shape must be such that the array is compatible for multiplication with the *internal* shape of input array S (see :class:`.cnvrep.CDU_ConvRepIndexing` for a discussion of the distinction between *external* and *internal* data layouts) after reshaping to the shape determined by :func:`.cnvrep.mskWshape`. opt : :class:`ConvBPDNMaskDictLearn.Options` object Algorithm options xmethod : string, optional (default 'admm') String selecting sparse coding solver. Valid values are documented in function :func:`.ConvBPDNMask`. dmethod : string, optional (default 'pgm') String selecting dictionary update solver. Valid values are documented in function :func:`.ConvCnstrMODMask`. dimK : int, optional (default 1) Number of signal dimensions. If there is only a single input signal (e.g. if `S` is a 2D array representing a single image) `dimK` must be set to 0. dimN : int, optional (default 2) Number of spatial/temporal dimensions """ if opt is None: opt = ConvBPDNMaskDictLearn.Options(xmethod=xmethod, dmethod=dmethod) if xmethod is None: xmethod = opt.xmethod if dmethod is None: dmethod = opt.dmethod if opt.xmethod != xmethod or opt.dmethod != dmethod: raise ValueError('Parameters xmethod and dmethod must have the ' 'same values used to initialise the Options ' 'object') self.opt = opt self.xmethod = xmethod self.dmethod = dmethod # Get dictionary size if self.opt['DictSize'] is None: dsz = D0.shape else: dsz = self.opt['DictSize'] # Construct object representing problem dimensions cri = cr.CDU_ConvRepIndexing(dsz, S, dimK, dimN) # Normalise dictionary D0 = cr.Pcn(D0, dsz, cri.Nv, dimN, cri.dimCd, crp=True, zm=opt['CCMOD', 'ZeroMean']) # Modify D update options to include initial values for Y if cri.C == cri.Cd: Y0b0 = np.zeros(cri.Nv + (cri.C, 1, cri.K)) else: Y0b0 = np.zeros(cri.Nv + (1, 1, cri.C * cri.K)) Y0b1 = cr.zpad(cr.stdformD(D0, cri.Cd, cri.M, dimN), cri.Nv) if dmethod == 'pgm': opt['CCMOD'].update({'X0': Y0b1}) else: if dmethod == 'cns': Y0 = Y0b1 else: Y0 = np.concatenate((Y0b0, Y0b1), axis=cri.axisM) opt['CCMOD'].update({'Y0': Y0}) # Create X update object xstep = ConvBPDNMask(D0, S, lmbda, W, opt['CBPDN'], method=xmethod, dimK=dimK, dimN=dimN) # Create D update object dstep = ConvCnstrMODMask(None, S, W, dsz, opt['CCMOD'], method=dmethod, dimK=dimK, dimN=dimN) # Configure iteration statistics reporting isc = dictlrn.IterStatsConfig( isfld=dc.isfld(xmethod, dmethod, opt), isxmap=dc.isxmap(xmethod, opt), isdmap=dc.isdmap(dmethod), evlmap=dc.evlmap(opt['AccurateDFid']), hdrtxt=dc.hdrtxt(xmethod, dmethod, opt), hdrmap=dc.hdrmap(xmethod, dmethod, opt), fmtmap={'It_X': '%4d', 'It_D': '%4d'}) # Call parent constructor super(ConvBPDNMaskDictLearn, self).__init__(xstep, dstep, opt, isc)
[docs] def getdict(self, crop=True): """Get final dictionary. If ``crop`` is ``True``, apply :func:`.cnvrep.bcrop` to returned array. """ return self.dstep.getdict(crop=crop)
[docs] def reconstruct(self, D=None, X=None): """Reconstruct representation.""" if D is None: D = self.getdict(crop=False) if X is None: X = self.getcoef() Df = rfftn(D, self.xstep.cri.Nv, self.xstep.cri.axisN) Xf = rfftn(X, self.xstep.cri.Nv, self.xstep.cri.axisN) DXf = inner(Df, Xf, axis=self.xstep.cri.axisM) return irfftn(DXf, self.xstep.cri.Nv, self.xstep.cri.axisN)
[docs] def evaluate(self): """Evaluate functional value of previous iteration.""" if self.opt['AccurateDFid']: DX = self.reconstruct() S = self.xstep.S dfd = (np.linalg.norm(self.xstep.W * (DX - S))**2) / 2.0 if self.xmethod == 'pgm': X = self.xstep.getcoef() else: X = self.xstep.var_y1() rl1 = np.sum(np.abs(X)) return dict(DFid=dfd, RegL1=rl1, ObjFun=dfd + self.xstep.lmbda * rl1) else: return None