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)

結果展示

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