花絮:
非常抱歉,之前我没有严格的分清梯度下降和随机行走的区别,导致前一篇SVD的博客部分说法上有点小问题。之前说要实现svd的稀疏矩阵做法,这里算是实现了,总体来说,只要调整一下块的大小,该源码距离实际应用来做大矩阵的分解应该已经很接近了。
SVD的python源代码:
近期看了一篇小文 ,很有收获,故而实现之。由于该文中的中间有详细的说明算法流程和原理,这儿不介绍。大家仔细看看其中的算法说明部分,尤其是那几个公式。和原作相比这里有几个改变值得一提:
# coding=utf-8
import numpy as np
import time
import math
__author__ = 'axuanwu'
# 2015年 9 月 25 日
class XMatrix():
def __init__(self, m=1000, n=100, step=4):
self.zero_like = 0.01 # 伪零: 差异小于该值认为无差异
self.step = int(step) # 数据块的切分方法
self.m = m # 原矩阵行数
self.n = n # 原矩阵列数
self.splits = int(math.ceil(1.0 * self.n / self.step)) # 每行记录的分块数
self.res = self.n % self.step
self.Memory = [] # 存储数据块的实体
self.dict_rowid = {} # 记录数据块的位置字典
self.Memory_max_rowid = -1
self.UUU = np.random.random((self.m, 2))
self.VVV = np.ones((2, 1)) # 本来用做归一化UUU 和 MMM后的 系数矩阵,目前未处理
self.MMM = np.random.random((2, self.n))
def set_data(self, tezhenshu):
self.tezhenshu = tezhenshu
self.UUU = np.random.random((self.m, tezhenshu))
self.VVV = np.zeros((tezhenshu, tezhenshu))
for i in xrange(0, tezhenshu):
self.VVV[i, i] = 1
self.MMM = np.random.random((tezhenshu, self.n))
def intoMatrix(self, i, j, data):
# 矩阵赋值
row_id = (int(j / self.step) + i * self.splits)
i_temp = self.dict_rowid.get(row_id, -1)
if i_temp == -1:
self.Memory_max_rowid += 1
i_temp = self.Memory_max_rowid
self.dict_rowid[row_id] = i_temp
self.Memory.append(np.array([0.0] * self.step)) # 增加一块 数据块
self.Memory[i_temp][j % self.step] = data
def getitem(self, i, j=-1):
# 读取稀疏矩阵
if j == -1:
# 返回一数据块
i_temp = self.dict_rowid.get(i, -1)
if i_temp == -1:
temp = np.array([0.0] * self.step)
else:
temp = self.Memory[i_temp]
if self.splits == (i % self.splits) + 1:
return temp[0:self.res]
else:
return temp
else:
# 返回元素
i_temp = self.dict_rowid.get((int(j / self.step) + i * self.splits), -1)
if i_temp == -1:
return 0
return self.Memory[i_temp][j % self.step]
def error_sum(self, k=0):
error_sum = 0
if k != 0: # 计算合成矩阵 与 与矩阵 的 最大差异
for i in xrange(0, self.m):
for j in xrange(0, self.splits):
start = j * self.step
end = min(start + self.step, self.n)
temp = np.dot(self.UUU[i, :], self.MMM[:, start:end])
i_temp = self.dict_rowid.get((j + i * self.splits), -1)
if i_temp == -1:
error_sum = max(error_sum, max(np.abs(temp)))
else:
error_sum = max(error_sum, max(np.abs(temp - self.Memory[i_temp][0:(end - start)])))
return error_sum
else: # 计算合成矩阵 与 与矩阵 所有元素差异的平方和
for i in xrange(0, self.m):
for j in xrange(0, self.splits):
start = j * self.step
end = min(start + self.step, self.n)
temp = np.dot(self.UUU[i, :], self.MMM[:, start:end])
i_temp = self.dict_rowid.get((j + i * self.splits), -1)
if i_temp == -1:
error_sum += sum(np.power(temp, 2))
else:
error_sum += sum(np.power(temp - self.Memory[i_temp][0:(end - start)], 2))
# print (np.power(self.test-np.dot(self.UUU[:, :], self.MMM[:, :]),2)).sum(0).sum(0)-error_sum
return error_sum
def SVD(self):
step = 1
iter = 0
t_start = time.time()
pre_error = self.error_sum()
print "-- ", iter, " --", time.time() - t_start, pre_error, self.error_sum(1)
while (1):
du = np.ones((self.m, self.tezhenshu))
dm = np.zeros((self.tezhenshu, self.n))
# UUU 阵
for i in xrange(0, self.m):
t = np.zeros((self.tezhenshu, self.n))
# t0 = np.zeros((self.tezhenshu, self.n))
for j in xrange(0, self.splits): # 按块遍历
js = j * self.step
je = min(js + self.step, self.n)
t[:, js:je] = (self.getitem(i * self.splits + j) - np.dot(self.UUU[i, :],
self.MMM[:, js:je])) * self.MMM[:, js:je]
du[i, :] = t.sum(1) # - ku * self.UUU[i,:]
# MMM 阵
for j in xrange(0, self.splits): # 按块遍历
js = j * self.step
je = min(js + self.step, self.n)
t = np.zeros((self.tezhenshu, je - js))
for i in xrange(0, self.m):
t[:, 0: je - js] += np.mat(self.UUU[i, :]).T * np.mat(
self.getitem(i * self.splits + j) - np.dot(self.UUU[i, :], self.MMM[:, js:je]))
dm[:, js:je] = t # - ku * self.MMM[:,js:je]
u = step / max((np.abs(dm)).max(), (np.abs(du)).max())
self.MMM += u * dm
self.UUU += u * du
iter += 1
error_sum = self.error_sum()
if error_sum > pre_error: # 到达当前极值
step *= 0.3 # 步长减少
print step, u, iter, " --", time.time() - t_start, error_sum, self.error_sum(1)
pre_error = error_sum
# print "-- ", iter, " --", time.localtime(), error_sum,self.error_sum(1)
if (self.error_sum(1) < self.zero_like) | (step < 10 ** (-8)):
print "-- ", iter, " --", time.time() - t_start, error_sum, self.error_sum(1)
break
if __name__ == "__main__":
m = 10 # 需要分解的矩阵行数
n = 8 # 需要分解的矩阵列数
a = np.zeros((m, n))
aaa = XMatrix(m, n, 6)
aaa.set_data(2) # 3 个主要成分
for i in xrange(0, m):
for j in xrange(0, n):
a[i, j] = i + j % 4 + (i % 5) * (5 - j % 4)
if a[i,j] == 0:
continue
aaa.intoMatrix(i, j, a[i, j]) # 将需要分解的矩阵的元素放入对象 aaa 中
aaa.SVD()
print aaa.error_sum(1)
稀疏块:
源码中矩阵的存储形式并不是用一个m行n列的矩阵来存储数据,而是将每一行元素按照 self.step 个元素做为一块,存储在self.Memory列表中,如果整个块取值都为0,那么这个数据块将不被存储。因此这个方法与稀疏矩阵相比:块的个数少于元素的个数,因此需要记录位置减少;而与原本的实矩阵相比,部分零元素是不用记录的。这种方法介于稀疏矩阵与实矩阵之间。以商品商品推荐为例,如果按照热度排行来排列商品序号,稀疏块同样能够极大地减少记录矩阵需要的存储空间。
块操作:
源码中在计算du,dm以及误差时均直接以数据块为单位进行矩阵运算,这样for循环的次数可以大约减少x倍(x为稀疏块的元素个数),而使用矩阵操作代替循环。就像R中的apply函数可以大幅增加计算性能一样,块操作对性能有很大提升。
步长控制:
很多梯度下降算法都喜欢用经验参数,换个数据集就可能不适应,各种无穷大,精度损失,这还真是有点坑。而本源码中步长调节参数u通过step步长间接控制,在找到当前step的最小值后step会减小,从而进行更精细的调整。能够确保误差稳定快速的收敛。
循环控制:
循环终止的条件有两个,1)精度达到要求,亦即任意元素与原矩阵对应元素的差的绝对值小于self.zero_like,2)矩阵稳定不再改变,亦即步长极小(亿分之一)。程序不会浪费太多时间做无用功。
目前的测试下,程序基本实现矩阵分解的功能,并且稳定不跳出异常。不过分解之后的矩阵各向量不正交。看了原文的同学可能华发现,与原来的相比,du,dm中我去掉了正则化项的影响,由于正则化的目的是减少过拟合,作为矩阵分解而言,在这里并不是有必要的。程序仍有两个不足:稀疏块是一个条形块,而不是一个方块,概率上来说,元素个数相同时方块更容易出现全0的块;目前并没有充分使用并行化技术来优化(块操作应该也属于并行运算的范畴)。
外话:
其实分解之前,我们不知道最少需要几个维度才能分解完矩阵,并且到精度要求。因此我也改了个一个维度一个维度分解的,由于未完成块操作部分的编码,里面仍然使用的是按元素操作。故而没有放这一个的源码。该方法的逻辑是将第一个维度分解到稳定后,第一个维度不变,分解下一个维度,每当前面的维度稳定就不再改变,如果精度未达到要求就增加一个维度,直到精度达标。 可惜的是在测试中,仍然不能满足正交。那么问题就来了:怎样通过梯度下降算法等价的实现奇异值分解?是最优函数的问题?还是……