mxnet实战Bytedance短视频推荐比赛 ICME 2019

代码地址:https://github.com/AlisaBen/marvel

## 比赛介绍

### 比赛背景

参赛者需要通过一个视频及用户交互行为数据集对用户兴趣进行建模,然后预测该用户在另一视频数据集上的点击行为。通过构建深度学习模型,预测测试数据中每个用户id在对应作品id上是否浏览完作品和是否对作品点赞的概率加权结果。

分为两个赛道:赛道1和赛道2,分别是大规模数据集,亿级别的数据信息和小规模数据集,千万级别的数据信息,本次选择的是赛道2.

### 比赛数据集

本次竞赛提供多模态的短视频内容特征,包括视觉特征、文本特征和音频特征,同时提供了脱敏后的用户点击、喜爱、关注等交互行为数据。

本次比赛主要选择的交互数据,可能也是最后成绩有限的原因,应该对数据处理和如何应用raw data这一块多做功课。

## 模型说明

本次参赛过程主要涉及的模型包括FM模型、DeepFM模型和xDeepFM模型,下面对这几个模型的基本思想进行介绍。

### FM模型

该模型的主要思路是在进行预估时除了要考虑单个特征之外,还要考虑组合特征。一般需要对离散型的数据进行one-hot处理,导致产生一个高维的稀疏矩阵,FM模型的特征组合部分通过矩阵分解,引入隐向量,完成对特征的参数估计。

> 但是该模型的隐向量部分并未在本次比赛的mxnet版本代码中实现,主要原因是将数据one-hot处理之后,在通过数据index转换参数矩阵进行求解过程中,因为有一些操作导致模型网络在构建过程中网络断掉,mxnet的自动求导失效,从而导致模型未更新.

推荐阅读博客FM算法解读

关于如何处理隐向量问题也可以参考博客隐向量

模型公式:

$$y(x) = w_{0} + \sum_{i=1}^nw_{i}x_{i}+\frac{1}{2} \sum_{f=1}^k((\sum_{i=1}^nv_{i,f}x_{i})^2-\sum_{i=1}^nv_{i,f}^2x_{i}^2)$$

具体的公式理解可以阅读上面推荐的博客

### DeepFM模型

DeepFM模型主要是讲FM模型与DNN(Deep Neural Network)进行结合,一般结合方式是串行和并行,该模型采取的方式是并行。

DeepFM模型结构如图所示:


模型理解和代码解读参考

代码主要参考

#### 模型结构图解读:

1. FM层将sparse Features输入到order-1部分

2. FM层将embedding部分处理的数据执行Inner Product操作

3. embedding向量同时作为DNN部分的输入,这部分的模型输入完全由自己设计。

> DeepFM模型论文对超参数做了对比实验,得出的结论是:relu比tanh更适合深度模型,除了IPNN;dropout的概率设置在0.6-0.9比较合适;DeeFM模型的DNN部分每层神经元的个数设置在400-800比较合适,最终设置的参数要针对不同的数据集做相应的调整,不是绝对的。

#### 实现思路:

1. FM部分的linear操作直接将raw features和参数矩阵相乘;

2. FM层的embedding处理特征的步骤:首先,对于每个连续性特征,输入到dense层,dense的输出神经元个数设置为embedding_size的大小比如某个连续型特征维度是(batch_size,1)输出维度为(batch_size,embedding_size)再进行reshape成(batch_size,1,embedding_size),对于离散型特征,输入到embedding层,embedding层的离散输入个数是对应离散型特征的离散值个数,输出神经元个数为embedding_size的大小,对某个离散型特征输出维度为(batch_size,1,embedding_size).然后对于连续性和离散型数据处理后的特征对第2个维度进行concat,形成(batch_size,field_num,embedding_size)维度的数据。将该数据当作order-2部分的数据执行Inner Product部分的计算,同时也作为DNN部分的输入/

3. DNN部分的设计就是凭感觉了,Dense\BatchNorm\Activation\dropout,凭感觉加,dropout一般看模型是否有过拟合来设计丢弃的概率。比赛的数据集在like上面的预测效果比较好,但是在finish预测上非常容易出现过拟合

4. 最后将Linear、Inner Product和DNN部分的输出相加输入到Sigmoid中,Sigmoid将结果映射到0-1区间,用来表示概率

### xDeepFM模型

在我看来xDeepFM模型与FM模型并没有什么关系,从论文的网络结构图中看出,网络的输出层的输入是由Linear、CIN网络输出和DNN网络输出构成,Linear部分和DNN的操作和DeepFM相同,而CIN网络主要是为了组合特征,学习Cross Network的思想,进行设计。

xDeepFM模型结构如图所示:


CIN网络的部分看论文的网络结构比较复杂,直接看的DeepCTR的源码,然后翻译成mxnet写的,自己写的模型性能和准确度上面可能不如源码的,在一个epoch,服务器上源码跑几分钟到十几分钟,自己的模型大概需要20-40分钟。

CIN网络结构如图所示:


重点说下CIN网络的设计思路,其他方面同DeepFM模型:

大体的思路是对于每一个样本的每一个embedding维度的向量进行 <math>\otimes</math> 运算,比如,x1是由 x0做 <math>\otimes</math> 运算得到的,x0的第一个样本的第一个embedding向量(fieldnum,1)和x0的第一个样本的第一个embedding向量的转置(1,fieldnum)做运算(4-a),得到一个(fieldnum,fieldnum)维度的矩阵,最后得到的矩阵维度是(embeddingsize,batchsize,fieldnum,fieldnum),经过reshape和transpose之后得到(batchsize,fieldnum$$*$$fieldnum,embeddingsize)维度的向量,输入到conv1d中(4-b)进行卷积运算,通道是fieldnum*fieldnum,kernel是1,最后输出的矩阵大小为(batchsize,layersize,embeddingsize)即为x1,同样地,x1和x0进行同样的运算得到x2,将这些x进行concat(4-c),再经过dense输出。

将embedding部分,dnn部分和cin网络部分的数据相加,经过sigmoid映射到0-1之间作为预测概率作为模型的输出。

## 代码解析

### 模型common部分

#### 模型初始化

```

class SingleFeat:

    def __init__(self,feat,num):

        self.feat_name = feat # 特征名字

        self.feat_num = num # 特征个数

```

核心在于模型初始化时输入一个特征字典,`{"sparse":[SingleFeat],"dense":[SingleFeat]}`key分别为sparse和dense,value为类型为SingleFeat的列表,对于离散型变量,featNum为改离散型变量的离散数目,用于初始化embedding层。

```

    def __init__(self, feature_dict, args, ctx, task,**kwargs):

self.embedding_dict = OrderedDict()

        self.dense_dict = OrderedDict()

with self.name_scope():

for feat in feature_dict['sparse']:

self.embedding_dict[feat.feat_name] = nn.Embedding(feat.feat_num, self.embedding_size)

self.register_child(self.embedding_dict[feat.feat_name])

for feat in feature_dict['dense']:

self.dense_dict[feat.feat_name] = nn.Dense(self.embedding_size)

self.register_child(self.dense_dict[feat.feat_name])

```

DeepFM模型和xDeepFM模型共用的数据处理部分代码,利用Embedding层代替one-hot处理离散型特征,利用Dense层处理连续性变量,使得最后连续性和离散型的数据输出shape相同:

```

    def get_embedding_array(self,input_sample):

        y = nd.zeros(shape=(input_sample.shape[0], 1, self.embedding_size),ctx=self.ctx)

        for single_feat in self.feature_dict['sparse']:

            x = input_sample[:, single_feat.feat_name].reshape((-1,1))

            y1 = self.embedding_dict[single_feat.feat_name](x) #b,1,e

            y = nd.concat(y, y1, dim=1) # b,n+1,e

        return y[:, 1:]#b,n,e

    def get_dense_array(self,input_sample):

        y = nd.zeros(shape=(input_sample.shape[0],1,self.embedding_size),ctx=self.ctx)

        for single_feat in self.feature_dict['dense']:

            x = input_sample[:, single_feat.feat_name].reshape((-1,1))

            y1 = self.dense_dict[single_feat.feat_name](x).reshape((-1,1,self.embedding_size))

            y = nd.concat(y,y1,dim=1) # b,n+1,e

        return y[:,1:]

```

DeepFM模型和xDeepFM模型共用的线性部分的操作

```

    def get_linear_dense_input(self, input_sample):

        y = nd.zeros(shape=(input_sample.shape[0], 1), ctx=self.ctx)

        for single_feat in self.feature_dict['dense']:

            x = input_sample[:, single_feat.feat_name].reshape((-1,1))

            y = nd.concat(y,x,dim=1)

        return y[:, 1:]

    def get_linear_logit(self,embedding_part_sparse,dense_input):

        embedding_part_sparse = embedding_part_sparse.sum(axis=1).sum(axis=1).reshape((-1,1))

        net = nn.Sequential()

        net.add(self.linear_logit_dense)

        net.add(self.linear_logit_bn)

        dense_linear_output = net(dense_input)

        net_embedding = nn.Sequential()

        net_embedding.add(self.linear_logit_embedding_bn)

        embedding_part_sparse = net_embedding(embedding_part_sparse)

        return embedding_part_sparse + dense_linear_output.reshape(embedding_part_sparse.shape)

```

### DeepFM模型

DeepFM网络结构

