예제 #1
0
파일: dictlrn.py 프로젝트: bwohlberg/sporco
    def __init__(self, xstep, dstep, opt=None, isc=None):
        """
        Parameters
        ----------
        xstep : bpdn (or similar interface) object
          Object handling X update step
        dstep : cmod (or similar interface) object
          Object handling D update step
        opt : :class:`DictLearn.Options` object
          Algorithm options
        isc : :class:`IterStatsConfig` object
          Iteration statistics and header display configuration
        """

        if opt is None:
            opt = DictLearn.Options()
        self.opt = opt

        if isc is None:
            isc = IterStatsConfig(
                isfld=['Iter', 'ObjFunX', 'XPrRsdl', 'XDlRsdl', 'XRho',
                       'ObjFunD', 'DPrRsdl', 'DDlRsdl', 'DRho', 'Time'],
                isxmap={'ObjFunX': 'ObjFun', 'XPrRsdl': 'PrimalRsdl',
                        'XDlRsdl': 'DualRsdl', 'XRho': 'Rho'},
                isdmap={'ObjFunD': 'DFid', 'DPrRsdl': 'PrimalRsdl',
                        'DDlRsdl': 'DualRsdl', 'DRho': 'Rho'},
                evlmap={},
                hdrtxt=['Itn', 'FncX', 'r_X', 's_X', u('ρ_X'),
                        'FncD', 'r_D', 's_D', u('ρ_D')],
                hdrmap={'Itn': 'Iter', 'FncX': 'ObjFunX',
                        'r_X': 'XPrRsdl', 's_X': 'XDlRsdl',
                        u('ρ_X'): 'XRho', 'FncD': 'ObjFunD',
                        'r_D': 'DPrRsdl', 's_D': 'DDlRsdl',
                        u('ρ_D'): 'DRho'}
            )
        self.isc = isc

        self.xstep = xstep
        self.dstep = dstep

        self.itstat = []
        self.j = 0
예제 #2
0
파일: common.py 프로젝트: bwohlberg/sporco
def hdrtxt(xmethod, dmethod, opt):
    """Return ``hdrtxt`` argument for ``.IterStatsConfig`` initialiser.
    """

    txt = ['Itn', 'Fnc', 'DFid', u('ℓ1'), 'Cnstr']
    if xmethod == 'admm':
        txt.extend(['r_X', 's_X', u('ρ_X')])
    else:
        if opt['CBPDN', 'BackTrack', 'Enabled']:
            txt.extend(['F_X', 'Q_X', 'It_X', 'L_X'])
        else:
            txt.append('L_X')
    if dmethod != 'fista':
        txt.extend(['r_D', 's_D', u('ρ_D')])
    else:
        if opt['CCMOD', 'BackTrack', 'Enabled']:
            txt.extend(['F_D', 'Q_D', 'It_D', 'L_D'])
        else:
            txt.append('L_D')
    return txt
예제 #3
0
파일: common.py 프로젝트: bwohlberg/sporco
def hdrmap(xmethod, dmethod, opt):
    """Return ``hdrmap`` argument for ``.IterStatsConfig`` initialiser.
    """

    hdr = {'Itn': 'Iter', 'Fnc': 'ObjFun', 'DFid': 'DFid',
           u('ℓ1'): 'RegL1', 'Cnstr': 'Cnstr'}
    if xmethod == 'admm':
        hdr.update({'r_X': 'XPrRsdl', 's_X': 'XDlRsdl', u('ρ_X'): 'XRho'})
    else:
        if opt['CBPDN', 'BackTrack', 'Enabled']:
            hdr.update({'F_X': 'X_F_Btrack', 'Q_X': 'X_Q_Btrack',
                        'It_X': 'X_ItBt', 'L_X': 'X_L'})
        else:
            hdr.update({'L_X': 'X_L'})
    if dmethod != 'fista':
        hdr.update({'r_D': 'DPrRsdl', 's_D': 'DDlRsdl', u('ρ_D'): 'DRho'})
    else:
        if opt['CCMOD', 'BackTrack', 'Enabled']:
            hdr.update({'F_D': 'D_F_Btrack', 'Q_D': 'D_Q_Btrack',
                        'It_D': 'D_ItBt', 'L_D': 'D_L'})
        else:
            hdr.update({'L_D': 'D_L'})
    return hdr
예제 #4
0
    def __init__(self, D0, S, lmbda=None, opt=None, dimK=1, dimN=2):
        """
        Initialise a ConvBPDNDictLearn object with problem size and options.


        Parameters
        ----------
        D0 : array_like
          Initial dictionary array
        S : array_like
          Signal array
        lmbda : float
          Regularisation parameter
        opt : :class:`ConvBPDNDictLearn.Options` object
          Algorithm options
        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 = ConvBPDNDictLearn.Options()
        self.opt = opt

        # 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 X
        opt['CCMOD'].update(
            {'X0': cr.zpad(cr.stdformD(D0, cri.C, cri.M, dimN), cri.Nv)})

        # Create X update object
        xstep = Fcbpdn.ConvBPDN(D0,
                                S,
                                lmbda,
                                opt['CBPDN'],
                                dimK=dimK,
                                dimN=dimN)

        # Create D update object
        dstep = ccmod.ConvCnstrMOD(None,
                                   S,
                                   dsz,
                                   opt['CCMOD'],
                                   dimK=dimK,
                                   dimN=dimN)

        print("L xstep in cbpdndl: ", xstep.L)
        print("L dstep in cbpdndl: ", dstep.L)

        # Configure iteration statistics reporting
        isfld = ['Iter', 'ObjFun', 'DFid', 'RegL1', 'Cnstr']
        hdrtxt = ['Itn', 'Fnc', 'DFid', u('ℓ1'), 'Cnstr']
        hdrmap = {
            'Itn': 'Iter',
            'Fnc': 'ObjFun',
            'DFid': 'DFid',
            u('ℓ1'): 'RegL1',
            'Cnstr': 'Cnstr'
        }

        if self.opt['AccurateDFid']:
            isxmap = {
                'X_F_Btrack': 'F_Btrack',
                'X_Q_Btrack': 'Q_Btrack',
                'X_ItBt': 'IterBTrack',
                'X_L': 'L',
                'X_Rsdl': 'Rsdl'
            }
            evlmap = {'ObjFun': 'ObjFun', 'DFid': 'DFid', 'RegL1': 'RegL1'}
        else:
            isxmap = {
                'ObjFun': 'ObjFun',
                'DFid': 'DFid',
                'RegL1': 'RegL1',
                'X_F_Btrack': 'F_Btrack',
                'X_Q_Btrack': 'Q_Btrack',
                'X_ItBt': 'IterBTrack',
                'X_L': 'L',
                'X_Rsdl': 'Rsdl'
            }
            evlmap = {}

        # If Backtracking enabled in xstep display the BT variables also
        if xstep.opt['BackTrack', 'Enabled']:
            isfld.extend(
                ['X_F_Btrack', 'X_Q_Btrack', 'X_ItBt', 'X_L', 'X_Rsdl'])
            hdrtxt.extend(['F_X', 'Q_X', 'It_X', 'L_X'])
            hdrmap.update({
                'F_X': 'X_F_Btrack',
                'Q_X': 'X_Q_Btrack',
                'It_X': 'X_ItBt',
                'L_X': 'X_L'
            })
        else:  # Add just L value to xstep display
            isfld.extend(['X_L', 'X_Rsdl'])
            hdrtxt.append('L_X')
            hdrmap.update({'L_X': 'X_L'})

        isdmap = {
            'Cnstr': 'Cnstr',
            'D_F_Btrack': 'F_Btrack',
            'D_Q_Btrack': 'Q_Btrack',
            'D_ItBt': 'IterBTrack',
            'D_L': 'L',
            'D_Rsdl': 'Rsdl'
        }

        # If Backtracking enabled in dstep display the BT variables also
        if dstep.opt['BackTrack', 'Enabled']:
            isfld.extend([
                'D_F_Btrack', 'D_Q_Btrack', 'D_ItBt', 'D_L', 'D_Rsdl', 'Time'
            ])
            hdrtxt.extend(['F_D', 'Q_D', 'It_D', 'L_D'])
            hdrmap.update({
                'F_D': 'D_F_Btrack',
                'Q_D': 'D_Q_Btrack',
                'It_D': 'D_ItBt',
                'L_D': 'D_L'
            })
        else:  # Add just L value to dstep display
            isfld.extend(['D_L', 'D_Rsdl', 'Time'])
            hdrtxt.append('L_D')
            hdrmap.update({'L_D': 'D_L'})

        isc = dictlrn.IterStatsConfig(isfld=isfld,
                                      isxmap=isxmap,
                                      isdmap=isdmap,
                                      evlmap=evlmap,
                                      hdrtxt=hdrtxt,
                                      hdrmap=hdrmap)

        # Call parent constructor
        super(ConvBPDNDictLearn, self).__init__(xstep, dstep, opt, isc)
예제 #5
0
파일: bpdndl.py 프로젝트: bwohlberg/sporco
    def __init__(self, D0, S, lmbda=None, opt=None):
        """

        |

        **Call graph**

        .. image:: ../_static/jonga/bpdndl_init.svg
           :width: 20%
           :target: ../_static/jonga/bpdndl_init.svg

        |


        Parameters
        ----------
        D0 : array_like, shape (N, M)
          Initial dictionary matrix
        S : array_like, shape (N, K)
          Signal vector or matrix
        lmbda : float
          Regularisation parameter
        opt : :class:`BPDNDictLearn.Options` object
          Algorithm options
        """

        if opt is None:
            opt = BPDNDictLearn.Options()
        self.opt = opt

        # Normalise dictionary according to D update options
        D0 = cmod.getPcn(opt['CMOD', 'ZeroMean'])(D0)

        # Modify D update options to include initial values for Y and U
        Nc = D0.shape[1]
        opt['CMOD'].update({'Y0': D0, 'U0': np.zeros((S.shape[0], Nc))})

        # Create X update object
        xstep = bpdn.BPDN(D0, S, lmbda, opt['BPDN'])

        # Create D update object
        Nm = S.shape[1]
        dstep = cmod.CnstrMOD(xstep.Y, S, (Nc, Nm), opt['CMOD'])

        # Configure iteration statistics reporting
        if self.opt['AccurateDFid']:
            isxmap = {'XPrRsdl': 'PrimalRsdl', 'XDlRsdl': 'DualRsdl',
                      'XRho': 'Rho'}
            evlmap = {'ObjFun': 'ObjFun', 'DFid': 'DFid', 'RegL1': 'RegL1'}
        else:
            isxmap = {'ObjFun': 'ObjFun', 'DFid': 'DFid', 'RegL1': 'RegL1',
                      'XPrRsdl': 'PrimalRsdl', 'XDlRsdl': 'DualRsdl',
                      'XRho': 'Rho'}
            evlmap = {}
        isc = dictlrn.IterStatsConfig(
            isfld=['Iter', 'ObjFun', 'DFid', 'RegL1', 'Cnstr', 'XPrRsdl',
                   'XDlRsdl', 'XRho', 'DPrRsdl', 'DDlRsdl', 'DRho', 'Time'],
            isxmap=isxmap,
            isdmap={'Cnstr':  'Cnstr', 'DPrRsdl': 'PrimalRsdl',
                    'DDlRsdl': 'DualRsdl', 'DRho': 'Rho'},
            evlmap=evlmap,
            hdrtxt=['Itn', 'Fnc', 'DFid', u('ℓ1'), 'Cnstr', 'r_X', 's_X',
                    u('ρ_X'), 'r_D', 's_D', u('ρ_D')],
            hdrmap={'Itn': 'Iter', 'Fnc': 'ObjFun', 'DFid': 'DFid',
                    u('ℓ1'): 'RegL1', 'Cnstr': 'Cnstr', 'r_X': 'XPrRsdl',
                    's_X': 'XDlRsdl', u('ρ_X'): 'XRho', 'r_D': 'DPrRsdl',
                    's_D': 'DDlRsdl', u('ρ_D'): 'DRho'}
            )

        # Call parent constructor
        super(BPDNDictLearn, self).__init__(xstep, dstep, opt, isc)
예제 #6
0
class ElasticNet(BPDN):
    """ADMM algorithm for the elastic net :cite:`zou-2005-regularization`
    problem.

    Solve the optimisation problem

    .. math::
       \mathrm{argmin}_\mathbf{x} \;
       (1/2) \| D \mathbf{x} - \mathbf{s} \|_2^2 + \lambda \| \mathbf{x} \|_1
       + (\mu/2) \| \mathbf{x} \|_2^2

    via the ADMM problem

    .. math::
       \mathrm{argmin}_\mathbf{x} \;
       (1/2) \| D \mathbf{x} - \mathbf{s} \|_2^2 + \lambda \| \mathbf{y} \|_1
       + (\mu/2) \| \mathbf{x} \|_2^2 \quad \\text{such that} \quad
       \mathbf{x} = \mathbf{y} \;\;.

    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) \| D \mathbf{x} - \mathbf{s} \|_2^2`

       ``RegL1`` : Value of regularisation term \
       :math:`\| \mathbf{x} \|_1`

       ``RegL2`` : Value of regularisation term \
       :math:`(1/2) \| \mathbf{x} \|_2^2`

       ``PrimalRsdl`` : Norm of primal residual

       ``DualRsdl`` : Norm of dual Residual

       ``EpsPrimal`` : Primal residual stopping tolerance \
       :math:`\epsilon_{\mathrm{pri}}`

       ``EpsDual`` : Dual residual stopping tolerance \
       :math:`\epsilon_{\mathrm{dua}}`

       ``Rho`` : Penalty parameter

       ``Time`` : Cumulative run time
    """

    itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1', 'RegL2')
    hdrtxt_objfn = ('Fnc', 'DFid', u('Regℓ1'), u('Regℓ2'))
    hdrval_objfun = {
        'Fnc': 'ObjFun',
        'DFid': 'DFid',
        u('Regℓ1'): 'RegL1',
        u('Regℓ2'): 'RegL2'
    }

    def __init__(self, D, S, lmbda=None, mu=0.0, opt=None):
        """
        Initialise an ElasticNet object with problem parameters.

        Parameters
        ----------
        D : array_like, shape (N, M)
          Dictionary matrix
        S : array_like, shape (M, K)
          Signal vector or matrix
        lmbda : float
          Regularisation parameter (l1)
        mu : float
          Regularisation parameter (l2)
        opt : :class:`BPDN.Options` object
          Algorithm options
        """

        if opt is None:
            opt = BPDN.Options()

        # Set dtype attribute based on S.dtype and opt['DataType']
        self.set_dtype(opt, S.dtype)

        self.mu = self.dtype.type(mu)

        super(ElasticNet, self).__init__(D, S, lmbda, opt)

    def setdict(self, D):
        """Set dictionary array."""

        self.D = np.asarray(D)
        self.DTS = self.D.T.dot(self.S)
        # Factorise dictionary for efficient solves
        self.lu, self.piv = sl.lu_factor(self.D, self.mu + self.rho)
        self.lu = np.asarray(self.lu, dtype=self.dtype)

    def xstep(self):
        """Minimise Augmented Lagrangian with respect to :math:`\mathbf{x}`."""

        self.X = np.asarray(sl.lu_solve_ATAI(
            self.D, self.mu + self.rho,
            self.DTS + self.rho * (self.Y - self.U), self.lu, self.piv),
                            dtype=self.dtype)

    def obfn_reg(self):
        """Compute regularisation term and contribution to objective
        function.
        """

        rl1 = linalg.norm((self.wl1 * self.obfn_gvar()).ravel(), 1)
        rl2 = 0.5 * linalg.norm(self.obfn_gvar())**2
        return (self.lmbda * rl1 + self.mu * rl2, rl1, rl2)

    def rhochange(self):
        """Re-factorise matrix when rho changes."""

        self.lu, self.piv = sl.lu_factor(self.D, self.mu + self.rho)
        self.lu = np.asarray(self.lu, dtype=self.dtype)
예제 #7
0
    def __init__(self, D0, S, lmbda=None, opt=None):
        """

        |

        **Call graph**

        .. image:: ../_static/jonga/bpdndl_init.svg
           :width: 20%
           :target: ../_static/jonga/bpdndl_init.svg

        |


        Parameters
        ----------
        D0 : array_like, shape (N, M)
          Initial dictionary matrix
        S : array_like, shape (N, K)
          Signal vector or matrix
        lmbda : float
          Regularisation parameter
        opt : :class:`BPDNDictLearn.Options` object
          Algorithm options
        """

        if opt is None:
            opt = BPDNDictLearn.Options()
        self.opt = opt

        # Normalise dictionary according to D update options
        D0 = cmod.getPcn(opt['CMOD', 'ZeroMean'])(D0)

        # Modify D update options to include initial values for Y and U
        Nc = D0.shape[1]
        opt['CMOD'].update({'Y0': D0, 'U0': np.zeros((S.shape[0], Nc))})

        # Create X update object
        xstep = bpdn.BPDN(D0, S, lmbda, opt['BPDN'])

        # Create D update object
        Nm = S.shape[1]
        dstep = cmod.CnstrMOD(xstep.Y, S, (Nc, Nm), opt['CMOD'])

        # Configure iteration statistics reporting
        if self.opt['AccurateDFid']:
            isxmap = {
                'XPrRsdl': 'PrimalRsdl',
                'XDlRsdl': 'DualRsdl',
                'XRho': 'Rho'
            }
            evlmap = {'ObjFun': 'ObjFun', 'DFid': 'DFid', 'RegL1': 'RegL1'}
        else:
            isxmap = {
                'ObjFun': 'ObjFun',
                'DFid': 'DFid',
                'RegL1': 'RegL1',
                'XPrRsdl': 'PrimalRsdl',
                'XDlRsdl': 'DualRsdl',
                'XRho': 'Rho'
            }
            evlmap = {}
        isc = dictlrn.IterStatsConfig(isfld=[
            'Iter', 'ObjFun', 'DFid', 'RegL1', 'Cnstr', 'XPrRsdl', 'XDlRsdl',
            'XRho', 'DPrRsdl', 'DDlRsdl', 'DRho', 'Time'
        ],
                                      isxmap=isxmap,
                                      isdmap={
                                          'Cnstr': 'Cnstr',
                                          'DPrRsdl': 'PrimalRsdl',
                                          'DDlRsdl': 'DualRsdl',
                                          'DRho': 'Rho'
                                      },
                                      evlmap=evlmap,
                                      hdrtxt=[
                                          'Itn', 'Fnc', 'DFid',
                                          u('ℓ1'), 'Cnstr', 'r_X', 's_X',
                                          u('ρ_X'), 'r_D', 's_D',
                                          u('ρ_D')
                                      ],
                                      hdrmap={
                                          'Itn': 'Iter',
                                          'Fnc': 'ObjFun',
                                          'DFid': 'DFid',
                                          u('ℓ1'): 'RegL1',
                                          'Cnstr': 'Cnstr',
                                          'r_X': 'XPrRsdl',
                                          's_X': 'XDlRsdl',
                                          u('ρ_X'): 'XRho',
                                          'r_D': 'DPrRsdl',
                                          's_D': 'DDlRsdl',
                                          u('ρ_D'): 'DRho'
                                      })

        # Call parent constructor
        super(BPDNDictLearn, self).__init__(xstep, dstep, opt, isc)
예제 #8
0
파일: bpdn.py 프로젝트: nikzasel/sporco
class BPDNJoint(BPDN):
    r"""
    ADMM algorithm for BPDN with joint sparsity via an :math:`\ell_{2,1}`
    norm term.

    |

    .. inheritance-diagram:: BPDNJoint
       :parts: 2

    |


    Solve the optimisation problem

    .. math::
       \mathrm{argmin}_X \; (1/2) \| D X - S \|_2^2 + \lambda \| X \|_1
       + \mu \| X \|_{2,1}

    via the ADMM problem

    .. math::
       \mathrm{argmin}_{X, Y} \; (1/2) \| D X - S \|_2^2 +
       \lambda \| Y \|_1 + \mu \| Y \|_{2,1} \quad \text{such that} \quad
       X = Y \;\;.

    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) \| D X - S
       \|_2^2`

       ``RegL1`` : Value of regularisation term :math:`\| X \|_1`

       ``RegL21`` : Value of regularisation term :math:`\| X \|_{2,1}`

       ``PrimalRsdl`` : Norm of primal residual

       ``DualRsdl`` : Norm of dual Residual

       ``EpsPrimal`` : Primal residual stopping tolerance
       :math:`\epsilon_{\mathrm{pri}}`

       ``EpsDual`` : Dual residual stopping tolerance
       :math:`\epsilon_{\mathrm{dua}}`

       ``Rho`` : Penalty parameter

       ``Time`` : Cumulative run time
    """

    itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1', 'RegL21')
    hdrtxt_objfn = ('Fnc', 'DFid', u('Regℓ1'), u('Regℓ2,1'))
    hdrval_objfun = {
        'Fnc': 'ObjFun',
        'DFid': 'DFid',
        u('Regℓ1'): 'RegL1',
        u('Regℓ2,1'): 'RegL21'
    }

    def __init__(self, D, S, lmbda=None, mu=0.0, opt=None):
        """
        |

        **Call graph**

        .. image:: ../_static/jonga/bpdnjnt_init.svg
           :width: 20%
           :target: ../_static/jonga/bpdnjnt_init.svg

        |


        Parameters
        ----------
        D : array_like, shape (N, M)
          Dictionary matrix
        S : array_like, shape (M, K)
          Signal vector or matrix
        lmbda : float
          Regularisation parameter (l1)
        mu : float
          Regularisation parameter (l2,1)
        opt : :class:`BPDN.Options` object
          Algorithm options
        """

        if opt is None:
            opt = BPDN.Options()
        super(BPDNJoint, self).__init__(D, S, lmbda, opt)
        self.mu = self.dtype.type(mu)

    def ystep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{y}`."""

        self.Y = np.asarray(sp.prox_sl1l2(self.AX + self.U,
                                          (self.lmbda / self.rho) * self.wl1,
                                          self.mu / self.rho,
                                          axis=-1),
                            dtype=self.dtype)
        GenericBPDN.ystep(self)

    def obfn_reg(self):
        r"""Compute regularisation terms and contribution to objective
        function. Regularisation terms are :math:`\| Y \|_1` and
        :math:`\| Y \|_{2,1}`.
        """

        rl1 = np.linalg.norm((self.wl1 * self.obfn_gvar()).ravel(), 1)
        rl21 = np.sum(np.sqrt(np.sum(self.obfn_gvar()**2, axis=1)))
        return (self.lmbda * rl1 + self.mu * rl21, rl1, rl21)
