<jupyter_start><jupyter_text>Example code<jupyter_code># initialize input parameters
k = 10
no_interactions = 10
total_CFs = 1
max_interacted_items_to_del = 4
user_id = 8

proximity_weight, diversity_weight = 1, 0

FLOAT_MAX = np.finfo(np.float32).max
items_interacted = test.sequences[test.user_ids==user_id][0]
predictions = -model.predict(items_interacted[:min(no_interactions, len(items_interacted))])
predictions[items_interacted] = FLOAT_MAX

print(f'Given the following interactions {items_interacted[:no_interactions]} for user {user_id}, the top-{k} recommended items are '
      f'{list(predictions.argsort()[:k])}')
cand_pos = input('Select item (position) to exclude from recommended list: ')
try:
    cand = predictions.argsort()[min(k, int(cand_pos)) - 1]
except ValueError:
    print("That's not an int!")

print(f'The selected item in pos {cand_pos} is {cand}\n')
from itertools import permutations, combinations
import torch.nn.functional as F


counter = 1
best_total_loss = ItemInteractionInfo(user_id, cand)
best_yloss = ItemInteractionInfo(user_id, cand, fobj=False)

for l in range(1, min(max_interacted_items_to_del, len(items_interacted)) + 1):
    print(f'Checking with {l} interacted items removed...')
#     produce permutations of various interactions
    perm = combinations(items_interacted[:min(no_interactions, len(items_interacted))], min(no_interactions, len(items_interacted)) - l)    

    for i in perm:        
#         predict next top-k items about to be selected        
        preds = model.predict(i)
#     convert logits produced by model, i.e., the probability distribution before normalization, by using softmax
        tensor = torch.from_numpy(preds).float()
        preds = F.softmax(tensor, dim=0)
        
        yloss = compute_yloss(preds.numpy()[cand], preds.numpy()[reversed(preds.argsort())[k]])
        proximity_loss = compute_proximity_loss(np.asarray(i)[np.newaxis, :], items_interacted)    
#     keep info about the best solution found depending on an objective function
        if best_total_loss.needs_update(dict(yloss=yloss, proximity=proximity_loss)):             
            best_total_loss.set_values(preds, i, items_interacted, [yloss, proximity_loss], counter, k)
        if best_yloss.needs_update(dict(yloss=yloss, proximity=proximity_loss)):            
            best_yloss.set_values(preds, i, items_interacted, [yloss, proximity_loss], counter, k) 

        counter += 1

similarity_rank = k - st.rankdata(similarity_matrix[cand, items_interacted]) + 1

print('\nBest total loss\n', '=' * len('Best total loss'), best_total_loss)
idx_items_exluded = np.where(np.isin(items_interacted, list(set(items_interacted).difference(set(best_total_loss.interactions)))))
print('Similarities of removed items: ', similarity_rank[idx_items_exluded])

print('\nBest yloss\n', '=' * len('Best yloss'), best_yloss)
idx_items_exluded = np.where(np.isin(items_interacted, list(set(items_interacted).difference(set(best_yloss.interactions)))))
print('Similarities of removed items: ', similarity_rank[idx_items_exluded])

print(f'\nTotal iterations: {counter}')<jupyter_output>Checking with 1 interacted items removed...
Checking with 2 interacted items removed...
Checking with 3 interacted items removed...
Checking with 4 interacted items removed...

Best total loss
 =============== 
user_id: 8, item_id: 485
yloss: 0.881, proximity_loss: 12
Item 485 is in position 19 now!!!
Found in iteration 17 and the interacted items are (384, 461, 236, 478, 86, 511, 60, 323)
10-best recommended items [(227, 0.0046888264), (594, 0.0045607807), (372, 0.004343697), (461, 0.003976637), (124, 0.0039312975), (539, 0.0037927055), (1277, 0.0037833683), (266, 0.0036134752), (325, 0.0035272632), (439, 0.0034671403)]

Similarities of removed items:  [ 4.  9.  3. -3. -5. -1.  8.  0.  1. 10.  5.  2.]

Best yloss
 ========== 
