《Micro Behaviors:A New Perspective in E-commerce Recommendation》一文主要探究了微观行为对于预测任务的影响,为了更加准确的描述用户的每一个微观行为,文中提出了RIB模型,鉴于文中没有公布github代码,所以我在这里就根据文章的说法进行了简单的复现。
这篇文章的优缺点都非常明显,优点在于论文对于用户的微观行为进行了非常详细的统计分析,阐述了为什么要做微观行为的意义,在创新度和说服力上都非常强。
缺点在于这篇文章对于模型的介绍并不是十分清晰,前后使用的变量符号也不统一,读起来就有点费解。在本blog中我根据自己的理解统一了一下符号含义,如有不对请多多指正!
这个blog主要的任务也就是在于还原代码的实现过程,数据集来源:https://jdata.jd.com/html/detail.html?id=8 京东2019年的比赛数据集。
不是原po的数据集,因为数据原因,对于模型也有些许变动,但是基本思路是一致的。
下面就开始实现代码的部分啦!拿到数据集后, 大家一起加油吧!
原文的数据集介绍见原文,本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'))
这篇文章所有的代码逻辑都极其非常的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)