예제 #9
0
    def __init__(self,
                 D,
                 S,
                 R,
                 opt=None,
                 lmbda=None,
                 optx=None,
                 dimK=None,
                 dimN=2,
                 *args,
                 **kwargs):
        """
        Parameters
        ----------
        xstep : internal xstep object (e.g. xstep.ConvBPDN)
        D : array_like
          Dictionary array
        S : array_like
          Signal array
        R : array_like
          Rank array
        lmbda : list of float
          Regularisation parameter
        opt : list containing :class:`ConvBPDN.Options` object
          Algorithm options for each individual solver
        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
        *args
          Variable length list of arguments for constructor of internal
          xstep object (e.g. mu)
        **kwargs
          Keyword arguments for constructor of internal xstep object
        """

        if opt is None:
            opt = AKConvBPDN.Options()
        self.opt = opt

        # Infer outer problem dimensions
        self.cri = cr.CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN)

        # Parse mu
        if 'mu' in kwargs:
            mu = kwargs['mu']
        else:
            mu = [0] * self.cri.dimN

        # Parse lmbda and optx
        if lmbda is None: lmbda = [None] * self.cri.dimN
        if optx is None: optx = [None] * self.cri.dimN

        # Parse isc
        if 'isc' in kwargs:
            isc = kwargs['isc']
        else:
            isc = None

        # Store parameters
        self.lmbda = lmbda
        self.optx = optx
        self.mu = mu
        self.R = R

        # Reshape D and S to standard layout
        self.D = np.asarray(D.reshape(self.cri.shpD), dtype=S.dtype)
        self.S = np.asarray(S.reshape(self.cri.shpS), dtype=S.dtype)

        # Compute signal in DFT domain
        self.Sf = sl.fftn(self.S, None, self.cri.axisN)
        # print('Sf shape %s \n' % (self.Sf.shape,))
        # print('S shape %s \n' % (self.S.shape,))
        # print('shpS %s \n' % (self.cri.shpS,))

        # Signal uni-dim (kruskal)
        # self.Skf = np.reshape(self.Sf,[np.prod(np.array(self.Sf.shape)),1],order='F')

        # Decomposed Kruskal Initialization
        self.K = []
        self.Kf = []
        Nvf = []
        for i, Nvi in enumerate(self.cri.Nv):  # Ui
            Ki = np.random.randn(Nvi, np.sum(self.R))
            Kfi = sl.pyfftw_empty_aligned(Ki.shape, self.Sf.dtype)
            Kfi[:] = sl.fftn(Ki, None, [0])
            self.K.append(Ki)
            self.Kf.append(Kfi)
            Nvf.append(Kfi.shape[0])

        self.Nvf = tuple(Nvf)

        # Fourier dimensions
        self.NC = int(np.prod(self.Nvf) * self.cri.Cd)

        # dict FFT
        self.setdict()

        # Init KCSC solver (Needs to be initiated inside AKConvBPDN because requires convolvedict() and reshapesignal())
        self.xstep = []
        for l in range(self.cri.dimN):

            Wl = self.convolvedict(l)  # convolvedict
            cri_l = KCSC_ConvRepIndexing(self.cri, self.R, l)  # cri KCSC

            self.xstep.append(KConvBPDN(Wl, np.reshape(self.Sf,cri_l.shpS,order='C'), cri_l,\
                                self.S.dtype, self.lmbda[l], self.mu[l], self.optx[l]))

        # Init isc
        if isc is None:

            isc_lst = []  # itStats from block-solver
            isc_fields = []
            for i in range(self.cri.dimN):
                str_i = '_{0!s}'.format(i)

                isc_i = IterStatsConfig(isfld=[
                    'ObjFun' + str_i, 'PrimalRsdl' + str_i, 'DualRsdl' + str_i,
                    'Rho' + str_i
                ],
                                        isxmap={
                                            'ObjFun' + str_i: 'ObjFun',
                                            'PrimalRsdl' + str_i: 'PrimalRsdl',
                                            'DualRsdl' + str_i: 'DualRsdl',
                                            'Rho' + str_i: 'Rho'
                                        },
                                        evlmap={},
                                        hdrtxt=[
                                            'Fnc' + str_i, 'r' + str_i,
                                            's' + str_i,
                                            u('ρ' + str_i)
                                        ],
                                        hdrmap={
                                            'Fnc' + str_i: 'ObjFun' + str_i,
                                            'r' + str_i: 'PrimalRsdl' + str_i,
                                            's' + str_i: 'DualRsdl' + str_i,
                                            u('ρ' + str_i): 'Rho' + str_i
                                        })
                isc_fields += isc_i.IterationStats._fields

                isc_lst.append(isc_i)

            # isc_it = IterStatsConfig(       # global itStats  -> No, to be managed in dictlearn
            #     isfld=['Iter','Time'],
            #     isxmap={},
            #     evlmap={},
            #     hdrtxt=['Itn'],
            #     hdrmap={'Itn': 'Iter'}
            # )
            #
            # isc_fields += isc_it._fields

        self.isc_lst = isc_lst
        # self.isc_it = isc_it
        self.isc = collections.namedtuple('IterationStats', isc_fields)

        # Required because dictlrn.DictLearn assumes that all valid
        # xstep objects have an IterationStats attribute
        # self.IterationStats = self.xstep.IterationStats

        self.itstat = []
        self.j = 0
