1. 前言

随着信息技术和互联网的发展, 我们已经步入了一个信息过载的时代,这个时代,无论是信息消费者还是信息生产者都遇到了很大的挑战:

  • 信息消费者:如何从大量的信息中找到自己感兴趣的信息?
  • 信息生产者:如何让自己生产的信息脱颖而出, 受到广大用户的关注?

为了解决这个矛盾, 推荐系统应时而生, 并飞速前进,在用户和信息之间架起了一道桥梁,一方面帮助用户发现对自己有价值的信息, 一方面让信息能够展现在对它感兴趣的用户前面。 推荐系统近几年有了深度学习的助推发展之势迅猛, 从前深度学习的传统推荐模型(协同过滤,矩阵分解,LR, FM, FFM, GBDT)到深度学习的浪潮之巅(DNN, Deep Crossing, DIN, DIEN, Wide&Deep, Deep&Cross, DeepFM, AFM, NFM, PNN, FNN, DRN), 现在正无时无刻不影响着大众的生活。

推荐系统通过分析用户的历史行为给用户的兴趣建模, 从而主动给用户推荐给能够满足他们兴趣和需求的信息, 能够真正的“懂你”。 想上网购物的时候, 推荐系统在帮我们挑选商品, 想看资讯的时候, 推荐系统为我们准备了感兴趣的新闻, 想学习充电的时候, 推荐系统为我们提供最合适的课程, 想消遣放松的时候, 推荐系统为我们奉上欲罢不能的短视频…, 所以当我们淹没在信息的海洋时, 推荐系统正在拨开一层层波浪, 为我们追寻多姿多彩的生活!

这段时间刚好开始学习推荐系统, 通过王喆老师的《深度学习推荐系统》已经梳理好了知识体系, 了解了当前推荐系统领域各种主流的模型架构和技术。 所以接下来的时间就开始对这棵大树开枝散叶,对每一块知识点进行学习总结。 所以接下来一块目睹推荐系统的风采吧!

这次整理重点放在推荐系统的模型方面, 先从传统推荐模型开始, 然后到深度学习模型。 传统模型的演化关系拿书上的一张图片, 便于梳理传统推荐模型的进化关系脉络, 对知识有个宏观的把握:

今天是第二篇, 依然来自于协同过滤算法族, 前面介绍协同过滤算法的时候做过铺垫, 协同过滤的特点就是完全没有利用到物品本身或者是用户自身的属性, 仅仅利用了用户与物品的交互信息就可以实现推荐,是一个可解释性很强, 非常直观的模型, 但是也存在一些问题, 第一个就是处理稀疏矩阵的能力比较弱, 所以为了使得协同过滤更好处理稀疏矩阵问题, 增强泛化能力, 从协同过滤中衍生出矩阵分解模型(Matrix Factorization,MF), 并发展出了矩阵分解的分支模型, 比如我们听到的很多名词隐语义模型(Latent Factor Model), LDA, 隐含类别模型, PLSA等,这里面提到最多的就是潜语义模型和矩阵分解。这俩说的差不多是一回事, 就是在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品, 挖掘用户和物品的隐含兴趣和隐含特征, 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。

矩阵分解算法在2006年Netfix的推荐算法大赛中大放异彩, 并开始流行, 它的核心思想是通过隐含特征来联系用户兴趣和物品, 所以下面一起来学习一下, 首先会解释一下我们总说的隐语义到底是啥意思, 因为每次看这个词语总是感觉会比较抽象, 这个其实类似于NLP里面的embedding, 如果你说embedding我也感觉长的抽象, 那么我后面会举一个例子, 让它变得具体一些哈哈。 然后就是为什么会用到隐语义模型? 它和协同过滤有哪些区别? 然后就是隐语义模型的核心部分,也就是原理, 隐语义简单的说就是找到隐含特征把用户的兴趣和物品联系了起来, 操作上就是把协同过滤那里的评分矩阵给分解成用户矩阵乘上物品矩阵的形式, 这里的矩阵里面都变成了隐向量, 这个东西能够保证相似的用户及它们喜欢的物品距离很近。 每一个用户和物品都会有一个隐向量, 如果是想预测某个用户对物品的评分的时候, 就直接物品向量与用户向量内积就可以得到了, 所以预测评分也是非常方便了,所以在隐语义模型里面这两个矩阵非常关键, 那么这两个矩阵是怎么得到的呢? 为了计算这两个矩阵, 出现了很多方法, 像BasicSVD, RSVD, ASVD, SVD++等, 大部分都是SVD算法,我们后面会一一来看, 最后就是用代码实现一下前面这几种SVD方法对上一篇里面用户打分的例子进行预测, 对做法做一个深入的了解。

大纲如下

  • 隐语义模型? 这家伙到底是个啥?
  • 矩阵分解算法的原理
  • 矩阵分解算法的求解(BacisSVD, RSVD, ASVD, SVD++)
  • 用BasicSVD, RSVD, SVD++预测用户的评分

Ok, let’s go!

2. 隐语义模型? 这家伙到底是个啥?

隐语义模型算法最早在文本领域被提出,用于找到文本的隐含语义。在2006年, 别用于推荐中, 它的核心思想是通过隐含特征(latent factor)联系用户兴趣和物品(item), 基于用户的行为找出潜在的主题和分类, 然后对item进行自动聚类,划分到不同类别/主题(用户的兴趣)。

这么说依然感觉会很抽象是不是, 啥子叫隐含特征? 听到隐含这俩字又增加了一点神秘感哈哈, 下面拿项亮老师《推荐系统实践》里面的那个例子看一下:

如果我们知道了用户A和用户B两个用户在豆瓣的读书列表, 从他们的阅读列表可以看出,用户A的兴趣涉及侦探小说、科普图书以及一些计算机技术书, 而用户B的兴趣比较集中在数学和机器学习方面。 那么如何给A和B推荐图书呢?

先说说协同过滤算法, 这样好对比不同:

  • 对于UserCF,首先需要找到和他们看了同样书的其他用户(兴趣相似的用户),然后给他们推荐那些用户喜欢的其他书。
  • 对于ItemCF,需要给他们推荐和他们已经看的书相似的书,比如作者B看了很多关于数据挖掘的书,可以给他推荐机器学习或者模式识别方面的书。

