DGCF代码解读之重新出发

hi我是小明哥,年少不知小明帅,为约妹子强说愁,哈哈。言归正传,每个新模型出来不到两个月,甚至不到一个月,就有人引用你的文章(你以为别人引用很开心,其实是人家为了踩你啊)马上与你的结果对比,效果就是比你的好,这种正常的竞争/竞赛我觉得很好,公平公正公开,不开源的就算了,谁知道有没有实现paper中宣称的结果,这种都是不足为信的。

总体来说,DGCF是CF协同过滤的大范畴之内的,当然要与最佳的模型对比了,目前来看最优的是GCN(LightGCN),这种方法如果维度小点能够达到相当不错的效果就好了,当数据量大时,比如百万用户,千万点击,这个模型参数必须减少(embedding size和n_layers),不然GPU/内存不够,又想模型好,又想覆盖全面,我说你是不是想多了,这种好事怕很难遇见吧(找对象不也是如此)。另外这个DGCF也是GNN方法的应用,与GCN方法是类似的(不同之处下次再说),而用到GNN的序列模型目前最好的是GCE-GNN,这个方法目前没有官方开源,第三方的codes笨菜鸟(就是我)还没实现(挺难的)。很多开源的新方法其实很难实际应用,1是数据量问题(空间内存都是问题),2是速度/时间问题,根本跑不起来,这方面的问题都很突出。一味地增加embedding size等参数使得效果变好会导致前面两个问题相当严重,结果只能退而求其次,降低后发现与旧方法效果没啥差别,哎,这就是一江烟雨许多愁了。

本大佬准备搞定这个模型今年就收官了,将精力放在调优之上,比如youtube怎么可能放弃呢?比如是不是可以覆盖全部的用户,就算降低了维度,单个用户CTR变少了,总体的会不会出现变大的情况?(会不会有这种好的幺蛾子?哈哈),是不是可以将训练的embedding保留下次还用,直到有更新?当然我依旧会选择在这里同诸位大佬分享我的生活(对,工作生活都接触才能了解一个人),不管前路如何,而我已然在这里等你消息,当然你也可以群里畅谈人生理想,聊聊生活的琐事与烦恼,小明哥将始终陪伴你左右,不离不弃。

For Video Recommendation in Deep learning QQ Group 277356808

For Visual in deep learning QQ Group 629530787

I'm here waiting for you

别加那么多,没必要,另外,不接受这个网页的私聊/私信!!!

 1-数据构建

一般来说CF是不讲究啥点击时间顺序的,所以在召回评价时用留一法是不对的,效果肯定比不上序列模型。先看训练集与测试集:

0 10 5 16 17 9 0 15 14 3 6 11 13 12
1 22 27 19 39 20 48 36 29 44 23 46 26 47 33 40 25 34 38 31 24 30 41 49 37
2 51 53 1 63 62 58 61 56 57 55
3 1 80 83 77 68 67 64 72 69 66 73 76 70 82 74
4 87 94 86 96 90 92 85 5 88 91 9 95
5 106 105 100 22 17 104 56 99 101 41
6 25 138 117 75 133 118 110 112 121 136 113 108 124 115 122 72 99 130 127 100 111 137 135 116 73 139 125 120 27 119 107
7 103 140 145 141 146 8 37 125 14 144 142 73
8 235 11 127 158 42 167 234 248 190 54 195 212 92 77 147 178 200 216 257 255 218 232 162 166 12 201 256 211 36 142 123 204 196 174 189 183 247 14 242 159 3 168 46 58 170 206 228 176 160 197 243 233 154 231 171 237 215 225 43 156 210 161 236 182 227 149 209 251 245 180 157 191 163 186 229 73 13 177 4 254 238 193 258 57 188 5 39 98 94 198 148 222 169 199 249 35 214 223 246 219 244 18 259 252
9 47 262 267 265 264 3 78 261 220 212 202 266 57 46 110 37 263 82 69