예제 #10
0
class ConvBPDNScalarTV(admm.ADMM):
    r"""
    ADMM algorithm for an extension of Convolutional BPDN including
    terms penalising the total variation of each coefficient map
    :cite:`wohlberg-2017-convolutional`.

    |

    .. inheritance-diagram:: ConvBPDNScalarTV
       :parts: 2

    |

    Solve the optimisation problem

    .. math::
       \mathrm{argmin}_\mathbf{x} \; \frac{1}{2}
       \left\| \sum_m \mathbf{d}_m * \mathbf{x}_m - \mathbf{s}
       \right\|_2^2 + \lambda \sum_m \| \mathbf{x}_m \|_1 +
       \mu \sum_m \left\| \sqrt{\sum_i (G_i \mathbf{x}_m)^2} \right\|_1
       \;\;,

    where :math:`G_i` is an operator computing the derivative along index
    :math:`i`, via the ADMM problem

    .. math::
       \mathrm{argmin}_\mathbf{x} \; (1/2) \left\| D \mathbf{x} -
       \mathbf{s} \right\|_2^2 + \lambda
       \| \mathbf{y}_L \|_1 + \mu \sum_m \left\| \sqrt{\sum_{i=0}^{L-1}
       \mathbf{y}_i^2} \right\|_1 \quad \text{ such that } \quad
       \left( \begin{array}{c} \Gamma_0 \\ \Gamma_1 \\ \vdots \\ I
       \end{array} \right) \mathbf{x} =
       \left( \begin{array}{c} \mathbf{y}_0 \\
       \mathbf{y}_1 \\ \vdots \\ \mathbf{y}_L \end{array}
       \right)  \;\;,

    where

    .. math::
       D = \left( \begin{array}{ccc} D_0 & D_1 & \ldots \end{array} \right)
       \qquad
       \mathbf{x} = \left( \begin{array}{c} \mathbf{x}_0 \\ \mathbf{x}_1 \\
       \vdots \end{array} \right) \qquad
       \Gamma_i = \left( \begin{array}{ccc}
          G_i & 0 & \ldots \\  0 & G_i & \ldots \\ \vdots & \vdots & \ddots
       \end{array} \right) \;\;.


    For multi-channel signals with a single-channel dictionary, scalar TV is
    applied independently to each coefficient map for channel :math:`c` and
    filter :math:`m`. Since multi-channel signals with a multi-channel
    dictionary also have one coefficient map per filter, the behaviour is
    the same as for single-channel signals.


    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`

       ``RegTV`` : Value of regularisation term :math:`\sum_m \left\|
       \sqrt{\sum_i (G_i \mathbf{x}_m)^2} \right\|_1`

       ``PrimalRsdl`` : Norm of primal residual

       ``DualRsdl`` : Norm of dual residual

       ``EpsPrimal`` : Primal residual stopping tolerance
       :math:`\epsilon_{\mathrm{pri}}`

       ``EpsDual`` : Dual residual stopping tolerance
       :math:`\epsilon_{\mathrm{dua}}`

       ``Rho`` : Penalty parameter

       ``XSlvRelRes`` : Relative residual of X step solver

       ``Time`` : Cumulative run time
    """
    class Options(cbpdn.ConvBPDN.Options):
        r"""ConvBPDNScalarTV algorithm options

        Options include all of those defined in
        :class:`.admm.cbpdn.ConvBPDN.Options`, together with additional
        options:

        ``TVWeight`` : An array of weights :math:`w_m` for the term
        penalising the gradient of the coefficient maps. If this
        option is defined, the regularization term is :math:`\sum_m w_m
        \left\| \sqrt{\sum_i (G_i \mathbf{x}_m)^2} \right\|_1`
        where :math:`w_m` is the weight for filter index :math:`m`. The
        array should be an :math:`M`-vector where :math:`M` is the number
        of filters in the dictionary.
        """

        defaults = copy.deepcopy(cbpdn.ConvBPDN.Options.defaults)
        defaults.update({'TVWeight': 1.0})

        def __init__(self, opt=None):
            """
            Parameters
            ----------
            opt : dict or None, optional (default None)
              ConvBPDNScalarTV algorithm options
            """

            if opt is None:
                opt = {}
            cbpdn.ConvBPDN.Options.__init__(self, opt)

    itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1', 'RegTV')
    itstat_fields_extra = ('XSlvRelRes', )
    hdrtxt_objfn = ('Fnc', 'DFid', u('Regℓ1'), u('RegTV'))
    hdrval_objfun = {
        'Fnc': 'ObjFun',
        'DFid': 'DFid',
        u('Regℓ1'): 'RegL1',
        u('RegTV'): 'RegTV'
    }

    def __init__(self, D, S, lmbda, mu=0.0, opt=None, dimK=None, dimN=2):
        """

        |

        **Call graph**

        .. image:: ../_static/jonga/cbpdnstv_init.svg
           :width: 20%
           :target: ../_static/jonga/cbpdnstv_init.svg

        |


        Parameters
        ----------
        D : array_like
          Dictionary matrix
        S : array_like
          Signal vector or matrix
        lmbda : float
          Regularisation parameter (l1)
        mu : float
          Regularisation parameter (gradient)
        opt : :class:`ConvBPDNScalarTV.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
        """

        if opt is None:
            opt = ConvBPDNScalarTV.Options()

        # Infer problem dimensions and set relevant attributes of self
        self.cri = cr.CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN)

        # Call parent class __init__
        Nx = np.product(np.array(self.cri.shpX))
        yshape = self.cri.shpX + (len(self.cri.axisN) + 1, )
        super(ConvBPDNScalarTV, self).__init__(Nx, yshape, yshape, S.dtype,
                                               opt)

        # Set l1 term scaling and weight array
        self.lmbda = self.dtype.type(lmbda)
        self.Wl1 = np.asarray(opt['L1Weight'], dtype=self.dtype)
        self.Wl1 = self.Wl1.reshape(cr.l1Wshape(self.Wl1, self.cri))

        self.mu = self.dtype.type(mu)
        if hasattr(opt['TVWeight'], 'ndim') and opt['TVWeight'].ndim > 0:
            self.Wtv = np.asarray(
                opt['TVWeight'].reshape((1, ) * (dimN + 2) +
                                        opt['TVWeight'].shape),
                dtype=self.dtype)
        else:
            # Wtv is a scalar: no need to change shape
            self.Wtv = np.asarray(opt['TVWeight'], dtype=self.dtype)

        # Set penalty parameter
        self.set_attr('rho',
                      opt['rho'],
                      dval=(50.0 * self.lmbda + 1.0),
                      dtype=self.dtype)

        # Set rho_xi attribute
        self.set_attr('rho_xi',
                      opt['AutoRho', 'RsdlTarget'],
                      dval=1.0,
                      dtype=self.dtype)

        # 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 = sl.rfftn(self.S, None, self.cri.axisN)

        self.Gf, GHGf = sl.gradient_filters(self.cri.dimN + 3,
                                            self.cri.axisN,
                                            self.cri.Nv,
                                            dtype=self.dtype)
        self.GHGf = self.Wtv**2 * GHGf

        # Initialise byte-aligned arrays for pyfftw
        self.YU = sl.pyfftw_empty_aligned(self.Y.shape, dtype=self.dtype)
        self.Xf = sl.pyfftw_rfftn_empty_aligned(self.cri.shpX, self.cri.axisN,
                                                self.dtype)

        self.setdict()

    def setdict(self, D=None):
        """Set dictionary array."""

        if D is not None:
            self.D = np.asarray(D, dtype=self.dtype)
        self.Df = sl.rfftn(self.D, self.cri.Nv, self.cri.axisN)
        # Compute D^H S
        self.DSf = np.conj(self.Df) * self.Sf
        if self.cri.Cd > 1:
            self.DSf = np.sum(self.DSf, axis=self.cri.axisC, keepdims=True)
        if self.opt['HighMemSolve'] and self.cri.Cd == 1:
            self.c = sl.solvedbi_sm_c(self.Df, np.conj(self.Df),
                                      self.rho * self.GHGf + self.rho,
                                      self.cri.axisM)
        else:
            self.c = None

    def rhochange(self):
        """Updated cached c array when rho changes."""

        if self.opt['HighMemSolve'] and self.cri.Cd == 1:
            self.c = sl.solvedbi_sm_c(self.Df, np.conj(self.Df),
                                      self.rho * self.GHGf + self.rho,
                                      self.cri.axisM)

    def xstep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{x}`."""

        self.YU[:] = self.Y - self.U
        YUf = sl.rfftn(self.YU, None, self.cri.axisN)

        # The sum is over the extra axis indexing spatial gradient
        # operators G_i, *not* over axisM
        b = self.DSf + self.rho * (YUf[..., -1] + self.Wtv * np.sum(
            np.conj(self.Gf) * YUf[..., 0:-1], axis=-1))

        if self.cri.Cd == 1:
            self.Xf[:] = sl.solvedbi_sm(self.Df,
                                        self.rho * self.GHGf + self.rho, b,
                                        self.c, self.cri.axisM)
        else:
            self.Xf[:] = sl.solvemdbi_ism(self.Df,
                                          self.rho * self.GHGf + self.rho, b,
                                          self.cri.axisM, self.cri.axisC)

        self.X = sl.irfftn(self.Xf, self.cri.Nv, self.cri.axisN)

        if self.opt['LinSolveCheck']:
            Dop = lambda x: sl.inner(self.Df, x, axis=self.cri.axisM)
            if self.cri.Cd == 1:
                DHop = lambda x: np.conj(self.Df) * x
            else:
                DHop = lambda x: sl.inner(
                    np.conj(self.Df), x, axis=self.cri.axisC)
            ax = DHop(Dop(
                self.Xf)) + (self.rho * self.GHGf + self.rho) * self.Xf
            self.xrrs = sl.rrs(ax, b)
        else:
            self.xrrs = None

    def ystep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{y}`."""

        AXU = self.AX + self.U
        self.Y[..., 0:-1] = sp.prox_l2(AXU[..., 0:-1], self.mu / self.rho)
        self.Y[..., -1] = sp.prox_l1(AXU[..., -1],
                                     (self.lmbda / self.rho) * self.Wl1)

    def obfn_fvarf(self):
        """Variable to be evaluated in computing data fidelity term,
        depending on ``fEvalX`` option value.
        """

        return self.Xf if self.opt['fEvalX'] else \
            sl.rfftn(self.Y[..., -1], None, self.cri.axisN)

    def var_y0(self):
        r"""Get :math:`\mathbf{y}_0` variable, consisting of all blocks of
        :math:`\mathbf{y}` corresponding to a gradient operator."""

        return self.Y[..., 0:-1]

    def var_y1(self):
        r"""Get :math:`\mathbf{y}_1` variable, the block of
        :math:`\mathbf{y}` corresponding to the identity operator."""

        return self.Y[..., -1:]

    def var_yx(self):
        r"""Get component block of :math:`\mathbf{y}` that is constrained
        to be equal to :math:`\mathbf{x}`."""

        return self.Y[..., -1]

    def var_yx_idx(self):
        r"""Get index expression for component block of :math:`\mathbf{y}`
        that is constrained to be equal to :math:`\mathbf{x}`.
        """

        return np.s_[..., -1]

    def getmin(self):
        """Get minimiser after optimisation."""

        return self.X if self.opt['ReturnX'] else self.var_y1()[..., 0]

    def getcoef(self):
        """Get final coefficient array."""

        return self.getmin()

    def obfn_g0var(self):
        """Variable to be evaluated in computing the TV regularisation
        term, depending on the ``gEvalY`` option value.
        """

        # Use of self.AXnr[..., 0:-1] instead of self.cnst_A0(None, self.Xf)
        # reduces number of calls to self.cnst_A0
        return self.var_y0() if self.opt['gEvalY'] else \
            self.AXnr[..., 0:-1]

    def obfn_g1var(self):
        r"""Variable to be evaluated in computing the :math:`\ell_1`
        regularisation term, depending on the ``gEvalY`` option value.
        """

        # Use of self.AXnr[...,-1:] instead of self.cnst_A1(self.X)
        # reduces number of calls to self.cnst_A1
        return self.var_y1() if self.opt['gEvalY'] else \
            self.AXnr[..., -1:]

    def obfn_gvar(self):
        """Method providing compatibility with the interface of
        :class:`.admm.cbpdn.ConvBPDN` and derived classes in order to make
        this class compatible with classes such as :class:`.AddMaskSim`.
        """

        return self.obfn_g1var()

    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:]

    def obfn_dfd(self):
        r"""Compute data fidelity term :math:`(1/2) \| \sum_m \mathbf{d}_m *
        \mathbf{x}_m - \mathbf{s} \|_2^2`.
        """

        Ef = sl.inner(self.Df, self.obfn_fvarf(), axis=self.cri.axisM) \
             - self.Sf
        return sl.rfl2norm2(Ef, self.S.shape, axis=self.cri.axisN) / 2.0

    def obfn_reg(self):
        """Compute regularisation term and contribution to objective
        function.
        """

        rl1 = np.linalg.norm((self.Wl1 * self.obfn_g1var()).ravel(), 1)
        rtv = np.sum(np.sqrt(np.sum(self.obfn_g0var()**2, axis=-1)))
        return (self.lmbda * rl1 + self.mu * rtv, rl1, rtv)

    def itstat_extra(self):
        """Non-standard entries for the iteration stats record tuple."""

        return (self.xrrs, )

    def cnst_A0(self, X, Xf=None):
        r"""Compute :math:`A_0 \mathbf{x}` component of ADMM problem
        constraint. In this case :math:`A_0 \mathbf{x} = (\Gamma_0^T \;\;
        \Gamma_1^T \;\; \ldots )^T \mathbf{x}`.
        """

        if Xf is None:
            Xf = sl.rfftn(X, axes=self.cri.axisN)
        return self.Wtv[..., np.newaxis] * sl.irfftn(
            self.Gf * Xf[..., np.newaxis], self.cri.Nv, axes=self.cri.axisN)

    def cnst_A0T(self, X):
        r"""Compute :math:`A_0^T \mathbf{x}` where :math:`A_0 \mathbf{x}`
        is a component of the ADMM problem constraint. In this case
        :math:`A_0^T \mathbf{x} = (\Gamma_0^T \;\; \Gamma_1^T \;\; \ldots )
        \mathbf{x}`.
        """

        Xf = sl.rfftn(X, axes=self.cri.axisN)
        return self.Wtv[..., np.newaxis] * sl.irfftn(
            np.conj(self.Gf) * Xf[..., 0:-1], self.cri.Nv, axes=self.cri.axisN)

    def cnst_A1(self, X):
        r"""Compute :math:`A_1 \mathbf{x}` component of ADMM problem
        constraint. In this case :math:`A_1 \mathbf{x} = \mathbf{x}`.
        """

        return X[..., np.newaxis]

    def cnst_A1T(self, X):
        r"""Compute :math:`A_1^T \mathbf{x}` where :math:`A_1 \mathbf{x}`
        is a component of the ADMM problem constraint. In this case
        :math:`A_1^T \mathbf{x} = \mathbf{x}`.
        """

        return X[..., -1]

    def cnst_A(self, X, Xf=None):
        r"""Compute :math:`A \mathbf{x}` component of ADMM problem
        constraint.  In this case :math:`A \mathbf{x} = (\Gamma_0^T \;\;
        \Gamma_1^T \;\; \ldots \;\; I)^T \mathbf{x}`.
        """

        return np.concatenate((self.cnst_A0(X, Xf), self.cnst_A1(X)), axis=-1)

    def cnst_AT(self, X):
        r"""Compute :math:`A^T \mathbf{x}` where :math:`A \mathbf{x}` is
        a component of ADMM problem constraint. In this case
        :math:`A^T \mathbf{x} = (\Gamma_0^T \;\; \Gamma_1^T \;\; \ldots
        \;\; I) \mathbf{x}`.
        """

        return np.sum(self.cnst_A0T(X), axis=-1) + self.cnst_A1T(X)

    def cnst_B(self, Y):
        r"""Compute :math:`B \mathbf{y}` component of ADMM problem constraint.
        In this case :math:`B \mathbf{y} = -\mathbf{y}`.
        """

        return -Y

    def cnst_c(self):
        r"""Compute constant component :math:`\mathbf{c}` of ADMM problem
        constraint. In this case :math:`\mathbf{c} = \mathbf{0}`.
        """

        return 0.0

    def relax_AX(self):
        """Implement relaxation if option ``RelaxParam`` != 1.0."""

        # We need to keep the non-relaxed version of AX since it is
        # required for computation of primal residual r
        self.AXnr = self.cnst_A(self.X, self.Xf)
        if self.rlx == 1.0:
            # If RelaxParam option is 1.0 there is no relaxation
            self.AX = self.AXnr
        else:
            # Avoid calling cnst_c() more than once in case it is expensive
            # (e.g. due to allocation of a large block of memory)
            if not hasattr(self, '_cnst_c'):
                self._cnst_c = self.cnst_c()
            # Compute relaxed version of AX
            alpha = self.rlx
            self.AX = alpha * self.AXnr - (1 - alpha) * (self.cnst_B(self.Y) -
                                                         self._cnst_c)

    def reconstruct(self, X=None):
        """Reconstruct representation."""

        if X is None:
            Xf = self.Xf
        else:
            Xf = sl.rfftn(X, None, self.cri.axisN)
        Sf = np.sum(self.Df * Xf, axis=self.cri.axisM)
        return sl.irfftn(Sf, self.cri.Nv, self.cri.axisN)
예제 #11
0
    def __init__(self, D0, S, lmbda=None, W=None, opt=None):
        """
        Parameters
        ----------
        D0 : array_like, shape (N, M)
          Initial dictionary matrix
        S : array_like, shape (N, K)
          Signal vector or matrix
        lmbda : float
          Regularisation parameter
        W : array_like, shape (N, K)
          Weight matrix
        opt : :class:`WeightedBPDNDictLearn.Options` object
          Algorithm options
        """

        if opt is None:
            opt = WeightedBPDNDictLearn.Options()
        self.opt = opt

        # Normalise dictionary according to D update options
        D0 = cmod.getPcn(opt['CMOD', 'ZeroMean'],
                         opt['CMOD', 'NonNegCoef'])(D0)

        # Modify D update options to include initial values for Y and U
        Nc = D0.shape[1]
        opt['CMOD'].update({'X0': D0})

        # Create X update object
        xstep = bpdn.WeightedBPDN(D0, S, lmbda, W=W, opt=opt['BPDN'])

        # Create D update object
        Nm = S.shape[1]
        dstep = cmod.WeightedCnstrMOD(xstep.Y, S, W=W, dsz=(Nc, Nm),
                                      opt=opt['CMOD'])

        if W is None:
            W = np.array([1.0], dtype=xstep.dtype)
        if W.ndim > 0:
            W = atleast_nd(2, W)
        self.W = np.asarray(W, dtype=xstep.dtype)

        # Configure iteration statistics reporting
        if self.opt['AccurateDFid']:
            isxmap = {'XRsdl': 'Rsdl', 'XL': 'L'}
            evlmap = {'ObjFun': 'ObjFun', 'DFid': 'DFid', 'RegL1': 'RegL1'}
        else:
            isxmap = {'ObjFun': 'ObjFun', 'DFid': 'DFid', 'RegL1': 'RegL1',
                      'XRsdl': 'Rsdl', 'XL': 'L'}
            evlmap = {}
        isc = dictlrn.IterStatsConfig(
            isfld=['Iter', 'ObjFun', 'DFid', 'RegL1', 'Cnstr', 'XRsdl',
                   'XL', 'DRsdl', 'DL', 'Time'],
            isxmap=isxmap,
            isdmap={'Cnstr': 'Cnstr', 'DRsdl': 'Rsdl', 'DL': 'L'},
            evlmap=evlmap,
            hdrtxt=['Itn', 'Fnc', 'DFid', u('ℓ1'), 'Cnstr', 'X_Rsdl',
                    'X_L', 'D_Rsdl', 'D_L'],
            hdrmap={'Itn': 'Iter', 'Fnc': 'ObjFun', 'DFid': 'DFid',
                    u('ℓ1'): 'RegL1', 'Cnstr': 'Cnstr', 'X_Rsdl': 'XRsdl',
                    'X_L': 'XL', 'D_Rsdl': 'DRsdl', 'D_L': 'DL'}
            )

        # Call parent constructor
        super(WeightedBPDNDictLearn, self).__init__(xstep, dstep, opt, isc)
예제 #12
0
class ConvProdDictL1L1GrdJoint(ConvProdDictL1L1Grd):
    r"""
    ADMM algorithm for a Convolutional Sparse Coding problem for
    multi-channel signals with a dictionary consisting of a product
    of convolutional and standard dictionaries and with an :math:`\ell_1`
    data fidelity term and  :math:`\ell_{2,1}`, and :math:`\ell_2` of
    gradient regularisation terms :cite:`garcia-2018-convolutional2`.

    Solve the optimisation problem

    .. math::
       \mathrm{argmin}_X \; \left\| D X B^T - S \right\|_1 +
       \lambda \| X \|_{2,1} + (\mu / 2) \sum_i \| G_i X \|_2^2

    where :math:`D` is a convolutional dictionary, :math:`B` is a
    standard dictionary, :math:`G_i` is an operator that computes the
    gradient along array axis :math:`i`, and :math:`S` is a multi-channel
    input image with

    .. math::
       S = \left( \begin{array}{ccc} \mathbf{s}_0 & \mathbf{s}_1 & \ldots
       \end{array} \right) \;.

    where the signal channels form the columns, :math:`\mathbf{s}_c`, of
    :math:`S`. This problem is solved via the ADMM problem
    :cite:`garcia-2018-convolutional2`

    .. math::
       \mathrm{argmin}_{X,Y} \;
       \left\| Y_0 \right\|_1 + \lambda \| Y_1 \|_{2,1} + (\mu / 2)
       \sum_i \| G_i X \|_2^2 \quad \text{such that} \quad
       Y_0 = D X B^T - S \;\;\; Y_1 = X \;\;.
    """
    class Options(ConvProdDictL1L1Grd.Options):
        r"""ConvBPDNJoint algorithm options

        Options include all of those defined in
        :class:`ConvProdDictL1L1Grd.Options`, together with additional
        options:

          ``L21Weight`` : An array of weights for the :math:`\ell_{2,1}`
          norm. The array shape must be such that the array is
          compatible for multiplication with the X/Y variables *after*
          the sum over ``axisC`` performed during the computation of the
          :math:`\ell_{2,1}` norm. If this option is defined, the
          regularization term is :math:`\mu \sum_i w_i \sqrt{ \sum_c
          \mathbf{x}_{i,c}^2 }` where :math:`w_i` are the elements of the
          weight array, subscript :math:`c` indexes the channel axis and
          subscript :math:`i` indexes all other axes.
        """

        defaults = copy.deepcopy(ConvProdDictL1L1Grd.Options.defaults)
        defaults.update({'L21Weight': 1.0})

    itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL21', 'RegGrad')
    hdrtxt_objfn = ('Fnc', 'DFid', u('Regℓ21'), u('Regℓ2∇'))
    hdrval_objfun = {
        'Fnc': 'ObjFun',
        'DFid': 'DFid',
        u('Regℓ21'): 'RegL21',
        u('Regℓ2∇'): 'RegGrad'
    }

    def __init__(self, D, B, S, lmbda, mu=0.0, opt=None, dimK=None, dimN=2):
        """
        Parameters
        ----------
        D : array_like
          Dictionary matrix
        B : array_like
          Standard dictionary array
        S : array_like
          Signal vector or matrix
        lmbda : float
          Regularisation parameter (l2,1)
        mu : float
          Regularisation parameter (gradient)
        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:`ConvProdDictL1L1GrdJoint.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(ConvProdDictL1L1GrdJoint, self).__init__(D,
                                                       B,
                                                       S,
                                                       lmbda,
                                                       mu,
                                                       opt=opt,
                                                       dimK=dimK,
                                                       dimN=dimN)
        self.wl21 = np.asarray(opt['L21Weight'], dtype=self.dtype)

    def ystep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{y}`.
        """

        AXU = self.AX + self.U
        Y0 = prox_l1(self.block_sep0(AXU) - self.S, (1.0 / self.rho) * self.W)
        Y1 = prox_sl1l2(self.block_sep1(AXU),
                        0.0, (self.lmbda / self.rho) * self.wl21,
                        axis=self.cri.axisC)
        self.Y = self.block_cat(Y0, Y1)
        cbpdn.ConvTwoBlockCnstrnt.ystep(self)

    def obfn_g1(self, Y1):
        r"""Compute :math:`g_1(\mathbf{y_1})` component of ADMM objective
        function.
        """

        return np.sum(self.wl21 * np.sqrt(np.sum(Y1**2, axis=self.cri.axisC)))
예제 #13
0
class ParConvBPDN(GenericConvBPDN):
    r"""
    Parallel ADMM algorithm for Convolutional BPDN (CBPDN) with or
    without a spatial mask :cite:`skau-2018-fast`.

    |

    .. inheritance-diagram:: ParConvBPDN
       :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, via the ADMM problem

    .. math::
       \mathrm{argmin}_{\mathbf{x},\mathbf{y}_0,\mathbf{y}_1} \;
       (1/2) \| W \left( \sum_l \mathbf{y}_{0,l} - \mathbf{s} \right)
       \|_2^2 + \lambda \| \mathbf{y}_1 \|_1 \;\text{such that}\;
       \left( \begin{array}{c} D_{G_0} \\ \vdots \\ D_{G_{L-1}} \\
       \alpha I \end{array} \right) \mathbf{x} - \left( \begin{array}{c}
       \mathbf{y}_{0,0} \\ \vdots \\ \mathbf{y}_{0,L-1} \\ \alpha
       \mathbf{y}_1 \end{array} \right) = \left( \begin{array}{c}
       \mathbf{0} \\ \vdots \\ \mathbf{0} \\ \mathbf{0} \end{array}
       \right) \;\;,

    where the :math:`M` dictionary filters are partitioned into
    :math:`L` groups, :math:`\{G_l\}_{l \in \{0,\dots,L-1\}}` where

    .. math::
       G_i \cap G_j = \emptyset \text{ for } i \neq j \text{
       and } \bigcup_l G_l = \{0, \dots, M-1\} \;,

    and :math:`D_{G_l}` is a linear operator such that :math:`D_{G_l}
    \mathbf{x} = \sum_{g \in G_l} \mathbf{d}_g * \mathbf{x}_g`.

    Multi-image and multi-channel problems are also supported. The
    multi-image problem is

    .. math::
       \mathrm{argmin}_\mathbf{x} \;
       (1/2) \sum_k \left\| W_k \left( \sum_m \mathbf{d}_m *
       \mathbf{x}_{k,m} - \mathbf{s}_k \right) \right\|_2^2 + \lambda
       \sum_k \sum_m \| \mathbf{x}_{k,m} \|_1

    with input images :math:`\mathbf{s}_k`, masks :math:`W_k`, and
    coefficient maps :math:`\mathbf{x}_{k,m}`. The multi-channel
    problem with input image channels :math:`\mathbf{s}_c` and a
    multi-channel mask :math:`W_c` is either

    .. math::
       \mathrm{argmin}_\mathbf{x} \;
       (1/2) \sum_c \left\| W_c \left( \sum_m \mathbf{d}_m *
       \mathbf{x}_{c,m} - \mathbf{s}_c \right) \right\|_2^2 +
       \lambda \sum_c \sum_m \| \mathbf{x}_{c,m} \|_1

    with single-channel dictionary filters :math:`\mathbf{d}_m` and
    multi-channel coefficient maps :math:`\mathbf{x}_{c,m}`, or

    .. math::
       \mathrm{argmin}_\mathbf{x} \;
       (1/2) \sum_c \left\| W_c \left( \sum_m \mathbf{d}_{c,m} *
       \mathbf{x}_m - \mathbf{s}_c \right) \right\|_2^2 + \lambda
       \sum_m \| \mathbf{x}_m \|_1

    with multi-channel dictionary filters :math:`\mathbf{d}_{c,m}` and
    single-channel coefficient maps :math:`\mathbf{x}_m`.

    After termination of the :meth:`solve` method, AttributeError
    :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) \| W \left(
       \sum_m \mathbf{d}_m * \mathbf{x}_m - \mathbf{s} \right) \|_2^2`

       ``RegL1`` : Value of regularisation term :math:`\sum_m \|
       \mathbf{x}_m \|_1`

       ``PrimalRsdl`` : Norm of primal residual

       ``DualRsdl`` : Norm of dual residual

       ``EpsPrimal`` : Primal residual stopping tolerance
       :math:`\epsilon_{\mathrm{pri}}`

       ``EpsDual`` : Dual residual stopping tolerance
       :math:`\epsilon_{\mathrm{dua}}`

       ``Rho`` : Penalty parameter

       ``XSlvRelRes`` : Not Implemented (relative residual of X step solver)

       ``Time`` : Cumulative run time
    """
    class Options(GenericConvBPDN.Options):
        r"""ParConvBPDN algorithm options

        Options include all of those defined in
        :class:`.admm.ADMMEqual.Options`, together with additional options:

          ``alpha`` : A float indicating the relative weight between
          the constraint :math:`D_{G_l} \mathbf{x} = \mathbf{y}_{0,l}`
          and :math:`\alpha \mathbf{x} = \mathbf{y}_1`. None value
          effectively defaults to no weight or :math:`\alpha = 1`.

          ``Y0`` : Initial value for :math:`\mathbf{y}_0`.

          ``U0`` : Initial value for :math:`\mathbf{u}_0`.

          ``Y1`` : Initial value for :math:`\mathbf{y}_1`.

          ``U1`` : Initial value for :math:`\mathbf{u}_1`.


        and the exceptions:

          ``AutoRho`` : Not implemented.

          ``LinSolveCheck`` : Not implemented.

        """
        defaults = copy.deepcopy(GenericConvBPDN.Options.defaults)
        defaults.update({
            'L1Weight': 1.0,
            'alpha': None,
            'Y1': None,
            'U1': None
        })

        def __init__(self, opt=None):
            """
            Parameters
            ----------
            opt : dict or None, optional (default None)
               ParConvBPDN algorithm options
            """

            if opt is None:
                opt = {}
            GenericConvBPDN.Options.__init__(self, opt)

    itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1')
    hdrtxt_objfn = ('Fnc', 'DFid', u('Regl1'))
    hdrval_objfun = {'Fnc': 'ObjFun', 'DFid': 'DFid', u('Regl1'): 'RegL1'}

    def __init__(self,
                 D,
                 S,
                 lmbda=None,
                 W=None,
                 opt=None,
                 nproc=None,
                 ngrp=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:`ParConvBPDN.Options` object
          Algorithm options
        nproc : int
          Number of processes
        ngrp : int
          Number of groups in partition of filter indices
        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
        """

        self.pool = None

        # Set default options if none specified
        if opt is None:
            opt = ParConvBPDN.Options()

        # 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 = cr.CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN)
            Df = sl.rfftn(D.reshape(cri.shpD), cri.Nv, axes=cri.axisN)
            Sf = sl.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)

        # Set penalty parameter
        self.set_attr('rho',
                      opt['rho'],
                      dval=(50.0 * self.lmbda + 1.0),
                      dtype=self.dtype)
        self.set_attr('alpha', opt['alpha'], dval=1.0, dtype=self.dtype)

        # Set rho_xi attribute (see Sec. VI.C of wohlberg-2015-adaptive)
        # if self.lmbda != 0.0:
        #     rho_xi = (1.0 + (18.3)**(np.log10(self.lmbda) + 1.0))
        # else:
        #     rho_xi = 1.0
        # self.set_attr('rho_xi', opt['AutoRho', 'RsdlTarget'], dval=rho_xi,
        #               dtype=self.dtype)

        # Call parent class __init__
        super(ParConvBPDN, self).__init__(D, S, opt, dimK, dimN)

        if nproc is None:
            if ngrp is None:
                self.nproc = min(mp.cpu_count(), self.cri.M)
                self.ngrp = self.nproc
            else:
                self.nproc = min(mp.cpu_count(), ngrp, self.cri.M)
                self.ngrp = ngrp
        else:
            if ngrp is None:
                self.ngrp = nproc
                self.nproc = nproc
            else:
                self.ngrp = ngrp
                self.nproc = nproc

        if W is None:
            W = np.array([1.0], dtype=self.dtype)
        self.W = np.asarray(W.reshape(cr.mskWshape(W, self.cri)),
                            dtype=self.dtype)
        self.wl1 = np.asarray(opt['L1Weight'], dtype=self.dtype)
        self.wl1 = self.wl1.reshape(cr.l1Wshape(self.wl1, self.cri))

        self.xrrs = None

        # Initialise global variables
        # Conv Rep Indexing and parameter values for multiprocessing
        global mp_nproc
        mp_nproc = self.nproc
        global mp_ngrp
        mp_ngrp = self.ngrp
        global mp_Nv
        mp_Nv = self.cri.Nv
        global mp_axisN
        mp_axisN = tuple(i + 1 for i in self.cri.axisN)
        global mp_C
        mp_C = self.cri.C
        global mp_Cd
        mp_Cd = self.cri.Cd
        global mp_axisC
        mp_axisC = self.cri.axisC + 1
        global mp_axisM
        mp_axisM = 0
        global mp_NonNegCoef
        mp_NonNegCoef = self.opt['NonNegCoef']
        global mp_NoBndryCross
        mp_NoBndryCross = self.opt['NoBndryCross']
        global mp_Dshp
        mp_Dshp = self.D.shape

        # Parameters for optimization
        global mp_lmbda
        mp_lmbda = self.lmbda
        global mp_rho
        mp_rho = self.rho
        global mp_alpha
        mp_alpha = self.alpha
        global mp_rlx
        mp_rlx = self.rlx
        global mp_wl1
        init_mpraw('mp_wl1', np.moveaxis(self.wl1, self.cri.axisM, mp_axisM))

        # Matrices used in optimization
        global mp_S
        init_mpraw('mp_S',
                   np.moveaxis(self.S * self.W**2, self.cri.axisM, mp_axisM))
        global mp_Df
        init_mpraw('mp_Df', np.moveaxis(self.Df, self.cri.axisM, mp_axisM))
        global mp_X
        init_mpraw('mp_X', np.moveaxis(self.Y, self.cri.axisM, mp_axisM))
        shp_X = list(mp_X.shape)
        global mp_Xnr
        mp_Xnr = mpraw_as_np(mp_X.shape, mp_X.dtype)
        global mp_Y0
        shp_Y0 = shp_X[:]
        shp_Y0[0] = self.ngrp
        shp_Y0[mp_axisC] = mp_C
        if self.opt['Y0'] is not None:
            init_mpraw(
                'Y0',
                np.moveaxis(self.opt['Y0'].astype(self.dtype, copy=True),
                            self.cri.axisM, mp_axisM))
        else:
            mp_Y0 = mpraw_as_np(shp_Y0, mp_X.dtype)
        global mp_Y0old
        mp_Y0old = mpraw_as_np(shp_Y0, mp_X.dtype)
        global mp_Y1
        if self.opt['Y1'] is not None:
            init_mpraw(
                'Y1',
                np.moveaxis(self.opt['Y1'].astype(self.dtype, copy=True),
                            self.cri.axisM, mp_axisM))
        else:
            mp_Y1 = mpraw_as_np(shp_X, mp_X.dtype)
        global mp_Y1old
        mp_Y1old = mpraw_as_np(shp_X, mp_X.dtype)
        global mp_U0
        if self.opt['U0'] is not None:
            init_mpraw(
                'U0',
                np.moveaxis(self.opt['U0'].astype(self.dtype, copy=True),
                            self.cri.axisM, mp_axisM))
        else:
            mp_U0 = mpraw_as_np(shp_Y0, mp_X.dtype)
        global mp_U1
        if self.opt['U1'] is not None:
            init_mpraw(
                'U1',
                np.moveaxis(self.opt['U1'].astype(self.dtype, copy=True),
                            self.cri.axisM, mp_axisM))
        else:
            mp_U1 = mpraw_as_np(shp_X, mp_X.dtype)
        global mp_DX
        mp_DX = mpraw_as_np(shp_Y0, mp_X.dtype)
        global mp_DXnr
        mp_DXnr = mpraw_as_np(shp_Y0, mp_X.dtype)

        # Variables used to solve the optimization efficiently
        global mp_inv_off_diag
        if self.W.ndim is self.cri.axisM + 1:
            init_mpraw(
                'mp_inv_off_diag',
                np.moveaxis(
                    -self.W**2 / (mp_rho * (mp_rho + self.W**2 * mp_ngrp)),
                    self.cri.axisM, mp_axisM))
        else:
            init_mpraw('mp_inv_off_diag',
                       -self.W**2 / (mp_rho * (mp_rho + self.W**2 * mp_ngrp)))
        global mp_grp
        mp_grp = [
            np.min(i)
            for i in np.array_split(np.array(range(self.cri.M)), mp_ngrp)
        ] + [
            self.cri.M,
        ]
        global mp_cache
        if self.opt['HighMemSolve'] and self.cri.Cd == 1:
            mp_cache = [
                sl.solvedbi_sm_c(mp_Df[k], np.conj(mp_Df[k]), mp_alpha**2,
                                 mp_axisM)
                for k in np.array_split(np.array(range(self.cri.M)), self.ngrp)
            ]
        else:
            mp_cache = [None for k in mp_grp]
        global mp_b
        shp_b = shp_Y0[:]
        shp_b[0] = 1
        mp_b = mpraw_as_np(shp_b, mp_X.dtype)

        # Residual and stopping criteria variables
        global mp_ry0
        mp_ry0 = mpraw_as_np((self.ngrp, ), mp_X.dtype)
        global mp_ry1
        mp_ry1 = mpraw_as_np((self.ngrp, ), mp_X.dtype)
        global mp_sy0
        mp_sy0 = mpraw_as_np((self.ngrp, ), mp_X.dtype)
        global mp_sy1
        mp_sy1 = mpraw_as_np((self.ngrp, ), mp_X.dtype)
        global mp_nrmAx
        mp_nrmAx = mpraw_as_np((self.ngrp, ), mp_X.dtype)
        global mp_nrmBy
        mp_nrmBy = mpraw_as_np((self.ngrp, ), mp_X.dtype)
        global mp_nrmu
        mp_nrmu = mpraw_as_np((self.ngrp, ), mp_X.dtype)

    def solve(self):
        """Start (or re-start) optimisation. This method implements the
        framework for the iterations of an ADMM algorithm.

        If option ``Verbose`` is ``True``, the progress of the
        optimisation is displayed at every iteration. At termination
        of this method, attribute :attr:`itstat` is a list of tuples
        representing statistics of each iteration, unless option
        ``FastSolve`` is ``True`` and option ``Verbose`` is ``False``.

        Attribute :attr:`timer` is an instance of :class:`.util.Timer`
        that provides the following labelled timers:

          ``init``: Time taken for object initialisation by
          :meth:`__init__`

          ``solve``: Total time taken by call(s) to :meth:`solve`

          ``solve_wo_func``: Total time taken by call(s) to
          :meth:`solve`, excluding time taken to compute functional
          value and related iteration statistics

          ``solve_wo_rsdl`` : Total time taken by call(s) to
          :meth:`solve`, excluding time taken to compute functional
          value and related iteration statistics as well as time take
          to compute residuals and implemented ``AutoRho`` mechanism
        """

        global mp_Y0old
        global mp_Y1old

        self.init_pool()

        fmtstr, nsep = self.display_start()

        # Start solve timer
        self.timer.start(['solve', 'solve_wo_func', 'solve_wo_rsdl'])

        first_iteration = self.k
        last_iteration = self.k + self.opt['MaxMainIter'] - 1
        # Main optimisation iterations
        for self.k in range(self.k, self.k + self.opt['MaxMainIter']):
            mp_Y0old[:] = np.copy(mp_Y0)
            mp_Y1old[:] = np.copy(mp_Y1)

            # Perform the variable updates.
            if self.k is first_iteration:
                self.distribute(par_initial_stepgrp, mp_ngrp)
            y0astep()
            if self.k is last_iteration:
                self.distribute(par_final_stepgrp, mp_ngrp)
            else:
                self.distribute(par_stepgrp, mp_ngrp)

            # Compute the residual variables
            self.timer.stop('solve_wo_rsdl')
            if self.opt['AutoRho', 'Enabled'] or not self.opt['FastSolve']:
                self.distribute(par_compute_residuals, mp_ngrp)
                r = np.sqrt(np.sum(mp_ry0) + np.sum(mp_ry1))
                s = np.sqrt(np.sum(mp_sy0) + np.sum(mp_sy1))

                epri = np.sqrt(self.Nc) * self.opt['AbsStopTol'] + \
                  np.max([np.sqrt(np.sum(mp_nrmAx)),
                          np.sqrt(np.sum(mp_nrmBy))]) * self.opt['RelStopTol']

                edua = np.sqrt(self.Nx) * self.opt['AbsStopTol'] + \
                  np.sqrt(np.sum(mp_nrmu)) * self.opt['RelStopTol']

            # Compute and record other iteration statistics and
            # display iteration stats if Verbose option enabled
            self.timer.stop(['solve_wo_func', 'solve_wo_rsdl'])
            if not self.opt['FastSolve']:
                itst = self.iteration_stats(self.k, r, s, epri, edua)
                self.itstat.append(itst)
                self.display_status(fmtstr, itst)
            self.timer.start(['solve_wo_func', 'solve_wo_rsdl'])

            # Automatic rho adjustment
            # self.timer.stop('solve_wo_rsdl')
            # if self.opt['AutoRho', 'Enabled'] or not self.opt['FastSolve']:
            #     self.update_rho(self.k, r, s)
            # self.timer.start('solve_wo_rsdl')

            # Call callback function if defined
            if self.opt['Callback'] is not None:
                if self.opt['Callback'](self):
                    break

            # Stop if residual-based stopping tolerances reached
            if self.opt['AutoRho', 'Enabled'] or not self.opt['FastSolve']:
                if r < epri and s < edua:
                    break

        # Increment iteration count
        self.k += 1

        # Record solve time
        self.timer.stop(['solve', 'solve_wo_func', 'solve_wo_rsdl'])

        # Print final separator string if Verbose option enabled
        self.display_end(nsep)

        self.Y = np.moveaxis(mp_Y1, mp_axisM, self.cri.axisM)
        self.X = np.moveaxis(mp_X, mp_axisM, self.cri.axisM)

        self.terminate_pool()

        return self.getmin()

    def init_pool(self):
        """Initialize multiprocessing pool if necessary."""

        # initialize the pool if needed
        if self.pool is None:
            if self.nproc > 1:
                self.pool = mp.Pool(processes=self.nproc)
            else:
                self.pool = None
        else:
            print('pool already initialized?')

    def distribute(self, f, n):
        """Distribute the computations amongst the multiprocessing pools

        Parameters
        ----------
        f : function
          Function to be distributed to the processors
        n : int
          The values in range(0,n) will be passed as arguments to the
          function f.
        """

        if self.pool is None:
            return [f(i) for i in range(n)]
        else:
            return self.pool.map(f, range(n))

    def terminate_pool(self):
        """Terminate and close the multiprocessing pool if necessary."""

        if self.pool is not None:
            self.pool.terminate()
            self.pool.join()
            del (self.pool)
            self.pool = None

    def obfn_gvar(self):
        """Variable to be evaluated in computing :meth:`ADMM.obfn_g`,
        depending on the ``gEvalY`` option value.
        """

        return mp_Y1 if self.opt['gEvalY'] else mp_X

    def obfn_fvar(self):
        """Variable to be evaluated in computing :meth:`ADMM.obfn_f`,
        depending on the ``fEvalX`` option value.
        """
        return mp_X if self.opt['fEvalX'] else mp_Y1

    def obfn_reg(self):
        r"""Compute regularisation term, :math:`\| x \|_1`, and
        contribution to objective function.
        """
        l1 = np.sum(mp_wl1 * np.abs(self.obfn_gvar()))
        return (self.lmbda * l1, l1)

    def obfn_dfd(self):
        r"""Compute data fidelity term :math:`(1/2) \| W \left( \sum_m
        \mathbf{d}_m * \mathbf{x}_m - \mathbf{s} \right) \|_2^2`.
        """
        XF = sl.rfftn(self.obfn_fvar(), mp_Nv, mp_axisN)
        DX = np.moveaxis(
            sl.irfftn(sl.inner(mp_Df, XF, mp_axisM), mp_Nv, mp_axisN),
            mp_axisM, self.cri.axisM)
        return np.sum((self.W * (DX - self.S))**2) / 2.0
예제 #14
0
class BPDN(fista.FISTA):
    r"""
    Base class for FISTA algorithm for the Basis Pursuit DeNoising (BPDN)
    :cite:`chen-1998-atomic` problem.

    |

    .. inheritance-diagram:: BPDN
       :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) \| D \mathbf{x} - \mathbf{s} \|_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:`D`, and coefficient maps :math:`\mathbf{x}`. 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) \| D
       \mathbf{x} - \mathbf{s} \|_2^2`

       ``RegL1`` : Value of regularisation term :math:`\lambda \|
       \mathbf{x} \|_1`

       ``Rsdl`` : Residual

       ``L`` : Inverse of gradient step parameter

       ``Time`` : Cumulative run time
    """
    class Options(fista.FISTA.Options):
        r"""BPDN algorithm options

        Options include all of those defined in
        :class:`.fista.FISTA.Options`, together with
        additional options:

          ``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
          \| \mathbf{w}_m \odot \mathbf{x} \|_1` where
          :math:`\mathbf{w}` denotes slices of the weighting array.

        """

        defaults = copy.deepcopy(fista.FISTADFT.Options.defaults)
        defaults.update({'L1Weight': 1.0})
        defaults.update({'L': 500.0})

        def __init__(self, opt=None):
            """
            Parameters
            ----------
            opt : dict or None, optional (default None)
              BPDN algorithm options
            """

            if opt is None:
                opt = {}
            fista.FISTA.Options.__init__(self, opt)

        def __setitem__(self, key, value):
            """Set options."""

            fista.FISTA.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):
        """
        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:`BPDN.Options` object
          Algorithm options
        """

        # Set default options if none specified
        if opt is None:
            opt = BPDN.Options()

        # 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:
            DTS = D.T.dot(S)
            lmbda = 0.1 * abs(DTS).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__
        Nc = D.shape[1]
        Nm = S.shape[1]

        xshape = (Nc, Nm)
        super(BPDN, self).__init__(xshape, S.dtype, opt)

        self.S = np.asarray(S, dtype=self.dtype)

        self.store_prev()
        self.Y = self.X.copy()
        self.Yprv = self.Y.copy() + 1e5

        self.setdict(D)

    def setdict(self, D):
        """Set dictionary array."""

        self.D = np.asarray(D, dtype=self.dtype)

    def getcoef(self):
        """Get final coefficient array."""

        return self.X

    def eval_grad(self):
        """Compute gradient in spatial domain for variable Y."""

        # Compute D^T(D Y - S)
        return self.D.T.dot(self.D.dot(self.Y) - self.S)

    def eval_proxop(self, V):
        """Compute proximal operator of :math:`g`."""

        return np.asarray(sl.shrink1(V, (self.lmbda / self.L) * self.wl1),
                          dtype=self.dtype)

    def rsdl(self):
        """Compute fixed point residual."""

        return np.linalg.norm((self.X - self.Yprv).ravel())

    def eval_objfn(self):
        """Compute components of objective function as well as total
        contribution to objective function.
        """

        dfd = self.obfn_f()
        reg = self.obfn_reg()
        obj = dfd + reg[0]
        return (obj, dfd) + reg[1:]

    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)

    def obfn_f(self, X=None):
        r"""Compute data fidelity term :math:`(1/2) \| D \mathbf{x} -
        \mathbf{s} \|_2^2`.
        """
        if X is None:
            X = self.X

        return 0.5 * np.linalg.norm((self.D.dot(X) - self.S).ravel())**2

    def reconstruct(self, X=None):
        """Reconstruct representation."""

        if X is None:
            X = self.X
        return self.D.dot(self.X)
예제 #15
0
    def __init__(self, D0, S, lmbda=None, opt=None, dimK=1, dimN=2):
        """
        Initialise a ConvBPDNDictLearn object with problem size and options.

        Parameters
        ----------
        D0 : array_like
          Initial dictionary array
        S : array_like
          Signal array
        lmbda : float
          Regularisation parameter
        opt : :class:`ConvBPDNDictLearn.Options` object
          Algorithm options
        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 = ConvBPDNDictLearn.Options()
        self.opt = opt

        # Get dictionary size
        if self.opt['DictSize'] is None:
            dsz = D0.shape
        else:
            dsz = self.opt['DictSize']

        # Construct object representing problem dimensions
        cri = ccmod.ConvRepIndexing(dsz, S, dimK, dimN)

        # Normalise dictionary
        D0 = ccmod.getPcn0(opt['CCMOD', 'ZeroMean'], dsz, dimN,
                           dimC=cri.dimCd)(D0)

        # Modify D update options to include initial values for Y and U
        opt['CCMOD'].update({'Y0' : ccmod.zpad(
            ccmod.stdformD(D0, cri.C, cri.M, dimN), cri.Nv),
                             'U0' : np.zeros(cri.shpD)})

        # Create X update object
        xstep = cbpdn.ConvBPDN(D0, S, lmbda, opt['CBPDN'], dimK=dimK,
                               dimN=dimN)

        # Create D update object
        dstep = ccmod.ConvCnstrMOD(None, S, dsz, opt['CCMOD'], dimK=dimK,
                                    dimN=dimN)

        # Configure iteration statistics reporting
        if self.opt['AccurateDFid']:
            isxmap = {'XPrRsdl' : 'PrimalRsdl', 'XDlRsdl' : 'DualRsdl',
                      'XRho' : 'Rho'}
            evlmap = {'ObjFun' : 'ObjFun', 'DFid' : 'DFid', 'RegL1' : 'RegL1'}
        else:
            isxmap = {'ObjFun' : 'ObjFun', 'DFid' : 'DFid', 'RegL1' : 'RegL1',
                      'XPrRsdl' : 'PrimalRsdl', 'XDlRsdl' : 'DualRsdl',
                      'XRho' : 'Rho'}
            evlmap = {}
        isc = dictlrn.IterStatsConfig(
            isfld=['Iter', 'ObjFun', 'DFid', 'RegL1', 'Cnstr', 'XPrRsdl',
                   'XDlRsdl', 'XRho', 'DPrRsdl', 'DDlRsdl', 'DRho', 'Time'],
            isxmap=isxmap,
            isdmap={'Cnstr' :  'Cnstr', 'DPrRsdl' : 'PrimalRsdl',
                    'DDlRsdl' : 'DualRsdl', 'DRho' : 'Rho'},
            evlmap=evlmap,
            hdrtxt=['Itn', 'Fnc', 'DFid', u('ℓ1'), 'Cnstr', 'r_X', 's_X',
                    u('ρ_X'), 'r_D', 's_D', u('ρ_D')],
            hdrmap={'Itn' : 'Iter', 'Fnc' : 'ObjFun', 'DFid' : 'DFid',
                    u('ℓ1') : 'RegL1', 'Cnstr' : 'Cnstr', 'r_X' : 'XPrRsdl',
                    's_X' : 'XDlRsdl', u('ρ_X') : 'XRho', 'r_D' : 'DPrRsdl',
                    's_D' : 'DDlRsdl', u('ρ_D') : 'DRho'}
            )

        # Call parent constructor
        super(ConvBPDNDictLearn, self).__init__(xstep, dstep, opt, isc)
예제 #16
0
class ConvBPDNSliceTwoBlockCnstrnt(admm.ADMMTwoBlockCnstrnt):

    class Options(admm.ADMMTwoBlockCnstrnt.Options):
        defaults = copy.deepcopy(admm.ADMMTwoBlockCnstrnt.Options.defaults)
        defaults.update({
            'RelaxParam': 1.8,
            'Gamma': 1.,
            'AuxVarObj': False,
            'Boundary': 'circulant_back',
            'Callback': _iter_recorder,
        })
        defaults['AutoRho'].update({
            'Enabled': True,
            'AutoScaling': False,
            'Period': 1,
            'Scaling': 2.,  # tau
            'RsdlRatio': 10.,  # mu
            'RsdlTarget': 1.,  # xi, initial value depends on lambda
        })

        def __init__(self, opt=None):
            if opt is None:
                opt = {}
            super().__init__(opt)

    # Although we split the variables in a different way, we record
    # the same objection function for comparison
    # follows exactly as cbpdn.ConvBPDN for actual comparison
    itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1')
    hdrtxt_objfn = ('Fnc', 'DFid', u('Regℓ1'), 'XStepAvg', 'YStepAvg')
    hdrval_objfun = {'Fnc': 'ObjFun', 'DFid': 'DFid', u('Regℓ1'): 'RegL1',
                     'XStepAvg': 'XStepTime', 'YStepAvg': 'YStepTime'}
    # timer for xstep/ystep
    # NOTE: You can't use Timer to measure each tic time.  Record average
    # time instead.
    itstat_fields_extra = ('XStepTime', 'YStepTime')

    def __init__(self, D, S, lmbda=None, opt=None, dimK=None, dimN=2):
        if opt is None:
            opt = ConvBPDNSliceTwoBlockCnstrnt.Options()
        # Set dtype attribute based on S.dtype and opt['DataType']
        self.set_dtype(opt, S.dtype)
        if not hasattr(self, 'cri'):
            self.cri = cr.CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN)
        self.lmbda = self.dtype.type(lmbda)
        # Set penalty parameter if not set
        self.set_attr('rho', opt['rho'], dval=(50.0*self.lmbda + 1.0),
                      dtype=self.dtype)
        # Set xi if not set
        self.set_attr('tau_xi', opt['AutoRho', 'RsdlTarget'],
                      dval=(1.0+18.3**(np.log10(self.lmbda)+1.0)),
                      dtype=self.dtype)
        # set boundary condition
        self.set_attr('boundary', opt['Boundary'], dval='circulant_back',
                      dtype=None)
        # set weight factor between two constraints
        self.set_attr('gamma', opt['Gamma'], dval=1., dtype=self.dtype)
        self.setdict(D)
        # Number of elements of sparse representation x is invariant to
        # slice/FFT solvers.
        Nx = np.product(self.cri.shpX)
        # Externally the input signal should have a data layout as
        # S(N0, N1, ..., C, K).
        # First convert to common pytorch Variable layout.
        # [H, W, C, K, 1] -> [K, C, H, W]
        self.S = np.asarray(S.reshape(self.cri.shpS), dtype=S.dtype)
        self.S = self.S.squeeze(-1).transpose((3, 2, 0, 1))
        # [K, n, N]
        self.S_slice = self.im2slices(self.S)
        yshape = (self.S_slice.shape[0], self.D.shape[0] + self.D.shape[1],
                  self.S_slice.shape[-1])
        super().__init__(Nx, yshape, 1, self.D.shape[0], S.dtype, opt)
        self.X = np.zeros_like(self._Y1, dtype=self.dtype)
        self.extra_timer = su.Timer(['xstep', 'ystep'])

    def setdict(self, D):
        """Set dictionary properly."""
        if D.ndim == 2:  # [patch_size, num_atoms]
            self.D = D.copy()
        elif D.ndim == 3:  # [patch_h, patch_w, num_atoms]
            self.D = D.reshape((-1, D.shape[-1]))
        elif D.ndim == 4:  # [patch_h, patch_w, channels, num_atoms]
            assert D.shape[-2] == 1 or D.shape[-2] == 3
            self.D = D.transpose(2, 0, 1, 3)
            self.D = self.D.reshape((-1, self.D.shape[-1]))
        else:
            raise ValueError('Invalid dict D dimension of {}'.format(D.shape))
        self.lu, self.piv = sl.lu_factor(self.D, self.gamma ** 2)
        self.lu = np.asarray(self.lu, dtype=self.dtype)

    def getcoef(self):
        """Returns coefficients and signals for solving

        .. math::
            \min_{D} (1/2)\|D x_i - y_i + u_i\|_2^2, D\in C.
        """
        return (self.X, self._Y0-self._U0)

    def im2slices(self, S):
        r"""Convert the input signal :math:`S` to a slice form.
        Assuming the input signal having a standard shape as pytorch variable
        (N, C, H, W).  The output slices have shape
        (batch_size, slice_dim, num_slices_per_batch).
        """
        kernel_size = self.cri.shpD[:2]
        pad_h, pad_w = kernel_size[0] - 1, kernel_size[1] - 1
        S_torch = globals()['_pad_{}'.format(self.boundary)](
            S, pad_h, pad_w
        )
        with torch.no_grad():
            S_torch = torch.from_numpy(S_torch)
            slices = F.unfold(S_torch, kernel_size=kernel_size)
        return slices.numpy()

    def slices2im(self, slices):
        r"""Reconstruct input signal :math:`\hat{S}` for slices.
        The input slices should have compatible size of
        (batch_size, slice_dim, num_slices_per_batch), and the
        returned signal has shape (N, C, H, W) as standard pytorch variable.
        """
        kernel_size = self.cri.shpD[:2]
        pad_h, pad_w = kernel_size[0] - 1, kernel_size[1] - 1
        output_h, output_w = self.cri.shpS[:2]
        with torch.no_grad():
            slices_torch = torch.from_numpy(slices)
            S_recon = F.fold(
                slices_torch, (output_h+pad_h, output_w+pad_w), kernel_size
            )
        S_recon = globals()['_crop_{}'.format(self.boundary)](
            S_recon.numpy(), pad_h, pad_w
        )
        return S_recon

    def yinit(self, yshape):
        """Initialization for variable Y."""
        Y = super().yinit(yshape)
        self._Y1 = self.block_sep1(Y)
        n = np.prod(self.cri.shpD[:2])
        self._Y0 = self.S_slice / n
        return self.block_cat(self._Y0, self._Y1)

    def uinit(self, ushape):
        """Initialization for variable U."""
        U = super().uinit(ushape)
        self._U0, self._U1 = self.block_sep(U)
        return U

    def xstep(self):
        self.extra_timer.start('xstep')
        rhs = self.cnst_A0T(self._Y0 - self._U0) + \
            self.gamma * (self.gamma * self._Y1 - self._U1)
        # NOTE:
        # Here we solve the sparse code X for each batch index i, since X is
        # organized as [batch_size, num_atoms, num_signals].  This interface
        # may be modified for online setting.
        for i in range(self.X.shape[0]):
            self.X[i] = np.asarray(
                sl.lu_solve_ATAI(self.D, self.gamma**2, rhs[i], self.lu, self.piv),
                dtype=self.dtype
            )
        self.extra_timer.stop('xstep')

    def relax_AX(self):
        self._AX0nr, self._AX1nr = self.cnst_A0(self.X), self.cnst_A1(self.X)
        if self.rlx == 1.0:
            self._AX0, self._AX1 = self._AX0nr, self._AX1nr
        else:
            # c0 and c1 are all zero
            alpha = self.rlx
            self._AX0 = alpha*self._AX0nr + (1-alpha)*self._Y0
            self._AX1 = alpha*self._AX1nr + (1-alpha)*self.gamma*self._Y1
        self.AXnr = self.block_cat(self._AX0nr, self._AX1nr)
        self.AX = self.block_cat(self._AX0, self._AX1)

    def ystep(self):
        self.extra_timer.start('ystep')
        self.y0step()
        self.y1step()
        self.Y = self.block_cat(self._Y0, self._Y1)
        self.extra_timer.stop('ystep')

    def y0step(self):
        p = self.S_slice / self.rho + self._AX0 + self._U0
        recon = self.slices2im(p)
        # n should be the dict size for each channel
        n = np.prod(self.cri.shpD[:2])
        self._Y0 = p - self.im2slices(recon) / (n + self.rho)

    def y1step(self):
        self._Y1 = sl.shrink1((self._AX1 + self._U1) / self.gamma,
                              self.lmbda/self.rho/self.gamma/self.gamma)

    def ustep(self):
        self._U0 += self._AX0 - self._Y0
        self._U1 += self._AX1 - self.gamma * self._Y1
        self.U = self.block_cat(self._U0, self._U1)

    def rsdl_r(self, AX, Y):
        return AX + self.cnst_B(Y)

    def rsdl_s(self, Yprev, Y):
        """Compute dual residual vector."""
        # TODO(leoyolo): figure out why this is valid.
        return self.rho*linalg.norm(self.cnst_AT(self.U))

    def rsdl_sn(self, U):
        """Compute dual residual normalisation term."""
        # TODO(leoyolo): figure out why this is valid.
        return self.rho*linalg.norm(U)

    def cnst_A0(self, X):
        return np.matmul(self.D, X)

    def cnst_A1(self, X):
        return self.gamma * X

    def cnst_A0T(self, Y0):
        return np.matmul(self.D.transpose(), Y0)

    def cnst_A1T(self, Y1):
        return self.gamma * Y1

    def cnst_B(self, Y):
        Y0, Y1 = self.block_sep(Y)
        return -self.block_cat(Y0, self.gamma * Y1)

    def var_y0(self):
        return self._Y0

    def var_y1(self):
        return self._Y1

    def getmin(self):
        """Reimplement getmin func to have unified output layout."""
        # [K, m, N] -> [N, K, m] -> [H, W, 1, K, m]
        minimizer = super().getmin()
        minimizer = minimizer.transpose((2, 0, 1))
        minimizer = minimizer.reshape(self.cri.shpX)
        return minimizer

    def eval_objfn(self):
        """Overwrite this function as in ConvBPDN."""
        dfd = self.obfn_dfd()
        reg = self.obfn_reg()
        obj = dfd + reg[0]
        return (obj, dfd) + reg[1:]

    def obfn_dfd(self):
        r"""Data fidelity term of the objective :math:`(1/2) \|s - \sum_i
        \mathbf{R}_i^T y_i\|_2^2`.
        """
        recon = self.slices2im(self.cnst_A0(self.X))
        return ((recon - self.S) ** 2).sum() / 2.0

    def obfn_reg(self):
        r"""Regularization term of the objective :math:`g(y)=\|y\|_1`.
        Returns a tuple where the first is the scaled combination of all
        regularization terms (if exist) and the sesequent ones are each term.
        """
        l1 = linalg.norm(self.X.ravel(), 1)
        return (self.lmbda * l1, l1)

    def reconstruct(self, X=None):
        """Reconstruct representation.  The reconstruction follows standard
        output layout."""
        if X is None:
            X = self.X
        else:
            # X has standard shape of (N0, N1, ..., 1, K, M) since we use
            # single channel coefficient array, first convert to
            # (num_atoms, batch_size, ...) and then to
            # (slice_dim, batch_size, num_slices_per_batch) by multiplying D.
            # [ H, W, 1, K, m ] -> [K, m, H, W, 1] -> [K, m, N]
            X = X.transpose((3, 4, 0, 1, 2))
            X = X.reshape(X.shape[0], X.shape[1], -1)
        recon = self.slices2im(np.matmul(self.D, X))
        # [K, C, H, W] -> [H, W, C, K, 1]
        recon = np.expand_dims(recon.transpose((2, 3, 1, 0)), axis=-1)
        return recon

    def update_rho(self, k, r, s):
        """Back to usual way of updating rho."""
        if self.opt['AutoRho', 'AutoScaling']:
            # If AutoScaling is enabled, use adaptive penalty parameters by
            # residual balancing as commonly used in SPORCO.
            super().update_rho(k, r, s)
        else:
            tau = self.rho_tau
            mu = self.rho_mu
            if k != 0 and ((k+1) % self.opt['AutoRho', 'Period'] == 0):
                if r > mu * s:
                    self.rho = tau * self.rho
                elif s > mu * r:
                    self.rho = self.rho / tau

    def itstat_extra(self):
        """Get xstep/ystep solve time. Return average time."""
        niters = self.k + 1
        xstep_elapsed = self.extra_timer.elapsed('xstep')
        ystep_elapsed = self.extra_timer.elapsed('ystep')
        return (xstep_elapsed/niters, ystep_elapsed/niters)
예제 #17
0
class ConvBPDNSlice(admm.ADMM):
    r"""Slice-based convolutional sparse coding solver using ADMM.
    This method is detailed in [1]. In specific, it solves the CSC problem
    in the following form:

    .. math::
        \min_{x_i,y_i} \frac{1}{2}\|s-\sum_i\mathbf{R}_i^T y_i\|_2^2
        + \lambda\sum_i \|x_i\|_1 \;\mathrm{suth\;that}\;
        y_i = D_l x_i\;\forall i.

    If we let :math:`g(y)=\frac{1}{2}\|s-\sum_i\mathbf{R}_i^Ty_i\|_2^2`,
    :math:`f(x)=\lambda\sum_i\|x_i\|_1`, then the objective can be
    updated using ADMM.

    [1] V. Papyan, Y. Romano, J. Sulam, and M. Elad, “Convolutional Dictionary
        Learning via Local Processing,” arXiv:1705.03239 [cs], May 2017.
    """

    class Options(admm.ADMM.Options):
        """Slice-based convolutional sparse coding options.
        Options include all fields of :class:`admm.cbpdn.ConvBPDN`,
        with `BPDN` from :class:`admm.bpdn.BPDN`.
        """
        defaults = copy.deepcopy(admm.ADMM.Options.defaults)
        defaults.update({
            'BPDN': copy.deepcopy(bpdn.BPDN.Options.defaults),
            'RelaxParam': 1.8,
            'Boundary': 'circulant_back',
        })
        defaults['BPDN'].update({
            'MaxMainIter': 1000,
            'Verbose': False,
        })
        defaults['AutoRho'].update({
            'Enabled': True,
            'AutoScaling': False,
            'Period': 1,
            'Scaling': 2.,  # tau
            'RsdlRatio': 10.,  # mu
            'RsdlTarget': 1.,  # xi, initial value depends on lambda
        })

        def __init__(self, opt=None):
            super().__init__({'BPDN': bpdn.BPDN.Options()})
            if opt is None:
                opt = {}
            self.update(opt)

    # follows exactly as cbpdn.ConvBPDN for actual comparison
    itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1')
    hdrtxt_objfn = ('Fnc', 'DFid', u('Regℓ1'), 'XStepAvg', 'YStepAvg')
    hdrval_objfun = {'Fnc': 'ObjFun', 'DFid': 'DFid', u('Regℓ1'): 'RegL1',
                     'XStepAvg': 'XStepTime', 'YStepAvg': 'YStepTime'}
    # timer for xstep/ystep
    # NOTE: You can't use Timer to measure each tic time.  Record average
    # time instead.
    itstat_fields_extra = ('XStepTime', 'YStepTime')

    def __init__(self, D, S, lmbda=None, opt=None, dimK=None, dimN=2):
        r"""We use the same layout as ConvBPDN as input and output, but for
        internal computation we use a differnt layout.

        Internal Parameters
        -------------------
        X: [K, m, N]
          Convolutional representation of the input signal. m is the size
          of atom in a dictionary, K is the batch size of input signals,
          and N is the number of slices extracted from each signal (usually
          number of pixels in an image).
        Y: [K, n, N]
          Splitted variable with contraint :math:`D_l x_i - y_i = 0`.
          n represents the size of each slice.
        U: [K, n, N]
          Dual variable with the same size as Y.
        """
        if opt is None:
            opt = ConvBPDNSlice.Options()
        # Set dtype attribute based on S.dtype and opt['DataType']
        self.set_dtype(opt, S.dtype)
        if not hasattr(self, 'cri'):
            self.cri = cr.CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN)
        self.boundary = opt['Boundary']
        # Number of elements of sparse representation x is invariant to
        # slice/FFT solvers.
        Nx = np.product(self.cri.shpX)
        # NOTE: To incorporate with im2slices/slices2im, where each slice
        # is organized as in_channels x patch_size x patch_size, the dictionary
        # is first transposed to [in_channels, patch_size, patch_size, out_channels],
        # and then reshape to 2-D (N, M).
        self.setdict(D)
        # Externally the input signal should have a data layout as
        # S(N0, N1, ..., C, K).
        # First convert to common pytorch Variable layout.
        # [H, W, C, K, 1] -> [K, C, H, W]
        self.S = np.asarray(S.reshape(self.cri.shpS), dtype=S.dtype)
        self.S = self.S.squeeze(-1).transpose((3, 2, 0, 1))
        # [K, n, N]
        self.S_slice = self.im2slices(self.S)
        self.lmbda = lmbda
        # Set penalty parameter if not set
        self.set_attr('rho', opt['rho'], dval=(50.0*self.lmbda + 1.0),
                      dtype=self.dtype)
        super().__init__(Nx, self.S_slice.shape, self.S_slice.shape,
                         S.dtype, opt)
        self.X = np.zeros(
            (self.S_slice.shape[0], self.D.shape[-1], self.Y.shape[-1]),
            dtype=self.dtype
        )
        self.extra_timer = su.Timer(['xstep', 'ystep'])

    def im2slices(self, S):
        r"""Convert the input signal :math:`S` to a slice form.
        Assuming the input signal having a standard shape as pytorch variable
        (N, C, H, W).  The output slices have shape
        (batch_size, slice_dim, num_slices_per_batch).
        """
        # NOTE: we simulate the boundary condition outside fold and unfold.
        kernel_size = self.cri.shpD[:2]
        pad_h, pad_w = kernel_size[0] - 1, kernel_size[1] - 1
        S_torch = globals()['_pad_{}'.format(self.boundary)](S, pad_h, pad_w)
        with torch.no_grad():
            S_torch = torch.from_numpy(S_torch)
            slices = F.unfold(S_torch, kernel_size=kernel_size)
        assert slices.size(1) == self.D.shape[0]
        return slices.numpy()

    def slices2im(self, slices):
        r"""Reconstruct input signal :math:`\hat{S}` for slices.
        The input slices should have compatible size of
        (batch_size, slice_dim, num_slices_per_batch), and the
        returned signal has shape (N, C, H, W) as standard pytorch variable.
        """
        kernel_size = self.cri.shpD[:2]
        pad_h, pad_w = kernel_size[0] - 1, kernel_size[1] - 1
        output_h, output_w = self.cri.shpS[:2]
        with torch.no_grad():
            slices_torch = torch.from_numpy(slices)
            S_recon = F.fold(
                slices_torch, (output_h+pad_h, output_w+pad_w), kernel_size
            )
        S_recon = globals()['_crop_{}'.format(self.boundary)](
            S_recon.numpy(), pad_h, pad_w
        )
        return S_recon

    def xstep(self):
        r"""Minimize with respect to :math:`x`.  This has the form:

        .. math::
            f(x)=\sum_i\left(\lambda\|x_i\|_1+\frac{\rho}{2}\|D_l x_i - y_i
            + u_i\|_2^2\right).

        This could be solved in parallel over all slice indices i and all
        batch indices k (implicit in the above form).
        """
        self.extra_timer.start('xstep')
        signal = self.Y - self.U
        signal = signal.transpose((1, 0, 2))
        # [K, n, N] -> [n, K, N] -> [n, K*N]
        signal = signal.reshape(signal.shape[0], -1)
        opt = copy.deepcopy(self.opt['BPDN'])
        opt['Y0'] = getattr(self, '_X_bpdn_cache', None)
        solver = bpdn.BPDN(self.D, signal, lmbda=self.lmbda/self.rho, opt=opt)
        self.X = solver.solve()
        self._X_bpdn_cache = copy.deepcopy(self.X)
        self.X = self.X.reshape(
            self.X.shape[0], self.Y.shape[0], self.Y.shape[2]
        ).transpose((1, 0, 2))
        self.extra_timer.stop('xstep')

    def ystep(self):
        r"""Minimize with respect to :math:`y`.  This has the form:

        .. math::
            g(y)=\frac{1}{2}\|s-\sum_i\mathbf{R}_i^T y_i\|_2^2+
            \frac{\rho}{2}\sum_i\|D_l x_i - y_i + u_i\|_2^2.

        This has a very nice solution

        .. math::
            p_i=\frac{1}{\rho}\mathbf{R}_i s + D_l x_i + u_i.
        .. math::
            \hat{s}=\sum_i \mathbf{R}_i^T p_i.
        .. math::
            y_i = p_i - \frac{1}{\rho+n}\mathbf{R}_i\hat{s}.

        """
        self.extra_timer.start('ystep')
        # Notice that AX = D*X.
        p = self.S_slice / self.rho + self.AX + self.U
        recon = self.slices2im(p)
        # n should be dict size for each channel
        n = np.prod(self.cri.shpD[:2])
        self.Y = p - self.im2slices(recon) / (n + self.rho)
        self.extra_timer.stop('ystep')

    def cnst_A(self, X):
        r"""Compute :math:`Ax`. Our constraint is

        ..math::
            D_l x_i - y_i = 0
        """
        return np.matmul(self.D, X)

    def cnst_AT(self, X):
        r"""Compute :math:`A^T x`. Our constraint is

        ..math::
            D_l x_i - y_i = 0
        """
        return np.matmul(self.D.transpose(), X)

    def cnst_B(self, Y):
        r""" Compute :math:`By`.  Our constraint is

        ..math::
            D_l x_i - y_i = 0
        """
        return -Y

    def cnst_c(self):
        r""" Compute :math:`c`.  Our constraint is

        ..math::
            D_l x_i - y_i = 0
        """
        return 0.

    def yinit(self, yshape):
        """Slices are initialized using signal slices."""
        _ = yshape
        y_init = self.S_slice.copy()
        n = np.prod(self.cri.shpD[:2])
        y_init /= n
        return y_init

    def getmin(self):
        """Reimplement getmin func to have unified output layout."""
        # [K, m, N] -> [N, K, m] -> [H, W, 1, K, m]
        minimizer = self.X.copy()
        minimizer = minimizer.transpose((2, 0, 1))
        minimizer = minimizer.reshape(self.cri.shpX)
        return minimizer

    def eval_objfn(self):
        """Overwrite this function as in ConvBPDN."""
        dfd = self.obfn_dfd()
        reg = self.obfn_reg()
        obj = dfd + reg[0]
        return (obj, dfd) + reg[1:]

    def obfn_dfd(self):
        r"""Data fidelity term of the objective :math:`(1/2) \|s - \sum_i
        \mathbf{R}_i^T y_i\|_2^2`.
        """
        # notice AX = D*X
        # use non-relaxed version to represent data fidelity term
        recon = self.slices2im(self.cnst_A(self.X))
        return ((recon - self.S) ** 2).sum() / 2.0

    def obfn_reg(self):
        r"""Regularization term of the objective :math:`g(y)=\|y\|_1`.
        Returns a tuple where the first is the scaled combination of all
        regularization terms (if exist) and the sesequent ones are each term.
        """
        l1 = linalg.norm(self.X.ravel(), 1)
        return (self.lmbda * l1, l1)

    def getcoef(self):
        """Returns coefficients and signals for solving

        .. math::
            \min_{D} (1/2)\|D x_i - y_i + u_i\|_2^2, D\in C.
        """
        return (self.X, self.Y-self.U)

    def setdict(self, D):
        """Set dictionary properly."""
        if D.ndim == 2:  # [patch_size, num_atoms]
            self.D = D.copy()
        elif D.ndim == 3:  # [patch_h, patch_w, num_atoms]
            self.D = D.reshape((-1, D.shape[-1]))
        elif D.ndim == 4:  # [patch_h, patch_w, channels, num_atoms]
            assert D.shape[-2] == 1 or D.shape[-2] == 3
            self.D = D.transpose(2, 0, 1, 3)
            self.D = self.D.reshape((-1, self.D.shape[-1]))
        else:
            raise ValueError('Invalid dict D dimension of {}'.format(D.shape))

    def reconstruct(self, X=None):
        """Reconstruct representation.  The reconstruction follows standard
        output layout."""
        if X is None:
            X = self.X
        else:
            # X has standard shape of (N0, N1, ..., 1, K, M) since we use
            # single channel coefficient array, first convert to
            # (num_atoms, batch_size, ...) and then to
            # (slice_dim, batch_size, num_slices_per_batch) by multiplying D.
            # [ H, W, 1, K, m ] -> [K, m, H, W, 1] -> [K, m, N]
            X = X.transpose((3, 4, 0, 1, 2))
            X = X.reshape(X.shape[0], X.shape[1], -1)
        recon = self.slices2im(np.matmul(self.D, X))
        # [K, C, H, W] -> [H, W, C, K, 1]
        recon = np.expand_dims(recon.transpose((2, 3, 1, 0)), axis=-1)
        return recon

    def update_rho(self, k, r, s):
        """Back to usual way of updating rho."""
        if self.opt['AutoRho', 'AutoScaling']:
            # If AutoScaling is enabled, use adaptive penalty parameters by
            # residual balancing as commonly used in SPORCO.
            super().update_rho(k, r, s)
        else:
            tau = self.rho_tau
            mu = self.rho_mu
            if k != 0 and ((k+1) % self.opt['AutoRho', 'Period'] == 0):
                if r > mu * s:
                    self.rho = tau * self.rho
                elif s > mu * r:
                    self.rho = self.rho / tau

    def itstat_extra(self):
        """Get xstep/ystep solve time. Return average time."""
        niters = self.k + 1
        xstep_elapsed = self.extra_timer.elapsed('xstep')
        ystep_elapsed = self.extra_timer.elapsed('ystep')
        return (xstep_elapsed/niters, ystep_elapsed/niters)
예제 #18
0
    def __init__(self, D0, S, lmbda, W, opt=None, dimK=1, dimN=2):
        """
        Initialise a MixConvBPDNMaskDcplDictLearn object with problem
        size and options.


        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).
        opt : :class:`MixConvBPDNMaskDcplDictLearn.Options` object
          Algorithm options
        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 = MixConvBPDNMaskDcplDictLearn.Options()
        self.opt = opt

        # 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 X
        X0 = cr.zpad(cr.stdformD(D0, cri.Cd, cri.M, dimN), cri.Nv)
        opt['CCMOD'].update({'X0': X0})

        # Create X update object
        xstep = Acbpdn.ConvBPDNMaskDcpl(D0,
                                        S,
                                        lmbda,
                                        W,
                                        opt['CBPDN'],
                                        dimK=dimK,
                                        dimN=dimN)

        # Create D update object
        dstep = ccmod.ConvCnstrMODMask(None,
                                       S,
                                       W,
                                       dsz,
                                       opt['CCMOD'],
                                       dimK=dimK,
                                       dimN=dimN)

        # Configure iteration statistics reporting
        if self.opt['AccurateDFid']:
            isxmap = {
                'XPrRsdl': 'PrimalRsdl',
                'XDlRsdl': 'DualRsdl',
                'XRho': 'Rho'
            }
            evlmap = {'ObjFun': 'ObjFun', 'DFid': 'DFid', 'RegL1': 'RegL1'}
        else:
            isxmap = {
                'ObjFun': 'ObjFun',
                'DFid': 'DFid',
                'RegL1': 'RegL1',
                'XPrRsdl': 'PrimalRsdl',
                'XDlRsdl': 'DualRsdl',
                'XRho': 'Rho'
            }
            evlmap = {}

        if dstep.opt['BackTrack', 'Enabled']:
            isfld = [
                'Iter', 'ObjFun', 'DFid', 'RegL1', 'Cnstr', 'XPrRsdl',
                'XDlRsdl', 'XRho', 'D_F_Btrack', 'D_Q_Btrack', 'D_ItBt', 'D_L',
                'Time'
            ]
            isdmap = {
                'Cnstr': 'Cnstr',
                'D_F_Btrack': 'F_Btrack',
                'D_Q_Btrack': 'Q_Btrack',
                'D_ItBt': 'IterBTrack',
                'D_L': 'L'
            }
            hdrtxt = [
                'Itn', 'Fnc', 'DFid',
                u('ℓ1'), 'Cnstr', 'r_X', 's_X',
                u('ρ_X'), 'F_D', 'Q_D', 'It_D', 'L_D'
            ]
            hdrmap = {
                'Itn': 'Iter',
                'Fnc': 'ObjFun',
                'DFid': 'DFid',
                u('ℓ1'): 'RegL1',
                'Cnstr': 'Cnstr',
                'r_X': 'XPrRsdl',
                's_X': 'XDlRsdl',
                u('ρ_X'): 'XRho',
                'F_D': 'D_F_Btrack',
                'Q_D': 'D_Q_Btrack',
                'It_D': 'D_ItBt',
                'L_D': 'D_L'
            }

        else:
            isfld = [
                'Iter', 'ObjFun', 'DFid', 'RegL1', 'Cnstr', 'XPrRsdl',
                'XDlRsdl', 'XRho', 'D_L', 'Time'
            ]
            isdmap = {'Cnstr': 'Cnstr', 'D_L': 'L'}
            hdrtxt = [
                'Itn', 'Fnc', 'DFid',
                u('ℓ1'), 'Cnstr', 'r_X', 's_X',
                u('ρ_X'), 'L_D'
            ]
            hdrmap = {
                'Itn': 'Iter',
                'Fnc': 'ObjFun',
                'DFid': 'DFid',
                u('ℓ1'): 'RegL1',
                'Cnstr': 'Cnstr',
                'r_X': 'XPrRsdl',
                's_X': 'XDlRsdl',
                u('ρ_X'): 'XRho',
                'L_D': 'D_L'
            }

        isc = dictlrn.IterStatsConfig(isfld=isfld,
                                      isxmap=isxmap,
                                      isdmap=isdmap,
                                      evlmap=evlmap,
                                      hdrtxt=hdrtxt,
                                      hdrmap=hdrmap)

        # Call parent constructor
        super(MixConvBPDNMaskDcplDictLearn,
              self).__init__(xstep, dstep, opt, isc)
예제 #19
0
    def solve(self):
        """Start (or re-start) optimisation. This method implements the
        framework for the alternation between `X` and `D` updates in a
        dictionary learning algorithm.

        If option ``Verbose`` is ``True``, the progress of the
        optimisation is displayed at every iteration. At termination
        of this method, attribute :attr:`itstat` is a list of tuples
        representing statistics of each iteration.

        Attribute :attr:`timer` is an instance of :class:`.util.Timer`
        that provides the following labelled timers:

          ``init``: Time taken for object initialisation by
          :meth:`__init__`

          ``solve``: Total time taken by call(s) to :meth:`solve`

          ``solve_wo_func``: Total time taken by call(s) to
          :meth:`solve`, excluding time taken to compute functional
          value and related iteration statistics
        """

        # Construct tuple of status display column titles and set status
        # display strings
        hdrtxt = ['Itn', 'Fnc', 'DFid', u('Regℓ1')]
        hdrstr, fmtstr, nsep = common.solve_status_str(
            hdrtxt, fwdth0=type(self).fwiter, fprec=type(self).fpothr)

        # Print header and separator strings
        if self.opt['Verbose']:
            if self.opt['StatusHeader']:
                print(hdrstr)
                print("-" * nsep)

        # Reset timer
        self.timer.start(['solve', 'solve_wo_eval'])

        # Create process pool
        if self.nproc > 0:
            self.pool = mp.Pool(processes=self.nproc)

        for self.j in range(self.j, self.j + self.opt['MaxMainIter']):

            # Perform a set of update steps
            self.step()

            # Evaluate functional
            self.timer.stop('solve_wo_eval')
            fnev = self.evaluate()
            self.timer.start('solve_wo_eval')

            # Record iteration stats
            tk = self.timer.elapsed('solve')
            itst = self.IterationStats(*((self.j, ) + fnev + (tk, )))
            self.itstat.append(itst)

            # Display iteration stats if Verbose option enabled
            if self.opt['Verbose']:
                print(fmtstr % itst[:-1])

            # Call callback function if defined
            if self.opt['Callback'] is not None:
                if self.opt['Callback'](self):
                    break

        # Clean up process pool
        if self.nproc > 0:
            self.pool.close()
            self.pool.join()

        # Increment iteration count
        self.j += 1

        # Record solve time
        self.timer.stop(['solve', 'solve_wo_eval'])

        # Print final separator string if Verbose option enabled
        if self.opt['Verbose'] and self.opt['StatusHeader']:
            print("-" * nsep)

        # Return final dictionary
        return self.getdict()
예제 #20
0
class ConvBPDNSliceFISTA(fista.FISTA):
    class Options(fista.FISTA.Options):
        defaults = copy.deepcopy(fista.FISTA.Options.defaults)
        defaults.update({
            'Boundary': 'circulant_back',
        })

        def __init__(self, opt=None):
            if opt is None:
                opt = {}
            super().__init__(opt)

    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):
        if opt is None:
            opt = ConvBPDNSliceFISTA.Options()

        if not hasattr(self, 'cri'):
            self.cri = cr.CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN)

        self.set_dtype(opt, S.dtype)
        self.lmbda = self.dtype.type(lmbda)
        # set boundary condition
        self.set_attr('boundary',
                      opt['Boundary'],
                      dval='circulant_back',
                      dtype=None)
        self.setdict(D)

        self.S = np.asarray(S.reshape(self.cri.shpS), dtype=S.dtype)
        self.S = self.S.squeeze(-1).transpose((3, 2, 0, 1))
        self.S_slice = self.im2slices(self.S)
        xshape = (self.S_slice.shape[0], self.D.shape[1],
                  self.S_slice.shape[-1])
        Nx = np.prod(xshape)
        super().__init__(Nx, xshape, S.dtype, opt)
        if self.opt['BackTrack', 'Enabled']:
            self.L /= self.lmbda
        self.Y = self.X.copy()
        self.residual = -self.S_slice.copy()

    def eval_grad(self):
        return np.matmul(self.D.T, self.residual)

    def eval_proxop(self, V):
        return sl.shrink1(V, self.lmbda / self.L)

    def eval_R(self, V):
        recon = self.slices2im(np.matmul(self.D, V))
        return linalg.norm(self.S - recon)**2 / 2.

    def combination_step(self):
        super().combination_step()
        self.residual = self.im2slices(
            self.slices2im(np.matmul(self.D, self.Y)) - self.S)

    def rsdl(self):
        return linalg.norm(self.X - self.Yprv)

    def eval_objfn(self):
        dfd = self.obfn_dfd()
        reg = self.obfn_reg()
        obj = dfd + reg[0]
        return (obj, dfd) + reg[1:]

    def obfn_dfd(self):
        return self.eval_Rx()

    def obfn_reg(self):
        reg = linalg.norm(self.X.ravel(), 1)
        return (self.lmbda * reg, reg)

    def getmin(self):
        """Reimplement getmin func to have unified output layout."""
        # [K, m, N] -> [N, K, m] -> [H, W, 1, K, m]
        minimizer = super().getmin()
        minimizer = minimizer.transpose((2, 0, 1))
        minimizer = minimizer.reshape(self.cri.shpX)
        return minimizer

    def reconstruct(self, X=None):
        """Reconstruct representation.  The reconstruction follows standard
        output layout."""
        if X is None:
            X = self.X
        else:
            # X has standard shape of (N0, N1, ..., 1, K, M) since we use
            # single channel coefficient array, first convert to
            # (num_atoms, batch_size, ...) and then to
            # (slice_dim, batch_size, num_slices_per_batch) by multiplying D.
            # [ H, W, 1, K, m ] -> [K, m, H, W, 1] -> [K, m, N]
            X = X.transpose((3, 4, 0, 1, 2))
            X = X.reshape(X.shape[0], X.shape[1], -1)
        recon = self.slices2im(np.matmul(self.D, X))
        # [K, C, H, W] -> [H, W, C, K, 1]
        recon = np.expand_dims(recon.transpose((2, 3, 1, 0)), axis=-1)
        return recon

    def setdict(self, D):
        """Set dictionary properly."""
        if D.ndim == 2:  # [patch_size, num_atoms]
            self.D = D.copy()
        elif D.ndim == 3:  # [patch_h, patch_w, num_atoms]
            self.D = D.reshape((-1, D.shape[-1]))
        elif D.ndim == 4:  # [patch_h, patch_w, channels, num_atoms]
            assert D.shape[-2] == 1 or D.shape[-2] == 3
            self.D = D.transpose(2, 0, 1, 3)
            self.D = self.D.reshape((-1, self.D.shape[-1]))
        else:
            raise ValueError('Invalid dict D dimension of {}'.format(D.shape))

    def getcoef(self):
        return self.X

    def im2slices(self, S):
        kernel_h, kernel_w = self.cri.shpD[:2]
        return im2slices(S, kernel_h, kernel_w, self.boundary)

    def slices2im(self, slices):
        kernel_h, kernel_w = self.cri.shpD[:2]
        output_h, output_w = self.cri.shpS[:2]
        return slices2im(slices, kernel_h, kernel_w, output_h, output_w,
                         self.boundary)
예제 #21
0
class KConvBPDN(cbpdn.GenericConvBPDN):
    r"""
    ADMM algorithm for the Convolutional BPDN (CBPDN)
    :cite:`wohlberg-2014-efficient` :cite:`wohlberg-2016-efficient`
    :cite:`wohlberg-2016-convolutional` problem.

    |

    .. inheritance-diagram:: ConvBPDN
       :parts: 2

    |

    Solve the optimisation problem

    .. math::
       \mathrm{argmin}_\mathbf{x} \;
       (1/2) \left\| \sum_m \mathbf{d}_m * \mathbf{x}_m -
       \mathbf{s} \right\|_2^2 + \lambda \sum_m \| \mathbf{x}_m \|_1

    for input image :math:`\mathbf{s}`, dictionary filters
    :math:`\mathbf{d}_m`, and coefficient maps :math:`\mathbf{x}_m`,
    via the ADMM problem

    .. math::
       \mathrm{argmin}_{\mathbf{x}, \mathbf{y}} \;
       (1/2) \left\| \sum_m \mathbf{d}_m * \mathbf{x}_m -
       \mathbf{s} \right\|_2^2 + \lambda \sum_m \| \mathbf{y}_m \|_1
       \quad \text{such that} \quad \mathbf{x}_m = \mathbf{y}_m \;\;.

    Multi-image and multi-channel problems are also supported. The
    multi-image problem is

    .. math::
       \mathrm{argmin}_\mathbf{x} \;
       (1/2) \sum_k \left\| \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

    with input images :math:`\mathbf{s}_k` and coefficient maps
    :math:`\mathbf{x}_{k,m}`, and the multi-channel problem with input
    image channels :math:`\mathbf{s}_c` is either

    .. math::
       \mathrm{argmin}_\mathbf{x} \;
       (1/2) \sum_c \left\| \sum_m \mathbf{d}_m * \mathbf{x}_{c,m} -
       \mathbf{s}_c \right\|_2^2 +
       \lambda \sum_c \sum_m \| \mathbf{x}_{c,m} \|_1

    with single-channel dictionary filters :math:`\mathbf{d}_m` and
    multi-channel coefficient maps :math:`\mathbf{x}_{c,m}`, or

    .. math::
       \mathrm{argmin}_\mathbf{x} \;
       (1/2) \sum_c \left\| \sum_m \mathbf{d}_{c,m} * \mathbf{x}_m -
       \mathbf{s}_c \right\|_2^2 + \lambda \sum_m \| \mathbf{x}_m \|_1

    with multi-channel dictionary filters :math:`\mathbf{d}_{c,m}` and
    single-channel coefficient maps :math:`\mathbf{x}_m`.

    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`

       ``PrimalRsdl`` : Norm of primal residual

       ``DualRsdl`` : Norm of dual residual

       ``EpsPrimal`` : Primal residual stopping tolerance
       :math:`\epsilon_{\mathrm{pri}}`

       ``EpsDual`` : Dual residual stopping tolerance
       :math:`\epsilon_{\mathrm{dua}}`

       ``Rho`` : Penalty parameter

       ``XSlvRelRes`` : Relative residual of X step solver

       ``Time`` : Cumulative run time
    """
    class Options(cbpdn.GenericConvBPDN.Options):
        r"""ConvBPDN algorithm options

        Options include all of those defined in
        :class:`.admm.ADMMEqual.Options`, together with additional options:

          ``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 (see
          :func:`.cnvrep.l1Wshape` for more details). 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(cbpdn.GenericConvBPDN.Options.defaults)
        defaults.update({'L1Weight': 1.0})

        def __init__(self, opt=None):
            """
            Parameters
            ----------
            opt : dict or None, optional (default None)
              ConvBPDN algorithm options
            """

            if opt is None:
                opt = {}
            cbpdn.GenericConvBPDN.Options.__init__(self, opt)

    itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1', 'RegL2')
    hdrtxt_objfn = ('Fnc', 'DFid', u('Regℓ1'), u('Regℓ2'))
    hdrval_objfun = {
        'Fnc': 'ObjFun',
        'DFid': 'DFid',
        u('Regℓ1'): 'RegL1',
        u('Regℓ2'): 'RegL2'
    }

    def __init__(self, Wf, Sf, cri_K, dtype, lmbda=None, mu=0.0, opt=None):
        """
        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`.


        |

        **Call graph**

        .. image:: ../_static/jonga/cbpdn_init.svg
           :width: 20%
           :target: ../_static/jonga/cbpdn_init.svg

        |


        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 = KConvBPDN.Options()

        # Set dtype attribute based on S.dtype and opt['DataType']
        self.set_dtype(opt, dtype)

        # problem dimensions
        self.cri = cri_K

        # Reshape D and S to standard layout (NOT NEEDED in AKConvBPDN)
        self.Wf = np.asarray(Wf.reshape(self.cri.shpD), dtype=self.dtype)
        self.Sf = np.asarray(Sf.reshape(self.cri.shpS), dtype=self.dtype)
        # self.Sf_ = np.moveaxis(Sf.reshape([self.cri.nv[0],self.cri.N_,1]),[0,1,2],[2,0,1])

        # Set default lambda value if not specified
        if lmbda is None:
            b = np.conj(Df) * Sf
            lmbda = 0.1 * abs(b).max()

        # Set l1 term scaling
        self.lmbda = self.dtype.type(lmbda)

        # Set l2 term scaling
        self.mu = self.dtype.type(mu)

        # Set penalty parameter
        self.set_attr('rho',
                      opt['rho'],
                      dval=(50.0 * self.lmbda + 1.0),
                      dtype=self.dtype)

        # Set rho_xi attribute (see Sec. VI.C of wohlberg-2015-adaptive)
        if self.lmbda != 0.0:
            rho_xi = float((1.0 + (18.3)**(np.log10(self.lmbda) + 1.0)))
        else:
            rho_xi = 1.0
        self.set_attr('rho_xi',
                      opt['AutoRho', 'RsdlTarget'],
                      dval=rho_xi,
                      dtype=self.dtype)

        # Call parent class __init__ (not ConvBPDN bc FFT domain data)
        super(cbpdn.GenericConvBPDN, self).__init__(self.cri.shpX, Sf.dtype,
                                                    opt)

        # Initialise byte-aligned arrays for pyfftw
        self.YU = sl.pyfftw_empty_aligned(self.Y.shape, dtype=self.dtype)
        self.Xf = sl.pyfftw_empty_aligned(self.Y.shape, self.dtype)

        self.c = [None] * self.cri.n  # to be filled with cho_factor

        self.setdictf()

        # Set l1 term weight array
        self.wl1 = np.asarray(opt['L1Weight'], dtype=self.dtype)
        self.wl1 = self.wl1.reshape(Kl1Wshape(self.wl1, self.cri))

        print('L1Weight %s \n' % (self.wl1, ))

    def setdictf(self, Wf=None):
        """Set dictionary array."""

        if Wf is not None:
            self.Wf = Wf
        # Compute D^H S
        print('Df shape %s \n' % (self.Wf.shape, ))
        print('Sf shape %s \n' % (self.Sf.shape, ))

        self.WSf = (np.sum(np.conj(self.Wf) * self.Sf, axis=0)).squeeze()
        # if self.cri.Cd > 1:
        #     self.WSf = np.sum(self.WSf, axis=self.cri.axisC, keepdims=True)

        # Df_full = self.Wf.reshape([self.cri.shpD[0],self.cri.shpD[1],self.cri.n])
        for s in range(self.cri.n):
            Df_ = self.Wf[:, :, s]
            Df_H = np.conj(Df_.transpose())
            self.c[s] = linalg.cho_factor(np.dot(Df_H,Df_) + (self.mu + self.rho) * \
                        np.identity(self.cri.MR,dtype=self.dtype),lower=False,check_finite=True)

    def uinit(self, ushape):
        """Return initialiser for working variable U"""

        if self.opt['Y0'] is None:
            return np.zeros(ushape, dtype=self.dtype)
        else:
            # If initial Y is non-zero, initial U is chosen so that
            # the relevant dual optimality criterion (see (3.10) in
            # boyd-2010-distributed) is satisfied.
            return (self.lmbda / self.rho) * np.sign(self.Y)

    def xstep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{x}`.
        """

        self.YU[:] = self.Y - self.U

        print('YU dtype %s \n' % (self.YU.dtype, ))

        b = (self.WSf +
             self.rho * sl.fftn(self.YU, None, self.cri.axisn).squeeze())

        # print('b shape %s \n' % (b.shape,))

        # if self.cri.Cd == 1:
        for s in range(self.cri.n):
            self.Xf[:, 0, s] = linalg.cho_solve(self.c[s],
                                                b[:, s],
                                                check_finite=True)
        # else:
        #     raise ValueError("Multi-channel dictionary not implemented")
        #     self.Xf[:] = sl.solvemdbi_ism(self.Wf, self.mu + self.rho, b,
        #                                   self.cri.axisM, self.cri.axisC)

        self.X = sl.irfftn(self.Xf, [self.cri.n], self.cri.axisn)

        # if self.opt['LinSolveCheck']:
        #     Dop = lambda x: sl.inner(self.Wf, x, axis=self.cri.axisM)
        #     if self.cri.Cd == 1:
        #         DHop = lambda x: np.conj(self.Wf) * x
        #     else:
        #         DHop = lambda x: sl.inner(np.conj(self.Wf), x,
        #                                   axis=self.cri.axisC)
        #     ax = DHop(Dop(self.Xf)) + (self.mu + self.rho)*self.Xf
        #     self.xrrs = sl.rrs(ax, b)
        # else:
        #     self.xrrs = None

    def ystep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{y}`."""

        scalar_factor = (self.lmbda / self.rho) * self.wl1
        print('AX dtype %s \n' % (self.AX.dtype, ))
        print('U dtype %s \n' % (self.U.dtype, ))
        print('sc_factor shape %s \n' % (scalar_factor.shape, ))

        self.Y = sp.prox_l1(self.AX + self.U,
                            (self.lmbda / self.rho) * self.wl1)
        super(KConvBPDN, self).ystep()

    def obfn_reg(self):
        """Compute regularisation term and contribution to objective
        function. (ConvElasticNet)
        """

        rl1 = np.linalg.norm((self.wl1 * self.obfn_gvar()).ravel(), 1)
        rl2 = 0.5 * np.linalg.norm(self.obfn_gvar())**2
        return (self.lmbda * rl1 + self.mu * rl2, rl1, rl2)

    def setdict(self):
        """Set dictionary array.

        Overriding this method is required.
        """

        raise NotImplementedError()

    def reconstruct(self):
        """Reconstruct representation.

        Overriding this method is required.
        """

        raise NotImplementedError()
예제 #22
0
class ConvProdDictBPDNJoint(ConvProdDictBPDN):
    r"""
    ADMM algorithm for the Convolutional BPDN (CBPDN) for multi-channel
    signals with a dictionary consisting of a product of convolutional
    and standard dictionaries, and with joint sparsity via an
    :math:`\ell_{2,1}` norm term :cite:`garcia-2018-convolutional2`.

    Solve the optimisation problem

    .. math::
       \mathrm{argmin}_X \; (1/2) \left\| D X B^T - S \right\|_2^2 +
       \lambda \| X \|_1 + \mu \| X \|_{2,1}

    where :math:`D` is a convolutional dictionary, :math:`B` is a
    standard dictionary, and :math:`S` is a multi-channel input image
    with

    .. math::
       S = \left( \begin{array}{ccc} \mathbf{s}_0 & \mathbf{s}_1 & \ldots
       \end{array} \right) \;.

    where the signal channels form the columns, :math:`\mathbf{s}_c`, of
    :math:`S`. This problem is solved via the ADMM problem
    :cite:`garcia-2018-convolutional2`

    .. math::
       \mathrm{argmin}_{X,Y} \;
       (1/2) \left\| D X B^T - S \right\|_2^2 + \lambda \| Y \|_1
       + \mu \| Y \|_{2,1} \quad \text{such that} \quad X = Y \;\;.
    """

    itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1', 'RegL21')
    hdrtxt_objfn = ('Fnc', 'DFid', u('Regℓ1'), u('Regℓ2,1'))
    hdrval_objfun = {
        'Fnc': 'ObjFun',
        'DFid': 'DFid',
        u('Regℓ1'): 'RegL1',
        u('Regℓ2,1'): 'RegL21'
    }

    def __init__(self, D, B, S, lmbda, mu=0.0, opt=None, dimK=None, dimN=2):
        """
        Parameters
        ----------
        D : array_like
          Convolutional dictionary array
        B : array_like
          Standard dictionary array
        S : array_like
          Signal array
        lmbda : float
          Regularisation parameter (l1)
        mu : float
          Regularisation parameter (l2,1)
        opt : :class:`ConvProdDictBPDNJoint.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
        """

        super(ConvProdDictBPDNJoint, self).__init__(D, B, S, lmbda, opt, dimK,
                                                    dimN)
        self.mu = self.dtype.type(mu)

    def ystep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{y}`.
        """

        self.Y = prox_sl1l2(self.AX + self.U,
                            (self.lmbda / self.rho) * self.wl1,
                            (self.mu / self.rho),
                            axis=self.cri.axisC)
        cbpdn.GenericConvBPDN.ystep(self)

    def obfn_reg(self):
        r"""Compute regularisation terms and contribution to objective
        function. Regularisation terms are :math:`\| Y \|_1` and
        :math:`\| Y \|_{2,1}`.
        """

        rl1 = np.linalg.norm((self.wl1 * self.obfn_gvar()).ravel(), 1)
        rl21 = np.sum(np.sqrt(np.sum(self.obfn_gvar()**2,
                                     axis=self.cri.axisC)))
        return (self.lmbda * rl1 + self.mu * rl21, rl1, rl21)
