Micro Behaviors:A New Perspective in E-commerce Recommendation 文章阅读以及代码实验【数据集来自京东2019年比赛数据】

Micro Behaviors:A New Perspective in E-commerce Recommendation 文章阅读以及代码实验【数据集来自京东2019年比赛数据】

概述

《Micro Behaviors:A New Perspective in E-commerce Recommendation》一文主要探究了微观行为对于预测任务的影响,为了更加准确的描述用户的每一个微观行为,文中提出了RIB模型,鉴于文中没有公布github代码,所以我在这里就根据文章的说法进行了简单的复现。

这篇文章的优缺点都非常明显,优点在于论文对于用户的微观行为进行了非常详细的统计分析,阐述了为什么要做微观行为的意义,在创新度和说服力上都非常强。

缺点在于这篇文章对于模型的介绍并不是十分清晰,前后使用的变量符号也不统一,读起来就有点费解。在本blog中我根据自己的理解统一了一下符号含义,如有不对请多多指正!

这个blog主要的任务也就是在于还原代码的实现过程,数据集来源:https://jdata.jd.com/html/detail.html?id=8 京东2019年的比赛数据集。
不是原po的数据集,因为数据原因,对于模型也有些许变动,但是基本思路是一致的。

下面就开始实现代码的部分啦!拿到数据集后, 大家一起加油吧!

论文解读以及代码实现过程分解

问题定义

  • 商品集合 P = {p1,p2,,pNp_1,p_2,\dots,p_N},即有 N 个物品。
  • 操作集合 A = {a1,a2,,ama_1,a_2,\dots,a_m}, 即有M种操作。
  • 停留时间集合 D = {d1,d2,,dkd_1,d_2,\dots,d_k}, 即有K种停留时间–作者人为分成的K段,但是我们这里没有,所以没有这个D。
    任务:给定用户的历史操作数据即(pi,aj,dkp_i,a_j,d_k)的三元组序列,去预测下一个操作的物品。
    ps:因为我不太确定这个停留时间应该如何计算,所以这里我没有使用dk,只是输入了(pi,ajp_i,a_j)的二元组序列。

数据集介绍

原文的数据集介绍见原文,本blog只介绍我使用的数据集。~~~~嘿嘿😁
ps:原文中出现了商品的category id ,但是我看了一遍文章,也没有看出来哪里使用了item information。

数据预处理

然后因为我做的其实是session recommend 时候实现的本文章,所以本文的所有处理逻辑也是session recommend,如果看官也是做session recommend 的话可以直接使用我处理的逻辑,不然的话可以不做这一部分的数据预处理,可以直接使用user id,即后面我使用session id 的地方,看官直接使用user id即可。

准备训练集和测试集

一个数据我们需要准备4个东西:data_padding,data_operation_padding, data_masks, data_targets,因为后面要用到item序列和operation序列,所以一起生成了,但是需要注意,训练的时候只能使用训练集中的item 和operation 序列:
基本代码如下:

def construct_data(data,max_session_length,item2id,item_tail=['0'],item_col='sku_id',operation_col='type'):
    session_dict = {}
    data_masks = []
    data_padding = []
    data_operation_padding = []
    data_targets = []
    data_is = []
    data_os = []
    for index, row in tqdm(data.iterrows()):
        session_id = row['sessionId']
        item = str(row[item_col])
        item = item2id[item] #item2dict key is str
        operation = str(row[operation_col])
        if session_id not in session_dict:
            session_dict[session_id] = {}
            session_dict[session_id]['items'] = []
            session_dict[session_id]['operations'] = []
        session_dict[session_id]['items'].append(item)
        session_dict[session_id]['operations'].append(operation)
    for sess in tqdm(session_dict.keys()):
        items = session_dict[sess]['items']
        operations =session_dict[sess]['operations']
        data_is.append(items)
        data_os.append(operations)
        if len(items) > max_session_length:  # 需要进行截断
            items = items[-max_session_length:]
            operations = operations[-max_session_length:]
        for i in range(1, len(items)):
            tar = items[-i]
            inputs = items[:-i]
            inputs_operations = operations[:-i]
            mask = [1] * len(inputs) + [0] * (max_session_length - len(inputs))
            data_pad = inputs + item_tail * (max_session_length - len(inputs))
            data_op_pad = inputs_operations+item_tail * (max_session_length - len(inputs))
            data_masks.append(mask)
            data_padding.append(data_pad)
            data_operation_padding.append(data_op_pad)
            data_targets += [tar]
    data_processed = (data_padding,data_operation_padding, data_masks, data_targets)
    return data_processed,data_is,data_os