而如果是隐语义模型的话, 它会先通过一些角度把用户兴趣和这些书归一下类, 当来了用户之后, 首先得到他的兴趣分类, 然后从这个分类中挑选他可能喜欢的书籍。

这里就看到了隐语义模型和协同过滤的不同, 这里说的角度其实就是这个隐含特征, 比如书籍的话它的内容, 作者, 年份, 主题等都可以算隐含特征,如果这个例子还不是很清晰的话, 那么下面再举个更为具体的例子, 看看是如何通过隐含特征来划分开用户兴趣和物品的。但是在这之前, 相信通过上面这个例子, 我们已经隐隐约约感受到了协同过滤和隐语义模型的区别了, 下面放上王喆老师《深度学习推荐系统》的一个原理图作为对比, 区别简直一目了然:


那下面依然是解密如何通过隐含特征把用户的兴趣和物品分类的:我们下面拿一个音乐评分的例子来看一下隐特征矩阵的含义。

假设每个用户都有自己的听歌偏好, 比如A喜欢带有小清新的吉他伴奏的王菲的歌曲,如果一首歌正好是王菲唱的, 并且是吉他伴奏的小清新, 那么就可以将这首歌推荐给这个用户。 也就是说是小清新, 吉他伴奏, 王菲这些元素连接起了用户和歌曲。 当然每个用户对不同的元素偏好不同, 每首歌包含的元素也不一样, 所以我们就希望找到下面的两个矩阵:

  1. 潜在因子—— 用户矩阵Q
    这个矩阵表示不同用户对于不同元素的偏好程度, 1代表很喜欢, 0代表不喜欢, 比如下面这样:

  1. 潜在因子——音乐矩阵P
    表示每种音乐含有各种元素的成分, 比如下表中, 音乐A是一个偏小清新的音乐, 含有小清新的Latent Factor的成分是0.9, 重口味的成分是0.1, 优雅成分0.2…

利用上面的这两个矩阵, 我们就能得出张三对音乐A的喜欢程度:

张三对小清新的偏好 * 音乐A含有小清新的成分 + 张三对重口味的偏好 * 音乐A含有重口味的成分 + 张三对优雅的偏好 * 音乐A含有优雅的成分…,

下面是对应的两个隐向量:

根据隐向量其实就可以得到张三对音乐A的打分,即: 0.6∗0.9+0.8∗0.1+0.1∗0.2+0.1∗0.4+0.7∗0=0.690.6 * 0.9 + 0.8 * 0.1 + 0.1 * 0.2 + 0.1 * 0.4 + 0.7 * 0 = 0.690.60.9+0.80.1+0.10.2+0.10.4+0.70=0.69
按照这个计算方式, 每个用户对每首歌其实都可以得到这样的分数, 最后就得到了我们的评分矩阵:


这里的红色表示用户没有打分,我们通过隐向量计算得到的。

通过上面的这个例子, 差不多能明白隐含特征是一个什么样的概念了吧, 上面的小清晰, 重口味, 优雅这些就可以看做是隐含特征, 而通过这个隐含特征就可以把用户的兴趣和音乐的进行一个分类, 其实就是找到了每个用户每个音乐的一个隐向量表达形式(embedding的原理其实也是这样, 那里是找到每个词的隐向量表达), 这个隐向量就可以反映出用户的兴趣和物品的风格,并能将相似的物品推荐给相似的用户等。 有没有感觉到是把协同过滤算法进行了一种延伸, 把用户的相似性和物品的相似性通过了一个叫做隐向量的方式进行表达, 这样是不是感觉什么隐向量, 隐含特征啥的不是那么抽象了啊。

But, 真实的情况下我们其实是没有上面那两个矩阵的, 音乐那么多, 用户那么多, 难道我们还得先去找一些隐特征去表示出这些东西? 还有个问题这个东西我们表示也不一定准, 对于每个用户或者每个物品的风格,我们每个人都有不同的看法。 所以事实上, 我们有的只有用户的评分矩阵, 也就是最后的结果, 并且一般这种矩阵长这样:


也就是非常的稀疏, 上一篇协同过滤那个是为了说明原理举得一个特例, 针对这样非常稀疏的矩阵, 其实协同过滤算法想基于用户相似性或者物品相似性去填充这个矩阵是不太容易的, 并且很容易出现长尾问题, 所以才有了协同过滤的延伸 — 矩阵分解或者叫隐语义模型。

隐语义模型其实就是在想办法基于这个评分矩阵去找到上面例子中的那两个矩阵, 也就是用户兴趣和物品的隐向量表达, 然后就把这个评分矩阵分解成Q和P两个矩阵乘积的形式, 这时候就可以基于这两个矩阵去预测某个用户对某个物品的评分了。 然后基于这个评分去进行推荐。这其实就是矩阵分解算法的原理。

3. 矩阵分解算法的原理

上面我们已经简单的解密了一下隐含特征,也初步了解了隐语义模型的原理, 像上面音乐评分预测的那个例子, 如果我们能找到隐向量去表达用户和物品, 并且这隐向量能保证相似的用户及用户可能喜欢的物品距离接近, 那么我们就可以基于隐向量得到用户对于物品的评分矩阵。

但是实际上, 我们其实得到的是残缺的评分矩阵, 也就是后者,在矩阵分解的算法框架下, 我们就可以通过分解协同过滤的共现矩阵来得到用户和物品的隐向量, 就是上面的用户矩阵Q和物品矩阵P, 这也是“矩阵分解”名字的由来。 下面拿王喆老师《深度学习推荐系统》的那个图来看一下矩阵分解的过程:

矩阵分解算法将m×nm\times nm×n维的共享矩阵RRR分解成m×km \times km×k维的用户矩阵UUUk×nk \times nk×n维的物品矩阵VVV相乘的形式。 其中mmm是用户数量, nnn是物品数量, kkk是隐向量维度, 也就是隐含特征个数, 只不过这里的隐含特征变得不可解释了, 即我们不知道具体含义了, 要模型自己去学。kkk的大小决定了隐向量表达能力的强弱, kkk越大, 表达信息就越强, 理解起来就是把用户的兴趣和物品的分类划分的越具体了嘛。