```

    def forward(self, input_sample):

        embedding_part_sparse = self.get_embedding_array(input_sample) #(?,n1,e)

        linear_dense_input = self.get_linear_dense_input(input_sample)

        linear_logit = self.get_linear_logit(embedding_part_sparse,linear_dense_input)

        dense_part_dense = self.get_dense_array(input_sample) # (?,n2,e)

        merge_sparse_dense = nd.concat(embedding_part_sparse,dense_part_dense,dim=1) # ?,f,e

        xv = nd.broadcast_mul(merge_sparse_dense,self.params.get('v').data())

        fm_embedding_part = nd.square(xv.sum(axis=1)) - nd.square(xv).sum(axis=1)

        fm_embedding_part = fm_embedding_part.sum(axis=1).reshape((-1,1)) / 2 # (?,1)

        net_embedding = nn.Sequential()

        net_embedding.add(self.bn_embedding)

        fm_embedding_part = net_embedding(fm_embedding_part)

        deep_input = merge_sparse_dense.flatten() # ?,f*e

        net = nn.Sequential()

        for i in range(len(self.dense_list)):

            net.add(self.dense_list[i])

            net.add(self.bn_list[i])

            net.add(self.activation_list[i])

            net.add(self.dropout_list[i])

        net.add(self.dnn_out)

        deep_output = net(deep_input)

        deep_fm = nd.sigmoid(linear_logit + fm_embedding_part + deep_output)

        return deep_fm

```

### xDeepFM模型

CIN网络结构

```

    def matmul(self, x, y, transpose_a=False,transpose_b=False):

        batch = x.shape[0]#batch

        m = x.shape[1]#field

        h_k = y.shape[1]

        x = nd.split(x, self.embedding_size, 2)

        y = nd.split(y, self.embedding_size, 2)

        res = nd.zeros(shape=(1,batch,m,h_k),ctx=self.ctx)

        for idx in range(self.embedding_size):

            array = nd.batch_dot(x[idx], y[idx], transpose_a,transpose_b=transpose_b).reshape((1,-1,m,h_k))

            res = nd.concat(res,array,dim=0) # embedding+1,batch,field,field

        return res[1:,:,:,:]

def cin(self,X):

        batch = X.shape[0]

        hidden_nn_layers = [X]

        split_tensor0 = hidden_nn_layers[0]

        final_result = nd.arange(batch *self.embedding_size,ctx=self.ctx).reshape((batch,1,self.embedding_size))

        for idx,layer_size in enumerate(self.layer_size):

            # time_start = time.time()

            split_tensor = hidden_nn_layers[-1]

            dot_result_m = self.matmul(split_tensor0, split_tensor, transpose_b=True)

            dot_result_o = dot_result_m.reshape((self.embedding_size, -1, self.field_nums[0] * self.field_nums[idx]))

            dot_result = nd.transpose(dot_result_o, axes=(1, 0, 2))

            dot_result = nd.transpose(dot_result,axes=(0, 2, 1))

            # print(dot_result.shape)

            net = nn.Sequential()

            net.add(self.conv_list[idx])

            curr_out = net(dot_result)

            final_result = nd.concat(final_result,curr_out,dim=1)

            hidden_nn_layers.append(curr_out)

        final_result = final_result[:,1:,:]

        result = final_result.sum(axis=2)

        net = nn.Sequential()

        net.add(self.cin_dense)

        net.add(self.cin_bn)

        result = net(result)

        return result

```

xDeepFM网络结构

```

    def forward(self, input_sample):

        embedding_part_sparse = self.get_embedding_array(input_sample) #(?,n1,e)

        linear_dense_input = self.get_linear_dense_input(input_sample)

        linear_logit = self.get_linear_logit(embedding_part_sparse,linear_dense_input)

        dense_part_dense = self.get_dense_array(input_sample) # (?,n2,e)

        merge_sparse_dense = nd.concat(embedding_part_sparse,dense_part_dense,dim=1) # ?,f,e

        deep_input = merge_sparse_dense.flatten() # ?,f*e

        cin_output = self.cin(merge_sparse_dense)

        net = nn.Sequential()

        for i in range(len(self.dense_list)):

            net.add(self.dense_list[i])

            net.add(self.bn_list[i])

            net.add(self.activation_list[i])

            net.add(self.dropout_list[i])

        net.add(self.dnn_out)

        deep_output = net(deep_input)

        deep_fm = nd.sigmoid(linear_logit + deep_output + cin_output)

        return deep_fm

```

## 经验教训

### 调参经验

1. 测试集的loss降低,验证机的loss仍然比较大或者上升,模型过拟合,可以用adam优化算法设置weight_decay参数,或者增加dropout

2. 过拟合的原因可能有数据集太小或者模型过于复杂,层数过多

3. 在模型训练过程中出现不收敛的现象时检测是否是因为梯度衰减或者梯度爆炸

### 其他经验

1. 当多次调参对结果的影响微乎其微的时候考虑一下模型的有效性,多打印一下模型的中间输出。

2. 对模型结构要深入理解之后再动手写代码,如果是多人写一份代码或者模型要多加探讨,避免模型理解错误,在模型理解一致的前提下进行分工写代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容