Robust Principal Component Analysis?(PCP)

Candes E J, Li X, Ma Y, et al. Robust principal component analysis[J]. Journal of the ACM, 2011, 58(3).

这篇文章,讨论的是这样的一个问题:

M=L0+S0

有这样的一个矩阵MRn1×n2,它由一个低秩的矩阵L0和稀疏矩阵S0混合而成,那么,能否通过ML0S0分别重构出来?而且,最关键的是,也是这篇文章的亮点所在:对于L0S0仅有一丢丢的微弱的假设。

一些微弱的假设:

关于L0:
低秩矩阵L0不稀疏,这个假设很弱,但有意义。因为如果M=e1e1(文章采用作为转置符号,这里沿用这种写法),即只有左上角第一个元素非零,那么这个矩阵如何分解为低秩和稀疏呢,因为压根没法区分。作者引入incoherence condition(不连贯条件?抱歉,不晓得如何翻译):
假设L0的SVD分解为:

L0=UΣV=i=1rσiuivi

其中r=rank(L0),σi为奇异值,并且,U=[u1,,ur],V=[v1,,vr]
incoherence condition:
在这里插入图片描述
分析这些条件可以发现,U,V的每一行的模都不能太大,UV的每个元素同样不能太大,所以最后结果L0的各个元素的大小会比较均匀,从而保证不稀疏?

关于S0:

类似地,需要假设S0不低秩,论文另辟蹊径,假设S0的基为m=Card(S0),其支撑(support)定义为:

Ω={(i,j)|[S0]i,j0}

论文假设S0的支撑Ω从一个均匀分布采样。即,S0不为零的项为m,从n1×n2个元素中挑选m个非零的组合有Cn1×n2m,论文假设每一种组合被采样的概率都是相同的。事实上,关于S0元素的符号,论文在后面说明,符号固定或者随机(服从伯努利分布),都能将L0,S0恢复出来。

问题的解决

论文反复强调,多么令人惊讶,多么不可思议,事实上的确如此,上述问题只需求解一个形式非常简单的凸优化问题:
在这里插入图片描述
其中L=i=1rσi(L)为其核范数。