예제 #23
0
 def config_itstats(self):
     """Config itstats output for fista."""
     # isfld
     isfld = ['Iter', 'ObjFun', 'DFid', 'RegL1', 'Cnstr']
     if self.opt['CBPDN', 'BackTrack', 'Enabled']:
         isfld.extend(
             ['X_F_Btrack', 'X_Q_Btrack', 'X_ItBt', 'X_L', 'X_Rsdl'])
     else:
         isfld.extend(['X_L', 'X_Rsdl'])
     if self.opt['CCMOD', 'BackTrack', 'Enabled']:
         isfld.extend(
             ['D_F_Btrack', 'D_Q_Btrack', 'D_ItBt', 'D_L', 'D_Rsdl'])
     else:
         isfld.extend(['D_L', 'D_Rsdl'])
     isfld.extend(['Time'])
     # isxmap/isdmap
     isxmap = {
         'X_F_Btrack': 'F_Btrack',
         'X_Q_Btrack': 'Q_Btrack',
         'X_ItBt': 'IterBTrack',
         'X_L': 'L',
         'X_Rsdl': 'Rsdl'
     }
     isdmap = {
         'Cnstr': 'Cnstr',
         'D_F_Btrack': 'F_Btrack',
         'D_Q_Btrack': 'Q_Btrack',
         'D_ItBt': 'IterBTrack',
         'D_L': 'L',
         'D_Rsdl': 'Rsdl'
     }
     # hdrtxt
     hdrtxt = ['Itn', 'Fnc', 'DFid', u('ℓ1'), 'Cnstr']
     if self.opt['CBPDN', 'BackTrack', 'Enabled']:
         hdrtxt.extend(['F_X', 'Q_X', 'It_X', 'L_X'])
     else:
         hdrtxt.append('L_X')
     if self.opt['CCMOD', 'BackTrack', 'Enabled']:
         hdrtxt.extend(['F_D', 'Q_D', 'It_D', 'L_D'])
     else:
         hdrtxt.append('L_D')
     # hdrmap
     hdrmap = {
         'Itn': 'Iter',
         'Fnc': 'ObjFun',
         'DFid': 'DFid',
         u('ℓ1'): 'RegL1',
         'Cnstr': 'Cnstr'
     }
     if self.opt['CBPDN', 'BackTrack', 'Enabled']:
         hdrmap.update({
             'F_X': 'X_F_Btrack',
             'Q_X': 'X_Q_Btrack',
             'It_X': 'X_ItBt',
             'L_X': 'X_L'
         })
     else:
         hdrmap.update({'L_X': 'X_L'})
     if self.opt['CCMOD', 'BackTrack', 'Enabled']:
         hdrmap.update({
             'F_D': 'D_F_Btrack',
             'Q_D': 'D_Q_Btrack',
             'It_D': 'D_ItBt',
             'L_D': 'D_L'
         })
     else:
         hdrmap.update({'L_D': 'D_L'})
     # evlmap
     _evlmap = {'ObjFun': 'ObjFun', 'DFid': 'DFid', 'RegL1': 'RegL1'}
     if self.opt['AccurateDFid']:
         evlmap = _evlmap
     else:
         evlmap = {}
         isxmap.update(_evlmap)
     # fmtmap
     fmtmap = {'It_X': '%4d', 'It_D': '%4d'}
     return dictlrn.IterStatsConfig(isfld=isfld,
                                    isxmap=isxmap,
                                    isdmap=isdmap,
                                    evlmap=evlmap,
                                    hdrtxt=hdrtxt,
                                    hdrmap=hdrmap,
                                    fmtmap=fmtmap)
예제 #24
0
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
    """
    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`.


        |

        **Call graph**

        .. image:: ../_static/jonga/fista_cbpdn_init.svg
           :width: 20%
           :target: ../_static/jonga/fista_cbpdn_init.svg

        |


        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 = cr.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 = cr.CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN)
            Df = sl.rfftn(D.reshape(cri.shpD), cri.Nv, axes=cri.axisN)
            Sf = sl.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 = sl.rfftn(self.S, None, self.cri.axisN)

        # Create byte aligned arrays for FFT calls
        self.Y = self.X.copy()
        self.X = sl.pyfftw_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 = sl.pyfftw_rfftn_empty_aligned(self.X.shape, self.cri.axisN,
                                                self.dtype)

        self.Xf = sl.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()

    def setdict(self, D=None):
        """Set dictionary array."""

        if D is not None:
            self.D = np.asarray(D, dtype=self.dtype)
        self.Df = sl.rfftn(self.D, self.cri.Nv, self.cri.axisN)

    def getcoef(self):
        """Get final coefficient array."""

        return self.X

    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

    def eval_Rf(self, Vf):
        """Evaluate smooth term in Vf."""

        return sl.inner(self.Df, Vf, axis=self.cri.axisM) - self.Sf

    def eval_proxop(self, V):
        """Compute proximal operator of :math:`g`."""

        return sp.prox_l1(V, (self.lmbda / self.L) * self.wl1)

    def rsdl(self):
        """Compute fixed point residual in Fourier domain."""

        diff = self.Xf - self.Yfprv
        return sl.rfl2norm2(diff, self.X.shape, axis=self.cri.axisN)

    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:]

    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 sl.rfl2norm2(Ef, self.S.shape, axis=self.cri.axisN) / 2.0

    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)

    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

    def reconstruct(self, X=None):
        """Reconstruct representation."""

        if X is None:
            X = self.X
        Xf = sl.rfftn(X, None, self.cri.axisN)
        Sf = np.sum(self.Df * Xf, axis=self.cri.axisM)
        return sl.irfftn(Sf, self.cri.Nv, self.cri.axisN)
예제 #25
0
    def __init__(self,
                 D0,
                 S,
                 lmbda=None,
                 opt=None,
                 method='cns',
                 dimK=1,
                 dimN=2,
                 stopping_pobj=None):
        """
        Initialise a ConvBPDNDictLearn object with problem size and options.

        |

        **Call graph**

        .. image:: _static/jonga/cbpdndl_init.svg
           :width: 20%
           :target: _static/jonga/cbpdndl_init.svg

        |


        Parameters
        ----------
        D0 : array_like
          Initial dictionary array
        S : array_like
          Signal array
        lmbda : float
          Regularisation parameter
        opt : :class:`ConvBPDNDictLearn.Options` object
          Algorithm options
        method : string, optional (default 'cns')
          String selecting dictionary update solver. Valid values are
          documented in function :func:`.ConvCnstrMOD`.
        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 = ConvBPDNDictLearn.Options(method=method)
        self.opt = opt

        self.stopping_pobj = stopping_pobj

        # 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 and U
        opt['CCMOD'].update(
            {'Y0': cr.zpad(cr.stdformD(D0, cri.C, cri.M, dimN), cri.Nv)})

        # Create X update object
        xstep = cbpdn.ConvBPDN(D0,
                               S,
                               lmbda,
                               opt['CBPDN'],
                               dimK=dimK,
                               dimN=dimN)

        # Create D update object
        dstep = ccmod.ConvCnstrMOD(None,
                                   S,
                                   dsz,
                                   opt['CCMOD'],
                                   method=method,
                                   dimK=dimK,
                                   dimN=dimN)

        # Configure iteration statistics reporting
        if self.opt['AccurateDFid']:
            isxmap = {
                'XPrRsdl': 'PrimalRsdl',
                'XDlRsdl': 'DualRsdl',
                'XRho': 'Rho'
            }
            evlmap = {'ObjFun': 'ObjFun', 'DFid': 'DFid', 'RegL1': 'RegL1'}
        else:
            isxmap = {
                'ObjFun': 'ObjFun',
                'DFid': 'DFid',
                'RegL1': 'RegL1',
                'XPrRsdl': 'PrimalRsdl',
                'XDlRsdl': 'DualRsdl',
                'XRho': 'Rho'
            }
            evlmap = {}
        isc = dictlrn.IterStatsConfig(isfld=[
            'Iter', 'ObjFun', 'DFid', 'RegL1', 'Cnstr', 'XPrRsdl', 'XDlRsdl',
            'XRho', 'DPrRsdl', 'DDlRsdl', 'DRho', 'Time'
        ],
                                      isxmap=isxmap,
                                      isdmap={
                                          'Cnstr': 'Cnstr',
                                          'DPrRsdl': 'PrimalRsdl',
                                          'DDlRsdl': 'DualRsdl',
                                          'DRho': 'Rho'
                                      },
                                      evlmap=evlmap,
                                      hdrtxt=[
                                          'Itn', 'Fnc', 'DFid',
                                          u('ℓ1'), 'Cnstr', 'r_X', 's_X',
                                          u('ρ_X'), 'r_D', 's_D',
                                          u('ρ_D')
                                      ],
                                      hdrmap={
                                          'Itn': 'Iter',
                                          'Fnc': 'ObjFun',
                                          'DFid': 'DFid',
                                          u('ℓ1'): 'RegL1',
                                          'Cnstr': 'Cnstr',
                                          'r_X': 'XPrRsdl',
                                          's_X': 'XDlRsdl',
                                          u('ρ_X'): 'XRho',
                                          'r_D': 'DPrRsdl',
                                          's_D': 'DDlRsdl',
                                          u('ρ_D'): 'DRho'
                                      })

        # Call parent constructor
        super(ConvBPDNDictLearn, self).__init__(xstep, dstep, opt, isc)
