Skip to content

Commit

Permalink
add mind.py and update youtubednn
Browse files Browse the repository at this point in the history
  • Loading branch information
ZiyaoGeng committed Apr 29, 2022
1 parent bd14d8c commit a774662
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ The experimental environment designed by Reclearn is different from that of some
<tr><td>NCF</td><td>0.5834</td><td>0.2219</td><td>0.3060</td><td>0.5448</td><td>0.2831</td><td>0.3451</td><td>0.7768</td><td>0.4273</td><td>0.5103</td></tr>
<tr><td>DSSM</td><td>0.5498</td><td>0.2148</td><td>0.2929</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
<tr><td>YoutubeDNN</td><td>0.6737</td><td>0.3414</td><td>0.4201</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
<tr><td>MIND(Error)</td><td>0.6366</td><td>0.2597</td><td>0.3483</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
<tr><td>GRU4Rec</td><td>0.7969</td><td>0.4698</td><td>0.5483</td><td>0.5211</td><td>0.2724</td><td>0.3312</td><td>0.8501</td><td>0.5486</td><td>0.6209</td></tr>
<tr><td>Caser</td><td>0.7916</td><td>0.4450</td><td>0.5280</td><td>0.5487</td><td>0.2884</td><td>0.3501</td><td>0.8275</td><td>0.5064</td><td>0.5832</td></tr>
<tr><td>SASRec</td><td>0.8103</td><td>0.4812</td><td>0.5605</td><td>0.5230</td><td>0.2781</td><td>0.3355</td><td>0.8606</td><td>0.5669</td><td>0.6374</td></tr>
Expand Down
83 changes: 83 additions & 0 deletions example/m_mind_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
Created on Apr 26, 2022
train MIND demo
@author: Ziyao Geng([email protected])
"""
import os
from absl import flags, app
from time import time
from tensorflow.keras.optimizers import Adam

from reclearn.models.matching import MIND
from reclearn.data.datasets import movielens as ml
from reclearn.evaluator import eval_pos_neg

FLAGS = flags.FLAGS

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
os.environ['CUDA_VISIBLE_DEVICES'] = '6'

# Setting training parameters
flags.DEFINE_string("file_path", "data/ml-1m/ratings.dat", "file path.")
flags.DEFINE_string("train_path", "data/ml-1m/ml_seq_train.txt", "train path. If set to None, the program will split the dataset.")
flags.DEFINE_string("val_path", "data/ml-1m/ml_seq_val.txt", "val path.")
flags.DEFINE_string("test_path", "data/ml-1m/ml_seq_test.txt", "test path.")
flags.DEFINE_string("meta_path", "data/ml-1m/ml_seq_meta.txt", "meta path.")
flags.DEFINE_integer("embed_dim", 64, "The size of embedding dimension.")
flags.DEFINE_float("embed_reg", 0.0, "The value of embedding regularization.")
flags.DEFINE_integer("num_interest", 1, "The number of user interests.")
flags.DEFINE_bool("stop_grad", True, "The weights in the capsule network are updated without gradient descent.")
flags.DEFINE_bool("label_attention", True, "Whether using label-aware attention or not.")
flags.DEFINE_float("learning_rate", 0.001, "Learning rate.")
flags.DEFINE_integer("neg_num", 2, "The number of negative sample for each positive sample.")
flags.DEFINE_integer("seq_len", 100, "The length of user's behavior sequence.")
flags.DEFINE_integer("epochs", 20, "train steps.")
flags.DEFINE_integer("batch_size", 512, "Batch Size.")
flags.DEFINE_integer("test_neg_num", 100, "The number of test negative samples.")
flags.DEFINE_integer("k", 10, "recall k items at test stage.")


def main(argv):
# TODO: 1. Split Data
if FLAGS.train_path == "None":
train_path, val_path, test_path, meta_path = ml.split_seq_data(file_path=FLAGS.file_path)
else:
train_path, val_path, test_path, meta_path = FLAGS.train_path, FLAGS.val_path, FLAGS.test_path, FLAGS.meta_path
with open(meta_path) as f:
_, max_item_num = [int(x) for x in f.readline().strip('\n').split('\t')]
# TODO: 2. Load Sequence Data
train_data = ml.load_seq_data(train_path, "train", FLAGS.seq_len, FLAGS.neg_num, max_item_num)
val_data = ml.load_seq_data(val_path, "val", FLAGS.seq_len, FLAGS.neg_num, max_item_num)
test_data = ml.load_seq_data(test_path, "test", FLAGS.seq_len, FLAGS.test_neg_num, max_item_num)
# TODO: 3. Set Model Hyper Parameters.
model_params = {
'item_num': max_item_num + 1,
'embed_dim': FLAGS.embed_dim,
'seq_len': FLAGS.seq_len,
'num_interest': FLAGS.num_interest,
'stop_grad': FLAGS.stop_grad,
'label_attention': FLAGS.label_attention,
'neg_num': FLAGS.neg_num,
'batch_size': FLAGS.batch_size,
'embed_reg': FLAGS.embed_reg
}
# TODO: 4. Build Model
model = MIND(**model_params)
model.compile(optimizer=Adam(learning_rate=FLAGS.learning_rate))
# TODO: 5. Fit Model
for epoch in range(1, FLAGS.epochs + 1):
t1 = time()
model.fit(
x=train_data,
epochs=1,
validation_data=val_data,
batch_size=FLAGS.batch_size
)
t2 = time()
eval_dict = eval_pos_neg(model, test_data, ['hr', 'mrr', 'ndcg'], FLAGS.k, FLAGS.batch_size)
print('Iteration %d Fit [%.1f s], Evaluate [%.1f s]: HR = %.4f, MRR = %.4f, NDCG = %.4f'
% (epoch, t2 - t1, time() - t2, eval_dict['hr'], eval_dict['mrr'], eval_dict['ndcg']))


if __name__ == '__main__':
app.run(main)
77 changes: 77 additions & 0 deletions reclearn/layers/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,3 +520,80 @@ def call(self, inputs):
logits = tf.matmul(inputs, self.W) # (None, neg_num + 1, 1)
weights = tf.nn.sigmoid(logits)
return weights


class CapsuleNetwork(Layer):
def __init__(self, embed_dim, seq_len, bilinear_type=0, num_interest=4, stop_grad=True):
super(CapsuleNetwork, self).__init__()
self.bilinear_type = bilinear_type
self.seq_len = seq_len
self.num_interest = num_interest
self.embed_dim = embed_dim
self.stop_grad = stop_grad

def build(self, input_shape):
if self.bilinear_type >= 2:
self.w = self.add_weight(
shape=[1, self.seq_len, self.num_interest * self.embed_dim, self.embed_dim],
initializer='random_normal',
name='weights'
)

def call(self, hist_emb, mask):
if self.bilinear_type == 0:
hist_emb_hat = tf.tile(hist_emb, [1, 1, self.num_interest]) # (None, seq_len, num_inter * embed_dim)
elif self.bilinear_type == 1:
hist_emb_hat = Dense(self.dim * self.num_interest, activation=None)(hist_emb)
else:
u = tf.expand_dims(hist_emb, axis=2) # (None, seq_len, 1, embed_dim)
hist_emb_hat = tf.reduce_sum(self.w * u, axis=3) # (None, seq_len, num_inter * embed_dim)
hist_emb_hat = tf.reshape(hist_emb_hat, [-1, self.seq_len, self.num_interest, self.embed_dim])
hist_emb_hat = tf.transpose(hist_emb_hat, [0, 2, 1, 3]) # (None, num_inter, seq_len, embed_dim)
hist_emb_hat = tf.reshape(hist_emb_hat, [-1, self.num_interest, self.seq_len, self.embed_dim])
if self.stop_grad:
hist_emb_iter = tf.stop_gradient(hist_emb_hat)
else:
hist_emb_iter = hist_emb_hat # (None, num_inter, seq_len, embed_dim)

if self.bilinear_type > 0:
self.capsule_weight = self.add_weight(
shape=[tf.shape(hist_emb_hat)[0], self.num_interest, self.seq_len],
initializer=tf.zeros_initializer()
)
else:
self.capsule_weight = tf.random.truncated_normal(
shape=[tf.shape(hist_emb_hat)[0], self.num_interest, self.seq_len],
stddev=1.0)
tf.stop_gradient(self.capsule_weight)

for i in range(3):
att_mask = tf.tile(tf.expand_dims(mask, axis=1), [1, self.num_interest, 1]) # (None, num_inter, seq_len)
paddings = tf.zeros_like(att_mask)

capsule_softmax_weight = tf.nn.softmax(self.capsule_weight, axis=1) # (None, num_inter, seq_len)
capsule_softmax_weight = tf.where(tf.equal(att_mask, 0), paddings, capsule_softmax_weight)
capsule_softmax_weight = tf.expand_dims(capsule_softmax_weight, 2) # (None, num_inter, 1, seq_len)

if i < 2:
interest_capsule = tf.matmul(capsule_softmax_weight, hist_emb_iter) # (None, num_inter, 1, embed_dim)
cap_norm = tf.reduce_sum(tf.square(interest_capsule), -1, keepdims=True)
scalar_factor = cap_norm / (1 + cap_norm) / tf.sqrt(cap_norm + 1e-9)
interest_capsule = scalar_factor * interest_capsule

delta_weight = tf.matmul(hist_emb_iter, tf.transpose(interest_capsule, [0, 1, 3, 2]))
delta_weight = tf.reshape(delta_weight, [-1, self.num_interest, self.seq_len])
self.capsule_weight = self.capsule_weight + delta_weight
else:
interest_capsule = tf.matmul(capsule_softmax_weight, hist_emb_hat)
cap_norm = tf.reduce_sum(tf.square(interest_capsule), -1, True)
scalar_factor = cap_norm / (1 + cap_norm) / tf.sqrt(cap_norm + 1e-9)
interest_capsule = scalar_factor * interest_capsule

interest_capsule = tf.reshape(interest_capsule, [-1, self.num_interest, self.embed_dim])
return interest_capsule






3 changes: 2 additions & 1 deletion reclearn/models/matching/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from reclearn.models.matching.attrec import AttRec
from reclearn.models.matching.caser import Caser
from reclearn.models.matching.fissa import FISSA
from reclearn.models.matching.mind import MIND


__all__ = ['PopRec', 'BPR', 'NCF', 'DSSM', 'YoutubeDNN', 'GRU4Rec', 'SASRec', 'AttRec', 'Caser', 'FISSA']
__all__ = ['PopRec', 'BPR', 'NCF', 'DSSM', 'YoutubeDNN', 'GRU4Rec', 'SASRec', 'AttRec', 'Caser', 'FISSA', 'MIND']
109 changes: 109 additions & 0 deletions reclearn/models/matching/mind.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
Created on Apr 25, 2022
Reference: "Multi-Interest Network with Dynamic Routing for Recommendation at Tmall", CIKM, 2019
@author: Ziyao Geng([email protected])
"""
import tensorflow as tf
from tensorflow.keras import Model
from tensorflow.keras.layers import Input
from tensorflow.keras.regularizers import l2