论文给出了以下结论:
在这里插入图片描述
在这里插入图片描述
即:令n(1)=max(n1,n2),n(2)=min(n1,n2),对于任意矩阵M=L0+S0Rn1×n2,只要L0S0满足上面提到的假设,那么就存在常数c使得,PCP的解(即(1.1)的解,且λ=1/n(1)L^S^1cn(1)10的概率重构L0S0,即L^=L0S^=S0。并且有下列性质被满足rank(L0)ρrn(2)μ1(logn(1))2,mρsn1n2

这个结果需要说明的几点是,常数c是根据S0的支撑Ω决定的(根据后面的理论,实际上是跟PΩPT有关),另外一点是,λ=1/n(1)。是的,论文给出了λ的选择(虽然这个选择不是唯一的),而且,这个值是通过理论分析得到的。

关于问题 (1.1)的求解有很多现成的算法,这篇论文大量篇幅用于证明上述的理论结果,只在最后提到利用ALM(Augmented Largrange multiplier)方法有很多优势,算法如下:
ALM
其中Sτ[x]=sgn(x)max(|x|τ,0), Dτ(X)=USτ(Σ)V,X=UΣV

ALM算法实际上是交替最小化下式:
在这里插入图片描述
其中<A,B>:=Tr(ATB)

固定L,Y,实际上就是最小化:

minλS1+<Y,S>+μ2Tr(STS2ST(ML))

其在S处的导数(虽然有些点并不可导)为:

λsgn(S)Y+μSμ(ML)

实际上,对于每个Si,j,与其相关的为:

aSi,j2+bSi,j+c|Si,j|,α>0

关于最小化这个式子,我们可以得到:

Si,j={b+c2ab+c2a0bc2abc2a00else

实际上就是Si,j=Sc/2a(b/2a),对S拆解,容易得到固定L,Y的条件下,S的最优解为:

Sλ/μ(ML+μ1Y)

S,Y固定的时候:

实际上就是最小化下式:

L+<Y,L>+μ2Tr(LTLLT(MS))

观测其次梯度:

UV+WY+μLμ(MS)

其中L=UΣVUW=0,WV=0,W1XX的算子范数。
最小值点应当满足0为其次梯度,即:

L=μ1UVμ1W+μ1Y+MS

有解。
A=MS+μ1Y
则:

L=μ1UVμ1W+AΣ=μ1I+UAV

假设A=UAΣAVA,那么:

Σ=μ1I+UUAΣAVAV

如果我们令U=UA,V=VA,则:

Σ=ΣAμ1I

但是我们知道,Σ的对角元素必须大于等于0,所以我可以这样构造解:
假设ΣA的对角元素,即奇异值降序排列σ1(A)σ2(A)σk(A)μ1>σk+1(A)σr(A),相对于的向量为u1(A),,ur(A)v1A,,vr(A)。我们令

L=i=1k(σi(A)μ1)ui(A)viT(A)W=i=k+1rμσi(A)ui(A)viT(A)

容易验证UW=0,WV=0,又μ1>σi(A),i>k,所以μσi(A)<1,所以W1也满足。同样的,次梯度为0的等式也满足,所以L就是我们要找到点(因为这个是凸函数,极值点就是最小值点)。

理论

这里只介绍一下论文的证明思路。
符号:
在这里插入图片描述

去随机

论文先证明,如果S0的符号是随机的(伯努利分布),在这种情况能恢复,那么S0的符号即便不随机也能恢复。
在这里插入图片描述

Dual Certificates(对偶保证?)

引理2.4

引理2.4告诉我们,只要我们能找到一对(W,F),满足UV+W=λ(sgn(S0)+F)(L0,S0)是优化问题(1.1)的唯一解。但是不够,因为这个条件太强了,W,F的构造是困难的。
在这里插入图片描述

引理2.5

进一步改进,有了引理2.5。
在这里插入图片描述
引理2.5的意义在哪呢?它意味着,我们只要找到一个W,满足:
在这里插入图片描述
那么,(L0,S0)就是问题(1.1)的最优解,而且是唯一的。

Golfing Scheme

接下来,作者介绍一种机制来构造W,并且证明这种证明能够大概率保证W能够满足上述的对偶条件。作者将W=WL+WS分解成俩部分,俩部分用不同的方式构造:
在这里插入图片描述
接下的引理和推论,总结起来便是最后的证明:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过引理2.8和2.9,再利用范数的三角不等式,容易验证W=WL+WS满足对偶条件。
最后提一下,定理的概率体现在哪里,在对偶条件的引理中,有这么一个假设PΩPT1PΩPT1/2,这些条件并不一定成立,根据Ω的采样,成立的大概率的。

数值实验

按照论文里,普通的人工生成矩阵,实验下来和论文的无异。也的确,S0的规模大小(每个元素的大小)对能否恢复几乎没有影响,有影响的是S0的稀疏度。实验下来,稀疏度越高(0基越小),恢复的越好,而且差距蛮大的。

我们的实验,L=XY,其中X,YR500×25依标准正态分布生成的,S0是每个元素有ρ的概率为100ρ的概率为112ρ的概率为0。ρ我们选了1/10,1/15,,1/55
下面的图,红色表示L^L0F/L0F,蓝色表示S^S0F/S0F。因为每种情况我只做了一次实验,所以结果有起伏,但是趋势是在的。
在这里插入图片描述

比较有意思的是图片的拆解,我在马路上拍了31张照片,按照论文的解释,通过将照片拉成向量,再混合成一个矩阵M,然后通过优化问题解出L^S^,那么L^的行向量(我做实验的时候是将一个照片作为一个行向量,论文里的是列向量)重新展成图片应该是类似背景的东西,而S^中对应的行向量应该是原先图片中会动的东西。

照片我进行了压缩(416×233)和灰度处理,处理M用了1412次迭代,时间大概15分钟?比我预想的快多了。
我挑选其中比较混乱的图(就是车比较多的):
第一组:
M:
在这里插入图片描述
L:
在这里插入图片描述
S:
在这里插入图片描述
第二组:

M:
在这里插入图片描述

L:
在这里插入图片描述
S:
在这里插入图片描述
第三组:

M:

在这里插入图片描述
L:
在这里插入图片描述
S:

在这里插入图片描述
比较遗憾的是,S^中都出现了面包车,我以为面包车会没有的,结果还是出现了。

代码


"""
Robust Principal Component Analysis?
"""


import numpy as np

class RobustPCA:

    def __init__(self, M, delta=1e-7):
        self.__M = np.array(M, dtype=float)
        self.__n1, self.__n2 = M.shape
        self.S = np.zeros((self.__n1, self.__n2), dtype=float)
        self.Y = np.zeros((self.__n1, self.__n2), dtype=float)
        self.L = np.zeros((self.__n1, self.__n2), dtype=float)
        self.mu = self.__n1 * self.__n2 / (4 * self.norm_l1)
        self.lam = 1 / np.sqrt(max(self.__n1, self.__n2))
        self.delta = delta

    @property
    def norm_l1(self):
        """返回M的l1范数"""
        return np.sum(np.abs(self.__M))

    @property
    def stoprule(self):
        """停止准则"""
        A = (self.__M - self.L - self.S) ** 2
        sum_A = np.sqrt(np.sum(A))
        bound = np.sqrt(np.sum(self.__M ** 2))
        if sum_A <= self.delta * bound:
            return True
        else:
            return False
    def squeeze(self, A, c):
        """压缩"""
        if c <= 0:
            return A
        B1 = np.where(np.abs(A) < c)
        B2 = np.where(A >= c)
        B3 = np.where(A <= -c)
        A[B1] = [0.] * len(B1[0])
        A[B2] = A[B2] - c
        A[B3] = A[B3] + c
        return A

    def newsvd(self, A):
        def sqrt(l):
            newl = []
            for i in range(len(l)):
                if l[i] > 0:
                    newl.append(np.sqrt(l[i]))
                else:
                    break
            return np.array(newl)
        m, n = A.shape
        if m < n:
            C = A @ A.T
            l, u = np.linalg.eig(C)
            s = sqrt(l)
            length = len(s)
            u = u[:, :length]
            D_inverse = np.diag(1 / s)
            vh = D_inverse @ u.T @ A
            return u, s, vh
        else:
            C = A.T @ A
            l, v = np.linalg.eig(C)
            s = sqrt(l)
            length = len(s)
            v = v[:, :length]
            D_inverse = np.diag(1 / s)
            u = A @ v @ D_inverse
            return u, s, v.T

    def update_L(self):
        """更新L"""
        A = self.__M - self.S + self.Y / self.mu
        u, s, vh = np.linalg.svd(A) #or self.newsvd(A)
        s = self.squeeze(s, 1 / self.mu)
        s = s[np.where(s > 0)]
        length = len(s)
        if length is 0:
            self.L = np.zeros((self.__n1, self.__n2), dtype=float)
        elif length is 1:
            self.L = np.outer(u[:, 0] * s[0], vh[0])
        else:
            self.L = u[:, :length] * s @ vh[:length]

    def update_S(self):
        """更新S"""
        A = self.__M - self.L + self.Y / self.mu
        self.S = self.squeeze(A, self.lam / self.mu)

    def update_Y(self):
        """更新Y"""
        A = self.__M - self.L - self.S
        self.Y = self.Y + self.mu * A

    def process(self):
        count = 0
        while not (self.stoprule):
            count += 1
            assert count < 10000, "something wrong..."
            self.update_L()
            self.update_S()
            self.update_Y()
        print(count)

注意到,我自己写了一个newsvd的方法,这是因为,在处理图片的时候,用numpy的np.linalg.svd会报memoryerror,所以就自己简单调整了一下。

posted @   馒头and花卷  阅读(808)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示