那么如果有了用户矩阵和物品矩阵的话, 我们就知道了如果想计算用户uuu对物品iii的评分, 只需要
Preference⁡(u,i)=rui=puTqi=∑f=1Fpu,kqk,i\operatorname{Preference}(u, i)=r_{u i}=p_{u}^{T} q_{i}=\sum_{f=1}^{F} p_{u, k} q_{k,i}Preference(u,i)=rui=puTqi=f=1Fpu,kqk,i

这里的pup_upu就是用户uuu的隐向量, 就类似与上面的张三向量, 注意这是列向量, qiq_iqi是物品iii的隐向量, 就类似于上面的音乐A向量, 这个也是列向量, 所以才用了puTqip_{u}^{T} q_{i}puTqi得到了一个数, 也就是用户的最终评分, 计算过程其实和上面例子中一样。 这里的pu,kp_{u,k}pu,kqi,kq_{i,k}qi,k是模型的参数, 也正是我们想办法要计算的, pu,kp_{u,k}pu,k度量的是用户uuu的兴趣和第kkk个隐类的关系, 而qi,kq_{i,k}qi,k度量了第kkk个隐类和物品iii之间的关系。

有了上面的例子作为铺垫, 感觉这个地方应该很容易理解了, 接下来的任务就是看看如何求这两个矩阵里面的具体参数了。

4. 矩阵分解算法的求解

谈到矩阵分解, 最常用的方法是特征值分解(EVD)或者奇异值分解(SVD), 关于这两个的具体原理我已经整理好了, 可以先过一下到底是怎么回事奇异值分解(SVD)的原理详解及推导, 虽然这种方法在这里不适用哈哈。

首先是EVD, 它要求分解的矩阵是方阵, 显然用户-物品矩阵不满足这个要求, 而传统的SVD分解, 会要求原始矩阵是稠密的, 而我们这里的这种矩阵一般情况下是非常稀疏的, 如果想用奇异值分解, 就必须对缺失的元素进行填充, 而一旦补全, 空间复杂度就会非常高, 且补的不一定对。 然后就是SVD分解计算复杂度非常高, 而我们的用户-物品矩阵非常大, 所以基本上无法使用。

4.1 Basic SVD

那么怎么办呢? 2006年的Netflix Prize之后, Simon Funk公布了一个矩阵分解算法叫做Funk-SVD, 后来被Netflix Prize的冠军Koren称为Latent Factor Model(LFM), 哦, 原来LFM是个具体的模型啊。 Funk-SVD的思想很简单: 把求解上面两个矩阵的参数问题转换成一个最优化问题, 可以通过训练集里面的观察值利用最小化来学习用户矩阵和物品矩阵, 这是什么意思?

我们上面已经知道了, 如果有了用户矩阵和物品矩阵的话, 我们就知道了如果想计算用户uuu对物品iii的评分, 只需要
Preference⁡(u,i)=rui=puTqi=∑f=1Fpu,kqk,i\operatorname{Preference}(u, i)=r_{u i}=p_{u}^{T} q_{i}=\sum_{f=1}^{F} p_{u, k} q_{k,i}Preference(u,i)=rui=puTqi=f=1Fpu,kqk,i

而现在, 我们有真实的ru,ir_{u,i}ru,i, 但是没有puTqip_{u}^{T} q_{i}puTqi, 那么我们可以初始化一个啊, 随机初始化一个用户矩阵UUU和一个物品矩阵VVV, 然后不就有puTqip_{u}^{T} q_{i}puTqi了? 当然你说, 随机初始化的肯定不准啊, But, 有了puTqip_{u}^{T} q_{i}puTqi之后, 我们就可以计算一个猜测的r^ui\hat{r}_{u i}r^ui, 即
r^ui=puTqi\hat{r}_{u i}=p_{u}^{T} q_{i}r^ui=puTqi
这时候, 肯定是不准, 那么这个猜测的和真实值之间就会有一个误差:
eui=rui−r^uie_{u i}=r_{u i}-\hat{r}_{u i}eui=ruir^ui

有了误差, 我们就可以计算出总的误差平方和:
SSE⁡=∑u,ieui2=∑u,i(rui−∑k=1Kpu,kqk,i)2\operatorname{SSE}=\sum_{u, i} e_{u i}^{2}=\sum_{u, i}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{k, i}\right)^{2}SSE=u,ieui2=u,i(ruik=1Kpu,kqk,i)2

有了损失, 我们就可以想办法进行训练, 把SSE降到最小, 那么我们的两个矩阵参数不就算出来了吗? 看到这里, 是不是有了中醍醐灌顶的感觉了, 这不是训练深度学习模型时候用的常用套路吗? 于是乎就把这个问题转成了最优化的的问题, 而我们的目标函数就是:

min⁡q∗,p∗∑(u,i)∈K(rui−puTqi)2\min _{\boldsymbol{q}^{*}, \boldsymbol{p}^{*}} \sum_{(u, i) \in K}\left(\boldsymbol{r}_{\mathrm{ui}}-p_{u}^{T} q_{i}\right)^{2}q,pmin(u,i)K(ruipuTqi)2
这里的KKK表示所有用户评分样本的集合。

这里其实就和训练模型的思路差不多, 我们拿到了一个用户物品的评分矩阵, 而我们要去计算两个参数矩阵U和V, 我们就可以用求解神经网络模型参数的思路计算这两个矩阵:

  1. 首先先初始化这两个矩阵
  2. 把用户评分矩阵里面已经评过分的那些样本当做训练集的label, 把对应的用户和物品的隐向量当做features, 这样就会得到(features, label)相当于训练集
  3. 通过两个隐向量乘积得到预测值pred
  4. 根据label和pred计算损失
  5. 然后反向传播, 通过梯度下降的方式,更新两个隐向量的值
  6. 未评过分的那些样本当做测试集, 通过两个隐向量就可以得到测试集的label值
  7. 这样就填充完了矩阵, 下一步就可以进行推荐了

有了目标函数, 那么我们就可以使用梯度下降算法来降低损失。 那么我们需要对目标函数求偏导, 得到梯度。 我们的目标函数如果是上面的SSE, 我们下面来推导一下最后的导数:

SSE⁡=∑u,ieui2=∑u,i(rui−∑k=1Kpu,kqk,i)2\operatorname{SSE}=\sum_{u, i} e_{u i}^{2}=\sum_{u, i}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{k,i}\right)^{2}SSE=u,ieui2=u,i(ruik=1Kpu,kqk,i)2

首先我们求SSE在pu,kp_{u,k}pu,k(也就是Q矩阵的第uuukkk列)的梯度:
∂∂pu,kSSE=∂∂pu,k(eui2)=2eui∂∂pu,keui=2eui∂∂pu,k(rui−∑k=1Kpu,kqk,i)=−2euiqk,i\frac{\partial}{\partial p_{u,k}} S S E=\frac{\partial}{\partial p_{u,k}}\left(e_{u i}^{2}\right) =2e_{u i} \frac{\partial}{\partial p_{u,k}} e_{u i}=2e_{u i} \frac{\partial}{\partial p_{u,k}}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{k,i}\right)=-2e_{u i} q_{k,i}pu,kSSE=pu,k(eui2)=2euipu,keui=2euipu,k(ruik=1Kpu,kqk,i)=2euiqk,i

然后求SSE在qk,iq_{k,i}qk,i处(也就是V矩阵的第kkkiii列)的梯度:

∂∂qk,iSSE=∂∂pk,i(eui2)=2eui∂∂pk,ieui=2eui∂∂pk,i(rui−∑k=1Kpu,kqk,i)=−2euipu,k\frac{\partial}{\partial q_{k,i}} S S E=\frac{\partial}{\partial p_{k,i}}\left(e_{u i}^{2}\right) =2e_{u i} \frac{\partial}{\partial p_{k,i}} e_{u i}=2e_{u i} \frac{\partial}{\partial p_{k,i}}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{k,i}\right)=-2e_{u i} p_{u,k}qk,iSSE=pk,i(eui2)=2euipk,ieui=2euipk,i(ruik=1Kpu,kqk,i)=2euipu,k

为了让公式更为简单, 把前面的2给他越掉, 即可以令SSE等于:
SSE⁡=12∑u,ieui2=12∑u,i(rui−∑k=1Kpukqki)2\operatorname{SSE}=\frac{1}{2} \sum_{u, i} e_{u i}^{2}=\frac{1}{2} \sum_{u, i}\left(r_{u i}-\sum_{k=1}^{K} p_{u k} q_{k i}\right)^{2}SSE=21u,ieui2=21u,i(ruik=1Kpukqki)2
这时候, 梯度就没有前面的系数了, 有了梯度, 接下来我们就可以用梯度下降算法更新梯度了:
pu,k=pu,k−η(−euiqk,i)=pu,k+ηeuiqk,iqk,i=qk,i−η(−euipu,k)=qk,i+ηeuipu,kp_{u, k}=p_{u,k}-\eta (-e_{ui}q_{k,i})=p_{u,k}+\eta e_{ui}q_{k,i} \\ q_{k, i}=q_{k, i}-\eta (-e_{ui}p_{u,k})=q_{k, i}+\eta e_{ui}p_{u,k}pu,k=pu,kη(euiqk,i)=pu,k+ηeuiqk,iqk,i=qk,iη(euipu,k)=qk,i+ηeuipu,k

这里的η\etaη是学习率, 控制步长用的。得到了更新的式子,现在开始来讨论这个更新要怎么进行。有两种选择:

  1. 计算完所有已知评分的预测误差后再对P、Q进行更新。
  2. 每计算完一个euie_{ui}eui后立即对pup_upuqiq_iqi进行更新。

第一种叫批梯度下降, 第二种叫随机梯度下降。两者的区别就是批梯度下降在下一轮迭代才能使用本次迭代的更新值,随机梯度下降本次迭代中当前样本使用的值可能就是上一个样本更新的值。由于随机性可以带来很多好处,比如有利于避免局部最优解,所以现在大多倾向于使用随机梯度下降进行更新。 这就是运用梯度下降的矩阵分解算法了, 这个貌似有个名字叫做Basic SVD。当然这里的叫法都不一致, 知道有这么个概念就行了。

但上面这个有个问题就是当参数很多的时候, 就是两个矩阵很大的时候, 往往容易陷入过拟合的困境, 这时候, 就需要在目标函数上面加上正则化的损失, 就变成了RSVD。

4.2 RSVD

在目标函数中加入正则化参数(加入惩罚项),对于目标函数来说,Q矩阵和V矩阵中的所有值都是变量,这些变量在不知道哪个变量会带来过拟合的情况下,对所有变量都进行惩罚:
SSE=12∑u,ieui2+12λ∑u∣pu∣2+12λ∑i∣qi∣2=12∑u,ieui2+12λ∑u∑k=0Kpu,k2+12λ∑i∑k=0Kqk,i2\begin{aligned} S S E &=\frac{1}{2} \sum_{u, i} e_{u i}^{2}+\frac{1}{2} \lambda \sum_{u}\left|p_{u}\right|^{2}+\frac{1}{2} \lambda \sum_{i}\left|q_{i}\right|^{2} \\ &=\frac{1}{2} \sum_{u, i} e_{u i}^{2}+\frac{1}{2} \lambda \sum_{u} \sum_{k=0}^{K} p_{u,k}^{2}+\frac{1}{2} \lambda \sum_{i} \sum_{k=0}^{K} q_{k,i}^{2} \end{aligned}SSE=21u,ieui2+21λupu2+21λiqi2=21u,ieui2+21λuk=0Kpu,k2+21λik=0Kqk,i2

这时候目标函数对参数的导数就发生了变化, 前面的那块没变, 无非就是加入了后面的梯度。 所以此时对pu,kp_{u,k}pu,k求导, 得到:
∂∂pu,kSSE=−euiqk,i+λpu,k∂∂qi,kSSE=−eu,ipu,k+λqi,k\frac{\partial}{\partial p_{u,k}} S S E=-e_{u i} q_{k,i}+\lambda p_{u,k} \\ \frac{\partial}{\partial q_{i,k}} S S E=-e_{u,i} p_{u,k}+\lambda q_{i,k}pu,kSSE=euiqk,i+λpu,kqi,kSSE=eu,ipu,k+λqi,k
这样, 正则化之后, 梯度的更新公式就变成了:
pu,k=pu,k+η(euiqk,i−λpu,k)qk,i=qk,i+η(euipu,k−λqi,k)p_{u, k}=p_{u,k}+\eta (e_{ui}q_{k,i}-\lambda p_{u,k}) \\ q_{k, i}=q_{k, i}+\eta (e_{ui}p_{u,k}-\lambda q_{i,k})pu,k=pu,k+η(euiqk,iλpu,k)qk,i=qk,i+η(euipu,kλqi,k)

4.3 消除用户和物品打分的偏差

通过上面隐向量的方式, 就可以将用户和物品联系到了一起, 但是实际情况中, 单纯的r^ui=puTqi\hat{r}_{u i}=p_{u}^{T} q_{i}r^ui=puTqi是不够的, 还要考虑其他的一些因素, 比如一个评分系统, 有些固有的属性和用户物品无关, 而用户也有些属性和物品无关, 物品也有些属性和用户无关。 因此, Netfix Prize中提出了另一种LFM, 在原来的基础上加了偏置项, 来消除用户和物品打分的偏差, 预测公式如下:
r^ui=μ+bu+bi+puT⋅qi\hat{r}_{ui}=\mu+b_{u}+b_{i}+p_{u}^{T} \cdot q_{i}r^ui=μ+bu+bi+puTqi
这个预测公式加入了3项偏置μ,bu,bi\mu, b_u, b_iμ,bu,bi, 作用如下:

  • μ\muμ: 训练集中所有记录的评分的全局平均数。 在不同网站中, 因为网站定位和销售物品不同, 网站的整体评分分布也会显示差异。 比如有的网站中用户就喜欢打高分, 有的网站中用户就喜欢打低分。 而全局平均数可以表示网站本身对用户评分的影响。
  • bub_ubu: 用户偏差系数, 可以使用用户uuu给出的所有评分的均值, 也可以当做训练参数。 这一项表示了用户的评分习惯中和物品没有关系的那种因素。 比如有些用户比较苛刻, 对什么东西要求很高, 那么他评分就会偏低, 而有些用户比较宽容, 对什么东西都觉得不错, 那么评分就偏高
  • bib_ibi: 物品偏差系数, 可以使用物品iii收到的所有评分的均值, 也可以当做训练参数。 这一项表示了物品接受的评分中和用户没有关系的因素。 比如有些物品本身质量就很高, 因此获得的评分相对比较高, 有的物品本身质量很差, 因此获得的评分相对较低。

加了用户和物品的打分偏差之后, 矩阵分解得到的隐向量更能反映不同用户对不同物品的“真实”态度差异, 也就更容易捕捉评价数据中有价值的信息, 从而避免推荐结果有偏。 此时的SSE变成了:
SSE⁡=12∑u,ieui2+12λ∑u∣pu∣2+12λ∑i∣qi∣2+12λ∑ubu2+12λ∑ubi2=12∑u,i(rui−μ−bu−bi−∑k=1Kpukqki)2+12λ∑u∣pu∣2+12λ∑i∣qi∣2+12λ∑ubu2+12λ∑ubi2\begin{array}{l} \operatorname{SSE}=\frac{1}{2} \sum_{u, i} e_{u i}^{2}+\frac{1}{2} \lambda \sum_{u}\left|\boldsymbol{p}_{u}\right|^{2}+\frac{1}{2} \lambda \sum_{i}\left|\boldsymbol{q}_{i}\right|^{2}+\frac{1}{2} \lambda \sum_{u} \boldsymbol{b}_{u}^{2}+\frac{1}{2} \lambda \sum_{u} \boldsymbol{b}_{i}^{2} \\ =\frac{1}{2} \sum_{u, i}\left(\boldsymbol{r}_{u i}-\boldsymbol{\mu}-\boldsymbol{b}_{u}-\boldsymbol{b}_{i}-\sum_{k=1}^{K} \boldsymbol{p}_{u k} \boldsymbol{q}_{k i}\right)^{2}+\frac{1}{2} \lambda \sum_{u}\left|\boldsymbol{p}_{u}\right|^{2}+\frac{1}{2} \lambda \sum_{i}\left|\boldsymbol{q}_{i}\right|^{2}+\frac{\mathbf{1}}{2} \lambda \sum_{u} \boldsymbol{b}_{u}^{2}+\frac{1}{2} \lambda \sum_{u} \boldsymbol{b}_{i}^{2} \end{array}SSE=21u,ieui2+21λupu2+21λiqi2+21λubu2+21λubi2=21u,i(ruiμbubik=1Kpukqki)2+21λupu2+21λiqi2+21λubu2+21λubi2
此时如果把bub_ububib_ibi当做训练参数的话, 那么它俩的梯度是:
∂∂buSSE=−eui+λbu∂∂biSSE=−eui+λbi\frac{\partial}{\partial b_{u}} S S E=-e_{u i}+\lambda b_{u} \\ \frac{\partial}{\partial b_{i}} S S E=-e_{u i}+\lambda b_{i}buSSE=eui+λbubiSSE=eui+λbi
更新公式为:
bu=bu+η(eui−λbu)bi=bi+η(eui−λbi)\begin{aligned} \boldsymbol{b}_{u}&=\boldsymbol{b}_{\boldsymbol{u}}+\boldsymbol{\eta}\left(\boldsymbol{e}_{u i}-\lambda \boldsymbol{b}_{\boldsymbol{u}}\right) \\ \boldsymbol{b}_{\boldsymbol{i}} &=\boldsymbol{b}_{\boldsymbol{i}}+\boldsymbol{\eta}\left(\boldsymbol{e}_{\boldsymbol{u} i}-\lambda \boldsymbol{b}_{\boldsymbol{i}}\right) \end{aligned}bubi=bu+η(euiλbu)=bi+η(euiλbi)
而对于pu,kp_{u,k}pu,kpk,ip_{k,i}pk,i, 导数没有变化, 更新公式也没有变化。

4.3 SVD++

前面的LFM模型中并没有显示的考虑用户的历史行为对用户评分预测的影响, 而我们知道, 如果某个用户喜欢电子产品, 并且已经买了很多个电子产品了, 如果我们预测该用户对一个电子产品和对一本书的评分, 那相应的电子产品的评分就会高一些, 而这里面, 他之前的历史行为记录也可以做一个参考。 所以Netflix Prize中提出了一个模型叫做SVD++, 它将用户历史评分的物品加入到了LFM模型里。也就是说, 上面的那些矩阵分解, 是只分解的当前的共现矩阵, 比如某个用户uuu对于某个物品iii的评分, 就单纯的分解成用户uuu的隐向量与物品iii的隐向量乘积再加上偏置项。 这时候注意并没有考虑该用户评分的历史物品, 所以这时候SVD++把这个考虑了进去, 但是SVD++可不是一下子就出来的, 它经过了一系列的演变过程。