测试集也是类似的,前面是user_id后面时item_id

0 1 8 2 4
1 28 50 21 35 32 0 45
2 60 59 24
3 65 81 75 79
4 98 97 93
5 36 102 73
6 105 129 114 131 132 123 128 109 126
7 13 47 143
8 165 132 253 208 221 203 240 51 187 119 230 175 155 172 220 151 28 152 226 184 143 192 164 96 179 224 48 217 202 153
9 135 48 6 142 121

user和item也都有对应编码,至于验证集在实际中没必要,将越多的数据用于训练不好吗?省点时间做召回不好吗?要啥验证集

org_id remap_id
1 0
2 1
3 2
5 3
6 4
7 5
8 6
9 7
10 8

org_id remap_id
1193 0
2355 1
1287 2
2804 3
595 4
1035 5
3105 6
1270 7
527 8

以上就是基本的数据,只要自己可以构建,即可开工,没必要一点不改照搬作者的代码,会改别人的代码才是看懂了代码流程。

2-关于数据的输入代码,这部分与LightGCN代码中是一样的

【看着人家来公司带娃我还是五味杂陈(捂脸),假若。。。该多好,人生的意义我啥时候才能参透呢?上帝的安排到底是什么啊?】

【昨天实在太困了,就回去了,洗衣服,和我妈打电话,无论开始是什么话题,最后结束的一定是"赶紧找对象啊",我也挺急的,但我似乎并不需要,不着急,慢慢来,还年轻,过年就又长了一岁,还不到30,还早,先搞好工作,做好工作,工作成就我,欢迎持续关注,欢迎吃瓜】

不同之处如下:(如果关于稀疏矩阵的处理整不好,就会出现内存爆炸,这部分直接限制了实际应用)

    def get_adj_mat(self, low=0.00006, high=1.0):
##        try:
        t1 = time.time()
        adj_mat, norm_adj_mat, norm_adj_mat_noeye, band_cross_adj_mat = self.create_adj_mat(low=low, high=high)
            
        print('already load adj matrix', adj_mat.shape, time.time() - t1)