예제 #26
0
파일: rpca.py 프로젝트: wmvanvliet/alphacsc
class RobustPCA(admm.ADMM):
    r"""ADMM algorithm for Robust PCA problem :cite:`candes-2011-robust`
    :cite:`cai-2010-singular`.

    Solve the optimisation problem

    .. math::
       \mathrm{argmin}_{X, Y} \;
        \| X \|_* + \lambda \| Y \|_1 \quad \text{such that}
        \quad X + Y = S \;\;.

    This problem is unusual in that it is already in ADMM form without
    the need for any variable splitting.

    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

       ``NrmNuc`` :  Value of nuclear norm term :math:`\| X \|_*`

       ``NrmL1`` : Value of :math:`\ell_1` norm term :math:`\| Y \|_1`

       ``Cnstr`` : Constraint violation :math:`\| X + Y - S\|_2`

       ``PrimalRsdl`` : Norm of primal residual

       ``DualRsdl`` : Norm of dual residual

       ``EpsPrimal`` : Primal residual stopping tolerance
       :math:`\epsilon_{\mathrm{pri}}`

       ``EpsDual`` : Dual residual stopping tolerance
       :math:`\epsilon_{\mathrm{dua}}`

       ``Rho`` : Penalty parameter

       ``Time`` : Cumulative run time
    """
    class Options(admm.ADMM.Options):
        """RobustPCA algorithm options

        Options include all of those defined in
        :class:`sporco.admm.admm.ADMM.Options`, together with
        an additional option:

          ``fEvalX`` : Flag indicating whether the :math:`f` component
          of the objective function should be evaluated using variable
          X (``True``) or Y (``False``) as its argument.

          ``gEvalY`` : Flag indicating whether the :math:`g` component
          of the objective function should be evaluated using variable
          Y (``True``) or X (``False``) as its argument.
        """

        defaults = copy.deepcopy(admm.ADMM.Options.defaults)
        defaults.update({'gEvalY': True, 'fEvalX': True, 'RelaxParam': 1.8})
        defaults['AutoRho'].update({
            'Enabled': True,
            'Period': 1,
            'AutoScaling': True,
            'Scaling': 1000.0,
            'RsdlRatio': 1.2
        })

        def __init__(self, opt=None):
            """Initialise RobustPCA algorithm options object."""

            if opt is None:
                opt = {}
            admm.ADMM.Options.__init__(self, opt)

            if self['AutoRho', 'RsdlTarget'] is None:
                self['AutoRho', 'RsdlTarget'] = 1.0

    itstat_fields_objfn = ('ObjFun', 'NrmNuc', 'NrmL1', 'Cnstr')
    hdrtxt_objfn = ('Fnc', 'NrmNuc', u('Nrmℓ1'), 'Cnstr')
    hdrval_objfun = {
        'Fnc': 'ObjFun',
        'NrmNuc': 'NrmNuc',
        u('Nrmℓ1'): 'NrmL1',
        'Cnstr': 'Cnstr'
    }

    def __init__(self, S, lmbda=None, opt=None):
        """
        Initialise a RobustPCA object with problem parameters.

        Parameters
        ----------
        S : array_like
          Signal vector or matrix
        lmbda : float
          Regularisation parameter
        opt : RobustPCA.Options object
          Algorithm options
        """

        if opt is None:
            opt = RobustPCA.Options()

        # 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:
            lmbda = 1.0 / np.sqrt(S.shape[0])
        self.lmbda = self.dtype.type(lmbda)

        # Set penalty parameter
        self.set_attr('rho',
                      opt['rho'],
                      dval=(2.0 * self.lmbda + 0.1),
                      dtype=self.dtype)

        Nx = S.size
        super(RobustPCA, self).__init__(Nx, S.shape, S.shape, S.dtype, opt)

        self.S = np.asarray(S, dtype=self.dtype)

    def uinit(self, ushape):
        """Return initialiser for working variable U"""

        if self.opt['Y0'] is None:
            return np.zeros(ushape, dtype=self.dtype)
        else:
            # If initial Y is non-zero, initial U is chosen so that
            # the relevant dual optimality criterion (see (3.10) in
            # boyd-2010-distributed) is satisfied.
            return (self.lmbda / self.rho) * np.sign(self.Y)

    def solve(self):
        """Start (or re-start) optimisation."""

        super(RobustPCA, self).solve()
        return self.X, self.Y

    def xstep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{x}`.
        """

        self.X, self.ss = sp.prox_nuclear(self.S - self.Y - self.U,
                                          1 / self.rho)

    def ystep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{y}`.
        """

        self.Y = np.asarray(sl.shrink1(self.S - self.AX - self.U,
                                       self.lmbda / self.rho),
                            dtype=self.dtype)

    def obfn_fvar(self):
        """Variable to be evaluated in computing regularisation term,
        depending on 'fEvalX' option value.
        """

        if self.opt['fEvalX']:
            return self.X
        else:
            return self.cnst_c() - self.cnst_B(self.Y)

    def obfn_gvar(self):
        """Variable to be evaluated in computing regularisation term,
        depending on 'gEvalY' option value.
        """

        if self.opt['gEvalY']:
            return self.Y
        else:
            return self.cnst_c() - self.cnst_A(self.X)

    def eval_objfn(self):
        """Compute components of objective function as well as total
        contribution to objective function.
        """

        if self.opt['fEvalX']:
            rnn = np.sum(self.ss)
        else:
            rnn = sp.norm_nuclear(self.obfn_fvar())
        rl1 = np.sum(np.abs(self.obfn_gvar()))
        cns = np.linalg.norm(self.X + self.Y - self.S)
        obj = rnn + self.lmbda * rl1
        return (obj, rnn, rl1, cns)

    def cnst_A(self, X):
        r"""Compute :math:`A \mathbf{x}` component of ADMM problem
        constraint.  In this case :math:`A \mathbf{x} = \mathbf{x}`.
        """

        return X

    def cnst_AT(self, X):
        r"""Compute :math:`A^T \mathbf{x}` where :math:`A \mathbf{x}` is
        a component of ADMM problem constraint. In this case
        :math:`A^T \mathbf{x} = \mathbf{x}`.
        """

        return X

    def cnst_B(self, Y):
        r"""Compute :math:`B \mathbf{y}` component of ADMM problem
        constraint.  In this case :math:`B \mathbf{y} = -\mathbf{y}`.
        """

        return Y

    def cnst_c(self):
        r"""Compute constant component :math:`\mathbf{c}` of ADMM problem
        constraint. In this case :math:`\mathbf{c} = \mathbf{s}`.
        """

        return self.S
예제 #27
0
    def __init__(self,
                 D0,
                 S,
                 lmbda,
                 W,
                 opt=None,
                 method='cns',
                 dimK=1,
                 dimN=2):
        """
        Initialise a ConvBPDNMaskDcplDictLearn object with problem size and
        options.

        |

        **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).
        opt : :class:`ConvBPDNMaskDcplDictLearn.Options` object
          Algorithm options
        method : string, optional (default 'cns')
          String selecting dictionary update solver. Valid values are
          documented in function :func:`.ConvCnstrMODMaskDcpl`.
        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 = ConvBPDNMaskDcplDictLearn.Options(method=method)
        self.opt = opt

        # 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 and U
        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 method == 'cns':
            Y0 = Y0b1
        else:
            Y0 = np.concatenate((Y0b0, Y0b1), axis=cri.axisM)
        opt['CCMOD'].update({'Y0': Y0})

        # Create X update object
        xstep = cbpdn.ConvBPDNMaskDcpl(D0,
                                       S,
                                       lmbda,
                                       W,
                                       opt['CBPDN'],
                                       dimK=dimK,
                                       dimN=dimN)

        # Create D update object
        dstep = ccmodmd.ConvCnstrMODMaskDcpl(None,
                                             S,
                                             W,
                                             dsz,
                                             opt['CCMOD'],
                                             method=method,
                                             dimK=dimK,
                                             dimN=dimN)

        # Configure iteration statistics reporting
        if self.opt['AccurateDFid']:
            isxmap = {
                'XPrRsdl': 'PrimalRsdl',
                'XDlRsdl': 'DualRsdl',
                'XRho': 'Rho'
            }
            evlmap = {'ObjFun': 'ObjFun', 'DFid': 'DFid', 'RegL1': 'RegL1'}
        else:
            isxmap = {
                'ObjFun': 'ObjFun',
                'DFid': 'DFid',
                'RegL1': 'RegL1',
                'XPrRsdl': 'PrimalRsdl',
                'XDlRsdl': 'DualRsdl',
                'XRho': 'Rho'
            }
            evlmap = {}
        isc = dictlrn.IterStatsConfig(isfld=[
            'Iter', 'ObjFun', 'DFid', 'RegL1', 'Cnstr', 'XPrRsdl', 'XDlRsdl',
            'XRho', 'DPrRsdl', 'DDlRsdl', 'DRho', 'Time'
        ],
                                      isxmap=isxmap,
                                      isdmap={
                                          'Cnstr': 'Cnstr',
                                          'DPrRsdl': 'PrimalRsdl',
                                          'DDlRsdl': 'DualRsdl',
                                          'DRho': 'Rho'
                                      },
                                      evlmap=evlmap,
                                      hdrtxt=[
                                          'Itn', 'Fnc', 'DFid',
                                          u('ℓ1'), 'Cnstr', 'r_X', 's_X',
                                          u('ρ_X'), 'r_D', 's_D',
                                          u('ρ_D')
                                      ],
                                      hdrmap={
                                          'Itn': 'Iter',
                                          'Fnc': 'ObjFun',
                                          'DFid': 'DFid',
                                          u('ℓ1'): 'RegL1',
                                          'Cnstr': 'Cnstr',
                                          'r_X': 'XPrRsdl',
                                          's_X': 'XDlRsdl',
                                          u('ρ_X'): 'XRho',
                                          'r_D': 'DPrRsdl',
                                          's_D': 'DDlRsdl',
                                          u('ρ_D'): 'DRho'
                                      })

        # Call parent constructor
        super(ConvBPDNMaskDcplDictLearn, self).__init__(xstep, dstep, opt, isc)
