FPSG 算法与 LIBMF API 浅析

声明

先导知识:随机梯度下降法,矩阵分解,并行/并发计算。

本文是对学习 LIBMF 2.01 的备忘录,其中相当多的内容直接来自于论文: W.-S. Chin, Y. Zhuang, Y.-C. Juan, and C.-J. Lin. A Fast Parallel Stochastic Gradient Method for Matrix Factorization in Shared Memory Systems. ACM TIST, 2015.

文中的计算单元可理解为一条线程或进程。

相关资料备份:论文 PDF 及源代码

简介

LIBMF(2.01)由台湾大学机器学习团队开发,是用于矩阵分解的开源API。LIBMF 主要使用了作者所提出的 FPSG 与 RPCS (可参考LIBMF分析的姊妹篇:RPCS —— SGD 下降速率方案与 LIBMF 分析)算法。LIBMF 的源代码本身没有多少注释,但是官网上公布了三篇相关论文介绍其算法,而且源码附带的 README 文件对关键数据结构与 API 做了较详细的解释。所以可以说“注释”是相当详尽的。

矩阵分解

以推荐系统为例。矩阵 $R{u \times v}$ 记录了u个用户对v项商品的购买评分,推荐系统根据已有的评分预测用户对未评商品的评分,从而向用户做商品推荐。矩阵分解所做的,给定 k ,把 $R{u \times v}$ 分解成 $P{k \times u}$ 和 $Q{k \times v}$ ,用 $P^\mathrm{T} Q = \hat{R}$ 近似表示 $R$ ,而 $\hat{R}$ 填充了 ${R}$ 中未知的部分。
其核心是解决最优化问题:
$$\min{P, Q} \sum{u, v}((r_{u,v} - p_u^\mathrm{T}q_v)^2 - \lambda_p ||p_u||^2 - \lambda_q ||q_v||^2) \label{opt} \tag{1}$$
其中 ${\lambda_p}$ 与 $\lambda_q$ 是正则化参数。

随机梯度下降

随机梯度下降法求解最优化问题 $\ref{opt}$,相对梯度下降法更快。求解出 $p_u$, $q_v$ 下降公式:
$$p_u \leftarrow pu + \gamma ((r{u,v} - p_u^\mathrm{T}q_v)q_v - \lambda_p p_u) \label{sgdP} \tag{2}$$
$$q_v \leftarrow qv + \gamma ((r{u,v} - p_u^\mathrm{T}q_v)p_u - \lambda_q q_v) \label{sgdQ} \tag{3}$$

FPSG

即Fast Parallel Stochastic Gradient,快速的并行梯度下降算法。顾名思义,在求解 $P$, $Q$ 时,运用并行计算和梯度下降算法实现对矩阵的快速分解。

相关算法

HogWild

运用并行计算和梯度下降算法。并行计算实现方法是将公式 $\ref{sgdP}$ 与公式 $\ref{sgdQ}$ 分别交由2个计算单元P1, P2处理,重要的是,P1、P2之间无需进行同步,避免了P1、P2对资源的竞争。

其隐藏的问题是,当2个计算单元同时选取了相同的u或v,可能出现数据的污染。比如,P1、P2同时选取了同一个u值,当P1写入 $p_u$ 时,P2可能同时读取 $p_u$ 的数据,P2可能读入只完成了部分写入的 $p_u$,即读入了错误的数据。然而,这个问题被证明是极少出现的。

DSGD

同样运用并行计算和梯度下降算法。它将 $R$ 划分成 $s \times s$ 的分块,再将其中的s个分块分给s个计算单元对 $P$、$Q$ 进行随机梯度下降计算,每个计算单元只被允许选取分块中的 (u, v)。为了避免数据污染,每个被分配的分块u、v值不能重合。例图:

其中,每一个大矩阵视作一次迭代,每次迭代中,R被分为了3X3的分块,灰色分块被选中分配给计算单元,而且明显每个灰色区域间u、v不重合。

FPSG

FPSG 主要在DSGD上做了2处重要改进。

无锁调度

增加分块

在 DSGD 中,由于 $R$ 被划分成 $s \times s$ 的分块后,最多只能分配给s个计算单元,当某个计算单元完成了一个分块计算后,只能等待其他所有计算单元完成任务重新随机分块进行下一次迭代。显然这造成了资源浪费。

FPSG 的方案是至少划分 $(s+1) \times (s+1)$ 的分块,当某个计算单元完成了一个分块计算,它可以马上选择其他分块而无需等待。例图:

其中有 $(3+1) \times (3+1)$ 个分块,3个非白色块是当前正在计算的分块,此时红色分块计算完成,有3个分块可以直接进行下一步计算。

均衡分块

同时,研发者发现,分块内记录( $r_{u,v} \neq 0$ )的稀疏程度影响了块内(u,v)被选中更新的概率。因为,如果分块内记录稀疏,意味着分块计算更快,使得分块更新的频率更高,进而增加了该分块内(u,v)更新的频率。这可能导致某些记录稠密的分块内 $p$、$q$ 更新不足。

为解决以上问题,FPSG在分块前将 $R$ 分别按行、按列打乱,使得记录分布更均匀。例图:

其中,黑点表示记录,在”After the random shuffle”中,黑点分布较为均匀,甚至在分块后,每个块的记录都是一个。

之后,FPSG做了更进一步优化。它启用了一个调度程序,用一个优先级队列记录每个分块被更新的次数,每当一个计算单元完成任务请求下一个分块时,调度程序分配给它更新最少(且未被占用)的分块。

按序更新

程序读取连续的数据相比读取分散的数据更快,所以FPSG的计算单元按行顺序更新分块,放弃了随机选择的方式。在 均衡分块 部分的配图“shuffle blocks”中,“Sort row indices in each block”部分在更新开始前对分块内的行进行了排序。例图:

其中按行选取分块内 $r_{u,v}$,这样,基本上 $p^\mathrm{T}$ 是被连续读取的。

这样处理似乎失去了采样的随机性,但FPSG已经在 无锁调度 中保证了一定程度的随机性。

LIBMF

作者给出了伪代码:

其中的 scheduler 程序:
在 GET_JOB 子程序中,scheduler 只抽取了优先级最高的空闲的块,而在 PUT_JOB 子程序中,为了避免出现2个分块优先级相同的情况,它为每个分块优先级加上一个随机小数。

工作线程程序: