引
这篇文章,讨论的是这样的一个问题:
有这样的一个矩阵 ,它由一个低秩的矩阵 和稀疏矩阵 混合而成,那么,能否通过 将 和 分别重构出来?而且,最关键的是,也是这篇文章的亮点所在:对于 和 仅有一丢丢的微弱的假设。
一些微弱的假设:
关于
:
低秩矩阵
不稀疏,这个假设很弱,但有意义。因为如果
(文章采用
作为转置符号,这里沿用这种写法),即只有左上角第一个元素非零,那么这个矩阵如何分解为低秩和稀疏呢,因为压根没法区分。作者引入incoherence condition(不连贯条件?抱歉,不晓得如何翻译):
假设
的SVD分解为:
其中
,
为奇异值,并且,
,
。
incoherence condition:
分析这些条件可以发现,
的每一行的模都不能太大,
的每个元素同样不能太大,所以最后结果
的各个元素的大小会比较均匀,从而保证不稀疏?
关于 :
类似地,需要假设
不低秩,论文另辟蹊径,假设
的基为
,其支撑(support)定义为:
论文假设
的支撑
从一个均匀分布采样。即,
不为零的项为
,从
个元素中挑选
个非零的组合有
,论文假设每一种组合被采样的概率都是相同的。事实上,关于
元素的符号,论文在后面说明,符号固定或者随机(服从伯努利分布),都能将
恢复出来。
问题的解决
论文反复强调,多么令人惊讶,多么不可思议,事实上的确如此,上述问题只需求解一个形式非常简单的凸优化问题:
其中
为其核范数。
论文给出了以下结论:
即:令
,对于任意矩阵
,只要
和
满足上面提到的假设,那么就存在常数
使得,PCP的解(即(1.1)的解,且
)
和
有
的概率重构
,
,即
,
。并且有下列性质被满足
。
这个结果需要说明的几点是,常数 是根据 的支撑 决定的(根据后面的理论,实际上是跟 有关),另外一点是, 。是的,论文给出了 的选择(虽然这个选择不是唯一的),而且,这个值是通过理论分析得到的。
关于问题 (1.1)的求解有很多现成的算法,这篇论文大量篇幅用于证明上述的理论结果,只在最后提到利用ALM(Augmented Largrange multiplier)方法有很多优势,算法如下:
其中
,
。
ALM算法实际上是交替最小化下式:
其中
。
固定
,实际上就是最小化:
其在
处的导数(虽然有些点并不可导)为:
实际上,对于每个
,与其相关的为:
关于最小化这个式子,我们可以得到:
实际上就是
,对
拆解,容易得到固定
的条件下,
的最优解为:
当
固定的时候:
实际上就是最小化下式:
观测其次梯度:
其中
,
为
的算子范数。
最小值点应当满足
为其次梯度,即:
有解。
令
则:
假设
,那么:
如果我们令
,则:
但是我们知道,
的对角元素必须大于等于0,所以我可以这样构造解:
假设
的对角元素,即奇异值降序排列
,相对于的向量为
和
。我们令
容易验证
,又
,所以
,所以
也满足。同样的,次梯度为0的等式也满足,所以
就是我们要找到点(因为这个是凸函数,极值点就是最小值点)。
理论
这里只介绍一下论文的证明思路。
符号:
去随机
论文先证明,如果
的符号是随机的(伯努利分布),在这种情况能恢复,那么
的符号即便不随机也能恢复。
Dual Certificates(对偶保证?)
引理2.4
引理2.4告诉我们,只要我们能找到一对
,满足
。
是优化问题(1.1)的唯一解。但是不够,因为这个条件太强了,
的构造是困难的。
引理2.5
进一步改进,有了引理2.5。
引理2.5的意义在哪呢?它意味着,我们只要找到一个
,满足:
那么,
就是问题
的最优解,而且是唯一的。
Golfing Scheme
接下来,作者介绍一种机制来构造
,并且证明这种证明能够大概率保证
能够满足上述的对偶条件。作者将
分解成俩部分,俩部分用不同的方式构造:
接下的引理和推论,总结起来便是最后的证明:
通过引理2.8和2.9,再利用范数的三角不等式,容易验证
满足对偶条件。
最后提一下,定理的概率体现在哪里,在对偶条件的引理中,有这么一个假设
和
,这些条件并不一定成立,根据
的采样,成立的大概率的。
数值实验
按照论文里,普通的人工生成矩阵,实验下来和论文的无异。也的确, 的规模大小(每个元素的大小)对能否恢复几乎没有影响,有影响的是 的稀疏度。实验下来,稀疏度越高(0基越小),恢复的越好,而且差距蛮大的。
我们的实验,
,其中
依标准正态分布生成的,
是每个元素有
的概率为
,
的概率为
,
的概率为0。
我们选了
。
下面的图,红色表示
,蓝色表示
。因为每种情况我只做了一次实验,所以结果有起伏,但是趋势是在的。
比较有意思的是图片的拆解,我在马路上拍了31张照片,按照论文的解释,通过将照片拉成向量,再混合成一个矩阵 ,然后通过优化问题解出 和 ,那么 的行向量(我做实验的时候是将一个照片作为一个行向量,论文里的是列向量)重新展成图片应该是类似背景的东西,而 中对应的行向量应该是原先图片中会动的东西。
照片我进行了压缩(
)和灰度处理,处理
用了1412次迭代,时间大概15分钟?比我预想的快多了。
我挑选其中比较混乱的图(就是车比较多的):
第一组:
M:
L:
S:
第二组:
M:
L:
S:
第三组:
M:
L:
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,所以就自己简单调整了一下。