예제 #28
0
class ConvBPDNRecTV(admm.ADMM):
    r"""
    ADMM algorithm for an extension of Convolutional BPDN including
    terms penalising the total variation of the reconstruction from the
    sparse representation :cite:`wohlberg-2017-convolutional`.

    |

    .. inheritance-diagram:: ConvBPDNRecTV
       :parts: 2

    |

    Solve the optimisation problem

    .. math::
       \mathrm{argmin}_\mathbf{x} \; \frac{1}{2}
       \left\| \sum_m \mathbf{d}_m * \mathbf{x}_m - \mathbf{s}
       \right\|_2^2 + \lambda \sum_m \| \mathbf{x}_m \|_1 +
       \mu \left\| \sqrt{\sum_i \left( G_i \left( \sum_m \mathbf{d}_m *
       \mathbf{x}_m  \right) \right)^2} \right\|_1 \;\;,

    where :math:`G_i` is an operator computing the derivative along index
    :math:`i`, via the ADMM problem

    .. math::
       \mathrm{argmin}_\mathbf{x} \; (1/2) \left\| D
       \mathbf{x} - \mathbf{s} \right\|_2^2  +
       \lambda \| \mathbf{y}_0 \|_1 + \mu \left\|
       \sqrt{\sum_{i=1}^L \mathbf{y}_i^2} \right\|_1 \quad \text{ such that }
       \quad \left( \begin{array}{c} I \\ \Gamma_0 \\ \Gamma_1 \\ \vdots \\
       \Gamma_{L-1} \end{array} \right) \mathbf{x} =
       \left( \begin{array}{c} \mathbf{y}_0 \\
       \mathbf{y}_1 \\ \mathbf{y}_2 \\ \vdots \\ \mathbf{y}_L \end{array}
       \right)  \;\;,

    where

    .. math::
       D = \left( \begin{array}{ccc} D_0 & D_1 & \ldots \end{array} \right)
       \qquad
       \mathbf{x} = \left( \begin{array}{c} \mathbf{x}_0 \\ \mathbf{x}_1 \\
       \vdots \end{array} \right) \qquad
       \Gamma_i = \left( \begin{array}{ccc} G_{i,0} & G_{i,1} & \ldots
       \end{array} \right) \;\;,

    and linear operator :math:`G_{i,m}` is defined such that

    .. math::
       G_{i,m} \mathbf{x} = \mathbf{g}_i * \mathbf{d}_m * \mathbf{x}
       \;\;,

    where :math:`\mathbf{g}_i` is the filter corresponding to :math:`G_i`,
    i.e. :math:`G_i \mathbf{x} = \mathbf{g}_i * \mathbf{x}`.


    For multi-channel signals, vector TV is applied jointly over the
    reconstructions of all channels.


    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`

       ``RegTV`` : Value of regularisation term :math:`\left\|
       \sqrt{\sum_i \left( G_i \left( \sum_m \mathbf{d}_m *
       \mathbf{x}_m  \right) \right)^2} \right\|_1`

       ``PrimalRsdl`` : Norm of primal residual

       ``DualRsdl`` : Norm of dual residual

       ``EpsPrimal`` : Primal residual stopping tolerance
       :math:`\epsilon_{\mathrm{pri}}`

       ``EpsDual`` : Dual residual stopping tolerance
       :math:`\epsilon_{\mathrm{dua}}`

       ``Rho`` : Penalty parameter

       ``XSlvRelRes`` : Relative residual of X step solver

       ``Time`` : Cumulative run time
    """
    class Options(cbpdn.ConvBPDN.Options):
        r"""ConvBPDNRecTV algorithm options

        Options include all of those defined in
        :class:`.admm.cbpdn.ConvBPDN.Options`, together with additional
        options:

        ``TVWeight`` : An array of weights :math:`w_m` for the term
        penalising the gradient of the coefficient maps. If this
        option is defined, the regularization term is :math:`\left\|
        \sqrt{\sum_i \left( G_i \left( \sum_m w_m (\mathbf{d}_m *
        \mathbf{x}_m)  \right) \right)^2} \right\|_1` where :math:`w_m`
        is the weight for filter index :math:`m`. The array should be an
        :math:`M`-vector where :math:`M` is the number of filters in the
        dictionary.
        """

        defaults = copy.deepcopy(cbpdn.ConvBPDN.Options.defaults)
        defaults.update({'TVWeight': 1.0})

        def __init__(self, opt=None):
            """
            Parameters
            ----------
            opt : dict or None, optional (default None)
              ConvBPDNRecTV algorithm options
            """

            if opt is None:
                opt = {}
            cbpdn.ConvBPDN.Options.__init__(self, opt)

    itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1', 'RegTV')
    itstat_fields_extra = ('XSlvRelRes', )
    hdrtxt_objfn = ('Fnc', 'DFid', u('Regℓ1'), u('RegTV'))
    hdrval_objfun = {
        'Fnc': 'ObjFun',
        'DFid': 'DFid',
        u('Regℓ1'): 'RegL1',
        u('RegTV'): 'RegTV'
    }

    def __init__(self, D, S, lmbda, mu=0.0, opt=None, dimK=None, dimN=2):
        """

        |

        **Call graph**

        .. image:: ../_static/jonga/cbpdnrtv_init.svg
           :width: 20%
           :target: ../_static/jonga/cbpdnrtv_init.svg

        |


        Parameters
        ----------
        D : array_like
          Dictionary matrix
        S : array_like
          Signal vector or matrix
        lmbda : float
          Regularisation parameter (l1)
        mu : float
          Regularisation parameter (gradient)
        opt : :class:`ConvBPDNRecTV.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
        """

        if opt is None:
            opt = ConvBPDNRecTV.Options()

        # Infer problem dimensions and set relevant attributes of self
        self.cri = cr.CSC_ConvRepIndexing(D, S, dimK=dimK, dimN=dimN)

        # Call parent class __init__
        Nx = np.product(np.array(self.cri.shpX))
        yshape = list(self.cri.shpX)
        yshape[self.cri.axisM] += len(self.cri.axisN) * self.cri.Cd
        super(ConvBPDNRecTV, self).__init__(Nx, yshape, yshape, S.dtype, opt)

        # Set l1 term scaling and weight array
        self.lmbda = self.dtype.type(lmbda)
        self.Wl1 = np.asarray(opt['L1Weight'], dtype=self.dtype)
        self.Wl1 = self.Wl1.reshape(cr.l1Wshape(self.Wl1, self.cri))

        self.mu = self.dtype.type(mu)
        if hasattr(opt['TVWeight'], 'ndim') and opt['TVWeight'].ndim > 0:
            self.Wtv = np.asarray(
                opt['TVWeight'].reshape((1, ) * (dimN + 2) +
                                        opt['TVWeight'].shape),
                dtype=self.dtype)
        else:
            # Wtv is a scalar: no need to change shape
            self.Wtv = self.dtype.type(opt['TVWeight'])

        # Set penalty parameter
        self.set_attr('rho',
                      opt['rho'],
                      dval=(50.0 * self.lmbda + 1.0),
                      dtype=self.dtype)

        # Set rho_xi attribute
        self.set_attr('rho_xi',
                      opt['AutoRho', 'RsdlTarget'],
                      dval=1.0,
                      dtype=self.dtype)

        # 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 = sl.rfftn(self.S, None, self.cri.axisN)

        self.Gf, GHGf = sl.gradient_filters(self.cri.dimN + 3,
                                            self.cri.axisN,
                                            self.cri.Nv,
                                            dtype=self.dtype)

        # Initialise byte-aligned arrays for pyfftw
        self.YU = sl.pyfftw_empty_aligned(self.Y.shape, dtype=self.dtype)
        self.Xf = sl.pyfftw_rfftn_empty_aligned(self.cri.shpX, self.cri.axisN,
                                                self.dtype)

        self.setdict()

    def setdict(self, D=None):
        """Set dictionary array."""

        if D is not None:
            self.D = np.asarray(D, dtype=self.dtype)
        self.Df = sl.rfftn(self.D, self.cri.Nv, self.cri.axisN)

        self.GDf = self.Gf * (self.Wtv * self.Df)[..., np.newaxis]

        # Compute D^H S
        self.DSf = np.conj(self.Df) * self.Sf
        if self.cri.Cd > 1:
            self.DSf = np.sum(self.DSf, axis=self.cri.axisC, keepdims=True)

    def block_sep0(self, Y):
        """Separate variable into component corresponding to Y0 in Y."""

        return Y[..., 0:self.cri.M]

    def block_sep1(self, Y):
        """Separate variable into component corresponding to Y1 in Y."""

        Y1 = Y[..., self.cri.M:]

        # If cri.Cd > 1 (multi-channel dictionary), we need to undo the
        # reshape performed in block_cat
        if self.cri.Cd > 1:
            shp = list(Y1.shape)
            shp[self.cri.axisM] = self.cri.dimN
            shp[self.cri.axisC] = self.cri.Cd
            Y1 = Y1.reshape(shp)

        # Axes are swapped here for similar reasons to those
        # motivating swapping in cbpdn.ConvTwoBlockCnstrnt.block_sep0
        Y1 = np.swapaxes(Y1[..., np.newaxis], self.cri.axisM, -1)

        return Y1

    def block_cat(self, Y0, Y1):
        """Concatenate components corresponding to Y0 and Y1 blocks
        into Y.
        """

        # Axes are swapped here for similar reasons to those
        # motivating swapping in cbpdn.ConvTwoBlockCnstrnt.block_cat
        Y1sa = np.swapaxes(Y1, self.cri.axisM, -1)[..., 0]

        # If cri.Cd > 1 (multi-channel dictionary) Y0 has a singleton
        # channel axis but Y1 has a non-singleton channel axis. To make
        # it possible to concatenate Y0 and Y1, we reshape Y1 by a
        # partial ravel of axisM and axisC onto axisM.
        if self.cri.Cd > 1:
            shp = list(Y1sa.shape)
            shp[self.cri.axisM] *= shp[self.cri.axisC]
            shp[self.cri.axisC] = 1
            Y1sa = Y1sa.reshape(shp)

        return np.concatenate((Y0, Y1sa), axis=self.cri.axisM)

    def xstep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{x}`."""

        self.YU[:] = self.Y - self.U
        YUf = sl.rfftn(self.YU, None, self.cri.axisN)
        YUf0 = self.block_sep0(YUf)
        YUf1 = self.block_sep1(YUf)

        b = self.rho * np.sum(np.conj(self.GDf) * YUf1, axis=-1)
        if self.cri.Cd > 1:
            b = np.sum(b, axis=self.cri.axisC, keepdims=True)
        b += self.DSf + self.rho * YUf0

        # Concatenate multiple GDf components on axisC. For
        # single-channel signals, and multi-channel signals with a
        # single-channel dictionary, we end up with sl.solvemdbi_ism
        # solving a linear system of rank dimN+1 (corresponding to the
        # dictionary and a gradient operator per spatial dimension) plus
        # an identity. For multi-channel signals with a multi-channel
        # dictionary, we end up with sl.solvemdbi_ism solving a linear
        # system of rank C.d (dimN+1) (corresponding to the dictionary
        # and a gradient operator per spatial dimension for each
        # channel) plus an identity.

        # The structure of the linear system to be solved depends on the
        # number of channels in the signal and dictionary. Both branches are
        # the same in the single-channel signal case (the choice of handling
        # it via the 'else' branch is somewhat arbitrary).
        if self.cri.C > 1 and self.cri.Cd == 1:
            # Concatenate multiple GDf components on the final axis
            # of GDf (that indexes the number of gradient operators). For
            # multi-channel signals with a single-channel dictionary,
            # sl.solvemdbi_ism has to solve a linear system of rank dimN+1
            # (corresponding to the dictionary and a gradient operator per
            # spatial dimension)
            DfGDf = np.concatenate([
                self.Df[..., np.newaxis],
            ] + [
                np.sqrt(self.rho) * self.GDf[..., k, np.newaxis]
                for k in range(self.GDf.shape[-1])
            ],
                                   axis=-1)
            self.Xf[:] = sl.solvemdbi_ism(DfGDf, self.rho, b[..., np.newaxis],
                                          self.cri.axisM, -1)[..., 0]
        else:
            # Concatenate multiple GDf components on axisC. For multi-channel
            # signals with a multi-channel dictionary, sl.solvemdbi_ism has
            # to solve a linear system of rank C.d (dimN+1) (corresponding to
            # the dictionary and a gradient operator per spatial dimension
            # for each channel) plus an identity.
            DfGDf = np.concatenate([
                self.Df,
            ] + [
                np.sqrt(self.rho) * self.GDf[..., k]
                for k in range(self.GDf.shape[-1])
            ],
                                   axis=self.cri.axisC)
            self.Xf[:] = sl.solvemdbi_ism(DfGDf, self.rho, b, self.cri.axisM,
                                          self.cri.axisC)

        self.X = sl.irfftn(self.Xf, self.cri.Nv, self.cri.axisN)

        if self.opt['LinSolveCheck']:
            if self.cri.C > 1 and self.cri.Cd == 1:
                Dop = lambda x: sl.inner(
                    DfGDf, x[..., np.newaxis], axis=self.cri.axisM)
                DHop = lambda x: sl.inner(np.conj(DfGDf), x, axis=-1)
                ax = DHop(Dop(self.Xf))[..., 0] + self.rho * self.Xf
            else:
                Dop = lambda x: sl.inner(DfGDf, x, axis=self.cri.axisM)
                DHop = lambda x: sl.inner(
                    np.conj(DfGDf), x, axis=self.cri.axisC)
                ax = DHop(Dop(self.Xf)) + self.rho * self.Xf
            self.xrrs = sl.rrs(ax, b)
        else:
            self.xrrs = None

    def ystep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{y}`."""

        AXU = self.AX + self.U
        self.block_sep0(self.Y)[:] = sp.prox_l1(
            self.block_sep0(AXU), (self.lmbda / self.rho) * self.Wl1)
        self.block_sep1(self.Y)[:] = sp.prox_l2(self.block_sep1(AXU),
                                                self.mu / self.rho,
                                                axis=(self.cri.axisC, -1))

    def obfn_fvarf(self):
        """Variable to be evaluated in computing data fidelity term,
        depending on ``fEvalX`` option value.
        """

        return self.Xf if self.opt['fEvalX'] else \
            sl.rfftn(self.block_sep0(self.Y), None, self.cri.axisN)

    def var_y0(self):
        r"""Get :math:`\mathbf{y}_0` variable, the block of
        :math:`\mathbf{y}` corresponding to the identity operator."""

        return self.block_sep0(self.Y)

    def var_y1(self):
        r"""Get :math:`\mathbf{y}_1` variable, consisting of all blocks of
        :math:`\mathbf{y}` corresponding to a gradient operator."""

        return self.block_sep1(self.Y)

    def var_yx(self):
        r"""Get component block of :math:`\mathbf{y}` that is constrained to
        be equal to :math:`\mathbf{x}`"""

        return self.var_y0()

    def var_yx_idx(self):
        r"""Get index expression for component block of :math:`\mathbf{y}`
        that is constrained to be equal to :math:`\mathbf{x}`.
        """

        return np.s_[..., 0:self.cri.M]

    def getmin(self):
        """Get minimiser after optimisation."""

        return self.X if self.opt['ReturnX'] else self.var_y0()

    def getcoef(self):
        """Get final coefficient array."""

        return self.getmin()

    def obfn_g0var(self):
        """Variable to be evaluated in computing the TV regularisation
        term, depending on the ``gEvalY`` option value.
        """

        # Use of self.block_sep0(self.AXnr) instead of self.cnst_A0(self.X)
        # reduces number of calls to self.cnst_A0
        return self.var_y0() if self.opt['gEvalY'] else \
            self.block_sep0(self.AXnr)

    def obfn_g1var(self):
        r"""Variable to be evaluated in computing the :math:`\ell_1`
        regularisation term, depending on the ``gEvalY`` option value.
        """

        # Use of self.block_sep1(self.AXnr) instead of self.cnst_A1(self.X)
        # reduces number of calls to self.cnst_A0
        return self.var_y1() if self.opt['gEvalY'] else \
            self.block_sep1(self.AXnr)

    def obfn_gvar(self):
        """Method providing compatibility with the interface of
        :class:`.admm.cbpdn.ConvBPDN` and derived classes in order to make
        this class compatible with classes such as :class:`.AddMaskSim`.
        """

        return self.obfn_g1var()

    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:]

    def obfn_dfd(self):
        r"""Compute data fidelity term :math:`(1/2) \| \sum_m \mathbf{d}_m *
        \mathbf{x}_m - \mathbf{s} \|_2^2`.
        """

        Ef = sl.inner(self.Df, self.obfn_fvarf(), axis=self.cri.axisM) \
             - self.Sf
        return sl.rfl2norm2(Ef, self.S.shape, axis=self.cri.axisN) / 2.0

    def obfn_reg(self):
        """Compute regularisation term and contribution to objective
        function.
        """

        rl1 = np.linalg.norm((self.Wl1 * self.obfn_g0var()).ravel(), 1)
        rtv = np.sum(
            np.sqrt(np.sum(self.obfn_g1var()**2, axis=(self.cri.axisC, -1))))
        return (self.lmbda * rl1 + self.mu * rtv, rl1, rtv)

    def itstat_extra(self):
        """Non-standard entries for the iteration stats record tuple."""

        return (self.xrrs, )

    def cnst_A0(self, X):
        r"""Compute :math:`A_0 \mathbf{x}` component of ADMM problem
        constraint. In this case :math:`A_0 \mathbf{x} = \mathbf{x}`.
        """

        return X

    def cnst_A0T(self, Y0):
        r"""Compute :math:`A_0^T \mathbf{y}_0` component of
        :math:`A^T \mathbf{y}`. In this case :math:`A_0^T \mathbf{y}_0 =
        \mathbf{y}_0`, i.e. :math:`A_0 = I`.
        """

        return Y0

    def cnst_A1(self, X, Xf=None):
        r"""Compute :math:`A_1 \mathbf{x}` component of ADMM problem
        constraint. In this case :math:`A_1 \mathbf{x} = (\Gamma_0^T \;\;
        \Gamma_1^T \;\; \ldots )^T \mathbf{x}`.
        """

        if Xf is None:
            Xf = sl.rfftn(X, axes=self.cri.axisN)
        return sl.irfftn(
            sl.inner(self.GDf, Xf[..., np.newaxis], axis=self.cri.axisM),
            self.cri.Nv, self.cri.axisN)

    def cnst_A1T(self, Y1):
        r"""Compute :math:`A_1^T \mathbf{y}_1` component of
        :math:`A^T \mathbf{y}`. In this case :math:`A_1^T \mathbf{y}_1 =
        (\Gamma_0^T \;\; \Gamma_1^T \;\; \ldots) \mathbf{y}_1`.
        """

        Y1f = sl.rfftn(Y1, None, axes=self.cri.axisN)
        return sl.irfftn(np.conj(self.GDf) * Y1f, self.cri.Nv, self.cri.axisN)

    def cnst_A(self, X, Xf=None):
        r"""Compute :math:`A \mathbf{x}` component of ADMM problem
        constraint. In this case :math:`A \mathbf{x} = (I \;\; \Gamma_0^T
        \;\; \Gamma_1^T \;\; \ldots)^T \mathbf{x}`.
        """

        return self.block_cat(self.cnst_A0(X), self.cnst_A1(X, Xf))

    def cnst_AT(self, Y):
        r"""Compute :math:`A^T \mathbf{y}`. In this case
        :math:`A^T \mathbf{y} = (I \;\; \Gamma_0^T \;\; \Gamma_1^T \;\;
        \ldots) \mathbf{y}`.
        """

        return self.cnst_A0T(self.block_sep0(Y)) + \
            np.sum(self.cnst_A1T(self.block_sep1(Y)), axis=-1)

    def cnst_B(self, Y):
        r"""Compute :math:`B \mathbf{y}` component of ADMM problem
        constraint. In this case :math:`B \mathbf{y} = -\mathbf{y}`.
        """

        return -Y

    def cnst_c(self):
        r"""Compute constant component :math:`\mathbf{c}` of ADMM problem
        constraint. In this case :math:`\mathbf{c} = \mathbf{0}`.
        """

        return 0.0

    def relax_AX(self):
        """Implement relaxation if option ``RelaxParam`` != 1.0."""

        # We need to keep the non-relaxed version of AX since it is
        # required for computation of primal residual r
        self.AXnr = self.cnst_A(self.X, self.Xf)
        if self.rlx == 1.0:
            # If RelaxParam option is 1.0 there is no relaxation
            self.AX = self.AXnr
        else:
            # Avoid calling cnst_c() more than once in case it is expensive
            # (e.g. due to allocation of a large block of memory)
            if not hasattr(self, '_cnst_c'):
                self._cnst_c = self.cnst_c()
            # Compute relaxed version of AX
            alpha = self.rlx
            self.AX = alpha * self.AXnr - (1 - alpha) * (self.cnst_B(self.Y) -
                                                         self._cnst_c)

    def reconstruct(self, X=None):
        """Reconstruct representation."""

        if X is None:
            Xf = self.Xf
        else:
            Xf = sl.rfftn(X, None, self.cri.axisN)
        Sf = np.sum(self.Df * Xf, axis=self.cri.axisM)
        return sl.irfftn(Sf, self.cri.Nv, self.cri.axisN)
예제 #29
0
class CSCl1l2(cbpdn.ConvBPDN):
    r"""Convolutional sparse coding with difference of :math:`\ell_1`
    and :math:`\ell_2` norm regularization.
    """
    class Options(cbpdn.ConvBPDN.Options):
        r"""CSCl1l2 algorithm options

        Options include all of those defined in
        :class:`.cbpdn.ConvBPDN.Options`, together with additional options:

          ``DatFidNoDC`` : Flag indicating whether the frequency domain
          weighting should be applied so that the value of the data
          fidelity term is independent of the DC offset of the signal.

          ``beta`` : Scaling factor in the regularization term
          :math:`\|\mathbf{x}\|_1 - \beta \|\mathbf{x}\|_2`. The default
          value is 1.0.
        """

        defaults = copy.deepcopy(cbpdn.ConvBPDN.Options.defaults)
        defaults.update({'DatFidNoDC': False, 'beta': 1.0})

    itstat_fields_objfn = ('ObjFun', 'DFid', 'RegL1L2')
    hdrtxt_objfn = ('Fnc', 'DFid', u('Regℓ1-ℓ2'))
    hdrval_objfun = {'Fnc': 'ObjFun', 'DFid': 'DFid', u('Regℓ1-ℓ2'): 'RegL1L2'}

    def __init__(self, D, S, lmbda, opt=None, dimN=2):
        """
        Initalize the CSC solver object.

        Parameters
        ----------
        D : array_like
          Dictionary array
        S : array_like
          Signal array
        opt : :class:`CSCl1l2.Options` object
          Algorithm options
        dimN : int, optional (default 2)
          Number of spatial/temporal dimensions
        """

        self.beta = opt['beta']
        super(CSCl1l2, self).__init__(D, S, lmbda=lmbda, opt=opt, dimN=dimN)

    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)

        if self.opt['DatFidNoDC']:
            if self.cri.dimN == 1:
                self.Df[0] = 0.0
            else:
                self.Df[0, 0] = 0.0

        self.DSf = np.conj(self.Df) * self.Sf
        if self.cri.Cd > 1:  # NB: Not tested
            self.DSf = np.sum(self.DSf, axis=self.cri.axisC, keepdims=True)
        if self.opt['HighMemSolve'] and self.cri.Cd == 1:
            self.c = sl.solvedbi_sm_c(self.Df, np.conj(self.Df), self.rho,
                                      self.cri.axisM)
        else:
            self.c = None

    def setS(self, S):
        """Set signal array."""

        self.S = np.asarray(S.reshape(self.cri.shpS), dtype=self.dtype)
        self.Sf = rfftn(self.S, None, self.cri.axisN)

    def ystep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{y}`.
        """

        self.Y = np.asarray(prox_dl1l2(self.AX + self.U,
                                       (self.lmbda / self.rho) * self.wl1,
                                       self.beta),
                            dtype=self.dtype)
        cbpdn.GenericConvBPDN.ystep(self)

    def obfn_dfd(self):
        """Compute data fidelity term."""

        if self.opt['DatFidNoDC']:
            Sf = self.Sf.copy()
            if self.cri.dimN == 1:
                Sf[0] = 0
            else:
                Sf[0, 0] = 0
        else:
            Sf = self.Sf
        Ef = inner(self.Df, self.obfn_fvarf(), axis=self.cri.axisM) - Sf
        return rfl2norm2(Ef, self.S.shape, axis=self.cri.axisN) / 2.0

    def obfn_reg(self):
        """Compute regularisation term."""

        rl1 = norm_dl1l2((self.wl1 * self.obfn_gvar()).ravel(), self.beta)
        return (self.lmbda * rl1, rl1)
