非负矩阵分解NMF(1): 非调包python实现

文章目录

        • 1. 矩阵分解(Matrix Factorization):
          • 1.1 公式推导
          • 1.2 代码实现
          • 1.3 在图像数据下的效果
        • 2. 非负矩阵分解(Non-negative Matrix Factorization)
          • 2.1 迭代公式
          • 2.2 代码部分
          • 2.3 在图像数据下的效果

在实现NMF(Non-negative Matrix Factorization)之前,先看普通的MF是怎么进行的,从而可以直观地理解为什么一定要非负矩阵分解。

1. 矩阵分解(Matrix Factorization):

1.1 公式推导

该方法由Albert Au Yeung 提供: 原文链接

首先矩阵分解的目的在于将大小为N x M的矩阵R分解成一个大小为N x K的矩阵P以及K x M的矩阵Q,使得:

R ≈ P Q T = R ^ R \approx PQ^T=\hat{R} RPQT=R^

原文中R是一个推荐系统中关于user用户和物品item的关系的矩阵,所以P的每一行都表示user和特征的相关度(Strength of Association),而Q的每一行(或 Q T Q^T QT的每一列)就表示item和特征的相关度。

这里不用理解他们表示的含义,只需要知道 R ^ \hat{R} R^中每个元素 r ^ i j = p i T . q j = Σ K = 1 K p i k q k j \hat{r}_{ij}=p_i^T.q_j=\Sigma_{K=1}^Kp_{ik}q_{kj} r^ij=piT.qj=ΣK=1Kpikqkj

那么预测值和真实值之间的error就等于:
e i j = r i j − r ^ i j = r i j − Σ K = 1 K p i k q k j e_{ij}=r_{ij}-\hat{r}_{ij} = r_{ij}-\Sigma_{K=1}^Kp_{ik}q_{kj} eij=rijr^ij=rijΣK=1Kpikqkj

e i j 2 e_{ij}^2 eij2关于 p i k p_{ik} pik求导/梯度就等于:

∂ ∂ p i k e i j 2 = 2 e i j e i j ′ = 2 e i j . ( − q k j ) = − 2 e i j q k j \frac{\partial}{\partial p_{ik}}e_{ij}^2 = 2e_{ij}e_{ij}'=2e_{ij}.(-q_{kj})=-2e_{ij}q_{kj} pikeij2=2eijeij=2eij.(qkj)=2eijqkj

同理关于 q k j q_{kj} qkj求导/梯度就等于:
∂ ∂ q k j e i j 2 = − 2 e i j p i k \frac{\partial}{\partial q_{kj}}e_{ij}^2=-2e_{ij}p_{ik} qkjeij2=2eijpik

根据梯度更新 p ′ = p − l e a r n i n g r a t e ∗ g r a d i e n t p' = p-learningrate*gradient p=plearningrategradient

p i k ′ = p i k + α ∂ ∂ p i k e i j 2 = p i k + 2 α e i j q k j p_{ik}'=p_{ik}+\alpha \frac{\partial}{\partial p_{ik}}e_{ij}^2=p_{ik}+2\alpha e_{ij}q_{kj} pik=pik+αpikeij2=pik+2αeijqkj

q k j ′ = q k j + α ∂ ∂ q k j e i j 2 = q k j + 2 α e i j p i k q_{kj}'=q_{kj}+\alpha \frac{\partial}{\partial q_{kj}}e_{ij}^2=q_{kj}+2\alpha e_{ij}p_{ik} qkj=qkj+αqkjeij2=qkj+2αeijpik

所以代码实现就很容易了,只需要提供两个随机初始化矩阵P,Q,以及R,学习率 α \alpha α,迭代次数以及一个K用来表示潜在的feature(它相当于限定分解之后的矩阵的大小)

当然,到这里其实已经可以实现代码了,不过原文中还加了正则Regularization用来防止过拟合。

添加了正则项之后的

e i j 2 = ( r i j − Σ K = 1 K p i k q k j ) 2 + β 2 Σ K = 1 K ( ∣ ∣ P ∣ ∣ 2 + ∣ ∣ Q ∣ ∣ 2 ) e_{ij}^2=( r_{ij}-\Sigma_{K=1}^Kp_{ik}q_{kj})^2+\frac{\beta}{2}\Sigma_{K=1}^K(||P||^2 + ||Q||^2) eij2=(rijΣK=1Kpikqkj)2+2βΣK=1K(P2+Q2)

梯度更新为:

p i k ′ = p i k + α ( 2 e i j q k j − β p i k ) p_{ik}'=p_{ik}+\alpha (2e_{ij}q_{kj}-\beta p_{ik}) pik=pik+α(2eijqkjβpik)

同理:
q k j ′ = q k j + α ( 2 e i j p i k − β q k j ) q_{kj}'=q_{kj}+\alpha (2e_{ij}p_{ik}-\beta q_{kj}) qkj=qkj+α(2eijpikβqkj)

1.2 代码实现