from reclearn.layers.core import CapsuleNetwork


class MIND(Model):
def __init__(self, item_num, embed_dim, seq_len=100, num_interest=4, stop_grad=True, label_attention=True,
neg_num=4, batch_size=512, embed_reg=0., seed=None):
"""MIND
Args:
:param item_num: An integer type. The largest item index + 1.
:param embed_dim: An integer type. Embedding dimension of item vector.
:param seq_len: An integer type. The length of the input sequence.
:param bilinear_type: An integer type. The number of user interests.
:param num_interest: An integer type. The number of user interests.
:param stop_grad: A boolean type. The weights in the capsule network are updated without gradient descent.
:param label_attention: A boolean type. Whether using label-aware attention or not.
:param neg_num: A integer type. The number of negative samples for each positive sample.
:param batch_size: A integer type. The number of samples per batch.
:param embed_reg: A float type. The regularizer of embedding.
:param seed: A Python integer to use as random seed.
:return
"""
super(MIND, self).__init__()
with tf.name_scope("Embedding_layer"):
# item embedding
self.item_embedding_table = self.add_weight(name='item_embedding_table',
shape=(item_num, embed_dim),
initializer='random_normal',
regularizer=l2(embed_reg),
trainable=True)
# embedding bias
self.embedding_bias = self.add_weight(name='embedding_bias',
shape=(item_num,),
initializer=tf.zeros_initializer(),
trainable=False)
self.capsule_network = CapsuleNetwork(embed_dim, seq_len, 0, num_interest, stop_grad)
self.seq_len = seq_len
self.num_interest = num_interest
self.label_attention = label_attention
self.item_num = item_num
self.embed_dim = embed_dim
self.neg_num = neg_num
self.batch_size = batch_size
# seed
tf.random.set_seed(seed)

