矩阵分解是数据处理经常使用的方法,广泛应用于数据分析领域,例如,用于推荐系统预测用户对商品的评分。试采用矩阵分解法,对下列矩阵中的空缺值进行预测。要给写出计算方法、相应python代码、计算结果及完成本次探究的心得体会。
原始矩阵
基本原理
基本原理可以参考这里推荐系统算法·矩阵分解:从SVD到ALS到WALS_哔哩哔哩_bilibili简洁易懂,这里就不多赘述了
计算方法
1. 定义 MF 类:
初始化模型参数,包括样本特征矩阵 X、潜在维度数量 k、学习率 alpha、正则化参数 beta 和迭代次数 iterations。
在初始化过程中,计算了样本特征矩阵的行数和列数,并创建了一个布尔型的索引数组 not_nan_index,用于标识非 NaN 值的位置。
2. 训练模型:train
- 初始化因子矩阵 U 和 V、偏置项 b_u 和 b_v,以及全局偏置项 b。
偏置项和初始化因子矩阵在矩阵分解算法中扮演着重要的角色,它们对于模型的性能和收敛速度都有着显著的影响。
偏置项:
- 全局偏置项(bias):全局偏置项表示整个样本-特征矩阵的整体平均值,它可以看作是模型的基准值。在预测时,全局偏置项能够捕捉到整体的偏差,例如用户对于评分的整体偏好。它的存在能够帮助模型更准确地进行预测。
- 样本偏置项和特征偏置项:样本偏置项和特征偏置项分别对应着样本和特征的偏差。它们能够捕捉到用户或物品的个性化特征,例如某些用户可能更倾向于给出高分或低分,某些物品可能更受欢迎。样本偏置项和特征偏置项的存在可以帮助模型更好地个性化地预测用户的行为。初始化因子矩阵:
- 因子矩阵是模型中的关键参数之一,它包含了样本和特征之间的关系。在矩阵分解算法中,使用随机初始化的因子矩阵作为初始值,然后通过梯度下降等优化算法来学习更好的因子矩阵。良好的初始因子矩阵能够加速模型的收敛速度和提高模型的性能。
- 通常情况下,初始化因子矩阵时会采用一些随机分布,例如正态分布。这样做的目的是为了使得初始因子矩阵具有一定的随机性,从而有利于模型找到更优的解决方案。
- 创建一个训练样本列表,包含了非 NaN 值的位置及其对应的值。
创建训练样本列表的目的是为了在随机梯度下降(SGD)优化过程中使用随机样本来更新模型参数。这种随机性有助于使模型更快地收敛,并且可以更好地避免局部最优解。
在矩阵分解中,我们通常使用随机梯度下降(SGD)来最小化损失函数,以学习出最优的因子矩阵和偏置项。随机梯度下降在每次迭代中使用一小部分样本来估计梯度,并更新参数。这样做的好处是可以大大减少计算开销,尤其是在处理大规模数据集时。
为了使用随机梯度下降,我们需要准备一组随机的训练样本。这些训练样本由非NaN值的位置和对应的真实值组成。在每次迭代中,我们都会随机打乱这些样本,然后依次使用它们来更新模型参数。这样做可以确保模型在每次迭代中都能够学习到不同的特征组合,从而更好地拟合数据。
- 使用随机梯度下降算法进行模型训练,迭代更新参数,并记录每次迭代的误差。
随机梯度下降(Stochastic Gradient Descent,SGD)是一种用于优化模型参数的迭代算法,特别适用于大规模数据集和高维参数空间。它是梯度下降算法的一种变体,通过使用随机抽样的方式来估计梯度,从而实现了高效的参数更新。
基本步骤:
1. 初始化参数:初始化模型的参数,如权重和偏置。
2. 选择样本:从训练集中随机选择一个样本。
3. 计算梯度:对于选择的样本,计算损失函数相对于模型参数的梯度。这里使用的是单个样本的梯度估计,而不是整个训练集的梯度。
4. 更新参数:根据计算得到的梯度,使用梯度下降更新模型参数。更新规则通常为:
其中,是模型参数,是学习率,是损失函数相对于参数的梯度。5. 重复迭代:重复步骤 2~4,直到达到停止条件,如达到最大迭代次数或损失函数收敛到足够小的值。
3. 计算误差:
通过预测矩阵和原始矩阵计算非 NaN 值之间的平方误差。计算非 NaN 值之间的平方误差是为了评估模型的预测精度。在矩阵分解中,我们的目标是通过学习到的模型参数来预测原始矩阵中缺失的值。因此,我们希望预测矩阵能够尽可能地接近原始矩阵,即预测值与真实值之间的误差尽可能小。
在计算平方误差时,只考虑非 NaN 值之间的误差,而不考虑 NaN 值。这是因为 NaN 值代表着原始矩阵中的缺失值,我们无法对其进行预测。因此,我们只关心模型对已知值的预测精度。
通过计算非 NaN 值之间的平方误差,我们可以得到一个量化的评估指标,用于衡量模型在拟合原始数据方面的表现。如果平方误差较小,说明模型预测得较为准确;反之,如果平方误差较大,说明模型预测得不够准确,可能需要调整模型参数或改进模型结构。
这里我们用square_error方法用于计算总平方误差:
def square_error(self):
"""
A function to compute the total square error
"""
predicted = self.full_matrix() # Compute the full matrix prediction
error = 0
for i in range(self.num_samples):
for j in range(self.num_features):
if self.not_nan_index[i, j]: # Check if the entry is not NaN
# Compute the squared difference between the original and predicted value
error += pow(self.X[i, j] - predicted[i, j], 2)
return error
4. 随机梯度下降:
sgd 方法执行随机梯度下降算法。
def sgd(self):
for i, j, x in self.samples:
# Compute prediction and error
prediction = self.get_x(i, j)
e = (x - prediction)
# 更新偏置项
self.b_u[i] += self.alpha * (2 * e - self.beta * self.b_u[i])
self.b_v[j] += self.alpha * (2 * e - self.beta * self.b_v[j])
# 更新因子矩阵 U 和 V
# 使用随机梯度下降更新因子矩阵 U 和 V
# 更新规则为 U = U + alpha * (2 * e * V - beta * U)
# V = V + alpha * (2 * e * U - beta * V)
self.U[i, :] += self.alpha * (2 * e * self.V[j, :] - self.beta * self.U[i,:])
self.V[j, :] += self.alpha * (2 * e * self.U[i, :] - self.beta * self.V[j,:])
对于每个样本 (i, j, x),其中 i 表示样本的索引,j表示特征的索引,x是样本中的实际值。
5. 预测值获取:
预测值由全局偏置项、样本偏置项、特征偏置项以及因子矩阵的点积计算得出。
1. 首先,从模型参数中获取偏置项:self.b 是整体的偏置项,self.b_u[i]是样本 i的偏置项self.b_v[j] 是特征 j的偏置项。
2. 从模型参数中获取因子矩阵的相应行和列:self.U[i, :]是样本i 对应的行向量,self.V[j, :]是特征 j 对应的列向量。
3. 将这两个向量进行点乘操作,即 self.U[i, :].dot(self.V[j, :].T),得到样本i和特征j之间的预测值。
4. 将所有部分的预测值相加,得到最终的预测值。
def get_x(self, i, j):
prediction = self.b + self.b_u[i] + self.b_v[j] + self.U[i, :].dot(self.V[j, :].T)
return prediction
6. 生成完整矩阵:
def full_matrix(self):
return self.b + self.b_u[:, np.newaxis] + self.b_v[np.newaxis, :] + self.U.dot(self.V.T)
1. 将整体偏置项 self.b 加到矩阵的每个位置上,以提供整体的偏移。
2. 将样本偏置项 self.b_u添加到每一行上,以提供不同样本之间的个体差异。
3. 将特征偏置项 self.b_v 添加到每一列上,以提供不同特征之间的个体差异。
4. 将样本因子矩阵 self.U与特征因子矩阵 self.V 的转置相乘,以获取样本与特征之间的交互作用。
5. 将上述三个部分相加,得到最终的完整预测矩阵。
7. 填充缺失值:
将预测矩阵中的缺失值填充回原始矩阵中。
def replace_nan(self, X_hat):
X = np.copy(self.X)
for i in range(self.num_samples):
for j in range(self.num_features):
if np.isnan(X[i, j]):
X[i, j] = X_hat[i, j]
return X
遍历原始矩阵,将预测矩阵中对应位置的值填充回去。
8. 输出结果:
打印原始矩阵 X、预测矩阵 X_hat、完整矩阵 X_comp 和原始矩阵 X 进行对比。
python代码
import numpy as np
class MF():
def __init__(self, X, k, alpha, beta, iterations):
"""
执行矩阵分解以预测矩阵中的 NaN 值。
参数
- X (ndarray) : 样本-特征矩阵
- k (int) : 潜在维度的数量
- alpha (float) : 学习率
- beta (float) : 正则化参数
"""
self.X = X
self.num_samples, self.num_features = X.shape
self.k = k
self.alpha = alpha
self.beta = beta
self.iterations = iterations
# True 表示不是 NaN 值
self.not_nan_index = (np.isnan(self.X) == False)
def train(self):
# 初始化因子矩阵 U 和 V
# 从正态分布中随机初始化因子矩阵,尺度为 1/k
self.U = np.random.normal(scale=1./self.k, size=(self.num_samples, self.k))
self.V = np.random.normal(scale=1./self.k, size=(self.num_features, self.k))
# 初始化偏置项
# 初始偏置项为 0
self.b_u = np.zeros(self.num_samples)
self.b_v = np.zeros(self.num_features)
# 计算全局平均值作为初始整体偏置项
self.b = np.mean(self.X[np.where(self.not_nan_index)])
# 创建训练样本列表
# 将非 NaN 值的样本存储为训练样本
self.samples = [
(i, j, self.X[i, j])
for i in range(self.num_samples)
for j in range(self.num_features)
if not np.isnan(self.X[i, j])
]
# 执行随机梯度下降
training_process = []
for i in range(self.iterations):
np.random.shuffle(self.samples)
self.sgd()
# 计算总平方误差
se = self.square_error()
training_process.append((i, se))
if (i+1) % 10 == 0:
print("迭代次数: %d ; 误差 = %.4f" % (i+1, se))
return training_process
def square_error(self):
"""
计算总平方误差
"""
predicted = self.full_matrix()
error = 0
for i in range(self.num_samples):
for j in range(self.num_features):
if self.not_nan_index[i, j]:
# 计算预测值与实际值之间的平方误差
error += pow(self.X[i, j] - predicted[i, j], 2)
return error
def sgd(self):
"""
执行随机梯度下降
"""
for i, j, x in self.samples:
# 计算预测值和误差
prediction = self.get_x(i, j)
e = (x - prediction)
# 更新偏置项
# 根据梯度下降法则更新偏置项
self.b_u[i] += self.alpha * (2 * e - self.beta * self.b_u[i])
self.b_v[j] += self.alpha * (2 * e - self.beta * self.b_v[j])
# 更新因子矩阵 U 和 V
"""
如果出现 RuntimeWarning: overflow encountered in multiply,
则减小学习率 alpha。
"""
# 使用梯度下降法则更新因子矩阵 U 和 V
# U 和 V 之间的更新规则分别是:
# U = U + alpha * (2 * e * V - beta * U)
# V = V + alpha * (2 * e * U - beta * V)
self.U[i, :] += self.alpha * (2 * e * self.V[j, :] - self.beta * self.U[i,:])
self.V[j, :] += self.alpha * (2 * e * self.U[i, :] - self.beta * self.V[j,:])
def get_x(self, i, j):
"""
获取样本 i 和特征 j 的预测值
"""
# 计算预测值
prediction = self.b + self.b_u[i] + self.b_v[j] + self.U[i, :].dot(self.V[j, :].T)
return prediction
def full_matrix(self):
"""
使用结果偏置项、U 和 V 计算完整矩阵
"""
# 计算完整预测矩阵
return self.b + self.b_u[:, np.newaxis] + self.b_v[np.newaxis, :] + self.U.dot(self.V.T)
def replace_nan(self, X_hat):
"""
用 X_hat 中相应的值替换 X 中的 NaN 值
"""
# 复制原始矩阵 X
X = np.copy(self.X)
# 遍历 X,用 X_hat 中相应的值替换 NaN 值
for i in range(self.num_samples):
for j in range(self.num_features):
if np.isnan(X[i, j]):
X[i, j] = X_hat[i, j]
return X
if __name__ == '__main__':
X = np.array([
[4, 3, 0, 1, 5, 3, 0],
[2, 2, 0, 1, 1, 3, 2],
[1, 2, 0, 5, 2, 0, 3],
[3, 0, 0, 4, 0, 0, 0],
[0, 3, 5, 4, 1, 0, 1],
], dtype=float)
# 将 0 替换为 NaN
X[X == 0] = np.nan
print(X)
mf = MF(X, k=2, alpha=0.1, beta=0.1, iterations=100)
mf.train()
X_hat = mf.full_matrix()
X_comp = mf.replace_nan(X_hat)
print(X_hat)
print(X_comp)
print(X)
结果分析
用nan代替0
预测矩阵
预测填补
可以观察到,在预测矩阵中 NaN 值已经被填补,与原始矩阵相比,填补后的矩阵与原始矩阵在非 NaN 值的位置上较为接近,这表明模型填补的结果是合理的。
心得体会
在这个练习中,我首先了解到矩阵分解是一种常用的数据处理方法,特别适用于降维和缺失数据填补的场景。通过将原始矩阵分解为两个或多个低维矩阵的乘积,我们能够更好地理解数据的结构和模式。随后,我学习了随机梯度下降算法在矩阵分解中的应用。这种优化算法具有高效、灵活的特点,能够在大规模数据集上快速收敛,并且对于参数的更新具有较好的鲁棒性。在练习中,通过多次迭代优化过程,不断调整模型参数,使得预测填补后的矩阵与原始数据的误差不断减小。在处理缺失数据时,我还学会了如何将原始矩阵中的缺失值替换为 NaN,并通过矩阵分解的方法进行填补。这样的处理方式能够更好地保留数据的原始特征,同时提高了数据的可用性和准确性。在代码实现过程中,我注意到了一些细节问题,比如如何初始化参数、如何处理数据的有效性等。这些细节决定了算法的性能和效果,需要认真对待和处理。