该方法由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} R≈PQT=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=rij−r^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} ∂pik∂eij2=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} ∂qkj∂eij2=−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′=p−learningrate∗gradient
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+α∂pik∂eij2=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+α∂qkj∂eij2=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(∣∣P∣∣2+∣∣Q∣∣2)
梯度更新为:
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)
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)的矩阵
建议在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)的原因。
理论参考至《矩阵分析于应用(第二版》(张贤达 著)第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(R∣∣PQ)=21∣∣R−PQ∣∣2(书中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−μik∂pik∂DE(R∣∣PQ)=pik+μik[(R−PQ)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−ηkj∂qkj∂DE(R∣∣PQ)=qkj+ηkj[PT(R−PQ)]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
将迭代改为矩阵形式的乘法算法(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了即已经达到(接近)一个极值点,可以终止迭代。
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(())
本文讲述的是乘法迭代的实现过程,其他方法,比如梯度下降法可以参考另一篇非负矩阵分解(2): 拟牛顿法和其他方法。