首先说用一下,用到了域名,纯IP地址理论上也是可以的,不过没试。网上搜了一大堆教程,全都要求改/etc/hosts
,对于一个没有root权限的用户来说,后面的教程全部都是一纸废话罢了。
这里就记录下自己的一次搭建过程,踩过的坑。
搭建主要有两部分,一个是Hadoop的分布式文件存储系统HDFS,另外一个是基于Yarn的Map Reduce框架。
首先说用一下,用到了域名,纯IP地址理论上也是可以的,不过没试。网上搜了一大堆教程,全都要求改/etc/hosts
,对于一个没有root权限的用户来说,后面的教程全部都是一纸废话罢了。
这里就记录下自己的一次搭建过程,踩过的坑。
搭建主要有两部分,一个是Hadoop的分布式文件存储系统HDFS,另外一个是基于Yarn的Map Reduce框架。
算是给自己整理一下吧。主要是MF和FM的对比。
假设有那么个用户数为6,物品数为5的大小为$6\times 5$的评分矩阵:
$$R=\begin{bmatrix}
4 & 0 & 5 & 1 & 0 \
3 & 5 & 1 & 0 & 0 \
2 & 5 & 0 & 0 & 2 \
0 & 0 & 0 & 5 & 4 \
1 & 5 & 5 & 0 & 0 \
0 & 0 & 4 & 4 & 3
\end{bmatrix}$$
空缺的评分用0填充。
这个是相对比较简单的,将MF的两个矩阵算一下点积,就能得到所有用户对所有物品的评分$\hat R$。
FM对输入数据$\bm x$(假设是行向量)的表示一般可以分为三部分:代表用户ID的one-hot向量$\bm{x}_u\in\mathbb{R}^6$,代表物品ID的one-hot向量$\bm{x}_i\in\mathbb{R}^5$,以及其他与该评分有关的上下文(context)特征向量$\bm{x}_c\in\mathbb{R}^c$($c$为特征维度数)。即:$\bm x = [\bm x_u, \bm x_i, \bm x_c]$。
如果context是跟item挂钩的,即每个item下的所有评分都共享同一个context特征的话,事情就好办多了(至少这次跑的实验是这样的)。因此,FM的预测公式可以分解为与item相关以及与user相关两部分,跟MF模型相似。
原本计算FM的实现如下,$X$为行向量,$\bm w$为列向量,而$V$为$k\times(11+c)$的交叉项权重矩阵。
1 | y_hat = w0 + X * w - sum((X .^ 2) * (V' .^ 2), 2) / 2 + sum((X * V') .^ 2, 2) / 2; |
由于用户ID和物品ID都是one-hot向量,而context是跟item挂钩的,可以简化成:
1 | y_hat = w0 + w(u) + w(i) + Xc(i) * wc - sum((Vu(u)' .^ 2) + (Vi(i)' .^ 2) + (Xc(i) .^ 2) * (Vc' .^ 2), 2) / 2 + sum((Vu(u)' + Vi(i)' + Xc(i) * Vc') .^ 2, 2) / 2 |
其中$u$和$i$是每条评分的用户和物品的ID。
然后跟MF一样,拆成与item挂钩的一部分:
1 | linear_i = w_item + ctx * w_ctx; % [n_item, 1] |
ctx为n_item$\times c$的context特征矩阵。
以及跟user挂钩的另一部分:
1 | linear_u = w_user; % [n_user, 1] |
最后对每个用户$u$的,预测公式如下:
1 | y_hat = w0 + linear_u(u) + linear_i + sum(cross_u_pos(u) + cross_i_pos, 2)/2 - sum(cross_u_neg(u) + cross_i_neg, 2)/2; % [n_item, 1] |
对用户没评过分的item,把itemID对应的位置置1,然后补上这个item的context特征进行预测就行了。
N指的是推荐列表的长度。对推荐系统而言,前N个item的预测标签为1,而在后面的均为0。而对于ground truth标签,用户为其评过分则为1,否则为0。
For个example,假如$N=3$,推荐系统为第一个user生成的推荐列表为1,3,5,4,2。
item ID | 预测 | GT |
---|---|---|
1 | 1 | 1 |
3 | 1 | 1 |
5 | 1 | 0 |
4 | 0 | 1 |
2 | 0 | 0 |
因此TP=2,FP=1,Precision@3=TP/(TP+FP)=2/3=0.667。
实际上,计算可以简化为Pre@N=TP/N,TP为true positive的item数。代表的含义是前N个item列表中出现用户评过分的item的概率。
跟上面类似,FN指的是出现在第N个item后面,用户有评过分的item数,如item 4。
因此,FN=1,Recall@3=TP/(TP+FN)=2/3=0.667。
实际上,计算也可以简化为Rec@N=TP/Np,Np为当前用户评过分的item个数。代表的含义是用户评过分的item出现在前N个item列表中的概率。这个数值是随N递增而单调递增的。
这个指标综合评估Precision和Recall的质量,跟N无关。顾名思义,AP是对Precision求均值,而具体做法则是以recall为横坐标,precision为纵坐标,对precision求均值。人懒,原理就不解释了。
好像是这么拼来着吧?平时都直接喊的嗯滴希鸡。
推荐系统按照所有item预测的评分分数做个倒序排序,得到一个item列表:
item ID | 评分$r$ | 折损因子$d$ |
---|---|---|
1 | 4 | log2(2) |
3 | 5 | log2(3) |
5 | 0 | log2(4) |
4 | 1 | log2(5) |
2 | 0 | log2(6) |
Cumulative Gain,既然是cumulative,那自然就少不了sum嘛,CG@N=sum(2^r-1),r按照生成item列表的顺序来。有些实现版本则是CG@N=sum(r),没有取指数。
同样假设$N=3$,CG@3=(2^4-1)+(2^5-1)=46。
Discounted Cumulative Gain,多了个折损因子,DCG@N=CG@N/d。DCG@3=(2^4-1)/log2(2) + (2^5-1)/log2(3)=15+31/1.585=34.5588。
而Normalized则需要计算理想的DCG,即Ideal DCG(IDCG),它是按照评分倒序排的:
item ID | 评分$r$ | 折损因子$d$ |
---|---|---|
3 | 5 | log2(2) |
1 | 4 | log2(3) |
4 | 1 | log2(4) |
2 | 0 | log2(5) |
5 | 0 | log2(6) |
根据上表算DCG得到IDCG@3=DCG@3=(2^5-1)/log2(2) + (2^4-1)/log2(3) + (2^1-1)/log2(4)=31+15/1.585+1/2=40.9639。
最后的NDCG@N=DCG@N/IDCG@N=34.5588/40.9639=0.8436。
而某些论文中(这里就不点名是谁啦),NDCG的计算只考虑了用户评过分的物品,而未评过分的物品则不参与计算,因此实际计算NDCG时的列表如下(即去掉评分为0的item):
item ID | 评分$r$ | 折损因子$d$ |
---|---|---|
1 | 4 | log2(2) |
3 | 5 | log2(3) |
4 | 1 | log2(4) |
计算IDCG时,是否去掉评分为0的item其实不影响结果,毕竟它们全排到最后面了嘛,求sum的时候都是0。
最后算出的DCG@3=15+31/1.585+1/2=35.0588。即NDCG@3=35.0588/40.9639=0.8558。用这种计算方法得出来的NDCG是偏高的,不过嘛,作者开心就好。
仓库地址: https://github.com/qhgz2013/shared_matrix
Bug该修的修好了,内存泄漏,多次free啥的操作基本上是没有了,跑小的数据集已经有挺好的效果的了:
RTX ON 共享内存 ON:
独立内存使用情况:
共享内存使用情况(在/dev/shm
中):
共享内存 OFF:
如果要跑大点的数据集,单用matlab自带的parallel.pool.Constant
,就算是128G的内存也out of memory了,现在把15G的数据直接扔共享内存里,还算跑得动:
代码改动也不大,以前是:
1 | x_const = parallel.pool.Constant(x); |
现在则是:
1 | x_host = shared_matrix_host(x); |
好像是繁琐了一点,但是后面优化一下加个直接套个struct一次性创建多个变量的话应该简单很多,比如:
1 | shared_memory_host = create_shmat_host(x, a, b, c); |
这个就留给以后再说了。
步骤:
...\MATLAB\R2018b\bin\win64\mexopts
下\HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\SxS\VS7
(找不到就新建)里添加字符串值:16.0
,C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\
(VS2019的目录)。完毕,重启matlab即可。
Windows下可以用Windows的API进行操作,而Linux下则可以通过POSIX的API进行操作,虽然过程有点区别,但终究是大同小异。
为了简单测试,这里就直接用Windows版Python自带的mmap
当实验:
1 | import numpy as np |
这段代码会常占4G的内存,顶峰的内存会达到数据量的2倍。(在写入共享内存的时候)
注:Linux版Python不支持这种创建共享内存的方式。
数据读取是用matlab的mex写的:
shmem_win_data_read.c
:
1 |
|
然后编译,运行:
1 | mex 'shmem_win_data_read.c' -g |
返回的第一个参数是数据,至于为什么要用cell,就是为了规避matlab自带的GC机制。一旦将数组的指针利用mxSetPr
设置为一个非通过调用mxMalloc
得到的值(如c语言自带的malloc
),GC机制尝试对它进行free的时候,后果自己可以想象。
返回的第二个参数是共享内存的handle,需要在使用完后释放掉。
cell的内存机制后面再说,注意的是,在调用mxSetPr
之前,一定要用mxGetPr
和mxFree
把最初创建返回数组pArr
时所申请到的内存释放掉,否则就会发生内存泄漏。
可以看出,数据是完全一致的。
一旦用完,就该妥善处理后事了(笑)。你大可以试试clear
直接清掉,看看matlab会不会崩。
shmem_win_data_free.c
:
1 |
|
为了简单实验起见,这里就直接用把内存改回matlab的托管内存了(用mxMalloc
申请的内存),然后手动把之前改的内存释放掉。
1 | mex 'shmem_win_data_free.c' -g |
合理利用matlab的“特性”,a{1}
和b
都会同时修改为一个新的数组,接着就可以直接clear
掉,不需要再担心什么了。
1 | v = zeros(1, 4096); |
这时候大可安心看系统的内存使用情况,妈妈再也不用担心内存占用啦。
mxSetPr
在mex
中一般用法效果如下:
1 |
|
上面代码的作用:直接申请一块新的内存,将这块内存的地址直接赋值到matlab(作为输入参数传入mex函数)的矩阵数组中。运行结果如下:
可以看到,在mex函数体内对数组进行的任何修改是无法直接影响到输入参数的。
还有一个关键点,就是必须用a{1}
当输出变量,如:
1 | a{1} = randn(5); |
而把一般变量作为输出,再套用cell,则只能影响到cell里面的元素,不会影响到最先赋值的变量
1 | t = randn(5); % 先赋值到一个新变量 |
其中do_cleanup_within_cell
可为如下(直接把输入数组清空为[]
):
1 | void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[]) { |
反正个人猜测这种现象肯定跟matlab的GC是脱不了钩的,由于没有官方的内存介绍文档,所以也不深究了。写代码讲究一件事,那就是能用就行。
除了double、float以及(u)int8/16/32/64外,还有complex、sparse、cell、struct和object等数据类型,一个完整的check如下:
1 |
|
接下来简单地验证完Linux的共享内存之后,就可以开始造(已经重复)的轮子了。
手头上只有Win版的R2018b和Linux版的R2017b服务器,想着版本差距不算大,直接改改代码,把WinAPI的Named shared memory改成POSIX的shm_open大概就行了。
把代码扔过去,编译,运行,哦豁完蛋,报了个在Windows下未曾出现过的崩溃错误:
突然告诉我还有个16Byte的header?喂不是吧大哥,我只把数据扔共享内存上了你告诉我header?
好吧,既然你想读,那就让你读呗。我把共享内存创建大一点,把数据扔后面一点,前面留16Byte的零,总算可以吧。然后:
你塔喵还要查header是吧?好好,我把mxGetPr往前16个字节的数据一并复制过去总行了吧。
嗯,这次好像真行了,而且能够正常退出没崩。
至于header是什么,说实话自己也没弄懂。随便弄了几个矩阵,往前挪16个字节print一下,结果如下:
131072*4096大小的double矩阵的header:00 00 00 00 01 00 00 00 CE FA ED FE 20 00 10 00
7*65537大小的double矩阵:40 00 38 00 00 00 00 00 CE FA ED FE 20 00 20 00
7*65538大小的double矩阵:70 00 38 00 00 00 00 00 CE FA ED FE 20 00 10 00
7*65538大小的int32/uint32/single矩阵:40 00 1C 00 00 00 00 00 CE FA ED FE 20 00 10 00
前面8字节明显跟数组的大小相关,那么后面8字节呢,跟type也不相关啊……特别是对sparse、complex这种数据类型也不变,而且倒数第二个0x10偶尔会变成0x20,也毫无规律可言。
目前只能推测跟matlab的内部GC有关(因为改一下清空变量的时候就会崩掉)。