##        except Exception as error:
##            print(error)
##            return
        return adj_mat, norm_adj_mat, norm_adj_mat_noeye, band_cross_adj_mat

    def create_adj_mat(self, low=0.0075, high=0.02):
        t1 = time.time()
        adj_mat = sp.dok_matrix((self.n_users + self.n_items, self.n_users + self.n_items), dtype=np.float32)
        adj_mat = adj_mat.tolil()
        R = self.R.tolil()
        # prevent memory from overflowing
        '''
        for i in range(5):
            adj_mat[int(self.n_users*i/5.0):int(self.n_users*(i+1.0)/5), self.n_users:] =\
            R[int(self.n_users*i/5.0):int(self.n_users*(i+1.0)/5)]
            adj_mat[self.n_users:,int(self.n_users*i/5.0):int(self.n_users*(i+1.0)/5)] =\
            R[int(self.n_users*i/5.0):int(self.n_users*(i+1.0)/5)].T
        '''

        adj_mat[:self.n_users, self.n_users:] = R
        adj_mat[self.n_users:, :self.n_users] = R.T        
        adj_mat = adj_mat.todok()
        print('already create adjacency matrix', adj_mat.shape, time.time() - t1)
        
        t2 = time.time()
        def normalized_adj_single(adj):
            rowsum = np.array(adj.sum(1))
            d_inv = np.power(rowsum, -1).flatten()
            d_inv[np.isinf(d_inv)] = 0.
            d_mat_inv = sp.diags(d_inv)

            norm_adj = d_mat_inv.dot(adj)
            print('generate single-normalized adjacency matrix.')
            return norm_adj.tocoo()
        
        def normalized_adj_laplacian(adj):
            rowsum = np.array(adj.sum(1))
            d_inv = np.power(rowsum, -0.5).flatten()
            d_inv[np.isinf(d_inv)] = 0.
            d_mat_inv = sp.diags(d_inv)
            row_norm_adj = d_mat_inv.dot(adj)
            norm_adj = row_norm_adj.dot(d_mat_inv)

            print('generate laplacian-normalized adjacency matrix.')
            return norm_adj.tocoo()

        def band_cross_hop_laplacian(adj, low_pass=0.0025, high_stop=1):
            cross_adj = adj.dot(adj)
            # cross_adj.data = np.where(cross_adj.data>filter_numer, cross_adj.data, 0.)
            rowsum = np.array(cross_adj.sum(1))
            d_inv = np.power(rowsum, -1/2).flatten()
            d_inv[np.isinf(d_inv)] = 0.
            d_mat_inv = sp.diags(d_inv)
            row_norm_adj = d_mat_inv.dot(cross_adj)
            norm_adj = row_norm_adj.dot(d_mat_inv)
            norm_adj.data[np.isinf(norm_adj.data)] = 0.
            norm_adj.data = np.where(norm_adj.data>low_pass, norm_adj.data, 0.)
            norm_adj.data = np.where(norm_adj.data<high_stop, norm_adj.data, 0.)
            norm_adj.eliminate_zeros()
            print('generate filtered laplacian-normalized cross-hop adjacency matrix.')
            return norm_adj.tocoo()
        
        def check_adj_if_equal(adj):
            dense_A = np.array(adj.todense())
            degree = np.sum(dense_A, axis=1, keepdims=False)

            temp = np.dot(np.diag(np.power(degree, -1)), dense_A)
            print('check normalized adjacency matrix whether equal to this laplacian matrix.')
            return temp
        '''
        norm_adj_mat = normalized_adj_single(adj_mat + sp.eye(adj_mat.shape[0]))
        mean_adj_mat = normalized_adj_single(adj_mat)
        '''
        laplacian_adj_mat = normalized_adj_laplacian(adj_mat + sp.eye(adj_mat.shape[0]))
        laplacian_adj_mat_noeye = normalized_adj_laplacian(adj_mat)
        cross_adj_mat = band_cross_hop_laplacian(adj_mat, low_pass=low, high_stop=high)
        print('already normalize adjacency matrix', time.time() - t2)
        #return adj_mat.tocsr(), norm_adj_mat.tocsr(), mean_adj_mat.tocsr()
        return adj_mat.tocsr(), laplacian_adj_mat.tocsr(), laplacian_adj_mat_noeye.tocsr(), cross_adj_mat.tocsr()

其中注释的部分是LightGCN的部分代码,其他则相同,但这部分在数据量大时问题相当突出,详见我的issue,当然代码地址也顺便告诉你了。

其他部分其实都差不多,比如BPR loss,这部分是一样的。

3-infer部分

模型训练好的结果依旧是user和item的embedding,得到这个后直接采用faiss召回即可。关于其指标评价可参考我的博文,HRMAPMRRNDCG等等

坚持吧,小明哥。

一片春愁待酒浇。江上舟摇,楼上帘招。

秋娘渡与泰娘桥,风又飘飘,雨又潇潇。

【20201122-20:32补充】

我看了下提及的那个DGCF——这个是7月份的SIGIR的,没有与LightGCN对比,而我这个博文的是11月份的,有与LightGCN对比。当然这种东西,你是DGCF,我也是DGCF,你是小明哥,我也是小明哥,只不过全称不同。

回家睡觉。

【20201123-18:42补充更新】

仔细看了下He老师团队的DGCF(也就是上面我提及的7月份的,下面推荐的阅读也是,题目是Disentangled Graph Collaborative Filtering),这个paper的效果与LightGCN相当啊,并没有太大差别,下面是数据指标对比。而本博文的效果要比LightGCN要好啊,最后一个表可见。

本文的DGCF效果如下

当然效果好也没啥子用,不能实际应用,所以很多paper能实际应用的不多,很少能实际用。

拜拜

猜你喜欢

转载自blog.csdn.net/SPESEG/article/details/109896156