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
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
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
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)
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)
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)
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)
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)
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
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)
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)
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)))
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
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)
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)
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)
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)
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)
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()
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)
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()
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)
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)
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)
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)
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
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)
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)
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)
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)
def hdrtxt(cls): """Construct tuple of status display column titles.""" return ('Itn',) + cls.hdrtxt_objfn + ('r', 's', u('ρ'))
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()
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)
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')
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