def call(self, inputs, training=False):
user_hist_emb = tf.nn.embedding_lookup(self.item_embedding_table, inputs['click_seq'])
mask = tf.cast(tf.not_equal(inputs['click_seq'], 0), dtype=tf.float32) # (None, seq_len)
user_hist_emb = tf.multiply(user_hist_emb, tf.expand_dims(mask, axis=-1)) # (None, seq_len, embed_dim)
# capsule network
interest_capsule = self.capsule_network(user_hist_emb, mask) # (None, num_inter, embed_dim)

if training:
if self.label_attention:
item_embed = tf.nn.embedding_lookup(self.item_embedding_table, tf.reshape(inputs['pos_item'], [-1, ]))
inter_att = tf.matmul(interest_capsule, tf.reshape(item_embed, [-1, self.embed_dim, 1])) # (None, num_inter, 1)
inter_att = tf.nn.softmax(tf.pow(tf.reshape(inter_att, [-1, self.num_interest]), 1))

user_info = tf.matmul(tf.reshape(inter_att, [-1, 1, self.num_interest]), interest_capsule) # (None, 1, embed_dim)
user_info = tf.reshape(user_info, [-1, self.embed_dim])
else:
user_info = tf.reduce_max(interest_capsule, axis=1) # (None, embed_dim)
# train, sample softmax loss
loss = tf.reduce_mean(tf.nn.sampled_softmax_loss(
weights=self.item_embedding_table,
biases=self.embedding_bias,
labels=tf.reshape(inputs['pos_item'], shape=[-1, 1]),
inputs=user_info,
num_sampled=self.neg_num * self.batch_size,
num_classes=self.item_num
))
# add loss
self.add_loss(loss)
return loss
else:
# predict/eval
pos_info = tf.nn.embedding_lookup(self.item_embedding_table, inputs['pos_item']) # (None, embed_dim)
neg_info = tf.nn.embedding_lookup(self.item_embedding_table, inputs['neg_item']) # (None, neg_num, embed_dim)