首先先把ItemCF的预测算法改成一个可以学习的模型, 就行LFM那样, 怎么改? ItemCF的预测算法公式如下:
r^ui=1∣N(u)∣∑j∈N(u)wij\hat{r}_{u i}=\frac{1}{\sqrt{|N(u)|}} \sum_{j \in N(u)} w_{i j}r^ui=N(u)

1jN(u)wij
还记得ItemCF吗? 这个式子是预测用户uuu对于物品iii的打分, N(u)N(u)N(u)表示用户uuu打过分的历史物品, wijw_{ij}wij表示物品ijijij的相似度, 当然这里的这个相似度不再是ItemCF那样, 通过向量计算的, 而是想向LFM那样, 让模型自己学出这个参数来, 那么相应的就可以通过优化的思想嘛:
SSE=∑(u,i)∈Train⁡(rui−∑j∈N(u)wijruj)2+λwij2SSE=\sum_{(u, i) \in \operatorname{Train}}\left(r_{u i}-\sum_{j \in N(u)} w_{i j} r_{u j}\right)^{2}+\lambda w_{i j}^{2}SSE=(u,i)TrainruijN(u)wijruj2+λwij2
但是呢, 这么模型有个问题, 就是www比较稠密, 存储需要很大的空间, 因为如果有nnn个物品, 那么模型的参数就是n2n^2n2, 参数一多, 就容易造成过拟合。 所以Koren提出应该对www矩阵进行分解, 将参数降到了2∗n∗F2*n*F2nF:
r^ui=1∣N(u)∣∑j∈N(u)xiTyj=1∣N(u)∣xiT∑j∈N(u)yj\hat{r}_{u i}=\frac{1}{\sqrt{|N(u)|}} \sum_{j \in N(u)} x_{i}^{T} y_{j}=\frac{1}{\sqrt{|N(u)|}} x_{i}^{T} \sum_{j \in N(u)} y_{j}r^ui=N(u)

1
jN(u)xiTyj=
N(u)

1
xiTjN(u)yj

相当于用xiTyjx_{i}^{T} y_{j}xiTyj代替了wijw_{ij}wij, 这里的xi,yjx_i, y_jxi,yj是两个FFF维的向量。 有没有发现在这里,就出现了点FM的改进身影了。这里其实就是又对物品iii和某个用户uuu买过的历史物品又学习一波隐向量, 这次是FFF维, 为了衡量出物品iii和历史物品jjj之间的相似性来。 这时候, 参数的数量降了下来, 并同时也考虑进来了用户的历史物品记录。 所以这个和之前的LFM相加就得到了:
r^ui=μ+bu+bi+puT⋅qi+1∣N(u)∣xiT∑j∈N(u)yj\hat{r}_{u i}=\mu+b_{u}+b_{i}+p_{u}^{T} \cdot q_{i}+\frac{1}{\sqrt{|N(u)|}} x_{i}^{T} \sum_{j \in N(u)} y_{j}r^ui=μ+bu+bi+puTqi+N(u)

1xiTjN(u)yj
前面的是我们之前分析的LFM模型, 而后面的这个是考虑进了用户购买的历史物品。 但是这样感觉参数太多了, 所以Koren提出令x=qx=qx=q, 因为既然同是商品iii, 就没有必要学习两个隐向量了嘛, 所以得到了该模型的最终预测方式:
r^ui=μ+bu+bi+qiT(pu+1∣N(u)∣∑j∈N(u)yj)\hat{r}_{u i}=\mu+b_{u}+b_{i}+q_{i}^{T}\left(p_{u}+\frac{1}{\sqrt{|N(u)|}} \sum_{j \in N(u)} y_{j}\right)r^ui=μ+bu+bi+qiTpu+N(u)

1
jN(u)yj

这一个就是SVD++模型了。 有了预测函数, 然后也知道真实值, 就可以由损失函数对各个参数求偏导, 和上面的一样了, 这里直接给出导数了, 不推了:

这些就是Koren在NetFlix大赛中用到的SVD算法, 除了ASVD, 这个有点复杂, 先不整理了, 感兴趣的可以看下面给出的第四个链接。

5. 编程实现

我们这里用代码实现一下上面的算法来预测上一篇文章里面的那个预测Alice对物品5的评分, 看看矩阵分解到底是怎么进行预测或者是推荐的。 我把之前的例子拿过来:

任务就是根据这个评分矩阵, 猜测Alice对物品5的打分。

在实现SVD之前, 先来回忆一下ItemCF和UserCF对于这个问题的做法, 首先ItemCF的做法, 根据已有的用户打分计算物品之间的相似度, 得到物品的相似度矩阵, 根据这个相似度矩阵, 选择出前K个与物品5最相似的物品, 然后基于Alice对这K个物品的得分, 猜测Alice对物品5的得分, 有一个加权的计算公式。 UserCF的做法是根据用户对其他物品的打分, 计算用户之间的相似度, 选择出与Alice最相近的K个用户, 然后基于那K个用户对物品5的打分计算出Alice对物品5的打分。 But, 这两种方式有个问题, 就是如果矩阵非常稀疏的话, 当然这个例子是个特例, 一般矩阵都是非常稀疏的, 那么预测效果就不好, 因为两个相似用户对同一物品打分的概率以及Alice同时对两个相似物品打分的概率可能都比较小。 另外, 这两种方法显然没有考虑到全局的物品或者用户, 只是基于了最相似的例子, 很可能有偏。

那么SVD在解决这个问题上是这么做的:

  1. 首先, 它会先初始化用户矩阵P和物品矩阵Q, P的维度是[users_num, F], Q的维度是[item_nums, F], 这个F是隐向量的维度。 也就是把通过隐向量的方式把用户的兴趣和F的特点关联了起来。 初始化这两个矩阵的方式很多, 但根据经验, 随机数需要和1/sqrt(F)成正比。 下面代码中会发现。
  2. 有了两个矩阵之后, 我就可以根据用户已经打分的数据去更新参数, 这就是训练模型的过程, 方法很简单, 就是遍历用户, 对于每个用户, 遍历它打分的物品, 这样就拿到了该用户和物品的隐向量, 然后两者相乘加上偏置就是预测的评分, 这时候与真实评分有个差距, 根据上面的梯度下降就可以进行参数的更新

这样训练完之后, 我们就可以得到用户Alice和物品5的隐向量, 根据这个就可以预测Alice对物品5的打分。 下面的代码的逻辑就是上面这两步, 由于上面整理了三个SVD算法, 这里只贴出一个来, 另外两个可以见最后的GitHub链接。 这里放出那个比较普通且常用的来,就是带有偏置项和正则项的那个SVD算法:

class BasicSVD():def __init__(self, rating_data, F=5, alpha=0.1, lmbda=0.1, max_iter=100):self.F = F           # 这个表示隐向量的维度self.P = dict()          #  用户矩阵P  大小是[users_num, F]self.Q = dict()     # 物品矩阵Q  大小是[item_nums, F]self.bu = dict()   # 用户偏差系数self.bi = dict()    # 物品偏差系数self.mu = 1.0        # 全局偏差系数self.alpha = alpha   # 学习率self.lmbda = lmbda    # 正则项系数self.max_iter = max_iter    # 最大迭代次数self.rating_data = rating_data # 评分矩阵# 初始化矩阵P和Q, 方法很多, 一般用随机数填充, 但随机数大小有讲究, 根据经验, 随机数需要和1/sqrt(F)成正比cnt = 0    # 统计总的打分数, 初始化mu用for user, items in self.rating_data.items():self.P[user] = [random.random() / math.sqrt(self.F)  for x in range(0, F)]self.bu[user] = 0cnt += len(items) for item, rating in items.items():if item not in self.Q:self.Q[item] = [random.random() / math.sqrt(self.F) for x in range(0, F)]self.bi[item] = 0self.mu /= cnt# 有了矩阵之后, 就可以进行训练, 这里使用随机梯度下降的方式训练参数P和Qdef train(self):for step in range(self.max_iter):for user, items in self.rating_data.items():for item, rui in items.items():rhat_ui = self.predict(user, item)   # 得到预测评分# 计算误差e_ui = rui - rhat_uiself.bu[user] += self.alpha * (e_ui - self.lmbda * self.bu[user])self.bi[item] += self.alpha * (e_ui - self.lmbda * self.bi[item])# 随机梯度下降更新梯度for k in range(0, self.F):self.P[user][k] += self.alpha * (e_ui*self.Q[item][k] - self.lmbda * self.P[user][k])self.Q[item][k] += self.alpha * (e_ui*self.P[user][k] - self.lmbda * self.Q[item][k])self.alpha *= 0.1    # 每次迭代步长要逐步缩小# 预测user对item的评分, 这里没有使用向量的形式def predict(self, user, item):return sum(self.P[user][f] * self.Q[item][f] for f in range(0, self.F)) + self.bu[user] + self.bi[item] + self.mu

下面我建立一个字典来存放数据, 之所以用字典, 是因为很多时候矩阵非常的稀疏, 如果用pandas的话, 会出现很多Nan的值, 反而不好处理。

# 定义数据集, 也就是那个表格, 注意这里我们采用字典存放数据, 因为实际情况中数据是非常稀疏的, 很少有情况是现在这样
def loadData():rating_data={1: {'A': 5, 'B': 3, 'C': 4, 'D': 4},2: {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3},3: {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5},4: {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4},5: {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1}}return rating_data# 接下来就是训练和预测
rating_data = loadData()
basicsvd = BasicSVD(rating_data, F=10)
basicsvd.train()
for item in ['E']:print(item, basicsvd.predict(1, item))## 结果:
E 3.252210242858994

通过这个方式, 得到的预测评分是3.25, 这个和隐向量的维度, 训练次数和训练方式有关, 这里只说一下这个东西应该怎么用, 具体结果可以不用纠结。 关于SVD++和RSVD的代码, 放到了下面的GitHub链接。

6. 总结

这篇文章零零散散的写了四五天, 篇幅也挺长, 所以下面先来简单的总结一下, 隐语义模型和矩阵分解是试图在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品, 挖掘用户和物品的隐含兴趣和隐含特征, 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。

首先, 整理了一下隐向量的含义以及通过举例的方式看了一下用户矩阵和物品矩阵的隐特征到底是怎么一回事, 评分矩阵如何进行分解, 然后整理了隐语义模型的原理, 就是基于现有的评分矩阵分解成用户矩阵和物品矩阵相乘的方式, 然后学习后面两个矩阵的数字表示, 比较常用且可行的方式就是SVD, 但传统的SVD计算复杂度太高 , 这里把求解隐向量的问题转成了最优化的问题, 进行求解。 所以这里介绍了SVD, RSVD, SVD++等几种常用的训练求解隐向量的算法, 并最后通过编程简单的实现了这三个算法。

了解了矩阵分解的原理之后, 我们就差不多明白为什么矩阵分解比协同过滤有更强的泛化能力了, 矩阵分解算法中, 由于隐向量的存在, 使得任意的用户和物品之间都可以得到预测分值, 而求解隐向量的过程其实是对评分矩阵进行全局拟合的过程, 这个过程中考虑进了所有的用户和评分, 因此隐向量是利用全局信息生成的, 有更强的泛化能力, 而协同过滤算法, 还是上面说的, 只是基于相似的局部个体, 并且两个相似用户对同一物品打分的概率以及同一用户同时对两个相似物品打分的概率可能都比较小, 这就导致了没法得到全局的信息。

那么矩阵分解算法有什么优缺点呢?

  • 优点:

    • 泛化能力强: 一定程度上解决了稀疏问题
    • 空间复杂度低: 由于用户和物品都用隐向量的形式存放, 少了用户和物品相似度矩阵, 空间复杂度由n2n^2n2降到了(n+m)∗f(n+m)*f(n+m)f
    • 更好的扩展性和灵活性:矩阵分解的最终产物是用户和物品隐向量, 这个深度学习的embedding思想不谋而合, 因此矩阵分解的结果非常便于与其他特征进行组合和拼接, 并可以与深度学习无缝结合。