예제 #30
0
파일: bpdn.py 프로젝트: nikzasel/sporco
class BPDN(GenericBPDN):
    r"""
    ADMM algorithm for the Basis Pursuit DeNoising (BPDN)
    :cite:`chen-1998-atomic` problem.

    |

    .. inheritance-diagram:: BPDN
       :parts: 2

    |


    Solve the Single Measurement Vector (SMV) BPDN problem

    .. math::
       \mathrm{argmin}_\mathbf{x} \;
       (1/2) \| D \mathbf{x} - \mathbf{s} \|_2^2 + \lambda \| \mathbf{x}
       \|_1

    via the ADMM problem

    .. math::
       \mathrm{argmin}_{\mathbf{x}, \mathbf{y}} \;
       (1/2) \| D \mathbf{x} - \mathbf{s} \|_2^2 + \lambda \| \mathbf{y}
       \|_1 \quad \text{such that} \quad \mathbf{x} = \mathbf{y} \;\;.


    The Multiple Measurement Vector (MMV) BPDN problem

    .. math::
       \mathrm{argmin}_X \;
       (1/2) \| D X - S \|_F^2 + \lambda \| X \|_1

    is 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) \| D
       \mathbf{x} - \mathbf{s} \|_2^2`

       ``RegL1`` : Value of regularisation term :math:`\| \mathbf{x}
       \|_1`

       ``PrimalRsdl`` : Norm of primal residual

       ``DualRsdl`` : Norm of dual residual

       ``EpsPrimal`` : Primal residual stopping tolerance
       :math:`\epsilon_{\mathrm{pri}}`

       ``EpsDual`` : Dual residual stopping tolerance
       :math:`\epsilon_{\mathrm{dua}}`

       ``Rho`` : Penalty parameter

       ``Time`` : Cumulative run time
    """
    class Options(GenericBPDN.Options):
        r"""BPDN algorithm options

        Options include all of those defined in
        :class:`.GenericBPDN.Options`, together with additional
        options:

          ``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
          \| \mathbf{w} \odot \mathbf{x} \|_1` where :math:`\mathbf{w}`
          denotes the weighting array.
        """

        defaults = copy.deepcopy(GenericBPDN.Options.defaults)
        defaults.update({'L1Weight': 1.0})

        def __init__(self, opt=None):
            """
            Parameters
            ----------
            opt : dict or None, optional (default None)
              BPDN algorithm options
            """

            if opt is None:
                opt = {}
            GenericBPDN.Options.__init__(self, opt)

    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):
        """
        |

        **Call graph**

        .. image:: ../_static/jonga/bpdn_init.svg
           :width: 20%
           :target: ../_static/jonga/bpdn_init.svg

        |


        Parameters
        ----------
        D : array_like, shape (N, M)
          Dictionary matrix
        S : array_like, shape (N, K)
          Signal vector or matrix
        lmbda : float
          Regularisation parameter
        opt : :class:`BPDN.Options` object
          Algorithm options
        """

        # Set default options if necessary
        if opt is None:
            opt = BPDN.Options()

        # 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:
            DTS = D.T.dot(S)
            lmbda = 0.1 * abs(DTS).max()

        # Set l1 term scaling and weight array
        self.lmbda = self.dtype.type(lmbda)
        self.wl1 = np.asarray(opt['L1Weight'], dtype=self.dtype)

        # Set penalty parameter
        self.set_attr('rho',
                      opt['rho'],
                      dval=(50.0 * self.lmbda + 1.0),
                      dtype=self.dtype)

        # Set rho_xi attribute (see Sec. VI.C of wohlberg-2015-adaptive)
        if self.lmbda != 0.0:
            rho_xi = float((1.0 + (18.3)**(np.log10(self.lmbda) + 1.0)))
        else:
            rho_xi = 1.0
        self.set_attr('rho_xi',
                      opt['AutoRho', 'RsdlTarget'],
                      dval=rho_xi,
                      dtype=self.dtype)

        super(BPDN, self).__init__(D, S, opt)

    def uinit(self, ushape):
        """Return initialiser for working variable U"""

        if self.opt['Y0'] is None:
            return np.zeros(ushape, dtype=self.dtype)
        else:
            # If initial Y is non-zero, initial U is chosen so that
            # the relevant dual optimality criterion (see (3.10) in
            # boyd-2010-distributed) is satisfied.
            return (self.lmbda / self.rho) * np.sign(self.Y)

    def ystep(self):
        r"""Minimise Augmented Lagrangian with respect to
        :math:`\mathbf{y}`."""

        self.Y = np.asarray(sp.prox_l1(self.AX + self.U,
                                       (self.lmbda / self.rho) * self.wl1),
                            dtype=self.dtype)
        super(BPDN, self).ystep()

    def obfn_reg(self):
        """Compute regularisation term and contribution to objective
        function.
        """

        rl1 = np.linalg.norm((self.wl1 * self.obfn_gvar()).ravel(), 1)
        return (self.lmbda * rl1, rl1)
예제 #31
0
파일: admm.py 프로젝트: manvhah/sporco
    def hdrtxt(cls):
        """Construct tuple of status display column titles."""

        return ('Itn',) + cls.hdrtxt_objfn + ('r', 's', u('ρ'))
예제 #32
0
    def solve(self):
        """Start (or re-start) optimisation. This method implements the
        framework for the alternation between `X` and `D` updates in a
        dictionary learning algorithm.

        If option ``Verbose`` is ``True``, the progress of the
        optimisation is displayed at every iteration. At termination
        of this method, attribute :attr:`itstat` is a list of tuples
        representing statistics of each iteration.

        Attribute :attr:`timer` is an instance of :class:`.util.Timer`
        that provides the following labelled timers:

          ``init``: Time taken for object initialisation by
          :meth:`__init__`

          ``solve``: Total time taken by call(s) to :meth:`solve`

          ``solve_wo_func``: Total time taken by call(s) to
          :meth:`solve`, excluding time taken to compute functional
          value and related iteration statistics
        """

        # Construct tuple of status display column titles and set status
        # display strings
        hdrtxt = ['Itn', 'Fnc', 'DFid', u('Regℓ1')]
        hdrstr, fmtstr, nsep = common.solve_status_str(
            hdrtxt, fwdth0=type(self).fwiter, fprec=type(self).fpothr)

        # Print header and separator strings
        if self.opt['Verbose']:
            if self.opt['StatusHeader']:
                print(hdrstr)
                print("-" * nsep)

        # Reset timer
        self.timer.start(['solve', 'solve_wo_eval'])

        # Create process pool
        if self.nproc > 0:
            self.pool = mp.Pool(processes=self.nproc)

        for self.j in range(self.j, self.j + self.opt['MaxMainIter']):

            # Perform a set of update steps
            self.step()

            # Evaluate functional
            self.timer.stop('solve_wo_eval')
            fnev = self.evaluate()
            self.timer.start('solve_wo_eval')

            # Record iteration stats
            tk = self.timer.elapsed('solve')
            itst = self.IterationStats(*((self.j,) + fnev + (tk,)))
            self.itstat.append(itst)

            # Display iteration stats if Verbose option enabled
            if self.opt['Verbose']:
                print(fmtstr % itst[:-1])

            # Call callback function if defined
            if self.opt['Callback'] is not None:
                if self.opt['Callback'](self):
                    break

        # Clean up process pool
        if self.nproc > 0:
            self.pool.close()
            self.pool.join()

        # Increment iteration count
        self.j += 1

        # Record solve time
        self.timer.stop(['solve', 'solve_wo_eval'])

        # Print final separator string if Verbose option enabled
        if self.opt['Verbose'] and self.opt['StatusHeader']:
            print("-" * nsep)

        # Return final dictionary
        return self.getdict()
예제 #33
0
class BPDN(fista.FISTA):
    r"""
    Class for FISTA algorithm for the Basis Pursuit DeNoising (BPDN)
    :cite:`chen-1998-atomic` problem.

    |

    .. inheritance-diagram:: BPDN
       :parts: 2

    |

    The problem form is

    .. math::
       \mathrm{argmin}_\mathbf{x} \; (1/2) \| D \mathbf{x} - \mathbf{s}
       \|_2^2  + \lambda \| \mathbf{x} \|_1

    where :math:`\mathbf{s}` is the input vector/matrix, :math:`D` is
    the dictionary, and :math:`\mathbf{x}` is the sparse representation.

    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) \| D
       \mathbf{x} - \mathbf{s} \|_2^2`

       ``RegL1`` : Value of regularisation term :math:`\lambda \|
       \mathbf{x} \|_1`

       ``Rsdl`` : Residual

       ``L`` : Inverse of gradient step parameter

       ``Time`` : Cumulative run time
    """
    class Options(fista.FISTA.Options):
        r"""BPDN algorithm options

        Options include all of those defined in
        :class:`.fista.FISTA.Options`, together with
        additional options:

          ``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
          \| \mathbf{w} \odot \mathbf{x} \|_1` where
          :math:`\mathbf{w}` denotes the weighting array.

        """

        defaults = copy.deepcopy(fista.FISTADFT.Options.defaults)
        defaults.update({'L1Weight': 1.0})
        defaults.update({'L': 500.0})

        def __init__(self, opt=None):
            """
            Parameters
            ----------
            opt : dict or None, optional (default None)
              BPDN algorithm options
            """

            if opt is None:
                opt = {}
            fista.FISTA.Options.__init__(self, opt)

        def __setitem__(self, key, value):
            """Set options."""

            fista.FISTA.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):
        """
        Parameters
        ----------
        D : array_like
          Dictionary array (2d)
        S : array_like
          Signal array (1d or 2d)
        lmbda : float
          Regularisation parameter
        opt : :class:`BPDN.Options` object
          Algorithm options
        """

        # Set default options if none specified
        if opt is None:
            opt = BPDN.Options()

        # 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:
            DTS = D.T.dot(S)
            lmbda = 0.1 * abs(DTS).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__
        Nc = D.shape[1]
        Nm = S.shape[1]

        xshape = (Nc, Nm)
        super(BPDN, self).__init__(xshape, S.dtype, opt)

        self.S = np.asarray(S, dtype=self.dtype)

        self.store_prev()
        self.Y = self.X.copy()
        self.Yprv = self.Y.copy() + 1e5

        self.setdict(D)

    def setdict(self, D):
        """Set dictionary array."""

        self.D = np.asarray(D, dtype=self.dtype)

    def getcoef(self):
        """Get final coefficient array."""

        return self.X

    def eval_grad(self):
        """Compute gradient in spatial domain for variable Y."""

        # Compute D^T(D Y - S)
        return self.D.T.dot(self.D.dot(self.Y) - self.S)

    def eval_proxop(self, V):
        """Compute proximal operator of :math:`g`."""

        return np.asarray(sp.prox_l1(V, (self.lmbda / self.L) * self.wl1),
                          dtype=self.dtype)

    def eval_objfn(self):
        """Compute components of objective function as well as total
        contribution to objective function.
        """

        dfd = self.obfn_f()
        reg = self.obfn_reg()
        obj = dfd + reg[0]
        return (obj, dfd) + reg[1:]

    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)

    def obfn_f(self, X=None):
        r"""Compute data fidelity term :math:`(1/2) \| D \mathbf{x} -
        \mathbf{s} \|_2^2`.
        """
        if X is None:
            X = self.X

        return 0.5 * np.linalg.norm((self.D.dot(X) - self.S).ravel())**2

    def reconstruct(self, X=None):
        """Reconstruct representation."""

        if X is None:
            X = self.X
        return self.D.dot(self.X)
예제 #34
0
    def hdrtxt(cls):
        """Construct tuple of status display column title."""

        #  return ('Itn', 'X r', 'X s', u('X ρ'), 'D cnstr', 'D dlt', u('D η'), 'Time')
        return ('Itn', 'Fnc', 'DFid', 'l1', 'r_X', 's_X', u('ρ_X'),
                'Cnstr_D', 'dlt_D', u('η_D'), 'Time')
예제 #35
0
파일: dictlrn.py 프로젝트: eglxiang/sporco
    def __init__(self, xstep, dstep, opt=None, isc=None):
        """
        Initialise a DictLearn object with problem size and options.

        Parameters
        ----------
        xstep : bpdn (or similar interface) object
          Object handling X update step
        dstep : cmod (or similar interface) object
          Object handling D update step
        opt : :class:`DictLearn.Options` object
          Algorithm options
        isc : :class:`IterStatsConfig` object
          Iteration statistics and header display configuration
        """

        if opt is None:
            opt = DictLearn.Options()
        self.opt = opt

        if isc is None:
            isc = IterStatsConfig(isfld=[
                'Iter', 'ObjFunX', 'XPrRsdl', 'XDlRsdl', 'XRho', 'ObjFunD',
                'DPrRsdl', 'DDlRsdl', 'DRho', 'Time'
            ],
                                  isxmap={
                                      'ObjFunX': 'ObjFun',
                                      'XPrRsdl': 'PrimalRsdl',
                                      'XDlRsdl': 'DualRsdl',
                                      'XRho': 'Rho'
                                  },
                                  isdmap={
                                      'ObjFunD': 'DFid',
                                      'DPrRsdl': 'PrimalRsdl',
                                      'DDlRsdl': 'DualRsdl',
                                      'DRho': 'Rho'
                                  },
                                  evlmap={},
                                  hdrtxt=[
                                      'Itn', 'FncX', 'r_X', 's_X',
                                      u('ρ_X'), 'FncD', 'r_D', 's_D',
                                      u('ρ_D')
                                  ],
                                  hdrmap={
                                      'Itn': 'Iter',
                                      'FncX': 'ObjFunX',
                                      'r_X': 'XPrRsdl',
                                      's_X': 'XDlRsdl',
                                      u('ρ_X'): 'XRho',
                                      'FncD': 'ObjFunD',
                                      'r_D': 'DPrRsdl',
                                      's_D': 'DDlRsdl',
                                      u('ρ_D'): 'DRho'
                                  })
        self.isc = isc

        self.xstep = xstep
        self.dstep = dstep

        self.itstat = []
        self.j = 0