小方哥哥 2019-06-27
这篇文章是如何使用几种不同的矩阵分解算法计算相关艺术家的分步指南。代码用Python编写,使用 Pandas 和SciPy进行计算,D3.js以交互方式可视化结果。
对于这里的帖子,我使用与 我的第一篇文章中相同的Last.fm数据集。这可以使用Pandas加载到稀疏矩阵中,只有几行代码:
# read in triples of user/artist/playcount from the input datasetdata = pandas.read_table("usersha1-artmbid-artname-plays.tsv", usecols=[0, 2, 3], names=['user', 'artist', 'plays'])# map each artist and user to a unique numeric valuedata['user'] = data['user'].astype("category")data['artist'] = data['artist'].astype("category")# create a sparse matrix of all the artist/user/play triplesplays = coo_matrix((data['plays'].astype(float), (data['artist'].cat.codes, data['user'].cat.codes)))
这里返回的矩阵有300,000名艺术家和360,000名用户,总共有大约1700万条目。每个条目都是用户播放艺术家的次数,其中的数据是从2008年的Last.fm API收集的。
通常用于此问题的一种技术是将用户 - 艺术家 - 戏剧的矩阵投影到低等级近似中,然后计算该空间中的距离。
我们的想法是采用原始的播放计数矩阵,然后将其减少到两个小得多的矩阵,这些矩阵在乘以时接近原始矩阵:
![clipboard.png](/img/bVbc6R1) Artist/User/Play CountsArtist FactorsUser Factors=×
代替将每个艺术家表示为所有360,000个可能用户的游戏计数的稀疏向量,在对矩阵进行因式分解之后,每个艺术家将由50维密集向量表示。
通过减少这样的数据的维数,我们实际上将输入矩阵压缩为两个小得多的矩阵。这种压缩会强制输入数据的泛化,而这种推广可以更好地理解数据。
分解输入矩阵的一种简单方法是在适当加权的矩阵上计算奇异值分解
SVD是那些非常有用的技术之一,也可以用于 主成分分析或 多维缩放等。我的目的包括它是如何工作的总结,但是Jeremy Kun最近写了一篇关于SVD的精彩概述 ,我甚至不打算尝试这样做。出于本文的目的,我们只需要知道SVD生成输入矩阵的低秩近似,并且我们可以使用该低秩表示来生成见解。
像这样使用SVD称为潜在语义分析(LSA)。所有真正涉及的是在这个分解空间中通过余弦距离获得最相关的艺术家,这可以通过以下方式完成:
class TopRelated(object): def __init__(self, artist_factors): # fully normalize artist_factors, so can compare with only the dot product norms = numpy.linalg.norm(artist_factors, axis=-1) self.factors = artist_factors / norms[:, numpy.newaxis] def get_related(self, artistid, N=10): scores = self.factors.dot(self.factors[artistid]) best = numpy.argpartition(scores, -N)[-N:] return sorted(zip(best, scores[best]), key=lambda x: -x[1])
潜在语义分析之所以得名,是因为在对矩阵进行分解之后,可以暴露输入数据中潜在的隐藏结构 - 这可以被认为是揭示输入数据的语义。
例如,'Arcade Fire'和'The Arcade Fire'在我正在使用的数据集中没有共同的监听器:它是由Last.fm历史生成的,人们在他们的音乐库中使用了一个标签或另一个标签。
这意味着所有比较艺术家的直接方法都认为这两个乐队完全不同。然而,这两个标签都指的是同一个频段 - 这是LSA设法接受的事实,因为它们被排名为彼此最相似:
类似于LSA的'Arcade Fire':
你也可以看到与 “枪炮玫瑰”和“枪炮玫瑰”, “尼克洞穴和坏种子”与“尼克洞穴和坏种子”相同的效果。随意进入其他艺术家,但请记住,这个数据集是从2008年开始的,因此这里没有更多的现代艺术家。
虽然LSA成功地概括了我们数据的某些方面,但这里的结果并不是那么好。看看Bob Dylan的结果就是一个例子。
为了在保持这种概括能力的同时做得更好,我们需要提出一个更好的模型。
隐含的交替最小二乘法
推荐系统经常使用矩阵分解模型来 为用户生成个性化推荐。 已发现这些模型在推荐项目时效果很好,并且可以很容易地重复用于计算相关艺术家。
推荐系统中使用的许多MF模型都采用了明确的数据,用户使用类似5星级评定标准评估了他们喜欢和不喜欢的内容。它们通常通过将丢失的数据视为未知数,然后使用SGD最小化重建错误来工作。
这里的数据是隐含的 - 我们可以假设听用艺术家的用户意味着他们喜欢它,但是我们没有用户不喜欢艺术家的相应信号。隐式数据通常比显式数据更丰富,更容易收集 - 即使您让用户给出5星评级,绝大多数评级都只是积极的,因此您无论如何都需要考虑隐含行为。
这意味着我们不能仅仅将丢失的数据视为未知数,而是将不听艺术家的用户视为用户可能不喜欢该艺术家的信号。
这对学习分解表示提出了一些挑战。
第一个挑战是有效地进行这种因式分解:通过将未知数视为负数,天真的实现将查看输入矩阵中的每个条目。由于此处的维度大约为360K乘300K - 总共有超过1000亿条目要考虑,而只有1700万非零条目。
第二个问题是我们不能确定没有听艺术家的用户实际上意味着他们不喜欢它。可能还有其他原因导致艺术家没有被收听,特别是考虑到我们在数据集中每个用户只有最多50位艺术家。
隐式反馈数据集的协作过滤以优雅的方式解释了这两个挑战。
为了处理我们对负面数据没有信心的情况,这种方法使用二元偏好的不同置信水平来学习分解矩阵表示:看不见的项目被视为负面且置信度低,其中当前项目被视为正面更高的信心。
那么目标是通过最小化平方误差损失函数的置信加权和来学习用户因子X u和艺术家因子Y i:
def alternating_least_squares(Cui, factors, regularization, iterations=20): users, items = Cui.shape X = np.random.rand(users, factors) * 0.01 Y = np.random.rand(items, factors) * 0.01 Ciu = Cui.T.tocsr() for iteration in range(iterations): least_squares(Cui, X, Y, regularization) least_squares(Ciu, Y, X, regularization) return X, Ydef least_squares(Cui, X, Y, regularization): users, factors = X.shape YtY = Y.T.dot(Y) for u in range(users): # accumulate YtCuY + regularization * I in A A = YtY + regularization * np.eye(factors) # accumulate YtCuPu in b b = np.zeros(factors) for i, confidence in nonzeros(Cui, u): factor = Y[i] A += (confidence - 1) * np.outer(factor, factor) b += confidence * factor # Xu = (YtCuY + regularization * I)^-1 (YtCuPu) X[u] = np.linalg.solve(A, b)
为了调用它,我使用与LSA中使用的置信矩阵相同的权重,然后以相同的方式计算相关的艺术家:
artist_factors ,user_factors = alternating_least_squares (bm25_weight (plays ),50 )
与仅使用LSA相比,该方法可以产生明显更好的结果。看一下 斜率图,比较Bob Dylan的结果作为一个例子:
这里LSA返回的无关结果被推出列表的头部,并被相关结果取代。
Implicit ALS的优点在于它仍然成功地推广了输入。举个例子,无论是Guns N'Roses还是Nick Cave和The Bad Seeds都会提出他们的同义词,只是没有LSA返回的一些边际结果。