python代码如下,和原文链接中代码有所不同

def matrix_factorization(R, P, Q, K, steps=500, alpha=0.0002, beta=0.02):
    Q = Q.T
    for step in range(steps):
        for i in range(len(R)):
            for j in range(len(R[i])):
                if R[i][j] > 0: #计算error
                    eij = R[i][j] - np.dot(P[i,:],Q[:,j]) 
                    for k in range(K): #更新
                        P[i][k] = P[i][k] + alpha * (2*eij*Q[k][j] - beta*P[i][k])
                        Q[k][j] = Q[k][j] + alpha * (2*eij*P[i][k] - beta*Q[k][j])
    return P, Q.T

拿一个矩阵试验一下:

R = np.array([
    [5, 3, 0, 1],
    [4, 0, 0, 1],
    [1, 1, 0, 5],
    [1, 0, 0, 4],
    [0, 1, 5, 4],
])

N = len(R)
M = len(R[0])
K = 2

# initialize 
rng = np.random.RandomState(1)
P = rng.rand(N,K)
Q = rng.rand(M,K)

P_estimate, Q_estimate = matrix_factorization(R, P, Q, K)
print(P_estimate,"\n")
print(Q_estimate)

结果就不展示了,会生成一个5x2( P P P)和4x2( Q T Q^T QT)的矩阵

1.3 在图像数据下的效果

建议在colab下运行,本地可能会有skimage报错问题。。

#导入相应的包
!pip install update scikit-image

import numpy as np
from sklearn.datasets import fetch_olivetti_faces
import matplotlib.pyplot as plt
from time import time
from skimage.transform import resize 
#导入图像数据,具体过程可以暂时不用理解
faces = fetch_olivetti_faces()
data_faces = faces.data
images_faces = faces['images']
number_of_train = 100
images_faces_train = images_faces[:number_of_train,:,:]
images_faces_train = np.transpose(resize(np.transpose(images_faces_train,[1,2,0]),[16,16]),[2,0,1])
data_faces_train = data_faces[:number_of_train,:]
data_faces_train = np.transpose(resize(np.transpose(data_faces_train.reshape(number_of_train,64,64),[1,2,0]),[16,16]),[2,0,1])
data_faces_train = data_faces_train.reshape(number_of_train,-1)
n_samples = len(images_faces_train)
image_shape = images_faces_train[0].shape
#查看一下图片
data_faces_centered = data_faces_train - data_faces_train.mean(axis=0)

# local centering
data_faces_centered -= data_faces_centered.mean(axis=1).reshape(n_samples, -1)

# Let's show some centered faces
plt.figure(figsize=(20, 2))
plt.suptitle("Centered Olivetti Faces", size=16)
for i in range(10):
    plt.subplot(1, 10, i+1)
    plt.imshow(data_faces_centered[i].reshape(image_shape), cmap=plt.cm.gray)
    plt.xticks(())
    plt.yticks(())
    
R = data_faces_centered

结果如下:
在这里插入图片描述
resize的作用可以使图像变模糊,使得后续的MF算法执行更快

测试MF代码:

N = len(R)
M = len(R[0])
K = 2
#图像太清晰了打印有点久
# initialize 
rng = np.random.RandomState(1)
P = rng.rand(N,K)
Q = rng.rand(M,K)

P_estimate, Q_estimate = matrix_factorization(R, P, Q, K)

#show the learned basis
plt.figure(figsize=(6, 3))
plt.suptitle("learned basises from MF", size=16)
for i in range(K):
    plt.subplot(1, K, i+1)
    plt.imshow(Q[:,i].reshape(16,16), cmap=plt.cm.gray)
    plt.xticks(())
    plt.yticks(())

结果如下:
非负矩阵分解NMF(1): 非调包python实现_第1张图片

可以看出经过分解之后的矩阵所包含的信息是不太具有分析价值的,这也是为什么要进行非负矩阵分解(NMF)的原因。

2. 非负矩阵分解(Non-negative Matrix Factorization)

2.1 迭代公式

理论参考至《矩阵分析于应用(第二版》(张贤达 著)第365页, 网上书籍链接

这里的实现是基于365页的“平方Euclidean距离最小化的乘法算法”,其他实现方式不在这里描述。

愿问题就变成了使用典型的平方欧式距离作为代价函数的无约束优化问题 m i n D E ( R ∣ ∣ P Q ) = 1 2 ∣ ∣ R − P Q ∣ ∣ 2 minD_E(R||PQ)=\frac{1}{2}||R-PQ||^2 minDE(RPQ)=21RPQ2(书中R,P,Q分别用X,A,S表示)

说的更好理解一点就是一个关于 R R R R ^ \hat{R} R^的最小二乘问题,使得分解之后由 P , Q P,Q P,Q构成的 R ^ \hat{R} R^ R R R更接近。

其梯度下降公式为:
p i k = p i k − μ i k ∂ D E ( R ∣ ∣ P Q ) ∂ p i k = p i k + μ i k [ ( R − P Q ) Q T ] i k p_{ik}=p_{ik}-\mu _{ik}\frac{\partial D_E(R||PQ)}{\partial p_{ik}}=p_{ik}+\mu _{ik}[(R-PQ)Q^T]_{ik} pik=pikμikpikDE(RPQ)=pik+μik[(RPQ)QT]ik

同理:
q k j = q k j − η k j ∂ D E ( R ∣ ∣ P Q ) ∂ q k j = q k j + η k j [ P T ( R − P Q ) ] k j q_{kj}=q_{kj}-\eta _{kj}\frac{\partial D_E(R||PQ)}{\partial q_{kj}}=q_{kj}+\eta _{kj}[P^T(R-PQ)]_{kj} qkj=qkjηkjqkjDE(RPQ)=qkj+ηkj[PT(RPQ)]kj

其中 μ , η \mu, \eta μ,η都是学习率。

关键一步,如何把梯度下降变乘法迭代,令:

μ i k = p i k [ P Q Q T ] i k , η k j = q k j [ P T P Q ] k j \mu _{ik}=\frac{p_{ik}}{[PQQ^T]_{ik}}, \eta _{kj}=\frac{q_{kj}}{[P^TPQ]_{kj}} μik=[PQQT]ikpik,ηkj=[PTPQ]kjqkj

代入原式,发现前面的原梯度 p i k p_{ik} pik正好可以和后面负号部分 μ i k ( − P Q Q T ) \mu_{ik}(-PQQ^T) μik(PQQT)抵消,则两个梯度更新公式变为:

p i k = μ i k R Q T = p i k [ R Q T ] i k [ P Q Q T ] i k p_{ik}=\mu _{ik}RQ^T=p_{ik}\frac{[RQ^T]_{ik}}{[PQQ^T]_{ik}} pik=μikRQT=pik[PQQT]ik[RQT]ik

同理:

q k j = q k j [ P T R ] k j [ P T P Q ] k j q_{kj}=q_{kj}\frac{[P^TR]_{kj}}{[P^TPQ]_{kj}} qkj=qkj[PTPQ]kj[PTR]kj

2.2 代码部分

将迭代改为矩阵形式的乘法算法(365页注释2),而不用对矩阵每个元素遍历(即三层for循环没了):

P i j = P i j [ R Q ] i j [ P Q T Q ] i j P_{ij}=P_{ij}\frac{[RQ]_{ij}}{[PQ^TQ]_{ij}} Pij=Pij[PQTQ]ij[RQ]ij (改了Q和Q^T)

Q i j T = Q i j T [ P T R ] i j [ P T P Q T ] i j Q_{ij}^T=Q_{ij}^T \frac{[P^TR]_{ij}}{[P^TPQ^T]_{ij}} QijT=QijT[PTPQT]ij[PTR]ij

代码如下:

def mf_multiplicative_update(R, P, Q, steps=5000):
   
    for step in range(steps):
        
        Pu = P*(R.dot(Q))/(P.dot(Q.T).dot(Q))+1e-7
        Qu = (Q.T*(Pu.T.dot(R))/(Pu.T.dot(Pu).dot(Q.T))).T+1e-7
        
        e_P = np.sqrt(np.sum((Pu-P)**2, axis=(0,1)))/P.size
        e_Q = np.sqrt(np.sum((Qu-Q)**2, axis=(0,1)))/Q.size
        if e_P<0.001 and e_Q<0.001:
            print("step is:",step)
            break
        P = Pu
        Q = Qu
    return P, Q

在P,Q更新的时候加入1e-7防止分母在下次迭代中变的很小,其中e_P和e_Q分别都是算上一次的P和这次的P(Pu)以及上次的Q和这次的Q(Qu)的距离差( √ 平 方 差 之 和 n \frac{\surd 平方差之和}{n} n,即欧式距离最小化),如果他们的变化都小于0.001了即已经达到(接近)一个极值点,可以终止迭代。

2.3 在图像数据下的效果
rng = np.random.RandomState(1)
P = rng.rand(N,K)
Q = rng.rand(M,K)

P_estimate, Q_estimate = mf_multiplicative_update(R, P, Q)
#print(P_estimate.dot(Q_estimate.T))
plt.figure(figsize=(6, 3))
plt.suptitle("learned basises from NMF", size=16)
for i in range(K):
    plt.subplot(1, K, i+1)
    plt.imshow(Q_estimate[:,i].reshape(16,16), cmap=plt.cm.gray)
    plt.xticks(())
    plt.yticks(())

结果如下:
非负矩阵分解NMF(1): 非调包python实现_第2张图片
可以发现该NMF算法可以有效分解矩阵提取特征。

本文讲述的是乘法迭代的实现过程,其他方法,比如梯度下降法可以参考另一篇非负矩阵分解(2): 拟牛顿法和其他方法。

你可能感兴趣的:(统计学/数据处理/机器学习,算法,python,机器学习,矩阵,线性代数)