BUT, 矩阵分解算法依然是只用到了评分矩阵, 没有考虑到用户特征, 物品特征和上下文特征, 这使得矩阵分解丧失了利用很多有效信息的机会, 同时在缺乏用户历史行为的时候, 无法进行有效的推荐。 所以为了解决这个问题, 逻辑回归模型及后续的因子分解机模型, 凭借其天然的融合不同特征的能力, 逐渐在推荐系统领域得到了更广泛的应用。

参考

  • 王喆 - 《深度学习推荐系统》
  • 项亮 - 《推荐系统实践》
  • 奇异值分解(SVD)的原理详解及推导
  • SVD在推荐系统中的应用详解以及算法推导
  • SVD++协同过滤
  • 推荐算法5—隐语义模型
  • 隐语义模型(LFM)
  • 【机器学习】–隐语义模型
  • 隐语义模型(简介)
  • 基于矩阵分解(MF,Matrix Factorization)的推荐算法

论文

  • Matrix factorization techniques for recommender systems原论文, 2009
  • Computation of the singular value decomposition, 2006
  • A Singularly Valuable Decomposition The SVD of a Matrix, 2002

整理这篇文章的同时, 也刚建立了一个GitHub项目, 准备后面把各种主流的推荐模型复现一遍,并用通俗易懂的语言进行注释和逻辑整理, 今天的SVD算法, 上面例子里面的另外两种SVD, SVD++算法, 并且还简单的用SVD基于用户电影推荐的数据集分析了一下,感兴趣的可以看一下

AI上推荐 之 隐语义模型(LFM)和矩阵分解(MF)相关推荐

  1. 【推荐系统】隐语义模型(LFD)与矩阵分解(Matrix Factorization)

    如果需要完整代码可以关注下方公众号,后台回复"代码"即可获取,阿光期待着您的光临~ 文章目录 1.隐语义模型与矩阵分解 2.隐语义模型(Latent Factor Model) 3 ...

  2. 推荐算法之隐语义模型(LFM)矩阵分解梯度下降算法实现

    推荐算法之隐语义模型(LFM)矩阵分解梯度下降算法实现 基于协同过滤的推荐一般分为基于近邻的推荐和基于模型的推荐,其中,基于近邻是指预测时直接使用用户已有的偏好数据,通过近邻数据来预测新物品的偏好.而 ...

  3. 隐语义模型LFM(Latent Factor Model)

    隐语义模型LFM(Latent Factor Model)是主题模型中的一种,跟其他主题模型一样,LFM也需要定义若干"主题",来表示个中隐含的关系,这些"主题" ...

  4. 隐语义模型( LFM )

    基于模型的协同过滤思想         ●基本思想                 -用户具有一定的特征,决定着他的偏好选择;                 -物品具有一定的特征,影响着用户需是否选 ...

  5. python实现lfm_【知识发现】隐语义模型(LFM,Latent Factor Model)推荐算法python实现

    1.隐语义模型: 物品:表示为长度为k的向量q(每个分量都表示  物品具有某个特征的程度) 用户兴趣:表示为长度为k的向量p(每个分量都表示  用户对某个特征的喜好程度) 用户u对物品i的兴趣可以表示 ...

  6. 推荐系统(5)—隐语义模型(LFM)

    https://www.toutiao.com/a6663676280782717454/ 2019-03-02 14:27:17 基本概念 LFM(latent factor model)隐语义模型 ...

  7. 【知识发现】隐语义模型LFM算法python实现(三)

    http://blog.csdn.net/fjssharpsword/article/details/78257126 基于上篇再优化. 1.回顾LFM原理,可以更好地理解代码 对于一个给定的用户行为 ...

  8. python实现lfm_推荐系统之隐语义模型(LFM)

    一 基本概念 LFM(latent factor model)隐语义模型,这也是在推荐系统中应用相当普遍的一种模型.那这种模型跟ItemCF或UserCF有什么不同呢?这里可以做一个对比: 对于Use ...

  9. 【知识发现】隐语义模型LFM算法python实现(一)

    1.隐语义模型: 物品:表示为长度为k的向量q(每个分量都表示  物品具有某个特征的程度) 用户兴趣:表示为长度为k的向量p(每个分量都表示  用户对某个特征的喜好程度) 用户u对物品i的兴趣可以表示 ...

  10. AI上推荐 之 逻辑回归模型与GBDT+LR(特征工程模型化的开端)

    1. 前言 随着信息技术和互联网的发展, 我们已经步入了一个信息过载的时代,这个时代,无论是信息消费者还是信息生产者都遇到了很大的挑战: 信息消费者:如何从大量的信息中找到自己感兴趣的信息? 信息生产 ...

最新文章

  1. J2EE JVM加载class文件的原理机制
  2. c 语言三种正规循环,C语言复习题库
  3. 把Python的200个标准库分类整理了下,供参考
  4. 【ARM】Tiny4412裸板编程之异常(软中断)
  5. C 语言高效编程与代码优化
  6. LeetCode 1039. 多边形三角剖分的最低得分(区间DP)
  7. java对docker_如何在docker中运行java程序
  8. 换手率与股价成交量 关系
  9. Intel汇编语言程序设计学习笔记1
  10. 【资源分享】Linux Scheduler
  11. 使用base64 对Json 的返回数据进行优化
  12. 布尔型Boolean+undefined+null(JS)
  13. qscoj:喵哈哈村的卢西奥
  14. jsp usebean_JSP动作标签– jsp useBean,包含,转发
  15. 应对互联网变局,这 8 件事必须要做。
  16. Python pip安装 win32com模块
  17. 对经太空搭载的“神舟三号口服液”口服液的生产菌株进行了科学鉴定.^
  18. 使用stress进行压力测试
  19. bugly怎么读_Bugly迁入
  20. 4. Python基础:Python内置函数详细介绍(全)

热门文章

  1. 短信验证码接收app必须注意的那些事
  2. 【Unity】制作一个商店场景
  3. 为什么程序员做外包会被瞧不起?
  4. 如何屏蔽搜狗浏览器中的广告 搜狗浏览器拦截广告的教程
  5. 小妙招:教你如何查询获取企业工商数据
  6. 录游戏视频的屏幕软件
  7. ubuntu Nvidia dkms 驱动恢复
  8. mysql如何创建全文索引_从零开始学习MySQL全文索引
  9. 天之涯地之角,raid信息丢了怎么找—记磁盘阵列数据恢复过程
  10. usb key 开发(二)