if self.label_attention:
user_info = tf.reduce_max(interest_capsule, axis=1) # (None, embed_dim)
else:
user_info = tf.reduce_max(interest_capsule, axis=1) # (None, embed_dim)

# calculate similar scores.
pos_scores = tf.reduce_sum(tf.multiply(user_info, pos_info), axis=-1, keepdims=True) # (None, 1)
neg_scores = tf.reduce_sum(tf.multiply(tf.expand_dims(user_info, axis=1), neg_info),
axis=-1) # (None, neg_num)
logits = tf.concat([pos_scores, neg_scores], axis=-1)
return logits

def summary(self):
inputs = {
'click_seq': Input(shape=(self.seq_len,), dtype=tf.int32),
'pos_item': Input(shape=(), dtype=tf.int32),
'neg_item': Input(shape=(1,), dtype=tf.int32) # suppose neg_num=1
}
Model(inputs=inputs, outputs=self.call(inputs)).summary()

8 changes: 6 additions & 2 deletions reclearn/models/matching/youtubednn.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,12 @@ def __init__(self, item_num, embed_dim, user_mlp, activation='relu',
tf.random.set_seed(seed)

def call(self, inputs, training=False):
# user info
user_info = tf.reduce_mean(tf.nn.embedding_lookup(self.item_embedding_table, inputs['click_seq']), axis=1)
seq_embed = tf.nn.embedding_lookup(self.item_embedding_table, inputs['click_seq'])
# mask
mask = tf.cast(tf.not_equal(inputs['click_seq'], 0), dtype=tf.float32) # (None, seq_len)
seq_embed = tf.multiply(seq_embed, tf.expand_dims(mask, axis=-1))
# user_info
user_info = tf.reduce_mean(seq_embed, axis=1) # (None, embed_dim)
# mlp
user_info = self.user_mlp_layer(user_info)
if user_info.shape[-1] != self.embed_dim:
Expand Down

0 comments on commit a774662

Please sign in to comment.