diff --git a/examples/fedhgnn/README.md b/examples/fedhgnn/README.md new file mode 100644 index 00000000..a407281e --- /dev/null +++ b/examples/fedhgnn/README.md @@ -0,0 +1,25 @@ +# FedHGNN +The source code of WWW 2024 paper "Federated Heterogeneous Graph Neural Network for Privacy-preserving Recommendation" + + +# Requirements +``` +dgl==1.1.0+cu113 +numpy==1.21.6 +ogb==1.3.6 +python==3.7.13 +scikit-learn==1.0.2 +scipy==1.7.3 +torch==1.12.1+cu113 +torchaudio==0.12.1+cu113 +torchvision==0.13.1+cu113 +``` + + +# Easy Run +``` +cd ./codes/FedHGNN +python main.py --dataset acm --shared_num 20 --p1 1 --p2 2 --lr 0.01 --device cuda:0 +``` + + diff --git a/examples/fedhgnn/codes/FedHGNN/client.py b/examples/fedhgnn/codes/FedHGNN/client.py new file mode 100644 index 00000000..746d97c6 --- /dev/null +++ b/examples/fedhgnn/codes/FedHGNN/client.py @@ -0,0 +1,225 @@ +import copy +import tensorlayerx as tlx +import os +os.environ['TL_BACKEND'] = 'torch' +import random + +from local_differential_privacy_library import * +from util import * +from random import sample +from sklearn.metrics.pairwise import cosine_similarity + +from warnings import simplefilter +simplefilter(action='ignore', category=FutureWarning) + + + +class Client(tlx.nn.Module): + def __init__(self, user_id, item_id, args): + super().__init__() + self.device = args.device + self.user_id = user_id + self.item_id = item_id #list + #self.semantic_neighbors = semantic_neighbors + + + def negative_sample(self, total_item_num): + '''生成item负样本集合''' + #从item列表里随机选取item作为user的负样本 + item_neg_ind = [] + #item_neg_ind和item_id数量一样 + for _ in self.item_id: + neg_item = np.random.randint(1, total_item_num) + while neg_item in self.item_id: + neg_item = np.random.randint(1, total_item_num) + item_neg_ind.append(neg_item) + '''生成item负样本集合end''' + return item_neg_ind + + def negative_sample_with_augment(self, total_item_num, sampled_items): + item_set = self.item_id+sampled_items + '''生成item负样本集合''' + #从item列表里随机选取item作为user的负样本 + item_neg_ind = [] + #item_neg_ind和item_id数量一样 + for _ in item_set: + neg_item = np.random.randint(1, total_item_num) + while neg_item in item_set: + neg_item = np.random.randint(1, total_item_num) + item_neg_ind.append(neg_item) + '''生成item负样本集合end''' + return item_neg_ind + + def sample_item_augment(self, item_num): + ls = [i for i in range(item_num) if i not in self.item_id] + sampled_items = sample(ls, 5) + + return sampled_items + + + def perturb_adj(self, value, label_author, author_label, label_count, shared_knowledge_rep, eps1, eps2): + #print(value.shape) #1,17431 + #此用户的item共可分成多少个groups + groups = {} + for item in self.item_id: + group = author_label[item] + if(group not in groups.keys()): + groups[group] = [item] + else: + groups[group].append(item) + + '''step1:EM''' + num_groups = len(groups) + quality = np.array([0.0]*len(label_author)) + G_s_u = groups.keys() + if(len(G_s_u)==0):#此用户没有交互的item,则各个位置quality平均 + for group in label_author.keys(): + quality[group] = 1 + num_groups = 1 + else: + for group in label_author.keys(): + qua = max([(cosine_similarity(shared_knowledge_rep[g], shared_knowledge_rep[group])+1)/2.0 for g in G_s_u]) + quality[group] = qua + + EM_eps = eps1/num_groups + EM_p = EM_eps*quality/2 #隐私预算1 eps + EM_p = softmax(EM_p) + + #按照概率选择group + select_group_keys = np.random.choice(range(len(label_author)), size = len(groups), replace = False, p = EM_p) + select_group_keys_temp = list(select_group_keys) + degree_list = [len(v) for _, v in groups.items()] + new_groups = {} + + for key in select_group_keys:#先把存在于当前用户的shared knowledge拿出来 + key_temp = key + if(key_temp in groups.keys()): + new_groups[key_temp] = groups[key_temp] + degree_list.remove(len(groups[key_temp])) + select_group_keys_temp.remove(key_temp) + + for key in select_group_keys_temp:#不存在的随机采样交互的item,并保持度一致 + key_temp = key + cur_degree = degree_list[0] + if(len(label_author[key_temp]) >= cur_degree): + new_groups[key_temp] = random.sample(label_author[key_temp], cur_degree) + else:#需要的度比当前group的size大,则将度设为当前group的size + new_groups[key_temp] = label_author[key_temp] + degree_list.remove(cur_degree) + + groups = new_groups + value = np.zeros_like(value)#一定要更新value + for group_id, items in groups.items(): + value[:,items] = 1 + '''pure em''' + #value_rr = value + + + + '''step2:rr''' + all_items = set(range(len(author_label))) + select_items = [] + for group_id, items in groups.items(): + select_items.extend(label_author[group_id]) + mask_rr = list(all_items - set(select_items)) + + '''rr''' + value_rr = perturbation_test(value, 1-value, eps2) + #print(np.sum(value_rr)) 4648 + value_rr[:, mask_rr] = 0 + # #print(np.sum(value_rr)) 469 + # + '''dprr''' + for group_id, items in groups.items(): + degree = len(items) + n = len(label_author[group_id]) + p = eps2p(eps2) + q = degree/(degree*(2*p-1) + (n)*(1-p)) + rnd = np.random.random(value_rr.shape) + #原来是0的一定还是0,原来是1的以概率q保持1,以达到degree减少 + dprr_results = np.where(rnd1 + :param q: probability of 0->1 + update: 2020.03.06 + :return: + """ + q = 1-p if q is None else q + if isinstance(bit_array, int): + probability = p if bit_array == 1 else q + return np.random.binomial(n=1, p=probability) + return np.where(bit_array == 1, np.random.binomial(1, p, bit_array.shape), np.random.binomial(1, q, bit_array.shape)) + +'''torch version''' +def random_response(bit_array: torch.Tensor, p, q=None): + """ + :param bit_array: + :param p: probability of 1->1 + :param q: probability of 0->1 + update: 2020.03.06 + :return: + """ + device = bit_array.device + q = 1-p if q is None else q + return torch.where(bit_array == 1, torch.distributions.bernoulli.Bernoulli(p).sample(bit_array.shape).to(device), + torch.distributions.bernoulli.Bernoulli(q).sample(bit_array.shape).to(device)) + +def unary_encoding(bit_array: np.ndarray, epsilon): + """ + the unary encoding, the default UE is SUE + update: 2020.02.25 + """ + if not isinstance(bit_array, np.ndarray): + raise Exception("Type Err: ", type(bit_array)) + return symmetric_unary_encoding(bit_array, epsilon) + + +def symmetric_unary_encoding(bit_array: np.ndarray, epsilon): + p = eps2p(epsilon / 2) / (eps2p(epsilon / 2) + 1) + q = 1 / (eps2p(epsilon / 2) + 1) + return random_response(bit_array, p, q) + + +def optimized_unary_encoding(bit_array: np.ndarray, epsilon): + p = 1 / 2 + q = 1 / (eps2p(epsilon) + 1) + return random_response(bit_array, p, q) + + +if __name__ == '__main__': + # a = np.array([[0.4,-0.6],[0.7,0.2]]) + # print(discretization1(a, -1, 1)) + # + # a = np.array([1]) + # print(discretization1(a, -1, 1)) + pass diff --git a/examples/fedhgnn/codes/FedHGNN/main.py b/examples/fedhgnn/codes/FedHGNN/main.py new file mode 100644 index 00000000..5e1562e9 --- /dev/null +++ b/examples/fedhgnn/codes/FedHGNN/main.py @@ -0,0 +1,279 @@ +from client import Client +from server import Server +import os +#import dgl + + +from time import time +import sys +import numpy as np +from ourparse import * +from scipy.sparse import lil_matrix +from gammagl.models.fedhgnn import model +from rec_dataset import * + +import copy +import random +import tensorlayerx as tlx +import gammagl.transforms as T +from gammagl.data import HeteroGraph +from gammagl.models import HAN +from gammagl.utils import mask_to_index, set_device +from tensorlayerx.model import TrainOneStep, WithLoss +from tensorlayerx.backend.ops.torch_backend import set_seed +import torch +os.environ['TL_BACKEND'] = 'torch' +from util import * +# device = "cuda" if torch.cuda.is_available() else "cpu" +# print("Using {} device".format(device)) + + +'''seed +def setup_seed(seed): + torch.manual_seed(seed) + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + np.random.seed(seed) + random.seed(seed) + torch.backends.cudnn.deterministic = True +''' + + +set_seed(20211111) +'''seed end''' + +'''log''' +file = os.path.basename(sys.argv[0])[0:-3]+"_"+str(time()) +print_log(args.log_dir+file) +'log end' + +def train_test_split(p_vs_a): + train_id = [] + train_fed_id = [] + test_id = [] + test_negative_id = [] + p_vs_a_ = copy.deepcopy(p_vs_a)# + p_vs_a_random = copy.deepcopy(p_vs_a) + p_vs_a_random = p_vs_a_random.tolil() + p_num = p_vs_a_.shape[0] + a_num = p_vs_a_.shape[1] + for i in range(p_num):#each paper + cur_a = p_vs_a_[i].nonzero()[1] + '''p_vs_a random''' + p_vs_a_random[i,:]=0 + sample_len = len(cur_a) + sample_a = random.sample(list(range(p_vs_a_.shape[1])), sample_len) + #print(sample_a) + p_vs_a_random[i, sample_a] = 1 + # print(p_vs_a_random[i].nonzero()[1]) + '''end''' + + if(len(cur_a)==1): + train_id.append([i, cur_a[0]]) + train_fed_id.append(list(cur_a))# + elif(len(cur_a)!=0): + sample_train = random.sample(list(cur_a), len(cur_a)-1) + train_fed_id.append(sample_train)# + for j in sample_train: + train_id.append([i, j]) + cur_test_id =list(set(cur_a)-set(sample_train))[0] + test_id.append([i, cur_test_id]) + p_vs_a_[i, cur_test_id] = 0 + + '''p_vs_a random''' + p_vs_a_random[i, cur_test_id] = 0#random + '''end''' + + test_negative_pool = list(set(range(a_num))-set(cur_a))#0-10... - + test_negative_id.append(random.sample(test_negative_pool, 99)) + else: + train_fed_id.append([]) + #print(len(train_fed_id)) + #print(test_negative_id[2]) + return p_vs_a_, p_vs_a_random, train_fed_id, train_id, test_id, test_negative_id + + +dataname = args.dataset +device = args.device +if 'cuda' in device: + device,device_id = device.split(':')[0],device.split(':')[1] + +tlx.set_device(device=device, id=device_id) + +meta_paths_dict = {'acm':{'user': [['pa','ap'],['pc','cp']],'item':[['ap','pa']]}, \ + 'dblp':{'user': [['pa','ap'], ['pc','cp']],'item':[['ap','pa']]}, \ + 'yelp':{'user': [['pa','ap'], ['pa','aca','caa', 'ap']],'item':[['ap', 'pa']]}, \ + 'DoubanBook':{'item':[['bu','ub'],['bg', 'gb']], 'user':[['ub','bu'],['ua','au']]}} + +data_path = '../../data/ACM/ACM.mat' +data = sio.loadmat(data_path) +p_vs_f = data['PvsL'] +p_vs_a = data['PvsA']#(12499, 17431) +p_vs_t = data['PvsT'] +p_vs_c = data['PvsC']#(12499, 14) + + + +'''test''' + +adj = (p_vs_f, p_vs_a, p_vs_t, p_vs_c) +label_count, labels, label_author, author_label, shared_knowledge_rep = gen_shared_knowledge(adj, args.shared_num)# +'''test end''' + +# We assign +# (1) KDD papers as class 0 (data mining), +# (2) SIGMOD and VLDB papers as class 1 (database), +# (3) SIGCOMM and MOBICOMM papers as class 2 (communication) +conf_ids = [0, 1, 9, 10, 13] +label_ids = [0, 1, 2, 2, 1] + +p_vs_c_filter = p_vs_c[:, conf_ids]#(12499, 5) +p_selected = (p_vs_c_filter.sum(1) != 0).A1.nonzero()[0] +# print(type(p_vs_c_filter.sum(1) != 0)) +p_vs_f = p_vs_f[p_selected]#(4025,73) +p_vs_a = p_vs_a[p_selected]#(4025,17431) +p_vs_t = p_vs_t[p_selected]#(4025,1903) +p_vs_c = p_vs_c[p_selected]#CSC (4025, 14) + + + +num_nodes_dict = {'paper': p_vs_a.shape[0], 'author': p_vs_a.shape[1], 'field': p_vs_f.shape[1], 'conf': p_vs_c.shape[1]} + + +p_vs_a, p_vs_a_random, train_fed_id, train_id, test_id, test_negative_id=train_test_split(p_vs_a) + +logging.info(args) +logging.info(meta_paths_dict) + + +#features_user = torch.FloatTensor(p_vs_t.toarray()) +features_user = np.random.normal(loc=0., scale=1., size=[p_vs_a.shape[0], args.in_dim]) +features_item = np.random.normal(loc=0., scale=1., size=[p_vs_a.shape[1], args.in_dim]) +features = (features_user, features_item) +features = features +#print(features.shape) + + + +test_dataset = RecDataSet(test_id, test_negative_id, is_training=False) +#test_dataloader = dataloader.DataLoader(dataset=test_dataset, batch_size=64, shuffle=False) +test_dataloader = tlx.dataflow.DataLoader(test_dataset, batch_size=64, shuffle=False) + + + +client_list = [] + +p_vs_a_ = [] +all_edges=0 +remain_edges=0 +all_edges_after=0 +for i, items in enumerate(train_fed_id):#each paper + pre_edges = list(p_vs_a[i].nonzero()[1]) + all_edges+=len(pre_edges) + + client = Client(i, items, args) + perturb_adj = client.perturb_adj(p_vs_a.todense()[i], label_author, author_label, label_count, + shared_knowledge_rep, args.p1, args.p2) + + cur_edges = list(lil_matrix(perturb_adj).nonzero()[1]) + all_edges_after+=len(cur_edges) + cur_remain_edges = len(set(pre_edges)&set(cur_edges)) + remain_edges+=cur_remain_edges + p_vs_a_.append(perturb_adj) + #client_list.append(client.to(torch.device(args.device))) + client_list.append(client.cuda()) +p_vs_a_ = np.squeeze(np.array(p_vs_a_)) +p_vs_a_ = lil_matrix(p_vs_a_) + +edge_index_pa = tlx.convert_to_tensor(np.array(p_vs_a_.nonzero()), dtype=torch.long) +edge_index_ap = tlx.convert_to_tensor(np.array(p_vs_a_.transpose().nonzero()), dtype=torch.long) +edge_index_pc = tlx.convert_to_tensor(np.array(p_vs_c.nonzero()), dtype=torch.long) +edge_index_cp = tlx.convert_to_tensor(np.array(p_vs_c.transpose().nonzero()), dtype=torch.long) + + + +hg_user = HeteroGraph() +hg_user.num_nodes = num_nodes_dict +hg_item = HeteroGraph() +hg_item.num_nodes = num_nodes_dict + +hg_user['paper', 'pa', 'author'].edge_index = edge_index_pa +hg_user['author', 'ap', 'paper'].edge_index = edge_index_ap + +hg_user['conf', 'cp', 'paper'].edge_index = edge_index_cp +hg_user['paper', 'pc', 'conf'].edge_index = edge_index_pc + +metapaths_user = [[("paper", "author"), ("author", "paper")], + [("paper","conf"), ("conf", "paper")]] + +hg_user = T.AddMetaPaths(metapaths_user,drop_orig_edges = True, drop_unconnected_nodes = True)(hg_user) +hg_item['author', 'ap', 'paper'].edge_index = edge_index_ap +hg_item['paper', 'pa', 'author'].edge_index = edge_index_pa +metapaths_item = [[("author", "paper"), ("paper", "author")]] +hg_item = T.AddMetaPaths(metapaths_item,drop_orig_edges = True, drop_unconnected_nodes = True)(hg_item) + +hg_user.node_types = 'paper' +hg_item.node_types = 'author' +#hg_user = hg_user.cuda() +#hg_item = hg_item.cuda() +hg = (hg_user, hg_item) +model_user = model(metadata=hg_user.metadata(),#meta_paths_dict[dataname]['user'], + in_size=features_user.shape[1], + hidden_size=8, + out_size=64, + num_heads=args.num_heads, + dropout=args.dropout).cuda() + +model_item = model(metadata=hg_item.metadata(),#meta_paths_dict[dataname]['item'], + in_size=features_item.shape[1], + hidden_size=8, + out_size=64, + num_heads=args.num_heads, + dropout=args.dropout).cuda() +model = (model_user, model_item) + +server = Server(client_list, model, hg, features, args).cuda() + + +loss = 0 +best_sum_score = 0 +best_epoch = 0 +best_score = () +for ep_index in range(args.epochs): + for va_index in range(args.valid_step): + t1 = time() + + sample_client = random.sample(client_list, args.batch_size)#64#采样 + server.distribute(sample_client)#model + '''train''' + param_list = [] + loss_list = [] + t = time() + for idx, client in enumerate(sample_client): + #print("yes") + client.train() + param, loss_c = client.train_(hg, server.user_emb, server.item_emb) + #hg_list[client.user_id] + + param_list.append(param) # ! + loss_list.append(loss_c.cpu()) + print(time() - t) + #聚合参数 + server.aggregate(param_list) #!聚合参数 + loss_ = np.mean(np.array(loss_list)).item() + loss+=loss_ + + logging.info('training average loss: %.5f, time:%.1f s' % ( + loss / (ep_index * args.valid_step + va_index + 1), time() - t1)) + + + #test + server.eval() + hit_at_5, hit_at_10, ndcg_at_5, ndcg_at_10 = server.predict(test_dataloader, ep_index) + cur_score = hit_at_5 + hit_at_10 + ndcg_at_5 + ndcg_at_10 + if(cur_score>best_sum_score): + best_sum_score = cur_score + best_epoch = ep_index + best_score = (hit_at_5, hit_at_10, ndcg_at_5, ndcg_at_10) + logging.info('Best Epoch: %d, hit_at_5 = %.10f, hit_at_10 = %.10f, ndcg_at_5 = %.10f, ndcg_at_10 = %.10f' + % (best_epoch, best_score[0], best_score[1], best_score[2], best_score[3])) diff --git a/examples/fedhgnn/codes/FedHGNN/ourparse.py b/examples/fedhgnn/codes/FedHGNN/ourparse.py new file mode 100644 index 00000000..1fb84292 --- /dev/null +++ b/examples/fedhgnn/codes/FedHGNN/ourparse.py @@ -0,0 +1,47 @@ +import argparse +import os +os.environ['TL_BACKEND'] = 'torch' +import torch +def parse_args(): + parser = argparse.ArgumentParser(description="Run Recommender Model.") + parser.add_argument('--fea_dim', type=int, default=64, help='Dim of feature vectors.') + parser.add_argument('--in_dim', type=int, default=64, help='Dim of input vectors.') + parser.add_argument('--hidden_dim', type=int, default=64, help='Dim of hidden vectors.') + parser.add_argument('--out_dim', type=int, default=64, help='Dim of output vectors.') + parser.add_argument('--shared_num', type=int, default=20, help='Dim of output vectors.') + parser.add_argument('--path', nargs='?', default='Data/', help='Input data path.') + parser.add_argument('--dataset', nargs='?', default='acm', help='Choose a dataset.')#lastfm + parser.add_argument('--device', type=str, default="cuda:0" if torch.cuda.is_available() else 'cpu', + help='Which device to run the model.') + parser.add_argument('--num_heads', type=list, default=[2],help='attention_head') + parser.add_argument('--eps', type=float, default=1, help='total privacy budget.') + parser.add_argument('--num_sample', type=int, default=0, help='number of sampled neighbors.') + parser.add_argument('--valid_step', type=int, default=5, help='valid step.') + parser.add_argument('--nonlinearity', type=str, default="relu", help='Which device to run the model.') + parser.add_argument('--log_dir', type=str, default="../../log/", help='Which device to run the model.') + parser.add_argument('--is_gcn', type=bool, default=False, help="whether using gcn") + parser.add_argument('--is_attention', type=bool, default=False, help="whether using attention") + parser.add_argument('--hetero', type=bool, default=True, help="whether using attention") + parser.add_argument('--is_trans', type=bool, default=False, help="whether using trans") + parser.add_argument('--is_random_init', type=bool, default=True, help="whether random user and item") + parser.add_argument('--is_graph', type=bool, default=True, help="whether using graph") + parser.add_argument('--local_train_num', type=int, default=1, help='Dim of latent vectors.') + parser.add_argument('--agg_mode', type=str, default="add", help='Dim of latent vectors.') + parser.add_argument('--agg_func', type=str, default="ATTENTION", help='Dim of latent vectors.') + parser.add_argument('--lr', type=float, default=0.01, help='Learning rate.') + parser.add_argument('--dropout', type=float, default=0, help='Dropout rate.') + parser.add_argument('--weight_decay', type=float, default=0, help='lr weight_decay in optimizer.') + parser.add_argument('--epochs', type=int, default=10000, help='Number of epochs.') + parser.add_argument('--batch_size', type=int, default=32, help='Batch size.') + parser.add_argument('--l2_reg', type=bool, default=True, help='L2 norm regularization in loss.') + parser.add_argument('--grad_limit', type=float, default=1.0, help='Limit of l2-norm of item gradients.') + parser.add_argument('--clients_limit', type=float, default=0.1, help='Limit of proportion of malicious clients.') + parser.add_argument('--items_limit', type=int, default=60, help='Limit of number of non-zero item gradients.') + parser.add_argument('--type', type=str, default="ATTENTION", help='Dim of latent vectors.') + parser.add_argument('--p1', type=float, default=1, help='lr weight_decay in optimizer.') + parser.add_argument('--p2', type=float, default=1, help='lr weight_decay in optimizer.') + parser.add_argument("-f", "--file", default="file") + return parser.parse_args() + + +args = parse_args() \ No newline at end of file diff --git a/examples/fedhgnn/codes/FedHGNN/rec_dataset.py b/examples/fedhgnn/codes/FedHGNN/rec_dataset.py new file mode 100644 index 00000000..2ea93e3f --- /dev/null +++ b/examples/fedhgnn/codes/FedHGNN/rec_dataset.py @@ -0,0 +1,24 @@ +from tensorlayerx.dataflow import Dataset +import os +os.environ['TL_BACKEND'] = 'torch' +import numpy as np + +class RecDataSet(Dataset): + def __init__(self, test_id, test_negative_id, is_training=True): + super(RecDataSet, self).__init__() + self.is_training = is_training + if(self.is_training==False): #test + self.data = (np.array(test_id), np.array(test_negative_id)) + + def __getitem__(self,index): + if(self.is_training==False): + user = self.data[0][index][0] + item = self.data[0][index][1] + negtive_item = self.data[1][index] + return user, item, negtive_item + + + def __len__(self): + #return self.x.size() + if(self.is_training==False): + return len(self.data[0]) \ No newline at end of file diff --git a/examples/fedhgnn/codes/FedHGNN/server.py b/examples/fedhgnn/codes/FedHGNN/server.py new file mode 100644 index 00000000..6f46fb81 --- /dev/null +++ b/examples/fedhgnn/codes/FedHGNN/server.py @@ -0,0 +1,164 @@ +import tensorlayerx as tlx +from tensorlayerx import nn +import torch +import os +os.environ['TL_BACKEND'] = 'torch' + +from gammagl.models.fedhgnn import * +from eval import * +from util import * + +class Server(nn.Module): + def __init__(self, client_list, model, hg, features, args): + super().__init__() + device = args.device + if 'cuda' in device: + self.device, self.device_id = device.split(':')[0], device.split(':')[1] + tlx.set_device(device=self.device, id=self.device_id) + self.user_emb = nn.Embedding(features[0].shape[0], features[0].shape[1]).cuda()#.to(self.device) + self.item_emb = nn.Embedding(features[1].shape[0], features[1].shape[1]).cuda()#.to(self.device) + hg_user = hg[0] + hg_item = hg[1] + + + + self.hg_user = hg_user + self.hg_item = hg_item + + + self.client_list = client_list + self.features = features + self.model_user = model[0]#(0:model_user, 1: model_item) + self.model_item = model[1] + + #self.user_emb.weight.data = nn.Parameter(tlx.convert_to_tensor(features[0]))#.to(self.device) + #self.item_emb.weight.data = nn.Parameter(tlx.convert_to_tensor(features[1]))#.to(self.device) + self.user_emb.embeddings.data = tlx.convert_to_tensor(features[0], dtype = 'float32') + self.item_emb.embeddings.data = tlx.convert_to_tensor(features[1], dtype = 'float32') + #self.user_emb.__dict__['_trainable_weights'][0] = tlx.convert_to_tensor(features[0]) + #self.item_emb.__dict__['_trainable_weights'][0] = tlx.convert_to_tensor(features[1]) + #nn.init.normal_(self.item_emb.weight, std=0.01) + self.lr = args.lr + self.weight_decay = args.weight_decay + + + + def aggregate(self, param_list): + flag = False + number = 0 + tlx.set_device(device=self.device, id=self.device_id) + gradient_item = tlx.zeros_like(self.item_emb.embeddings) + gradient_user = tlx.zeros_like(self.user_emb.embeddings) + item_count = tlx.zeros((self.item_emb.embeddings.shape[0],)).cuda() + user_count = tlx.zeros((self.user_emb.embeddings.shape[0],)).cuda() + for parameter in param_list: + model_grad_user, model_grad_item = parameter['model'] + item_grad, returned_items = parameter['item'] + user_grad, returned_users = parameter['user'] + num = len(returned_items) + item_count[returned_items] += 1 + user_count[returned_users] += num + + number += num + if not flag: + flag = True + gradient_model_user = [] + gradient_model_item = [] + gradient_item[returned_items, :] += item_grad * num + gradient_user[returned_users, :] += user_grad * num + for i in range(len(model_grad_user)): + gradient_model_user.append(model_grad_user[i]* num) + for i in range(len(model_grad_item)): + gradient_model_item.append(model_grad_item[i]* num) + else: + gradient_item[returned_items, :] += item_grad * num + gradient_user[returned_users, :] += user_grad * num + for i in range(len(model_grad_user)): + gradient_model_user[i] += model_grad_user[i] * num + for i in range(len(model_grad_item)): + gradient_model_item[i] += model_grad_item[i] * num + + item_count[item_count == 0] = 1 + user_count[user_count == 0] = 1 + gradient_item /= item_count.unsqueeze(1) + gradient_user /= user_count.unsqueeze(1) + for i in range(len(gradient_model_user)): + gradient_model_user[i] = gradient_model_user[i] / number + for i in range(len(gradient_model_item)): + gradient_model_item[i] = gradient_model_item[i] / number + + + #更新model参数 + ls_model_param_user = list(self.model_user.parameters()) + ls_model_param_item = list(self.model_item.parameters()) + for i in range(len(ls_model_param_user)): + ls_model_param_user[i].data = ls_model_param_user[i].data - self.lr * gradient_model_user[i] - self.weight_decay * ls_model_param_user[i].data + for i in range(len(ls_model_param_item)): + ls_model_param_item[i].data = ls_model_param_item[i].data - self.lr * gradient_model_item[i] - self.weight_decay * ls_model_param_item[i].data + + # for i in range(len(list(self.model_user.parameters()))): + # print(ls_model_param_user[i].data) + # break + #更新item/user参数 + item_index = gradient_item.sum(dim = -1) != 0 + user_index = gradient_user.sum(dim = -1) != 0 + with torch.no_grad():#不加会报错 + self.item_emb.embeddings[item_index] = self.item_emb.embeddings[item_index] - self.lr * gradient_item[item_index] - self.weight_decay * self.item_emb.embeddings[item_index] + self.user_emb.embeddings[user_index] = self.user_emb.embeddings[user_index] - self.lr * gradient_user[user_index] - self.weight_decay * self.user_emb.embeddings[user_index] + + + + def distribute(self, client_list): + for client in client_list: + client.update(self.model_user, self.model_item) + + + def predict(self, test_dataloader, epoch): + hit_at_5 = [] + hit_at_10 = [] + ndcg_at_5 = [] + ndcg_at_10 = [] + + self.model_item.eval() + self.model_user.eval() + logits_user = self.model_user(self.hg_user, self.user_emb.embeddings) + logits_item = self.model_item(self.hg_item, self.item_emb.embeddings) + for u, i, neg_i in test_dataloader: #test_i算上了test_negative, 真实的放在最后一位[99] + cur_user = logits_user[u] + cur_item = logits_item[i] + rating = tlx.reduce_sum(cur_user * cur_item, axis=-1)#当前client user和所有item点乘(include test item) + + for eva_idx, eva in enumerate(rating): + cur_neg = logits_item[neg_i[eva_idx]] + cur_rating_neg = tlx.reduce_sum(cur_user[eva_idx] * cur_neg, axis=-1) + #print(np.shape(cur_rating_neg)) + cur_eva = tlx.concat([cur_rating_neg, tlx.expand_dims(rating[eva_idx], 0)], axis=0) + #print(np.shape(rating[eva_idx])) + # print(cur_eva) + hit_at_5_ = evaluate_recall(cur_eva, [99], 5)#[99]是测试集(ground truth) + hit_at_10_ = evaluate_recall(cur_eva, [99], 10) + ndcg_at_5_ = evaluate_ndcg(cur_eva, [99], 5) + ndcg_at_10_ = evaluate_ndcg(cur_eva, [99], 10) + #print(hit_at_10_) + hit_at_5.append(hit_at_5_) + hit_at_10.append(hit_at_10_) + ndcg_at_5.append(ndcg_at_5_) + ndcg_at_10.append(ndcg_at_10_) + hit_at_5 = np.mean(np.array(hit_at_5)).item() + hit_at_10 = np.mean(np.array(hit_at_10)).item() + ndcg_at_5 = np.mean(np.array(ndcg_at_5)).item() + ndcg_at_10 = np.mean(np.array(ndcg_at_10)).item() + + logging.info('Epoch: %d, hit_at_5 = %.4f, hit_at_10 = %.4f, ndcg_at_5 = %.4f, ndcg_at_10 = %.4f' + % (epoch, hit_at_5, hit_at_10, ndcg_at_5, ndcg_at_10)) + return hit_at_5, hit_at_10, ndcg_at_5, ndcg_at_10 + + + + + + + + + + diff --git a/examples/fedhgnn/codes/FedHGNN/util.py b/examples/fedhgnn/codes/FedHGNN/util.py new file mode 100644 index 00000000..340d5c51 --- /dev/null +++ b/examples/fedhgnn/codes/FedHGNN/util.py @@ -0,0 +1,156 @@ +#coding:utf-8 +import numpy as np +from sklearn.manifold import TSNE +import matplotlib.pyplot as plt +import scipy.io as sio +from sklearn.cluster import KMeans +from sklearn.decomposition import PCA +import scipy.sparse +import logging + +def print_log(file): + # 配置日志 + logging.basicConfig( + level=logging.DEBUG, # 设置日志级别,可以根据需要调整 + format='%(asctime)s - %(levelname)s - %(message)s', # 设置日志格式 + handlers=[ + logging.StreamHandler(), # 输出到终端 + logging.FileHandler(file, mode='w'), # 输出到文件 + ] + ) + # 输出日志信息 + #logging.debug('信息将同时输出到终端和文件。') + logging.info('信息会同时显示在终端和文件中。') + + +def pca_reduce(data, dim): + pca = PCA(n_components=dim) + pca = pca.fit(data) + x = pca.transform(data) + return x + +def softmax(x): + e_x = np.exp(x - np.max(x)) + return e_x / e_x.sum(axis=0) + + +'''cluster''' +def cluster(feature_list, n_clusters): + s = KMeans(n_clusters=n_clusters).fit(feature_list) + #print(len(s.cluster_centers_)) + #每个样本所属的簇 + #print(len(s.labels_)) + label_count = {} + for i in s.labels_: + if(i not in label_count.keys()): + label_count[i] = 1 + else: + label_count[i]+=1 + + #print(label_count) + #print(s.labels_) + + label_author = {} + author_label = {} + labels = [] + for i, k in enumerate(s.labels_): + author = i + label = k + labels.append(label) + + author_label[author] = label + + if(label not in label_author.keys()): + label_author[label] = [author] + else: + label_author[label].append(author) + + # with open("./data_event/author_label", "w") as f: + # for l in author_label: + # f.write(l[0] + '\t' + l[1] + '\n') + return label_count, labels, label_author, author_label +#cluster() + +'''get shared knowledge rep(每个shared HIN的表示为它所包含的item的表示的平均)''' +def get_shared_knowledge_rep(item_feature_list, label_author): + shared_knowledge_rep = {} + for label, author_list in label_author.items(): + features = item_feature_list[author_list] + rep = np.mean(features, 0) + # sum = np.array([0.0]*len(item_feature_list[0])) + # l = len(author_list) + # for author in author_list: + # sum+= item_feature_list[author] + # rep = sum/l + shared_knowledge_rep[label] = rep + return shared_knowledge_rep + + + + + + + +def tsne(feature_list): + tsne = TSNE(n_components=2) + tsne.fit_transform(feature_list) + #print(tsne.embedding_) + + feature_list = tsne.embedding_ + print(np.shape(feature_list))#14795,2 + + x = feature_list[:,0] + y = feature_list[:,1] + + return x, y + +#l = sio.loadmat("./data_event/author_tsne.mat") +# x= l['x'] +# y =l['y'] + + +def plot_embedding_2d(x, y, labels): + """Plot an embedding X with the class label y colored by the domain d.""" + # x_min, x_max = np.min(X, 0), np.max(X, 0) + # X = (X - x_min) / (x_max - x_min) + + plt.scatter(x, y, c=labels) + + # plt.xlim((-1.5, 1.5)) + # plt.xticks([]) # ignore xticks + # plt.ylim((-1.5, 1.5)) + # plt.yticks([]) # ignore yticks + plt.show() + +#plot_embedding_2d(x,y) + + +def gen_shared_knowledge(adj, group_num): + p_vs_f = adj[0]#(4025,73) + p_vs_a = adj[1]#(4025,17431) + p_vs_t = adj[2]#(4025,1903) + p_vs_c = adj[3]#CSC (4025, 14) + a_vs_t = p_vs_a.T * p_vs_t + a_vs_f = p_vs_a.T * p_vs_f + a_vs_c = p_vs_a.T * p_vs_c + a_vs_p = p_vs_a.T + a_vs_t_dense = a_vs_t.todense() + a_vs_f_dense = a_vs_f.todense() + a_vs_c_dense = a_vs_c.todense() + a_vs_p_dense = a_vs_p.todense() + #print(np.sum(a_vs_c_dense.sum(-1)==0))#大部分(10264)=0 + #print(a_vs_t_dense[1]) + a_feature = np.concatenate([a_vs_c_dense], -1) + label_count, labels, label_author, author_label = cluster(a_feature, group_num) #20 + # x,y = tsne(a_feature) + # plot_embedding_2d(x, y, labels) + shared_knowledge_rep = get_shared_knowledge_rep(a_feature, label_author) + return label_count, labels, label_author, author_label, shared_knowledge_rep + +# if __name__ == 'main': +# # feature_list = [] +# # for index in author_id_list:# +# # fea = features[index] +# # #print(len(fea)) +# # feature_list.append(fea) +# # feature_list = np.array(feature_list) diff --git a/examples/fedhgnn/data/acm/ACM.mat b/examples/fedhgnn/data/acm/ACM.mat new file mode 100644 index 00000000..3f58633f Binary files /dev/null and b/examples/fedhgnn/data/acm/ACM.mat differ diff --git a/gammagl/models/fedhgnn.py b/gammagl/models/fedhgnn.py new file mode 100644 index 00000000..08b93797 --- /dev/null +++ b/gammagl/models/fedhgnn.py @@ -0,0 +1,110 @@ +import tensorlayerx as tlx +from tensorlayerx import nn + +import os +os.environ['TL_BACKEND'] = 'torch' +from gammagl.layers.conv import MessagePassing +from gammagl.layers.conv import GATConv, HANConv + + + +class SemanticAttention(nn.Module): + def __init__(self, in_size, hidden_size=128): + super(SemanticAttention, self).__init__() + + self.project = nn.Sequential( + nn.Linear(in_features=in_size, out_features=hidden_size,act=0), + nn.Tanh(), + nn.Linear(in_features = hidden_size, out_features=1,act=0, b_init=False) + ) + def forward(self, z): + + z = z.permute(1, 0, 2) + + w = self.project(z).mean(0) # (M, 1) + beta = tlx.softmax(w, axis=0) # (M, 1) + beta = beta.expand((z.shape[0],) + beta.shape) # (N, M, 1) + + return (beta * z).sum(1) # (N, D * K) + +class HANLayer(MessagePassing): + def __init__(self, + in_channels, + out_channels, + metadata, + heads=1, + dropout_rate=0.5): + super(HANLayer, self).__init__() + metadata[0].append(metadata[1][0][0]) + if not isinstance(in_channels, dict): + in_channels = {node_type: in_channels for node_type in metadata[0]} + print('in_channels',in_channels) + self.in_channels = in_channels + self.out_channels = out_channels + self.metadata = metadata + + self.heads = heads + self.dropout_rate = dropout_rate + self.gat_dict = nn.ModuleDict({}) + for edge_type in metadata[1]: + src_type, _, dst_type = edge_type + edge_type = '__'.join(edge_type) + + self.gat_dict[edge_type] = GATConv(in_channels = in_channels[src_type], out_channels = out_channels, heads = heads, + dropout_rate = dropout_rate) + self.sem_att_aggr = SemanticAttention(in_size=out_channels*heads, + hidden_size=out_channels) + + def forward(self, x_dict, edge_index_dict, num_nodes_dict): + out_dict = {} + # Iterate over node types: + + x_dict = {next(iter(num_nodes_dict)):x_dict} + for node_type, x_node in x_dict.items(): + out_dict[node_type] = [] + + # node level attention aggregation + for edge_type, edge_index in edge_index_dict.items(): + src_type, _, dst_type = edge_type + edge_type = '__'.join(edge_type) + out = self.gat_dict[edge_type](x_dict[src_type], + edge_index.cuda())#, + #num_nodes = num_nodes_dict[dst_type]) + out = tlx.elu(out) + out_dict[dst_type].append(out) + + # semantic attention aggregation + for node_type, outs in out_dict.items(): + outs = tlx.stack(outs) + out_dict[node_type] = self.sem_att_aggr(outs) + + return out_dict + + def __repr__(self) -> str: + return (f'{self.__class__.__name__}({self.out_channels}, ' + f'heads={self.heads})') + +class model(nn.Module): + def __init__(self, metadata, in_size, hidden_size, out_size, num_heads,dropout): + super(model,self).__init__() + self.han_conv = HANLayer(in_size, + hidden_size, + metadata, + heads=num_heads[0], + dropout_rate=dropout) + self.lin = nn.Linear(in_features=hidden_size * num_heads[0], + out_features=out_size) + + def forward(self, data,h): + x_dict, edge_index_dict,node_type = h, data.edge_index_dict,data['node_types'] + + num_nodes_dict = {node_type:None} + x = self.han_conv(x_dict, edge_index_dict, num_nodes_dict) + + out = {} + for node_type, _ in num_nodes_dict.items(): + out[node_type] = self.lin(x[node_type]) + + + return next(iter(out.values())) +