user_id: 8, item_id: 485
yloss: 0.5166, proximity_loss: 14
Item 485 is in position 88 now!!!
Found in iteration 178 and the interacted items are (384, 461, 236, 478, 86, 60)
10-best recommended items [(325, 0.006310096), (227, 0.005587188), (471, 0.005130091), (6[...]<jupyter_text>Optimized technique using item-similarities<jupyter_code># compute the item-item similarity matrix utilizing ratings
# adjusted cosine similarity on ratings

# construct 
M = np.zeros((dataset.num_users, dataset.num_items))
for u in range(1, dataset.num_users):    
    np.add.at(M[u], dataset.item_ids[dataset.user_ids==u], dataset.ratings[dataset.user_ids==u])

M_u = M.mean(axis=1)
item_mean_subtracted = M - M_u[:, np.newaxis]
similarity_matrix = 1 - squareform(pdist(item_mean_subtracted.T, 'cosine'))
# compute the item-item similarity matrix utilizing implicit feedback, i.e., whether interacted or not with an item
# jaccard is used to build the matrix

M = np.zeros((dataset.num_users, dataset.num_items), dtype=np.bool)
for u in trange(1, dataset.num_users):    
    np.add.at(M[u], dataset.item_ids[dataset.user_ids==u], dataset.ratings[dataset.user_ids==u])

similarity_matrix = 1 - squareform(pdist(M.T, 'jaccard'))
from itertools import permutations, combinations
import torch.nn.functional as F


# initial input parameters
max_length_permutation_tuples = 8
cand_pos = 7
top_k = 10
total_CFs = 1

best_tot_loss_data = []
best_yloss_data = []
users_no_data = []

# print(f'Checking for user', end='')
# delimiter = ''
for user_id in trange(1, 500):  # dataset.num_users):
#     print(f'{delimiter} {user_id}', end='')
#     delimiter = ','
    
    seq_size = len(test.sequences[test.user_ids==user_id])
    best_total_loss = [None] * seq_size
    best_yloss = [None] * seq_size
    
    for j in range(seq_size):    
        if all(v > 0 for v in test.sequences[test.user_ids==user_id][j]):    
            items_interacted = test.sequences[test.user_ids==user_id][j]
            predictions = -model.predict(items_interacted)
            predictions[items_interacted] = FLOAT_MAX
            
            kth_item = predictions.argsort()[top_k - 1]
            rec_item_exclude = predictions.argsort()[min(top_k, int(cand_pos)) - 1]
            
            best_total_loss[j] = ItemInteractionInfo(user_id, rec_item_exclude)
            best_yloss[j] = ItemInteractionInfo(user_id, rec_item_exclude, fobj=False)

            counter = 1        

            for l in range(len(items_interacted) - 1, max(0, len(items_interacted) - max_length_permutation_tuples), -1):
                if best_total_loss[j].solution_found: break
                
                # produce permutations of various interactions
                perm = combinations(items_interacted, l)
                
                for i in perm:
                    # predict next top-k items about to be selected        
                    preds = model.predict(i) 
                    # convert logits produced by model, i.e., the probability distribution before normalization, by using softmax
                    tensor = torch.from_numpy(preds).float()
                    preds = F.softmax(tensor, dim=0)

                    yloss = compute_yloss(preds.numpy()[rec_item_exclude], preds.numpy()[kth_item])
                    proximity_loss = compute_proximity_loss(np.asarray(i)[np.newaxis, :], items_interacted)    
                    # keep info about the best solution found depending on an objective function
                    if best_total_loss[j].needs_update(dict(yloss=yloss, proximity=proximity_loss)):                        
                        best_total_loss[j].set_values(preds, i, items_interacted, [yloss, proximity_loss], counter, top_k)                        
#                     if best_yloss[j].needs_update(dict(yloss=yloss, proximity=proximity_loss)):
#                         best_yloss[j].set_values(preds, i, items_interacted, [yloss, proximity_loss], counter, k)                 

                    counter += 1     
            
#     except IndexError as err:        
#         users_no_data.append(user_id)
        
    best_tot_loss_data.append(best_total_loss)
    best_yloss_data.append(best_yloss)

# print('\nProcess ended')
# save results to df

non_solvable_cases = []
total_data = []
for items in best_tot_loss_data:
    for idx, rec in enumerate(items):
        if rec is None: continue
        
        if not rec.solution_found: 
            non_solvable_cases.append(rec.user_id)
            continue
                
        similarity_rank = len(rec.complete_interactions) - st.rankdata(similarity_matrix[rec.item_id, rec.complete_interactions]) + 1
        del_items_indices = np.where(np.isin(rec.complete_interactions, list(set(rec.complete_interactions).difference(set(rec.interactions)))))
#         total_data.extend(list(similarity_rank[del_items_indices].astype(int)))
        
        total_data.append([
            rec.user_id, idx, rec.item_id, rec.y_loss, rec.proximity_loss, 
            ','.join(map(str, sorted(similarity_rank[del_items_indices].astype(int)))),
            ','.join(map(str, set(rec.complete_interactions).difference(set(rec.interactions)))),
        ])
df_3th = pd.DataFrame(total_data, columns=['user_id', 'pos', 'rec_item_id', 'yloss', 'proximity_loss', 'sim_rank', 'del_iitems'])
df_3th['proximity_loss'] = df_3th['proximity_loss'].astype(int)
df_7th = pd.DataFrame(total_data, columns=['user_id', 'pos', 'rec_item_id', 'yloss', 'proximity_loss', 'sim_rank', 'del_iitems'])
df_7th['proximity_loss'] = df_7th['proximity_loss'].astype(int)<jupyter_output><empty_output><jupyter_text>Stats for 7th item excluded from the top-10 recommendation list<jupyter_code>from collections import Counter


non_solvable_cases = []
total_data = []
for items in best_tot_loss_data:
    for rec in items:
        if rec is None: continue
            
        if not rec.solution_found: 
            non_solvable_cases.append(rec.user_id)
            continue

        similarity_rank = len(rec.complete_interactions) - st.rankdata(similarity_matrix[rec.item_id, rec.complete_interactions]) + 1
        del_items_indices = np.where(np.isin(rec.complete_interactions, list(set(rec.complete_interactions).difference(set(rec.interactions)))))
        total_data.extend(list(similarity_rank[del_items_indices].astype(int)))
    
cnt = Counter(total_data)
# with matplotlib
# plt.hist(list(cnt.keys()), weights=list(cnt.values()), width=0.8)
fig, axs = plt.subplots(1,2, figsize=(15, 5))

df = pd.DataFrame.from_dict(cnt, orient='index', columns=['count'])
df['percentage'] = df/df.sum()
df.sort_index().plot.bar(y='count', ax=axs[0])
df.sort_index().plot.bar(y='percentage', ax=axs[1])

plt.suptitle('Counting all similarity positions of each discovered counterfactual')
from collections import Counter


non_solvable_cases = []
total_data = []
for items in best_tot_loss_data:
    for rec in items:
        if rec is None: continue
            
        if not rec.solution_found: 
            non_solvable_cases.append(rec.user_id)
            continue

        total_data.append(rec.proximity_loss)
    
cnt = Counter(total_data)
fig, axs = plt.subplots(1,2, figsize=(15, 5))

df = pd.DataFrame.from_dict(cnt, orient='index', columns=['count'])
df['percentage'] = df/df.sum()
df.sort_index().plot.bar(y='count', ax=axs[0])
df.sort_index().plot.bar(y='percentage', ax=axs[1])

plt.suptitle('No of items to exclude from interacted list to have the recommendations change accordingly')
from collections import Counter


non_solvable_cases = []
total_data = []
for items in best_tot_loss_data:
    for rec in items:
        if rec is None: continue
            
        if not rec.solution_found: 
            non_solvable_cases.append(rec.user_id)
            continue

        similarity_rank = len(rec.complete_interactions) - st.rankdata(similarity_matrix[rec.item_id, rec.complete_interactions]) + 1
        del_items_indices = np.where(np.isin(rec.complete_interactions, list(set(rec.complete_interactions).difference(set(rec.interactions)))))
        total_data.extend(sorted(similarity_rank[del_items_indices].astype(int)[-1:]))        
    
cnt = Counter(total_data)
fig, axs = plt.subplots(1,2, figsize=(15, 5))

df = pd.DataFrame.from_dict(cnt, orient='index', columns=['count'])
df['percentage'] = df/df.sum()
df.sort_index().plot.bar(y='count', ax=axs[0])
df.sort_index().plot.bar(y='percentage', ax=axs[1])

plt.suptitle('Counting the worst similarity position of each discovered counterfactual')<jupyter_output><empty_output><jupyter_text>3rd item excluded stats from the top-10 recommendation list<jupyter_code>from collections import Counter


non_solvable_cases = []
total_data = []
for items in best_tot_loss_data:
    for rec in items:
        if rec is None: continue
            
        if not rec.solution_found: 
            non_solvable_cases.append(rec.user_id)
            continue

        similarity_rank = len(rec.complete_interactions) - st.rankdata(similarity_matrix[rec.item_id, rec.complete_interactions]) + 1
        del_items_indices = np.where(np.isin(rec.complete_interactions, list(set(rec.complete_interactions).difference(set(rec.interactions)))))
        total_data.extend(list(similarity_rank[del_items_indices].astype(int)))
    
cnt = Counter(total_data)
# with matplotlib
# plt.hist(list(cnt.keys()), weights=list(cnt.values()), width=0.8)
fig, axs = plt.subplots(1,2, figsize=(15, 5))

df = pd.DataFrame.from_dict(cnt, orient='index', columns=['count'])
df['percentage'] = df/df.sum()
df.sort_index().plot.bar(y='count', ax=axs[0])
df.sort_index().plot.bar(y='percentage', ax=axs[1])

plt.suptitle('Counting all similarity positions of each discovered counterfactual')
from collections import Counter


non_solvable_cases = []
total_data = []
for items in best_tot_loss_data:
    for rec in items:
        if rec is None: continue
            
        if not rec.solution_found: 
            non_solvable_cases.append(rec.user_id)
            continue

        total_data.append(rec.proximity_loss)
    
cnt = Counter(total_data)
fig, axs = plt.subplots(1,2, figsize=(15, 5))

df = pd.DataFrame.from_dict(cnt, orient='index', columns=['count'])
df['percentage'] = df/df.sum()
df.sort_index().plot.bar(y='count', ax=axs[0])
df.sort_index().plot.bar(y='percentage', ax=axs[1])

plt.suptitle('No of items to exclude from interacted list to have the recommendations change accordingly')
from collections import Counter


non_solvable_cases = []
total_data = []
for items in best_tot_loss_data:
    for rec in items:
        if rec is None: continue
            
        if not rec.solution_found: 
            non_solvable_cases.append(rec.user_id)
            continue

        similarity_rank = len(rec.complete_interactions) - st.rankdata(similarity_matrix[rec.item_id, rec.complete_interactions]) + 1
        del_items_indices = np.where(np.isin(rec.complete_interactions, list(set(rec.complete_interactions).difference(set(rec.interactions)))))
        total_data.extend(sorted(similarity_rank[del_items_indices].astype(int)[-1:]))        
    
cnt = Counter(total_data)
fig, axs = plt.subplots(1,2, figsize=(15, 5))

df = pd.DataFrame.from_dict(cnt, orient='index', columns=['count'])
df['percentage'] = df/df.sum()
df.sort_index().plot.bar(y='count', ax=axs[0])
df.sort_index().plot.bar(y='percentage', ax=axs[1])

plt.suptitle('Counting the worst similarity position of each discovered counterfactual')<jupyter_output><empty_output><jupyter_text>Use Pooling representation  train and save **pooling** model<jupyter_code>from spotlight.sequence.implicit import ImplicitSequenceModel
from spotlight.evaluation import sequence_mrr_score

model = ImplicitSequenceModel(
    batch_size=256,
    embedding_dim=8,
    l2=0.0,
    learning_rate=0.1,
    n_iter=11,
    representation='pooling',
    loss='adaptive_hinge',
#     use_cuda=torch.cuda.is_available(),
    random_state=np.random.RandomState(2020)
)
model.fit(train)

mrr = sequence_mrr_score(model, test)
ofile = 'pooling_model_1m_20interactions.pt'
torch.save(model, os.path.join(models_path, ofile))<jupyter_output><empty_output><jupyter_text>load **pooling** model<jupyter_code>ofile = 'pooling_model_1m_20interactions.pt'
model = torch.load(os.path.join(models_path, ofile))
# from spotlight.sequence.representations import PoolNet
from spotlight.torch_utils import minibatch

seqs = test.sequences.astype(np.int64)
seqs_tensor = torch.from_numpy(seqs)

counter = 2
for batch_seq in minibatch(seqs_tensor, batch_size=model._batch_size):
    _all, _final = model._net.user_representation(batch_seq)
    print(_all.shape, len(_all[0]), _final[0])
    print(_all.permute(0, 2, 1).shape)
    if counter > 1:
        break
        
    counter += 1
def gpu_embeddings_to_cosine_similarity_matrix(E):
    """ 
    Converts a tensor of n embeddings to an (n, n) tensor of similarities.
    """
    dot = E @ E.t()
    norm = torch.norm(E, 2, 1)
    x = torch.div(dot, norm)
    x = torch.div(x, torch.unsqueeze(norm, -1))
    return x
sims_on_item_embeds = gpu_embeddings_to_cosine_similarity_matrix(model._net.item_embeddings(torch.arange(0, test.num_items, dtype=torch.int64)))
from torch.nn.functional import cosine_similarity

def embeddings_to_cosine_similarity_matrix(E):
    """ 
    Converts a a tensor of n embeddings to an (n, n) tensor of similarities.
    """
    similarities = [[cosine_similarity(a, b, dim=0) for a in E] for b in E]
#     similarities = list(map(torch.cat, similarities))
    similarities = list(map(lambda x: torch.stack(x, dim=-1), similarities))
    return torch.stack(similarities)
embeddings_to_cosine_similarity_matrix(model._net.item_embeddings(torch.arange(1, 10, dtype=torch.int64)))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12235547007580043, 'fraction_numerical': 0.024041181129087, 'mean_word_length': 3.881004969630039, 'pattern_counts': {'":': 0, '<': 24, '<?xml version=': 0, '>': 26, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '463486', 'license': 'MIT', 'n_tokens_mistral': 6304, 'n_tokens_neox': 5832, 'n_words': 1209, 'path': 'notebooks/brute_force_rec_expl.ipynb', 'repo_name': 'vkaff/CFExplainability', 'size': '17678'}