def construct_RIB_data(train,test,item2id,item_tail=['0']):
    session_length = train.groupby('sessionId').size()
    max_session_length = int(session_length.quantile(q=0.99))
    train_processed,train_is,train_os = construct_data(train,max_session_length,item2id,item_tail=item_tail)
    test_processed,_,_ = construct_data(test, max_session_length, item2id, item_tail=item_tail)
    return train_processed,train_is,train_os,test_processed

train_processed,_,_,test_processed = construct_RIB_data(train_data,test_data,item2id_dict)

为了方便我们还可以在这里存储一个picke文件,因为生成一次需要挺长时间的,后面哪一步失败了都重头来就太累了。

pickle.dump(train_processed,open('./data/output/train_processed.pkl','wb'))
pickle.dump(test_processed,open('./data/output/test_processed.pkl','wb'))

embeddling layer

这篇文章所有的代码逻辑都极其非常的naive,我们先看原文是怎么说的.
4.1节部分首先说我们可以用one-hot去表示这些不同的物品和操作,????(我真的黑人问号),然后说这样表示太稀疏了,所以我们用word2vec对P,A,D分别embedding,然后concat起来,这样维度就比原来小多了。。。。。。

The vocabulary sized of P,A,D are V,M,K respectively, and there are VKM tuples in total. Therefore, the input data is extremely sparse and high-dimensional…The new representation of xt,et is dense with dimension of dP+dA+dD,which is much smaller than V* M * K.

首先这里就有一个问题,原来的表示维度也不过是 V+M+K,不知道为什么作者写成了✖️,这里希望有人帮助我解答一下~

那么这里的代码就非常简单了,先把操作序列整理成 item 序列的文本以及操作序列的文本格式,例子如下图所示:
数据处理需要特别注意:item和操作的编号都从1开始!!因为后面padding需要用到0

train_items.txt 文件:
在这里插入图片描述
train_operations.txt 文件
在这里插入图片描述
然后开始调用gensim 包,这里采用skip-gram ,hidden_size=100.这里后期考虑变换一下。

import gensim
from gensim.models import word2vec
def train_embedding(sentence_file,model_file):
    sentences = word2vec.LineSentence(sentence_file)
    model = word2vec.Word2Vec(sentences, hs=1,min_count=1,window=3,size=100)
    model.save(model_file)
    return model
item_model = train_embedding(sentence_file='./data/output/train_items.txt',model_file='./data/output/items.model')
operation_model = train_embedding(sentence_file='./data/output/train_operations.txt',model_file='./data/output/operations.model')

后面使用了rnn,所以我们这里为了方便,把padding用的[0]都加上去。
但是我不想动原有的模型,所以就加载出来之后新添加了一个词,所以这里也体现了模型的动态可更迭行~~~

#item_model.train([['0','0','0','0','0']],total_examples=1,epochs=1)
item_model = word2vec.Word2Vec.load('./data/output/items.model')
item_model.build_vocab(sentences=[['0','0','0','0','0']],update=True)
#item_model.wv[['1','2','3','0']]
operation_model = word2vec.Word2Vec.load('./data/output/operations.model')
operation_model.build_vocab(sentences=[['0','0','0','0','0']],update=True)

模型部分

4.2节就是本文的模型介绍部分,花里胡哨的其实简而言之一句话:我们用gru做了模型的序列化部分。
作者花时间说了bptt的梯度消失和弥散问题,然后说了lstm,又说gru比lstm简单,然后用gru,我也是醉了,
然后这里出现了一句神奇的话不知道我的解读对不对:

in our task, each ht means the representation of the tth product and the micro-behaviors on it.

4.3 指的是,用一个attention layer 把之前输出的那些隐藏层链接起来,就用了一个非常简朴的attention 公式。
然后大家发现这里使用了 K这个符号,在之前的指代中K为dwell time 的类别个数,在这里怎么都说不通,所以我觉得,这里可能是V,就是item 的个数,因为后面loss function直接就交叉熵了, 所以说这里输出的应该就是每个item 的概率,所以这里的维度就开始迷醉了,难道最后还有一个mlp??
但是文章的模型图画到output layer就结束了。
这里真的非常迷醉,我先实现一下我的版本,然后解释一下:
然后文章说了使用归一化的embedding算loss function,其实也没有很大的毛病,按照这里的理解应该是instance 归一化,即每一个instance 归一化。无所谓了~
这里的归一化应该是softmax归一化。直接softmax好了

class RIB(Module):
    def __init__(self,feature_size,hidden_size,k_size):
        super(RIB,self).__init__()
        self.gru_cell = nn.GRU(input_size=feature_size*2,hidden_size=hidden_size,num_layers=2,batch_first=True)
        self.M_layer_1 = nn.Linear(hidden_size, k_size, bias=True)
        self.A_layer_1 = nn.Linear(k_size, feature_size, bias=True)
        self.optimizer = torch.optim.Adam(self.parameters(),lr=0.001)
        self.loss_function  = self.loss_cross_fn()
        
    @staticmethod
    def loss_cross_fn(self,T, O, eps=0.0000001):
        O = torch.log(O + eps)
        tmp = -torch.mul(T, O)
        loss = torch.sum(tmp)
        return loss

    def forward(self,input_e):
        output, hidden = gru(input_e, None)
        mt1 = self.M_layer_1(output)
        mt2 = torch.tanh(mt1)
        at1 = self.A_layer_1(mt2)
        at2 = nn.functional.softmax(at1, dim=-1)
        o1 = torch.sum(output * at2, dim=1)
        return o1

模型训练

#划分batch
import math
def generate_batch_slices(len_data,shuffle=True,batch_size=128): #padding,masks,targets
    n_batch = math.ceil(len_data / batch_size)
    shuffle_args = np.arange(n_batch*batch_size)
    if shuffle:
        np.random.shuffle(shuffle_args)
    slices = np.split(shuffle_args,n_batch) #np.split必须能整除才能等分
    slices = [i[i<len_data] for i in slices]
    return slices
def get_batch_embedding(data,slices):
    b_is, b_os, b_ms,b_ts = data[0][slices],data[1][slices],data[2][slices],data[3][slices]
    b_is_trans = np.array([[item_wordlist_dict[i] for i in is_] for is_ in b_is])
    b_is_e = net_item_embedding(torch.Tensor(b_is_trans).long())
    b_os_trans = np.array([[operation_wordlist_dict[i] for i in is_] for is_ in b_os])
    b_os_e = net_operation_embedding(torch.Tensor(b_os_trans).long())
    b_fs_e = torch.cat([b_is_e, b_os_e], dim=2)
    b_ts_trans = np.array([item_wordlist_dict[t] for t in b_ts])
    b_ts_e = net_item_embedding(torch.Tensor(b_ts_trans).long())
    return b_fs_e,b_ts_e

#往torch 中加载 embeddings

item_wordlist = list(set(item_model.wv.vocab))
print(len(item_wordlist))
item_wordlist_dict = {}
for i,word in enumerate(item_wordlist):
    word = word
    item_wordlist_dict[word] = i
item_vocab_size = len(item_wordlist)
print(item_vocab_size)
embed_size = 100
weight = torch.zeros(item_vocab_size, embed_size)
for k,v in item_wordlist_dict.items():
    weight[v, :] = torch.from_numpy(np.array(item_model.wv[[k]].data))
net_item_embedding = nn.Embedding.from_pretrained(weight)

operation_wordlist = list(set(operation_model.wv.vocab))
print(len(operation_wordlist))
operation_wordlist_dict = {}
for i,word in enumerate(operation_wordlist):
    word = word
    operation_wordlist_dict[word] = i
operation_vocab_size = len(operation_wordlist)
print(operation_vocab_size)
embed_size = 100
operation_weight = torch.zeros(operation_vocab_size, embed_size)
for k,v in operation_wordlist_dict.items():
    operation_weight[v, :] = torch.from_numpy(np.array(operation_model.wv[[k]].data))
net_operation_embedding = nn.Embedding.from_pretrained(operation_weight)
del operation_weight

net = RIB(feature_size=100,hidden_size=100,k_size=50)

net.scheduler.step()
total_loss_list = []
for epoch in range(5):
    print('epoch:',epoch)
    train_slices = generate_batch_slices(len(train_processed[0]))
    net.train()
    total_loss = 0
    for slice in train_slices:
        net.optimizer.zero_grad()
        slice_f_embedding,slice_target_embedding = get_batch_embedding(train_processed,slice)
        slice_o_embedding = net(slice_f_embedding)
        loss = net.loss_cross_fn(slice_target_embedding,slice_o_embedding)
        loss.backward()
        net.optimizer.step()
        total_loss+=loss.item()
    print('the total loss of %d epoch is %.4f'%(epoch,total_loss))
    total_loss_list.append(total_loss)
    torch.save(net.state_dict,'./data/ouput/model_tmp_%d_state_dict.pkl' % epoch)

结果展示

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章