├── BPR.py ├── NCF.py ├── Profile2Vec.py ├── README.md ├── PJFNN.py ├── IPJF.py ├── DQN.py ├── GRU.py ├── APJFNN.py ├── transformer.py ├── DAPJF.py ├── JRMPM.py ├── DPJFMBS.py └── DoubleQNet.py /BPR.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 4 | 5 | class BPRMF(torch.nn.Module): 6 | def __init__(self, user_num, item_num, factor_num): 7 | super(BPRMF, self).__init__() 8 | """ 9 | user_num: number of users; 10 | item_num: number of items; 11 | factor_num: number of predictive factors. 12 | """ 13 | self.embed_user = torch.nn.Embedding(user_num, factor_num) 14 | torch.nn.init.normal_(self.embed_user.weight, std=0.01) 15 | self.embed_user.weight.requires_grad = True 16 | self.embed_item = torch.nn.Embedding(item_num, factor_num) 17 | torch.nn.init.normal_(self.embed_item.weight, std=0.01) 18 | self.embed_item.weight.requires_grad = True 19 | 20 | self.item_idx = torch.LongTensor(torch.arange(0, item_num)).to(DEVICE) 21 | 22 | def forward(self, user, items): 23 | user_embed = self.embed_user(user) 24 | item_embeds = self.embed_item(items) 25 | scores = torch.bmm(user_embed.unsqueeze(dim=1), item_embeds.permute((0,2,1))).squeeze(dim=1) 26 | return scores -------------------------------------------------------------------------------- /NCF.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | import torch.nn.functional as F 4 | from torch.nn.utils.rnn import pack_padded_sequence 5 | 6 | from src.Preprocess.profile_word2vec import EMBED_DIM 7 | 8 | USER_EMBED_DIM = 100 9 | 10 | class NCF(torch.nn.Module): 11 | def __init__(self, n_expects, n_jobs): 12 | super(NCF, self).__init__() 13 | self.n_expects = n_expects 14 | self.n_jobs = n_jobs 15 | 16 | # embeddings: learnable 17 | self.expect_embeddings = torch.nn.Embedding(n_expects, USER_EMBED_DIM) 18 | self.expect_embeddings.weight.requires_grad = True 19 | self.job_embeddings = torch.nn.Embedding(n_jobs, USER_EMBED_DIM) 20 | self.job_embeddings.weight.requires_grad = True 21 | 22 | 23 | # MLP 24 | self.mlp = torch.nn.Sequential( 25 | torch.nn.Linear(2 * USER_EMBED_DIM, USER_EMBED_DIM), 26 | torch.nn.ReLU(inplace=True), 27 | torch.nn.Linear(USER_EMBED_DIM, USER_EMBED_DIM), 28 | torch.nn.ReLU(inplace=True), 29 | torch.nn.Linear(USER_EMBED_DIM, USER_EMBED_DIM), 30 | ) 31 | 32 | # MLP 33 | self.final_mlp = torch.nn.Sequential( 34 | torch.nn.Linear(2 * USER_EMBED_DIM, USER_EMBED_DIM), 35 | torch.nn.Sigmoid(), 36 | torch.nn.Linear(USER_EMBED_DIM, 1), 37 | torch.nn.Sigmoid() 38 | ) 39 | 40 | def predict(self, ids1, ids2, e2j_flag=True): 41 | expect_embed, job_embed = None, None 42 | if e2j_flag: 43 | expect_embed = self.expect_embeddings(ids1) 44 | job_embed = self.job_embeddings(ids2) 45 | else: 46 | job_embed = self.job_embeddings(ids1) 47 | expect_embed = self.expect_embeddings(ids2) 48 | 49 | gmf_features = expect_embed * job_embed 50 | mlp_features = self.mlp(torch.cat([expect_embed, job_embed], dim=-1)) 51 | return self.final_mlp(torch.cat([gmf_features, mlp_features], dim=-1)).squeeze(-1) 52 | 53 | 54 | def forward(self, ids, ids_pair, e2j_flag=True): 55 | scores_pos, scores_neg = None, None 56 | if e2j_flag: 57 | scores_pos = self.predict(ids, ids_pair[:, 0], True) 58 | scores_neg = self.predict(ids, ids_pair[:, 1], True) 59 | else: 60 | scores_pos = self.predict(ids, ids_pair[:, 0], False) 61 | scores_neg = self.predict(ids, ids_pair[:, 1], False) 62 | return torch.sigmoid(scores_pos - scores_neg) 63 | -------------------------------------------------------------------------------- /Profile2Vec.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn.functional as F 3 | from torch.nn.utils.rnn import pack_padded_sequence 4 | 5 | DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 6 | 7 | WORD_EMBED_DIM = 32 8 | USER_EMBED_DIM = 16 9 | 10 | class Profile2Vec(torch.nn.Module): 11 | def __init__(self, word_embeddings, name): 12 | super(Profile2Vec, self).__init__() 13 | self.name = name 14 | # profile: word embeddings for look_up 15 | # embedding_matrix = [[0...0], [...], ...[]] 16 | self.Word_Embeds = word_embeddings 17 | 18 | self.ConvNet = torch.nn.Sequential( 19 | # in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,... 20 | torch.nn.Conv1d(in_channels=WORD_EMBED_DIM, out_channels=WORD_EMBED_DIM, kernel_size=5), 21 | # BatchNorm1d只处理第二个维度 22 | torch.nn.BatchNorm1d(WORD_EMBED_DIM), 23 | torch.nn.ReLU(inplace=True), 24 | torch.nn.MaxPool1d(kernel_size=3), 25 | 26 | # in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,... 27 | torch.nn.Conv1d(in_channels=WORD_EMBED_DIM, out_channels=USER_EMBED_DIM, kernel_size=5), 28 | # BatchNorm1d只处理第二个维度 29 | torch.nn.BatchNorm1d(USER_EMBED_DIM), 30 | torch.nn.ReLU(inplace=True), 31 | torch.nn.MaxPool1d(kernel_size=50) 32 | ) 33 | 34 | # profiles: [batch_size, MAX_PROFILELEN, MAX_TERMLEN] = (40, 15, 50), word idx 35 | # return [batch_size, USER_EMBED_DIM] 36 | def forward(self, profiles): 37 | # word level: 38 | # [batch_size, MAX_PROFILELEN, MAX_TERMLEN] (40, 15, 50) -> 39 | # [batch_size, MAX_PROFILELEN * MAX_TERMLEN](40 * 15, 50) 40 | shape = profiles.shape 41 | profiles_ = profiles.view([shape[0], -1]) 42 | 43 | # embeddings: [batch_size, MAX_PROFILELEN * MAX_TERMLEN, EMBED_DIM] 44 | profiles_wordembed = self.Word_Embeds(profiles_).float() 45 | 46 | # permute for conv1d 47 | # embeddings: [batch_size, EMBED_DIM, MAX_PROFILELEN * MAX_TERMLEN] 48 | profiles_wordembed_ = profiles_wordembed.permute(0, 2, 1) 49 | 50 | # [batch_size, EMBED_DIM, x] 51 | profiles_convs_out = self.ConvNet(profiles_wordembed_) 52 | 53 | # [batch_size, EMBED_DIM, x] -> [batch_size, EMBED_DIM, 1] 54 | profiles_len = profiles_convs_out.shape[-1] 55 | profiles_final_out = torch.nn.MaxPool1d(kernel_size=profiles_len)(profiles_convs_out).squeeze(-1) 56 | return profiles_final_out -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This work was done during my internship in Boss Zhipin, just be used for the purpose of academic exchange! 2 | 3 | # Beyond Matching: Modeling Two-Sided Multi-Behavioral Sequences For Dynamic Person-Job Fit 4 | 5 | Paper: 6 | Bin Fu, Hongzhi Liu*, Yao Zhu, Yang Song, Tao Zhang, Zhonghai Wu*. Beyond Matching: Modeling Two-Sided Multi-Behavioral Sequences For Dynamic Person-Job Fit. The 26th International Conference on Database Systems for Advanced Applications (DASFAA 2021), April 11-14, 2021. 7 | 8 | Abstract: 9 | Online recruitment aims to match right talents with right jobs (Person-Job Fit, PJF) online by satisfying the preferences of both persons (job seekers) and jobs (recruiters). Recently, some research tried to solve this problem by deep semantic matching of curriculum vitaes and job postings. But those static profiles don’t (fully) reflect users’ personalized preferences. In addition, most existing preference learning methods are based on users’ matching behaviors. However, matching behaviors are sparse due to the nature of PJF and not fine-grained enough to reflect users’ dynamic preferences. 10 | With going deep into the process of online PJF, we observed abundant auxiliary behaviors generated by both sides before achieving a matching, such as click, invite/apply and chat. To solve the above problems, we propose to collect and utilize these behaviors along the timeline to capture users’ dynamic preferences. We design a Dynamic Multi-Key Value Memory Network to capture users’ dynamic preferences from their multi-behavioral sequences. Furthermore, a Bilateral Cascade Multi-Task Learning framework is designed to transfer two-sided preferences learned from auxiliary behaviors to the matching task with consideration of their cascade relations. Offline experimental results on two real-world datasets show our method outperforms the state-of-the-art methods. 11 | 12 | 13 | 14 | **Running Requirements:** 15 | * python 3.6+ 16 | * pytorch 17 | * cuda10 + cudnn7 1.4.0 18 | 19 | The source code is a demo used for academic exchange. 20 | Due to the company privacy policy, if you need this code for research (only for research), please contact with binfu@pku.edu.cn. 21 | 22 | 23 | Bibtex: 24 | ``` 25 | @inproceedings{ 26 | author = {Bin Fu and 27 | Hongzhi Liu and 28 | Yao Zhu and 29 | Yang Song and 30 | Tao Zhang and 31 | Zhonghai Wu} 32 | title = {Beyond Matching: Modeling Two-Sided Multi-Behavioral Sequences For Dynamic Person-Job Fit}, 33 | booktitle = {The 26th International Conference on Database Systems for Advanced Applications (DASFAA 2021), April 11-14, 2021}, 34 | year = {2021} 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /PJFNN.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | import torch.nn.functional as F 4 | from torch.nn.utils.rnn import pack_padded_sequence 5 | 6 | from src.Preprocess.profile_word2vec import EMBED_DIM 7 | 8 | 9 | class PJFNN(torch.nn.Module): 10 | def __init__(self, word_embeddings,): 11 | super(PJFNN, self).__init__() 12 | 13 | # embedding_matrix = [[0...0], [...], ...[]] 14 | self.Word_Embeds = torch.nn.Embedding.from_pretrained(word_embeddings, padding_idx=0) 15 | self.Word_Embeds.weight.requires_grad = False 16 | 17 | self.Expect_ConvNet = torch.nn.Sequential( 18 | # in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,... 19 | torch.nn.Conv1d(in_channels=EMBED_DIM, out_channels=EMBED_DIM, kernel_size=5), 20 | # BatchNorm1d只处理第二个维度 21 | torch.nn.BatchNorm1d(EMBED_DIM), 22 | torch.nn.ReLU(inplace=True), 23 | torch.nn.MaxPool1d(kernel_size=3), 24 | 25 | # in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,... 26 | torch.nn.Conv1d(in_channels=EMBED_DIM, out_channels=EMBED_DIM, kernel_size=5), 27 | # BatchNorm1d只处理第二个维度 28 | torch.nn.BatchNorm1d(EMBED_DIM), 29 | torch.nn.ReLU(inplace=True), 30 | torch.nn.MaxPool1d(kernel_size=50) 31 | ) 32 | 33 | self.Job_ConvNet = torch.nn.Sequential( 34 | # in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,... 35 | torch.nn.Conv1d(in_channels=EMBED_DIM, out_channels=EMBED_DIM, kernel_size=5), 36 | # BatchNorm1d只处理第二个维度 37 | torch.nn.BatchNorm1d(EMBED_DIM), 38 | torch.nn.ReLU(inplace=True), 39 | torch.nn.MaxPool1d(kernel_size=2), 40 | 41 | # in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,... 42 | torch.nn.Conv1d(in_channels=EMBED_DIM, out_channels=EMBED_DIM, kernel_size=3), 43 | # BatchNorm1d只处理第二个维度 44 | torch.nn.BatchNorm1d(EMBED_DIM), 45 | torch.nn.ReLU(inplace=True), 46 | torch.nn.MaxPool1d(kernel_size=50) 47 | ) 48 | 49 | # [batch_size *2, MAX_PROFILELEN, MAX_TERMLEN] = (40, 15, 50) 50 | # term: padding same, word: padding 0 51 | # expects_sample, jobs_sample are in same format 52 | def forward(self, expects, jobs): 53 | 54 | # word level: 55 | # [batch_size, MAX_PROFILELEN, MAX_TERMLEN] (40, 15, 50) -> 56 | # [batch_size, MAX_PROFILELEN * MAX_TERMLEN](40 * 15, 50) 57 | shape = expects.shape 58 | expects_, jobs_ = expects.view([shape[0], -1]), jobs.view([shape[0], -1]) 59 | 60 | # embeddings: [batch_size, MAX_PROFILELEN * MAX_TERMLEN, EMBED_DIM] 61 | expects_wordembed = self.Word_Embeds(expects_).float() 62 | jobs_wordembed = self.Word_Embeds(jobs_).float() 63 | 64 | # permute for conv1d 65 | # embeddings: [batch_size, EMBED_DIM, MAX_PROFILELEN * MAX_TERMLEN] 66 | expects_wordembed_ = expects_wordembed.permute(0, 2, 1) 67 | jobs_wordembed_ = jobs_wordembed.permute(0, 2, 1) 68 | 69 | # [batch_size, EMBED_DIM, x] 70 | expect_convs_out = self.Expect_ConvNet(expects_wordembed_) 71 | job_convs_out = self.Job_ConvNet(jobs_wordembed_) 72 | 73 | # [batch_size, EMBED_DIM, x] -> [batch_size, EMBED_DIM, 1] 74 | expect_len, job_len = expect_convs_out.shape[-1], job_convs_out.shape[-1] 75 | expect_final_out = torch.nn.AvgPool1d(kernel_size=expect_len)(expect_convs_out).squeeze(-1) 76 | job_final_out = torch.nn.MaxPool1d(kernel_size=job_len)(job_convs_out).squeeze(-1) 77 | 78 | # cosine_similarity 79 | return torch.cosine_similarity(expect_final_out, job_final_out, dim=-1) -------------------------------------------------------------------------------- /IPJF.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | import torch.nn.functional as F 4 | from torch.nn.utils.rnn import pack_padded_sequence 5 | 6 | from src.Preprocess.profile_word2vec import EMBED_DIM 7 | 8 | 9 | class IPJF(torch.nn.Module): 10 | def __init__(self, word_embeddings,): 11 | super(IPJF, self).__init__() 12 | 13 | # embedding_matrix = [[0...0], [...], ...[]] 14 | self.Word_Embeds = torch.nn.Embedding.from_pretrained(word_embeddings, padding_idx=0) 15 | self.Word_Embeds.weight.requires_grad = False 16 | 17 | self.Expect_ConvNet = torch.nn.Sequential( 18 | # in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,... 19 | torch.nn.Conv1d(in_channels=EMBED_DIM, out_channels=EMBED_DIM, kernel_size=5), 20 | # BatchNorm1d只处理第二个维度 21 | # torch.nn.BatchNorm1d(EMBED_DIM), 22 | torch.nn.ReLU(inplace=True), 23 | torch.nn.MaxPool1d(kernel_size=3), 24 | 25 | # in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,... 26 | # torch.nn.Conv1d(in_channels=EMBED_DIM, out_channels=EMBED_DIM, kernel_size=5), 27 | # # BatchNorm1d只处理第二个维度 28 | # torch.nn.BatchNorm1d(EMBED_DIM), 29 | # torch.nn.ReLU(inplace=True), 30 | # torch.nn.MaxPool1d(kernel_size=50) 31 | ) 32 | 33 | self.Job_ConvNet = torch.nn.Sequential( 34 | # in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,... 35 | torch.nn.Conv1d(in_channels=EMBED_DIM, out_channels=EMBED_DIM, kernel_size=5), 36 | # BatchNorm1d只处理第二个维度 37 | # torch.nn.BatchNorm1d(EMBED_DIM), 38 | torch.nn.ReLU(inplace=True), 39 | torch.nn.MaxPool1d(kernel_size=2), 40 | 41 | # in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,... 42 | # torch.nn.Conv1d(in_channels=EMBED_DIM, out_channels=EMBED_DIM, kernel_size=3), 43 | # # BatchNorm1d只处理第二个维度 44 | # torch.nn.BatchNorm1d(EMBED_DIM), 45 | # torch.nn.ReLU(inplace=True), 46 | # torch.nn.MaxPool1d(kernel_size=50) 47 | ) 48 | 49 | # match mlp 50 | self.Match_MLP = torch.nn.Sequential( 51 | torch.nn.Linear(2 * EMBED_DIM, EMBED_DIM), 52 | torch.nn.Tanh(), 53 | torch.nn.Linear(EMBED_DIM, 1), 54 | torch.nn.Sigmoid() 55 | ) 56 | 57 | # [batch_size *2, MAX_PROFILELEN, MAX_TERMLEN] = (40, 15, 50) 58 | # term: padding same, word: padding 0 59 | # expects_sample, jobs_sample are in same format 60 | def forward(self, expects, jobs): 61 | 62 | # word level: 63 | # [batch_size, MAX_PROFILELEN, MAX_TERMLEN] (40, 15, 50) -> 64 | # [batch_size, MAX_PROFILELEN * MAX_TERMLEN](40 * 15, 50) 65 | shape = expects.shape 66 | expects_, jobs_ = expects.view([shape[0], -1]), jobs.view([shape[0], -1]) 67 | 68 | # embeddings: [batch_size, MAX_PROFILELEN * MAX_TERMLEN, EMBED_DIM] 69 | expects_wordembed = self.Word_Embeds(expects_).float() 70 | jobs_wordembed = self.Word_Embeds(jobs_).float() 71 | 72 | # permute for conv1d 73 | # embeddings: [batch_size, EMBED_DIM, MAX_PROFILELEN * MAX_TERMLEN] 74 | expects_wordembed_ = expects_wordembed.permute(0, 2, 1) 75 | jobs_wordembed_ = jobs_wordembed.permute(0, 2, 1) 76 | 77 | # [batch_size, EMBED_DIM, x] 78 | expect_convs_out = self.Expect_ConvNet(expects_wordembed_) 79 | job_convs_out = self.Job_ConvNet(jobs_wordembed_) 80 | 81 | # [batch_size, EMBED_DIM, x] -> [batch_size, EMBED_DIM, 1] 82 | expect_len, job_len = expect_convs_out.shape[-1], job_convs_out.shape[-1] 83 | expect_final_out = torch.nn.AvgPool1d(kernel_size=expect_len)(expect_convs_out).squeeze(-1) 84 | job_final_out = torch.nn.MaxPool1d(kernel_size=job_len)(job_convs_out).squeeze(-1) 85 | 86 | return self.Match_MLP(torch.cat([expect_final_out, job_final_out], dim=-1)).squeeze(-1) -------------------------------------------------------------------------------- /DQN.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch.nn.utils.rnn import pack_padded_sequence 3 | DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 4 | 5 | ITEM_EMBED_DIM = 32 6 | STATE_GRU_DIM = 32 7 | 8 | class StateRep(torch.nn.Module): 9 | def __init__(self, ): 10 | super(StateRep, self).__init__() 11 | self.GRU = torch.nn.GRU(input_size=ITEM_EMBED_DIM, hidden_size=STATE_GRU_DIM, 12 | num_layers=1, batch_first=True, bidirectional=False) 13 | def forward(self, item_seqs, item_seq_len): 14 | # sort for pack 15 | lens_sort, ind_sort = item_seq_len.sort(dim=0, descending=True) 16 | item_seqs_ = item_seqs[ind_sort, :, :] 17 | # pack 18 | seq_pack = pack_padded_sequence(item_seqs_, lens_sort, batch_first=True) 19 | # gru 20 | seq_output, seq_state = self.GRU(seq_pack) 21 | return seq_state.squeeze(0)[ind_sort, :] 22 | def parameters(self, ): 23 | return list(self.GRU.parameters()) 24 | 25 | 26 | class DQN(torch.nn.Module): 27 | def __init__(self, layer_sizes): 28 | super(DQN, self).__init__() 29 | 30 | self.DQN_MLP = torch.nn.Sequential() 31 | for ind, layer_size in enumerate(layer_sizes): 32 | in_size = STATE_GRU_DIM + ITEM_EMBED_DIM if ind==0 else layer_sizes[ind-1] 33 | self.DQN_MLP.add_module('linear-'+str(ind), torch.nn.Linear(in_size, layer_sizes[ind])) 34 | # self.DQN_MLP.add_module('relu-'+str(ind), torch.nn.ReLU()) 35 | self.DQN_MLP.add_module('tanh-'+str(ind), torch.nn.Tanh()) 36 | # self.DQN_MLP.add_module('sigmoid-'+str(ind), torch.nn.Sigmoid()) 37 | self.DQN_MLP.add_module('linear', torch.nn.Linear(layer_sizes[-1], 1)) 38 | print('DQN_MLP=', self.DQN_MLP) 39 | def forward(self, state_embed, item_embed): 40 | return self.DQN_MLP(torch.cat([state_embed, item_embed], dim=-1)).squeeze(-1) 41 | # item_masks [0,1] 42 | def forward_max(self, state_embed, item_embeds, item_masks): 43 | state_embeds = torch.unsqueeze(state_embed, 1) 44 | shape = state_embeds.shape 45 | state_embeds = state_embeds.expand(shape[0], item_embeds.shape[1], shape[2]) 46 | scores = torch.sigmoid(self.DQN_MLP(torch.cat([state_embeds, item_embeds], dim=-1)).squeeze(-1)) 47 | scores = scores * item_masks 48 | scores_max, indices_max = torch.max(scores, dim=1) 49 | return scores_max 50 | def parameters(self): 51 | return list(self.DQN_MLP.parameters()) 52 | 53 | class DQN_REC(torch.nn.Module): 54 | def __init__(self, isize, discounts): 55 | super(DQN_REC, self).__init__() 56 | self.isize = isize 57 | self.discounts = discounts 58 | 59 | self.item_embeds = torch.nn.Embedding(isize+1, ITEM_EMBED_DIM, padding_idx=0) 60 | torch.nn.init.normal_(self.item_embeds.weight, std=0.01) 61 | self.item_embeds.weight.requires_grad = True 62 | 63 | self.dqn = DQN([32, 16]) 64 | self.staterep = StateRep() 65 | 66 | self.item_idx = torch.LongTensor(torch.arange(0, isize+1)).to(DEVICE) 67 | 68 | def forward(self, states, states_len, actions, rewards_kstep, 69 | next_states, next_states_len, next_action_mask): 70 | states_ = self.item_embeds(states).float() 71 | state_embed = self.staterep(states_, states_len) 72 | action_embed = self.item_embeds(actions) 73 | current_rewards = self.dqn(state_embed, action_embed) 74 | 75 | next_states_ = self.item_embeds(next_states).float() 76 | next_state_embed = self.staterep(next_states_, next_states_len) 77 | item_embed = self.item_embeds(self.item_idx).unsqueeze(dim=0).repeat((next_state_embed.shape[0], 1, 1)) 78 | # detach or not 79 | next_rewards = self.dqn.forward_max(next_state_embed, item_embed, next_action_mask) # .detach() 80 | return current_rewards, sum(self.discounts[:-1]) + self.discounts[-1] * next_rewards 81 | 82 | def predict(self, states, states_len, action_mask, topN): 83 | states_ = self.item_embeds(states).float() 84 | state_embed = self.staterep(states_, states_len) 85 | 86 | state_embeds = torch.unsqueeze(state_embed, 1) 87 | shape = state_embeds.shape 88 | state_embeds = state_embeds.expand(shape[0], self.isize+1, shape[2]) 89 | 90 | item_embed = self.item_embeds(self.item_idx).unsqueeze(dim=0).repeat((state_embeds.shape[0], 1, 1)) 91 | scores = torch.sigmoid(self.dqn(state_embeds, item_embed)) 92 | scores = scores * action_mask 93 | _, inds = torch.topk(scores, topN) 94 | return inds 95 | 96 | def parameters(self, ): 97 | return list(self.item_embeds.parameters()) + self.dqn.parameters() + self.staterep.parameters() -------------------------------------------------------------------------------- /GRU.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn.functional as F 3 | from torch.autograd import Variable 4 | from torch.nn.utils.rnn import pack_padded_sequence 5 | import numpy as np 6 | import os 7 | 8 | DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 9 | 10 | WORD_EMBED_DIM = 32 11 | USER_EMBED_DIM = 16 12 | 13 | WORD_EMBED_DIM = 32 14 | MAX_PROFILELEN = 10 15 | 16 | class GRUNetwork(torch.nn.Module): 17 | def __init__(self, word_embeddings, model_name): 18 | super(GRUNetwork, self).__init__() 19 | 20 | # profile: word embeddings for look_up 21 | # embedding_matrix = [[0...0], [...], ...[]] 22 | self.word_embeddings = torch.nn.Embedding.from_pretrained(word_embeddings, padding_idx=0) 23 | self.word_embeddings.weight.requires_grad = False 24 | 25 | # bilstm: int(USER_EMBED_DIM/2) * 2 = USER_EMBED_DIM 26 | self.words_gru = torch.nn.GRU(input_size=WORD_EMBED_DIM, hidden_size=int(USER_EMBED_DIM/2), 27 | num_layers=1, batch_first=True, bidirectional=True, dropout=0.) 28 | self.term_gru = torch.nn.GRU(input_size=USER_EMBED_DIM, hidden_size=int(USER_EMBED_DIM/2), 29 | num_layers=1, batch_first=True, bidirectional=True, dropout=0.) 30 | 31 | # gru for behavior sequence 32 | self.behavior_gru = torch.nn.GRU(input_size=USER_EMBED_DIM, hidden_size=USER_EMBED_DIM, 33 | num_layers=1, batch_first=True, bidirectional=False, dropout=0.) 34 | 35 | self.match_MLP = torch.nn.Sequential( 36 | torch.nn.Linear(4 * USER_EMBED_DIM, USER_EMBED_DIM), 37 | torch.nn.Tanh(), 38 | torch.nn.Linear(USER_EMBED_DIM, 1), 39 | torch.nn.Sigmoid()) 40 | 41 | # profiles: [batch_size, MAX_PROFILELEN, MAX_TERMLEN] = (40, 15, 50), word idx 42 | # return: [batch_size, USER_EMBED_DIM] 43 | def get_profile_embeddings(self, profiles): 44 | # word level: 45 | # [batch_size, MAX_PROFILELEN, MAX_TERMLEN] (40, 15, 50) -> 46 | # [batch_size * MAX_PROFILELEN, MAX_TERMLEN](40 * 15, 50) 47 | shape = profiles.shape 48 | profiles_ = profiles.contiguous().view([-1, shape[-1]]) 49 | # sort expects_sample_: large to small 50 | # sorted [batch_size * MAX_PROFILELEN, MAX_TERMLEN](40 * 15, 50) 51 | lens = (profiles_ > 0).sum(dim=-1) 52 | lens_sort, ind_sort = lens.sort(dim=0, descending=True) 53 | profiles_sort = profiles_[ind_sort] 54 | # embeddings: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, EMBED_DIM] 55 | profile_embed = self.word_embeddings(profiles_sort).float() 56 | # compress: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, EMBED_DIM] 57 | profile_pack = pack_padded_sequence(profile_embed, lens_sort, batch_first=True) 58 | # [batch_size * MAX_PROFILELEN, hidden_dim] 59 | _, term_state = self.words_gru(profile_pack) 60 | # print(shape, term_state.shape) 61 | term_state = term_state.view([shape[0], shape[1], -1]) 62 | # print(term_state.shape) 63 | 64 | # [batch_size * MAX_PROFILELEN, hidden_dim] 65 | _, profile_state = self.term_gru(term_state) 66 | return profile_state.view([shape[0], -1]) 67 | 68 | # b_profiless: [batch, max_seq_len, sent, word] [1, 3, 20, 50], gpu tensor 69 | # b_seq_lens: [], list 70 | # b_seq_labels: [[], ...], 0,1,2,3 71 | # return: [batch, USER_EMBED_DIM] 72 | def process_seq(self, b_seq_profiless, b_seq_lens): 73 | # [batch, max_seq_len, sent, word] ->[batch_size, MAX_PROFILELEN, MAX_TERMLEN] 74 | shape = b_seq_profiless.shape 75 | b_seq_profiless_ = b_seq_profiless.view([-1, shape[-2], shape[-1]]) 76 | # (batch, hidden_size) -> [batch, max_seq_len, embed] 77 | b_seq_embeds = self.get_profile_embeddings(b_seq_profiless_).view([shape[0], shape[1], -1]) 78 | 79 | # sort expects_sample_: large to small 80 | # sorted [batch, max_seq_len, embed] 81 | lens = torch.from_numpy(np.array(b_seq_lens)).to(DEVICE) 82 | lens_sort, ind_sort = lens.sort(dim=0, descending=True) 83 | b_seq_embeds_ = b_seq_embeds[ind_sort, :, :] 84 | 85 | # compress: [batch, max_seq_len, embed] -> [batch, embed] 86 | seq_pack = pack_padded_sequence(b_seq_embeds_, lens_sort, batch_first=True) 87 | seq_output, seq_state = self.behavior_gru(seq_pack) 88 | 89 | return seq_state.squeeze(0)[ind_sort] 90 | 91 | 92 | # a_seq_profiless: [batch, max_seq_len, sent, word] [1, 3, 20, 50], gpu tensor 93 | # a_seq_lens: [], list 94 | # a_profiles: [batch, sent, word] 95 | def predict(self, a_seq_profiless, a_seq_lens, a_profiles, b_profiles): 96 | # read profile 97 | a_embeddings = self.get_profile_embeddings(a_profiles) 98 | b_embeddings = self.get_profile_embeddings(b_profiles) 99 | # state 100 | a_state = self.process_seq(a_seq_profiless, a_seq_lens) 101 | 102 | # interact 103 | interact = a_state * b_embeddings 104 | feature = torch.cat([a_state, interact, b_embeddings, a_embeddings], dim=-1) 105 | 106 | return self.match_MLP(feature).squeeze(-1) -------------------------------------------------------------------------------- /APJFNN.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn.functional as F 3 | from torch.nn.utils.rnn import pack_padded_sequence 4 | 5 | WORD_EMBED_DIM = 32 6 | WORD_LSTM_EMBED_DIM = 16 7 | TERM_LSTM_EMBED_DIM = 8 8 | 9 | MAX_PROFILELEN = 10 10 | MAX_TERMLEN = 30 11 | 12 | 13 | class APJFNN(torch.nn.Module): 14 | def __init__(self, word_embeddings): 15 | super(APJFNN, self).__init__() 16 | 17 | # embedding_matrix = [[0...0], [...], ...[]] 18 | self.Word_Embeds = torch.nn.Embedding.from_pretrained(word_embeddings, padding_idx=0) 19 | self.Word_Embeds.weight.requires_grad = False 20 | 21 | # 双向lstm 22 | self.Expect_Word_BiLSTM = torch.nn.LSTM(input_size=WORD_EMBED_DIM, hidden_size=WORD_LSTM_EMBED_DIM, 23 | num_layers=1, batch_first=True, bidirectional=True) 24 | self.Job_Word_BiLSTM = torch.nn.LSTM(input_size=WORD_EMBED_DIM, hidden_size=WORD_LSTM_EMBED_DIM, 25 | num_layers=1, batch_first=True, bidirectional=True) 26 | # 双向lstm 27 | self.Expect_Term_BiLSTM = torch.nn.LSTM(input_size=WORD_LSTM_EMBED_DIM*2, hidden_size=TERM_LSTM_EMBED_DIM, 28 | num_layers=1, batch_first=True, bidirectional=True) 29 | self.Job_Term_BiLSTM = torch.nn.LSTM(input_size=WORD_LSTM_EMBED_DIM*2, hidden_size=TERM_LSTM_EMBED_DIM, 30 | num_layers=1, batch_first=True, bidirectional=True) 31 | 32 | 33 | # word-level attention-layer 34 | self.Expect_Word_Attn_Layer = torch.nn.Sequential( 35 | torch.nn.Linear(WORD_LSTM_EMBED_DIM * 2, WORD_LSTM_EMBED_DIM), 36 | torch.nn.Tanh(), # LeakyReLU 37 | torch.nn.Linear(WORD_LSTM_EMBED_DIM, 1, bias=False), 38 | ) 39 | self.Job_Word_Attn_Layer = torch.nn.Sequential( 40 | torch.nn.Linear(WORD_LSTM_EMBED_DIM * 2, WORD_LSTM_EMBED_DIM), 41 | torch.nn.Tanh(), # LeakyReLU 42 | torch.nn.Linear(WORD_LSTM_EMBED_DIM, 1, bias=False), 43 | ) 44 | # term-level attention-layer 45 | self.Expect_Term_Attn_Layer = torch.nn.Sequential( 46 | torch.nn.Linear(TERM_LSTM_EMBED_DIM* 2, TERM_LSTM_EMBED_DIM), 47 | torch.nn.Tanh(), 48 | torch.nn.Linear(TERM_LSTM_EMBED_DIM, 1, bias=False), 49 | ) 50 | self.Job_Term_Attn_Layer = torch.nn.Sequential( 51 | torch.nn.Linear(TERM_LSTM_EMBED_DIM* 2, TERM_LSTM_EMBED_DIM), 52 | torch.nn.Tanh(), 53 | torch.nn.Linear(TERM_LSTM_EMBED_DIM, 1, bias=False), 54 | ) 55 | 56 | # match mlp 57 | self.Match_MLP = torch.nn.Sequential( 58 | torch.nn.Linear(3 * 2 * TERM_LSTM_EMBED_DIM, 2 * TERM_LSTM_EMBED_DIM), 59 | torch.nn.Tanh(), 60 | torch.nn.Linear(2 * TERM_LSTM_EMBED_DIM, 1), 61 | torch.nn.Sigmoid() 62 | ) 63 | 64 | # [batch_size *2, MAX_PROFILELEN, MAX_TERMLEN] = (40, 15, 50) 65 | # term: padding same, word: padding 0 66 | # expects_sample, jobs_sample are in same format 67 | def forward(self, expects, jobs): 68 | # word level: 69 | # [batch_size, MAX_PROFILELEN, MAX_TERMLEN] (40, 15, 50) -> 70 | # [batch_size * MAX_PROFILELEN, MAX_TERMLEN](40 * 15, 50) 71 | shape = expects.shape 72 | expects_, jobs_ = expects.view([-1, shape[-1]]), jobs.view([-1, shape[-1]]) 73 | 74 | # sort expects_: large to small 75 | # sorted [batch_size * MAX_PROFILELEN, MAX_TERMLEN](40 * 15, 50) 76 | expects_lens = (expects_ > 0).sum(dim=-1) 77 | expects_lens_sort, expects_ind_sort = expects_lens.sort(dim=0, descending=True) 78 | expects_sort = expects_[expects_ind_sort, :] 79 | # sort jobs_sample_: large to small 80 | jobs_lens = (jobs_ > 0).sum(dim=-1) 81 | jobs_lens_sort, jobs_ind_sort = jobs_lens.sort(dim=0, descending=True) 82 | jobs_sort = jobs_[jobs_ind_sort, :] 83 | 84 | # embeddings: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, EMBED_DIM] 85 | expects_wordembed_sort = self.Word_Embeds(expects_sort).float() 86 | jobs_wordembed_sort = self.Word_Embeds(jobs_sort).float() 87 | 88 | # compress: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, EMBED_DIM] 89 | expects_wordpack_sort = pack_padded_sequence(expects_wordembed_sort, expects_lens_sort, batch_first=True) 90 | jobs_wordpack_sort = pack_padded_sequence(jobs_wordembed_sort, jobs_lens_sort, batch_first=True) 91 | 92 | # dynamic: to execute the LSTM over only the valid timesteps 93 | # output_sequence: [batch_size * MAX_PROFILELEN, time_step, hidden_size] 94 | # h: [num_layers*num_directions, batch_size * MAX_PROFILELEN, hidden_size] 95 | # c: [num_layers*num_directions, batch_size * MAX_PROFILELEN, hidden_size] 96 | expects_wordoutput_pack_sort, _ = self.Expect_Word_BiLSTM(expects_wordpack_sort) 97 | jobs_wordoutput_pack_sort, _ = self.Job_Word_BiLSTM(jobs_wordpack_sort) 98 | 99 | # output: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, words_lstm_hidden_dims * 2] 100 | expects_wordoutput_sort, _ = torch.nn.utils.rnn.pad_packed_sequence(expects_wordoutput_pack_sort, batch_first=True) 101 | jobs_wordoutput_sort, _ = torch.nn.utils.rnn.pad_packed_sequence(jobs_wordoutput_pack_sort, batch_first=True) 102 | 103 | # rollback-sort 104 | # print(shape, expects_wordoutput_sort.shape, jobs_wordoutput_sort.shape) 105 | # print(expects_ind_sort.shape) 106 | expects_wordoutput = expects_wordoutput_sort[expects_ind_sort, :, :] 107 | jobs_wordoutput = jobs_wordoutput_sort[jobs_ind_sort, :, :] 108 | 109 | # attention: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, words_lstm_hidden_dims * 2] 110 | expects_wordattn = F.softmax(self.Expect_Word_Attn_Layer(expects_wordoutput), dim=-2) 111 | jobs_wordattn = F.softmax(self.Job_Word_Attn_Layer(jobs_wordoutput), dim=-2) 112 | 113 | # [batch_size * MAX_PROFILELEN, MAX_TERMLEN, words_lstm_hidden_dims * 2] -> 114 | # [batch_size * MAX_PROFILELEN, words_lstm_hidden_dims * 2] 115 | expects_terms = (expects_wordattn * expects_wordoutput).sum(-2)\ 116 | .view([shape[0], MAX_PROFILELEN, 2 * WORD_LSTM_EMBED_DIM]) 117 | jobs_terms = (jobs_wordattn * jobs_wordoutput).sum(-2)\ 118 | .view([shape[0], MAX_PROFILELEN, 2 * WORD_LSTM_EMBED_DIM]) 119 | 120 | # term level: 121 | # [batch_size * MAX_PROFILELEN, words_lstm_hidden_dims * 2] -> 122 | # output_sequence: [batch_size, MAX_PROFILELEN, terms_lstm_hidden_dims * 2] 123 | expects_termoutput, _ = self.Expect_Term_BiLSTM(expects_terms) 124 | jobs_termoutput, _ = self.Job_Term_BiLSTM(jobs_terms) 125 | 126 | # attention: [batch_size, MAX_PROFILELEN, terms_lstm_hidden_dims * 2] 127 | expect_termattn = F.softmax(self.Expect_Term_Attn_Layer(expects_termoutput), dim=-2) 128 | job_termattn = F.softmax(self.Job_Term_Attn_Layer(jobs_termoutput), dim=-2) 129 | 130 | # [batch_size, MAX_PROFILELEN, terms_lstm_hidden_dims*2] -> [batch_size, terms_lstm_hidden_dims*2] 131 | expects_embed = (expect_termattn * expects_termoutput).sum(-2) 132 | jobs_embed = (job_termattn * jobs_termoutput).sum(-2) 133 | 134 | # profile-level interaction 135 | return self.Match_MLP(torch.cat([jobs_embed, expects_embed, jobs_embed - expects_embed], dim=-1)).squeeze(-1) 136 | 137 | # def parameters(self): 138 | # return list(self.Expect_Word_BiLSTM.parameters()) + list(self.Expect_Term_BiLSTM.parameters()) + \ 139 | # list(self.Expect_Word_Attn_Layer.parameters()) + list(self.Expect_Term_Attn_Layer.parameters()) + \ 140 | # list(self.Job_Word_BiLSTM.parameters()) + list(self.Job_Term_BiLSTM.parameters()) + \ 141 | # list(self.Job_Word_Attn_Layer.parameters()) + list(self.Job_Term_Attn_Layer.parameters()) 142 | -------------------------------------------------------------------------------- /transformer.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | import numpy as np 5 | import copy 6 | 7 | 8 | class Config(object): 9 | 10 | """配置参数""" 11 | def __init__(self, dataset, embedding): 12 | self.model_name = 'Transformer' 13 | self.train_path = dataset + '/data/train.txt' # 训练集 14 | self.dev_path = dataset + '/data/dev.txt' # 验证集 15 | self.test_path = dataset + '/data/test.txt' # 测试集 16 | self.class_list = [x.strip() for x in open( 17 | dataset + '/data/class.txt', encoding='utf-8').readlines()] # 类别名单 18 | self.vocab_path = dataset + '/data/vocab.pkl' # 词表 19 | self.save_path = dataset + '/saved_dict/' + self.model_name + '.ckpt' # 模型训练结果 20 | self.log_path = dataset + '/log/' + self.model_name 21 | self.embedding_pretrained = torch.tensor( 22 | np.load(dataset + '/data/' + embedding)["embeddings"].astype('float32'))\ 23 | if embedding != 'random' else None # 预训练词向量 24 | self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备 25 | 26 | self.dropout = 0.5 # 随机失活 27 | self.require_improvement = 2000 # 若超过1000batch效果还没提升,则提前结束训练 28 | self.num_classes = len(self.class_list) # 类别数 29 | self.n_vocab = 0 # 词表大小,在运行时赋值 30 | self.num_epochs = 20 # epoch数 31 | self.batch_size = 128 # mini-batch大小 32 | self.pad_size = 32 # 每句话处理成的长度(短填长切) 33 | self.learning_rate = 5e-4 # 学习率 34 | self.embed = self.embedding_pretrained.size(1)\ 35 | if self.embedding_pretrained is not None else 300 # 字向量维度 36 | self.dim_model = 300 37 | self.hidden = 1024 38 | self.last_hidden = 512 39 | self.num_head = 5 40 | self.num_encoder = 2 41 | 42 | 43 | '''Attention Is All You Need''' 44 | 45 | 46 | class Model(nn.Module): 47 | def __init__(self, config): 48 | super(Model, self).__init__() 49 | if config.embedding_pretrained is not None: 50 | self.embedding = nn.Embedding.from_pretrained(config.embedding_pretrained, freeze=False) 51 | else: 52 | self.embedding = nn.Embedding(config.n_vocab, config.embed, padding_idx=config.n_vocab - 1) 53 | 54 | self.postion_embedding = Positional_Encoding(config.embed, config.pad_size, config.dropout, config.device) 55 | self.encoder = Encoder(config.dim_model, config.num_head, config.hidden, config.dropout) 56 | self.encoders = nn.ModuleList([ 57 | copy.deepcopy(self.encoder) 58 | # Encoder(config.dim_model, config.num_head, config.hidden, config.dropout) 59 | for _ in range(config.num_encoder)]) 60 | 61 | self.fc1 = nn.Linear(config.pad_size * config.dim_model, config.num_classes) 62 | # self.fc2 = nn.Linear(config.last_hidden, config.num_classes) 63 | # self.fc1 = nn.Linear(config.dim_model, config.num_classes) 64 | 65 | def forward(self, x): 66 | out = self.embedding(x[0]) 67 | out = self.postion_embedding(out) 68 | for encoder in self.encoders: 69 | out = encoder(out) 70 | out = out.view(out.size(0), -1) 71 | # out = torch.mean(out, 1) 72 | out = self.fc1(out) 73 | return out 74 | 75 | 76 | class Encoder(nn.Module): 77 | def __init__(self, dim_model, num_head, hidden, dropout): 78 | super(Encoder, self).__init__() 79 | self.attention = Multi_Head_Attention(dim_model, num_head, dropout) 80 | self.feed_forward = Position_wise_Feed_Forward(dim_model, hidden, dropout) 81 | 82 | def forward(self, x): 83 | out = self.attention(x) 84 | out = self.feed_forward(out) 85 | return out 86 | 87 | 88 | class Positional_Encoding(nn.Module): 89 | def __init__(self, embed, pad_size, dropout, device): 90 | super(Positional_Encoding, self).__init__() 91 | self.device = device 92 | self.pe = torch.tensor([[pos / (10000.0 ** (i // 2 * 2.0 / embed)) for i in range(embed)] for pos in range(pad_size)]) 93 | self.pe[:, 0::2] = np.sin(self.pe[:, 0::2]) 94 | self.pe[:, 1::2] = np.cos(self.pe[:, 1::2]) 95 | self.dropout = nn.Dropout(dropout) 96 | 97 | def forward(self, x): 98 | out = x + nn.Parameter(self.pe, requires_grad=False).to(self.device) 99 | out = self.dropout(out) 100 | return out 101 | 102 | 103 | class Scaled_Dot_Product_Attention(nn.Module): 104 | '''Scaled Dot-Product Attention ''' 105 | def __init__(self): 106 | super(Scaled_Dot_Product_Attention, self).__init__() 107 | 108 | def forward(self, Q, K, V, scale=None): 109 | ''' 110 | Args: 111 | Q: [batch_size, len_Q, dim_Q] 112 | K: [batch_size, len_K, dim_K] 113 | V: [batch_size, len_V, dim_V] 114 | scale: 缩放因子 论文为根号dim_K 115 | Return: 116 | self-attention后的张量,以及attention张量 117 | ''' 118 | attention = torch.matmul(Q, K.permute(0, 2, 1)) 119 | if scale: 120 | attention = attention * scale 121 | # if mask: # TODO change this 122 | # attention = attention.masked_fill_(mask == 0, -1e9) 123 | attention = F.softmax(attention, dim=-1) 124 | context = torch.matmul(attention, V) 125 | return context 126 | 127 | 128 | class Multi_Head_Attention(nn.Module): 129 | def __init__(self, dim_model, num_head, dropout=0.0): 130 | super(Multi_Head_Attention, self).__init__() 131 | self.num_head = num_head 132 | assert dim_model % num_head == 0 133 | self.dim_head = dim_model // self.num_head 134 | self.fc_Q = nn.Linear(dim_model, num_head * self.dim_head) 135 | self.fc_K = nn.Linear(dim_model, num_head * self.dim_head) 136 | self.fc_V = nn.Linear(dim_model, num_head * self.dim_head) 137 | self.attention = Scaled_Dot_Product_Attention() 138 | self.fc = nn.Linear(num_head * self.dim_head, dim_model) 139 | self.dropout = nn.Dropout(dropout) 140 | self.layer_norm = nn.LayerNorm(dim_model) 141 | 142 | def forward(self, x): 143 | batch_size = x.size(0) 144 | Q = self.fc_Q(x) 145 | K = self.fc_K(x) 146 | V = self.fc_V(x) 147 | Q = Q.view(batch_size * self.num_head, -1, self.dim_head) 148 | K = K.view(batch_size * self.num_head, -1, self.dim_head) 149 | V = V.view(batch_size * self.num_head, -1, self.dim_head) 150 | # if mask: # TODO 151 | # mask = mask.repeat(self.num_head, 1, 1) # TODO change this 152 | scale = K.size(-1) ** -0.5 # 缩放因子 153 | context = self.attention(Q, K, V, scale) 154 | 155 | context = context.view(batch_size, -1, self.dim_head * self.num_head) 156 | out = self.fc(context) 157 | out = self.dropout(out) 158 | out = out + x # 残差连接 159 | out = self.layer_norm(out) 160 | return out 161 | 162 | 163 | class Position_wise_Feed_Forward(nn.Module): 164 | def __init__(self, dim_model, hidden, dropout=0.0): 165 | super(Position_wise_Feed_Forward, self).__init__() 166 | self.fc1 = nn.Linear(dim_model, hidden) 167 | self.fc2 = nn.Linear(hidden, dim_model) 168 | self.dropout = nn.Dropout(dropout) 169 | self.layer_norm = nn.LayerNorm(dim_model) 170 | 171 | def forward(self, x): 172 | out = self.fc1(x) 173 | out = F.relu(out) 174 | out = self.fc2(out) 175 | out = self.dropout(out) 176 | out = out + x # 残差连接 177 | out = self.layer_norm(out) 178 | return out -------------------------------------------------------------------------------- /DAPJF.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn.functional as F 3 | from torch.nn.utils.rnn import pack_padded_sequence 4 | from torch.autograd import Variable 5 | 6 | WORD_EMBED_DIM = 32 7 | WORD_LSTM_EMBED_DIM = 16 8 | TERM_LSTM_EMBED_DIM = 8 9 | 10 | MAX_PROFILELEN = 10 11 | MAX_TERMLEN = 30 12 | 13 | DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") 14 | 15 | class DAPJF(torch.nn.Module): 16 | def __init__(self, word_embeddings): 17 | super(DAPJF, self).__init__() 18 | 19 | # embedding_matrix = [[0...0], [...], ...[]] 20 | self.Word_Embeds = torch.nn.Embedding.from_pretrained(word_embeddings, padding_idx=0) 21 | self.Word_Embeds.weight.requires_grad = False 22 | 23 | # 双向lstm 24 | self.Expect_Word_BiLSTM = torch.nn.LSTM(input_size=WORD_EMBED_DIM, hidden_size=WORD_LSTM_EMBED_DIM, 25 | num_layers=1, batch_first=True, bidirectional=True) 26 | self.Job_Word_BiLSTM = torch.nn.LSTM(input_size=WORD_EMBED_DIM, hidden_size=WORD_LSTM_EMBED_DIM, 27 | num_layers=1, batch_first=True, bidirectional=True) 28 | # 双向lstm 29 | self.Expect_Term_BiLSTM = torch.nn.LSTM(input_size=WORD_LSTM_EMBED_DIM*2, hidden_size=TERM_LSTM_EMBED_DIM, 30 | num_layers=1, batch_first=True, bidirectional=True) 31 | self.Job_Term_BiLSTM = torch.nn.LSTM(input_size=WORD_LSTM_EMBED_DIM*2, hidden_size=TERM_LSTM_EMBED_DIM, 32 | num_layers=1, batch_first=True, bidirectional=True) 33 | 34 | 35 | # word-level attention-layer 36 | self.Expect_Word_Attn_Layer = torch.nn.Sequential( 37 | torch.nn.Linear(WORD_LSTM_EMBED_DIM * 2, WORD_LSTM_EMBED_DIM), 38 | torch.nn.Tanh(), # LeakyReLU 39 | torch.nn.Linear(WORD_LSTM_EMBED_DIM, 1, bias=False), 40 | ) 41 | self.Job_Word_Attn_Layer = torch.nn.Sequential( 42 | torch.nn.Linear(WORD_LSTM_EMBED_DIM * 2, WORD_LSTM_EMBED_DIM), 43 | torch.nn.Tanh(), # LeakyReLU 44 | torch.nn.Linear(WORD_LSTM_EMBED_DIM, 1, bias=False), 45 | ) 46 | # term-level attention-layer 47 | self.Expect_Term_Attn_Layer = torch.nn.Sequential( 48 | torch.nn.Linear(TERM_LSTM_EMBED_DIM* 2, TERM_LSTM_EMBED_DIM), 49 | torch.nn.Tanh(), 50 | torch.nn.Linear(TERM_LSTM_EMBED_DIM, 1, bias=False), 51 | ) 52 | self.Job_Term_Attn_Layer = torch.nn.Sequential( 53 | torch.nn.Linear(TERM_LSTM_EMBED_DIM* 2, TERM_LSTM_EMBED_DIM), 54 | torch.nn.Tanh(), 55 | torch.nn.Linear(TERM_LSTM_EMBED_DIM, 1, bias=False), 56 | ) 57 | 58 | self.A = Variable(torch.FloatTensor(torch.rand(50, 2*TERM_LSTM_EMBED_DIM)).to(DEVICE), requires_grad = True) 59 | self.B_D1 = Variable(torch.FloatTensor(torch.rand(50, 2*TERM_LSTM_EMBED_DIM)).to(DEVICE), requires_grad = True) 60 | self.B_D2 = Variable(torch.FloatTensor(torch.rand(50, 2*TERM_LSTM_EMBED_DIM)).to(DEVICE), requires_grad = True) 61 | 62 | # match mlp 63 | self.Match_MLP = torch.nn.Sequential( 64 | torch.nn.Linear(2 * 2 * TERM_LSTM_EMBED_DIM + 3 * MAX_PROFILELEN, 2 * TERM_LSTM_EMBED_DIM), 65 | torch.nn.Tanh(), 66 | torch.nn.Linear(2 * TERM_LSTM_EMBED_DIM, 1), 67 | torch.nn.Sigmoid() 68 | ) 69 | 70 | self.ConvNet = torch.nn.Sequential( 71 | torch.nn.Conv1d(in_channels=MAX_PROFILELEN, out_channels=MAX_PROFILELEN, kernel_size=5), 72 | # torch.nn.Conv2d(MAX_PROFILELEN, MAX_PROFILELEN, kernel_size=(4,4)), 73 | # BatchNorm1d只处理第二个维度 74 | torch.nn.BatchNorm1d(MAX_PROFILELEN), 75 | torch.nn.ReLU(inplace=True), 76 | torch.nn.MaxPool1d(kernel_size=2) 77 | ) 78 | 79 | # [batch_size *2, MAX_PROFILELEN, MAX_TERMLEN] = (40, 15, 50) 80 | # term: padding same, word: padding 0 81 | # expects_sample, jobs_sample are in same format 82 | def forward(self, expects, jobs, domain=1): 83 | # word level: 84 | # [batch_size, MAX_PROFILELEN, MAX_TERMLEN] (40, 15, 50) -> 85 | # [batch_size * MAX_PROFILELEN, MAX_TERMLEN](40 * 15, 50) 86 | shape = expects.shape 87 | expects_, jobs_ = expects.view([-1, shape[-1]]), jobs.view([-1, shape[-1]]) 88 | 89 | # sort expects_: large to small 90 | # sorted [batch_size * MAX_PROFILELEN, MAX_TERMLEN](40 * 15, 50) 91 | expects_lens = (expects_ > 0).sum(dim=-1) 92 | expects_lens_sort, expects_ind_sort = expects_lens.sort(dim=0, descending=True) 93 | expects_sort = expects_[expects_ind_sort, :] 94 | # sort jobs_sample_: large to small 95 | jobs_lens = (jobs_ > 0).sum(dim=-1) 96 | jobs_lens_sort, jobs_ind_sort = jobs_lens.sort(dim=0, descending=True) 97 | jobs_sort = jobs_[jobs_ind_sort, :] 98 | 99 | # embeddings: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, EMBED_DIM] 100 | expects_wordembed_sort = self.Word_Embeds(expects_sort).float() 101 | jobs_wordembed_sort = self.Word_Embeds(jobs_sort).float() 102 | 103 | # compress: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, EMBED_DIM] 104 | expects_wordpack_sort = pack_padded_sequence(expects_wordembed_sort, expects_lens_sort, batch_first=True) 105 | jobs_wordpack_sort = pack_padded_sequence(jobs_wordembed_sort, jobs_lens_sort, batch_first=True) 106 | 107 | # dynamic: to execute the LSTM over only the valid timesteps 108 | # output_sequence: [batch_size * MAX_PROFILELEN, time_step, hidden_size] 109 | # h: [num_layers*num_directions, batch_size * MAX_PROFILELEN, hidden_size] 110 | # c: [num_layers*num_directions, batch_size * MAX_PROFILELEN, hidden_size] 111 | expects_wordoutput_pack_sort, _ = self.Expect_Word_BiLSTM(expects_wordpack_sort) 112 | jobs_wordoutput_pack_sort, _ = self.Job_Word_BiLSTM(jobs_wordpack_sort) 113 | 114 | # output: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, words_lstm_hidden_dims * 2] 115 | expects_wordoutput_sort, _ = torch.nn.utils.rnn.pad_packed_sequence(expects_wordoutput_pack_sort, batch_first=True) 116 | jobs_wordoutput_sort, _ = torch.nn.utils.rnn.pad_packed_sequence(jobs_wordoutput_pack_sort, batch_first=True) 117 | 118 | # rollback-sort 119 | # print(shape, expects_wordoutput_sort.shape, jobs_wordoutput_sort.shape) 120 | # print(expects_ind_sort.shape) 121 | expects_wordoutput = expects_wordoutput_sort[expects_ind_sort, :, :] 122 | jobs_wordoutput = jobs_wordoutput_sort[jobs_ind_sort, :, :] 123 | 124 | # attention: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, words_lstm_hidden_dims * 2] 125 | expects_wordattn = F.softmax(self.Expect_Word_Attn_Layer(expects_wordoutput), dim=-2) 126 | jobs_wordattn = F.softmax(self.Job_Word_Attn_Layer(jobs_wordoutput), dim=-2) 127 | 128 | # [batch_size * MAX_PROFILELEN, MAX_TERMLEN, words_lstm_hidden_dims * 2] -> 129 | # [batch_size * MAX_PROFILELEN, words_lstm_hidden_dims * 2] 130 | expects_terms = (expects_wordattn * expects_wordoutput).sum(-2)\ 131 | .view([shape[0], MAX_PROFILELEN, 2 * WORD_LSTM_EMBED_DIM]) 132 | jobs_terms = (jobs_wordattn * jobs_wordoutput).sum(-2)\ 133 | .view([shape[0], MAX_PROFILELEN, 2 * WORD_LSTM_EMBED_DIM]) 134 | 135 | # term level: 136 | # [batch_size * MAX_PROFILELEN, words_lstm_hidden_dims * 2] -> 137 | # output_sequence: [batch_size, MAX_PROFILELEN, terms_lstm_hidden_dims * 2] 138 | expects_termoutput, _ = self.Expect_Term_BiLSTM(expects_terms) 139 | jobs_termoutput, _ = self.Job_Term_BiLSTM(jobs_terms) 140 | 141 | # attention: [batch_size, MAX_PROFILELEN, terms_lstm_hidden_dims * 2] 142 | expect_termattn = F.softmax(self.Expect_Term_Attn_Layer(expects_termoutput), dim=-2) 143 | job_termattn = F.softmax(self.Job_Term_Attn_Layer(jobs_termoutput), dim=-2) 144 | 145 | # [batch_size, MAX_PROFILELEN, terms_lstm_hidden_dims*2] -> [batch_size, terms_lstm_hidden_dims*2] 146 | expects_embed = (expect_termattn * expects_termoutput).sum(-2) 147 | jobs_embed = (job_termattn * jobs_termoutput).sum(-2) 148 | 149 | # conv 150 | if domain == 1: 151 | W = torch.mm(torch.transpose(self.A, 1, 0), self.B_D1) 152 | else: 153 | W = torch.mm(torch.transpose(self.A, 1, 0), self.B_D2) 154 | 155 | # [batch_size, MAX_PROFILELEN, terms_lstm_hidden_dims * 2] 156 | expects_termoutput_1 = torch.mm(expects_termoutput.contiguous().view([shape[0]*shape[1], -1]), W).view([shape[0], shape[1], -1]) 157 | # [batch_size, MAX_PROFILELEN, MAX_PROFILELEN] 158 | M = torch.bmm(expects_termoutput_1, jobs_termoutput.permute([0, 2, 1])) 159 | interaction = self.ConvNet(M).view([shape[0], -1]) 160 | 161 | # profile-level interaction 162 | return self.Match_MLP(torch.cat([jobs_embed, expects_embed, interaction], dim=-1)).squeeze(-1) 163 | 164 | # def parameters(self): 165 | # return list(self.Expect_Word_BiLSTM.parameters()) + list(self.Expect_Term_BiLSTM.parameters()) + \ 166 | # list(self.Expect_Word_Attn_Layer.parameters()) + list(self.Expect_Term_Attn_Layer.parameters()) + \ 167 | # list(self.Job_Word_BiLSTM.parameters()) + list(self.Job_Term_BiLSTM.parameters()) + \ 168 | # list(self.Job_Word_Attn_Layer.parameters()) + list(self.Job_Term_Attn_Layer.parameters()) 169 | -------------------------------------------------------------------------------- /JRMPM.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | import torch.nn.functional as F 4 | from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence 5 | import numpy as np 6 | 7 | DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") 8 | 9 | WORD_EMBED_DIM = 32 10 | USER_EMBED_DIM = 16 11 | 12 | MAX_PROFILELEN = 10 13 | MAX_TERMLEN = 30 14 | 15 | 16 | class JRMPM(torch.nn.Module): 17 | def __init__(self, word_embeddings): 18 | super(JRMPM, self).__init__() 19 | 20 | # profile: word embeddings for look_up 21 | # embedding_matrix = [[0...0], [...], ...[]] 22 | self.word_embeddings = torch.nn.Embedding.from_pretrained(word_embeddings, padding_idx=0) 23 | self.word_embeddings.weight.requires_grad = False 24 | 25 | # BI-GRU: int(USER_EMBED_DIM/2) * 2 = USER_EMBED_DIM 26 | self.expect_words_gru = torch.nn.GRU(input_size=WORD_EMBED_DIM, hidden_size=int(USER_EMBED_DIM/2), 27 | num_layers=1, batch_first=True, bidirectional=True) 28 | self.job_words_gru = torch.nn.GRU(input_size=WORD_EMBED_DIM, hidden_size=int(USER_EMBED_DIM/2), 29 | num_layers=1, batch_first=True, bidirectional=True) 30 | 31 | # GRU: USER_EMBED_DIM 32 | self.expect_sent_gru = torch.nn.GRU(input_size=USER_EMBED_DIM, hidden_size=USER_EMBED_DIM, 33 | num_layers=1, batch_first=True, bidirectional=False) 34 | self.job_sent_gru = torch.nn.GRU(input_size=USER_EMBED_DIM, hidden_size=USER_EMBED_DIM, 35 | num_layers=1, batch_first=True, bidirectional=False) 36 | 37 | # memory profiling 38 | self.expect_momery = torch.nn.Embedding(MAX_PROFILELEN, USER_EMBED_DIM) 39 | self.expect_momery.weight.requires_grad = True 40 | self.job_momery = torch.nn.Embedding(MAX_PROFILELEN, USER_EMBED_DIM) 41 | self.job_momery.weight.requires_grad = True 42 | 43 | 44 | # update pi: beta, gamma 45 | self.expect_update_pi = torch.nn.Sequential( 46 | torch.nn.Linear(MAX_PROFILELEN, MAX_PROFILELEN, bias=False), 47 | torch.nn.Tanh(), 48 | torch.nn.Softmax(dim=-2) 49 | ) 50 | self.job_update_pi = torch.nn.Sequential( 51 | torch.nn.Linear( MAX_PROFILELEN, MAX_PROFILELEN, bias=False), 52 | torch.nn.Tanh(), 53 | torch.nn.Softmax(dim=-2) 54 | ) 55 | 56 | # update g: 57 | self.expect_g_update = torch.nn.Sequential( 58 | torch.nn.Linear(3 * USER_EMBED_DIM, 1, bias=False), 59 | torch.nn.Sigmoid() 60 | ) 61 | self.job_g_update = torch.nn.Sequential( 62 | torch.nn.Linear(3 * USER_EMBED_DIM, 1, bias=False), 63 | torch.nn.Sigmoid() 64 | ) 65 | 66 | # read phi: alpha 67 | self.expect_read_phi = torch.nn.Sequential( 68 | torch.nn.Linear(MAX_PROFILELEN, MAX_PROFILELEN, bias=False), 69 | torch.nn.Tanh(), 70 | torch.nn.Softmax(dim=-2) 71 | ) 72 | self.job_read_phi = torch.nn.Sequential( 73 | torch.nn.Linear(MAX_PROFILELEN, MAX_PROFILELEN, bias=False), 74 | torch.nn.Tanh(), 75 | torch.nn.Softmax(dim=-2) 76 | ) 77 | # read g: 78 | self.expect_g_read = torch.nn.Sequential( 79 | torch.nn.Linear(3 * USER_EMBED_DIM, 1, bias=False), 80 | torch.nn.Sigmoid() 81 | ) 82 | self.job_g_read = torch.nn.Sequential( 83 | torch.nn.Linear(3 * USER_EMBED_DIM, 1, bias=False), 84 | torch.nn.Sigmoid() 85 | ) 86 | 87 | # match 88 | self.MLP = torch.nn.Sequential( 89 | torch.nn.Linear(2 * MAX_PROFILELEN * USER_EMBED_DIM, MAX_PROFILELEN * USER_EMBED_DIM), 90 | torch.nn.Tanh(), 91 | torch.nn.Linear(MAX_PROFILELEN * USER_EMBED_DIM, 1), 92 | torch.nn.Sigmoid() 93 | ) 94 | 95 | 96 | 97 | # profiles: [batch_size, MAX_PROFILELEN, MAX_TERMLEN] = (40, 15, 50), word idx 98 | def __words_BiGRU__(self, profiles, isexpect=True): 99 | # word level: 100 | shape = profiles.shape # [132, 20, 50] 101 | profiles_ = profiles.contiguous().view([-1, shape[-1]]) 102 | # sort expects_sample_: large to small 103 | # sorted [batch_size * MAX_PROFILELEN, MAX_TERMLEN](40 * 15, 50) 104 | lens = (profiles_ > 0).sum(dim=-1) 105 | lens_sort, ind_sort = lens.sort(dim=0, descending=True) 106 | profiles_sort = profiles_[ind_sort] 107 | # embeddings: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, EMBED_DIM] 108 | profile_embed = self.word_embeddings(profiles_sort).float() 109 | profile_pack = pack_padded_sequence(profile_embed, lens_sort, batch_first=True) 110 | if isexpect: 111 | _, sent_hidden = self.expect_words_gru(profile_pack) 112 | else: 113 | _, sent_hidden = self.job_words_gru(profile_pack) 114 | # [2640, 2, 50] 115 | sent_hidden = sent_hidden.permute(1, 0, 2).contiguous().view([-1, USER_EMBED_DIM]) 116 | sent_hidden = sent_hidden[ind_sort].view([shape[0], shape[1], -1]) 117 | # [132, 20, 100] 118 | return sent_hidden 119 | 120 | # sents: [batch_size, MAX_PROFILELEN, dim] 121 | def __sents_GRU__(self, sent_hidden, isexpect=True): 122 | if isexpect: 123 | out, _ = self.expect_sent_gru(sent_hidden) 124 | else: 125 | out, _ = self.job_sent_gru(sent_hidden) 126 | return out 127 | 128 | def profile2sent(self, profiles, isexpect): 129 | return self.__sents_GRU__(self.__words_BiGRU__(profiles, isexpect), isexpect) 130 | 131 | # memory: [batch, MAX_PROFILELEN, USER_EMBED_DIM] [1, 20, 100] 132 | # a_sents: [batch, MAX_PROFILELEN, USER_EMBED_DIM] [1, 20, 100] 133 | # b_sents: [batch, MAX_PROFILELEN, USER_EMBED_DIM] [1, 20, 100] 134 | # col_mask: [batch] 135 | def update(self, memory, a_sents, b_sents, col_mask, isexpect=True): 136 | if isexpect: 137 | # [batch, n, n*] 138 | beta = self.expect_update_pi(torch.bmm(memory, a_sents.permute(0, 2, 1))) 139 | gamma = self.expect_update_pi(torch.bmm(memory, b_sents.permute(0, 2, 1))) 140 | else: 141 | beta = self.job_update_pi(torch.bmm(memory, a_sents.permute(0, 2, 1))) 142 | gamma = self.job_update_pi(torch.bmm(memory, b_sents.permute(0, 2, 1))) 143 | 144 | # [batch, n, n*] * [batch, n, dim] = [batch, n, dim] 145 | i_update = torch.bmm(beta, a_sents) + torch.bmm(gamma, b_sents) 146 | # [batch, n, dim] 147 | if isexpect: 148 | g_update = self.expect_g_update(torch.cat([memory, i_update, memory * i_update], dim=-1)) 149 | else: 150 | g_update = self.job_g_update(torch.cat([memory, i_update, memory * i_update], dim=-1)) 151 | # m_{k+1} 152 | # [batch, MAX_PROFILELEN, USER_EMBED_DIM] 153 | memory_update = g_update * memory + (1-g_update) * memory 154 | 155 | # mask 156 | shape = memory_update.shape 157 | memory_update_mask = (torch.unsqueeze(col_mask, 1) * memory_update.view([shape[0], -1])).view(shape) 158 | memory_noupdate_mask = (torch.unsqueeze(1.-col_mask, 1) * memory.contiguous().view([shape[0], -1])).view(shape) 159 | 160 | return memory_update_mask + memory_noupdate_mask 161 | 162 | # memory: [batch, n, dim] [1, 20, 100] 163 | # hidden_last: [batch, n, dim] [1, 20, 100] 164 | # a_sents: [batch, n, dim] [1, 20, 100] 165 | def read(self, memory, hidden_last, a_sents, isexpect=True): 166 | # [batch, n, n*] 167 | if isexpect: 168 | alpha = self.expect_read_phi(torch.bmm(memory, (hidden_last * a_sents).permute(0, 2, 1))) 169 | else: 170 | alpha = self.job_read_phi(torch.bmm(memory, (hidden_last * a_sents).permute(0, 2, 1))) 171 | 172 | # [batch, n, n*] * [batch, n, dim] = [batch, n, dim] 173 | i_read = torch.bmm(alpha, memory) 174 | # [batch, n, dim], 175 | if isexpect: 176 | g_read = self.expect_g_read(torch.cat([a_sents, i_read, a_sents * i_read], dim=-1)) 177 | else: 178 | g_read = self.job_g_read(torch.cat([a_sents, i_read, a_sents * i_read], dim=-1)) 179 | 180 | # [batch, n, dim] 181 | hidden = g_read * i_read + (1 - g_read) * hidden_last 182 | return hidden 183 | 184 | # a_profiles: [batch, sent, word] [1, 20, 50], tensor 185 | # b_profiless: [batch, max_seq_len, sent, word] [1, 3, 20, 50], tensor 186 | # b_seq_lens: [], list 187 | def process_seq(self, a_profiles, b_seq_profiless, b_seq_lens, isexpect=True): 188 | 189 | # [batch, MAX_PROFILELEN, USER_EMBED_DIM] [1, 20, 100] 190 | batch_a_sents = self.profile2sent(a_profiles, isexpect) 191 | # [batch, MAX_PROFILELEN, USER_EMBED_DIM] [1, 20, 100] 192 | batch_memory = batch_hidden = self.profile2sent(a_profiles, not isexpect) 193 | 194 | for i in range(max(b_seq_lens)): 195 | # [1,0,... ] 196 | col_mask = torch.from_numpy((np.array(b_seq_lens)-i>0)+0.).float().to(DEVICE) 197 | # [batch, MAX_PROFILELEN, USER_EMBED_DIM] [1, 20, 100] 198 | batch_b_sents = self.profile2sent(b_seq_profiless[:, i, :, :], not isexpect) 199 | 200 | # batch_memory: [batch, MAX_PROFILELEN, USER_EMBED_DIM] [1, 20, 100] 201 | # batch_a_sents: [batch, MAX_PROFILELEN, USER_EMBED_DIM] [1, 20, 100] 202 | # batch_b_sents: [batch, MAX_PROFILELEN, USER_EMBED_DIM] [1, 20, 100] 203 | # batch_memory: [batch, MAX_PROFILELEN, USER_EMBED_DIM] [1, 20, 100]) 204 | batch_memory = self.update(batch_memory, batch_a_sents, 205 | batch_b_sents, col_mask, isexpect) 206 | batch_hidden = self.read(batch_memory, batch_hidden, batch_a_sents, isexpect) 207 | 208 | return batch_hidden 209 | 210 | 211 | # [100, 20, 100] [100, 20, 100] 212 | def predict(self, expect_hidden, job_hidden): 213 | 214 | expect_hidden_ = expect_hidden.reshape([expect_hidden.shape[0], -1]) 215 | job_hidden_ = job_hidden.reshape([job_hidden.shape[0], -1]) 216 | 217 | return self.MLP(torch.cat([expect_hidden_, job_hidden_], -1)) 218 | 219 | 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /DPJFMBS.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn.functional as F 3 | from torch.autograd import Variable 4 | from torch.nn.utils.rnn import pack_padded_sequence 5 | import numpy as np 6 | import os 7 | 8 | DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 9 | 10 | WORD_EMBED_DIM = 32 11 | USER_EMBED_DIM = 32 12 | 13 | MAX_PROFILELEN = 20 14 | MAX_TERMLEN = 50 15 | 16 | MEMORY_SLOT = 5 17 | 18 | class Model(torch.nn.Module): 19 | def __init__(self, word_embeddings, behavior_typenum): 20 | super(Model, self).__init__() 21 | self.behavior_typenum = behavior_typenum 22 | 23 | print('MEMORY_SLOT=', MEMORY_SLOT) 24 | 25 | # profile: word embeddings for look_up 26 | # embedding_matrix = [[0...0], [...], ...[]] 27 | self.word_embeddings = torch.nn.Embedding.from_pretrained(word_embeddings, padding_idx=0) 28 | self.word_embeddings.weight.requires_grad = False 29 | 30 | # bilstm: int(USER_EMBED_DIM/2) * 2 = USER_EMBED_DIM 31 | self.words_gru = torch.nn.GRU(input_size=WORD_EMBED_DIM, hidden_size=int(USER_EMBED_DIM/2), 32 | num_layers=1, batch_first=True, bidirectional=True, dropout=0.) 33 | # attention layer 34 | self.words_attention_layer = torch.nn.Sequential( 35 | torch.nn.Linear(USER_EMBED_DIM, USER_EMBED_DIM), 36 | torch.nn.Tanh(), # LeakyReLU 37 | torch.nn.Linear(USER_EMBED_DIM, 1, bias=False), 38 | ) 39 | # attention layer 40 | self.terms_attention_layer = torch.nn.Sequential( 41 | torch.nn.Linear(USER_EMBED_DIM, USER_EMBED_DIM), 42 | torch.nn.Tanh(), 43 | torch.nn.Linear(USER_EMBED_DIM, 1, bias=False), 44 | ) 45 | 46 | self.KEY_MATs = [] 47 | for i in range(self.behavior_typenum): 48 | # [MEMORY_SLOT, USER_EMBED_DIM] 49 | self.KEY_MATs.append(Variable(torch.randn((MEMORY_SLOT, USER_EMBED_DIM)), requires_grad = True).float().to(DEVICE)) 50 | 51 | self.update_mlp = torch.nn.Sequential( 52 | torch.nn.Linear(USER_EMBED_DIM, USER_EMBED_DIM), 53 | torch.nn.Sigmoid(), 54 | ) 55 | 56 | self.e2j_MTL_MLPs = [] 57 | for i in range(self.behavior_typenum-1): 58 | self.e2j_MTL_MLPs.append(torch.nn.Sequential( 59 | torch.nn.Linear( 4 *USER_EMBED_DIM, USER_EMBED_DIM), 60 | torch.nn.Tanh(), 61 | torch.nn.Linear( USER_EMBED_DIM, 1), 62 | torch.nn.Sigmoid()).to(DEVICE)) 63 | self.j2e_MTL_MLPs = [] 64 | for i in range(self.behavior_typenum-1): 65 | self.j2e_MTL_MLPs.append(torch.nn.Sequential( 66 | torch.nn.Linear( 4 *USER_EMBED_DIM, USER_EMBED_DIM), 67 | torch.nn.Tanh(), 68 | torch.nn.Linear( USER_EMBED_DIM, 1), 69 | torch.nn.Sigmoid()).to(DEVICE)) 70 | self.match_MLP = torch.nn.Sequential( 71 | torch.nn.Linear( 4 *USER_EMBED_DIM, USER_EMBED_DIM), 72 | torch.nn.Tanh(), 73 | torch.nn.Linear( USER_EMBED_DIM, 1), 74 | torch.nn.Sigmoid()).to(DEVICE) 75 | self.e2j_MTL_MLPs.append(self.match_MLP) 76 | self.j2e_MTL_MLPs.append(self.match_MLP) 77 | 78 | # profiles: [batch_size, MAX_PROFILELEN, MAX_TERMLEN] = (40, 15, 50), word idx 79 | # return: [batch_size, USER_EMBED_DIM] 80 | def get_profile_embeddings(self, profiles, isexpect): 81 | # word level: 82 | # [batch_size, MAX_PROFILELEN, MAX_TERMLEN] (40, 15, 50) -> 83 | # [batch_size * MAX_PROFILELEN, MAX_TERMLEN](40 * 15, 50) 84 | shape = profiles.shape 85 | profiles_ = profiles.view([-1, shape[-1]]) 86 | 87 | # sort expects_sample_: large to small 88 | # sorted [batch_size * MAX_PROFILELEN, MAX_TERMLEN](40 * 15, 50) 89 | lens = (profiles_ > 0).sum(dim=-1) 90 | lens_sort, ind_sort = lens.sort(dim=0, descending=True) 91 | profiles_sort = profiles_[ind_sort] 92 | 93 | # embeddings: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, EMBED_DIM] 94 | profile_embed = self.word_embeddings(profiles_sort).float() 95 | # compress: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, EMBED_DIM] 96 | profile_pack = pack_padded_sequence(profile_embed, lens_sort, batch_first=True) 97 | 98 | words_output, _ = self.words_gru(profile_pack) 99 | 100 | # output: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, words_lstm_hidden_dims * 2] 101 | words_output_, _ = torch.nn.utils.rnn.pad_packed_sequence(words_output, batch_first=True) 102 | 103 | # attention: [batch_size * MAX_PROFILELEN, MAX_TERMLEN, words_lstm_hidden_dims * 2] 104 | words_attention = F.softmax(self.words_attention_layer(words_output_), dim=-2) 105 | 106 | # [batch_size * MAX_PROFILELEN, MAX_TERMLEN, words_lstm_hidden_dims * 2] -> 107 | # [batch_size * MAX_PROFILELEN, words_lstm_hidden_dims * 2] 108 | terms_ = (words_attention * words_output_).sum(-2)\ 109 | .view([shape[0], MAX_PROFILELEN, USER_EMBED_DIM]) 110 | 111 | # (batch, 1, USER_EMBED_DIM) * (batch_size, MAX_PROFILELEN, hidden_size) -> 112 | # (batch, MAX_PROFILELEN, USER_EMBED_DIM) -> softmax(batch, MAX_PROFILELEN, 1) 113 | attention = torch.softmax(self.terms_attention_layer(terms_), dim=-2) 114 | 115 | # (batch, MAX_PROFILELEN, 1) * (batch_size, MAX_PROFILELEN, hidden_size) -> 116 | # (batch, hidden_size) 117 | profile_embeddings = torch.sum(attention * terms_, dim=1) 118 | 119 | return profile_embeddings 120 | 121 | # memory: [batch, MEMORY_SLOT, USER_EMBED_DIM] 122 | # b_embedding: [batch, USER_EMBED_DIM] 123 | # col_label: [] 124 | # return: [batch, USER_EMBED_DIM] 125 | def read(self, memory, b_embedding, col_label): 126 | preference = None 127 | for i in range(self.behavior_typenum): 128 | # [batch, USER_EMBED_DIM] mm [MEMORY_SLOT, USER_EMBED_DIM].T -> [batch, MEMORY_SLOT] 129 | attention = torch.softmax(torch.matmul(b_embedding, torch.transpose(self.KEY_MATs[i], dim0=1, dim1=0)), dim=-1) 130 | # [batch, 1, MEMORY_SLOT] bmm [batch, MEMORY_SLOT, USER_EMBED_DIM] -> [batch, 1, USER_EMBED_DIM] 131 | # -> [batch, USER_EMBED_DIM] 132 | embedding = torch.squeeze(torch.bmm(torch.unsqueeze(attention, 1), memory), 1) 133 | 134 | mask_label = torch.from_numpy(np.array([label == i for label in col_label])).float().to(DEVICE) 135 | if i==0: 136 | preference = embedding * torch.unsqueeze(mask_label, -1) 137 | else: 138 | preference += embedding * torch.unsqueeze(mask_label, -1) 139 | return preference 140 | 141 | # memory: [batch, MEMORY_SLOT, USER_EMBED_DIM] 142 | # b_embedding: [batch, USER_EMBED_DIM] 143 | # col_label: [] 144 | # col_mask: tensor([]) 145 | # return: [batch, MEMORY_SLOT, USER_EMBED_DIM] 146 | def update(self, memory, b_embedding, col_label, col_mask, isaexpect): 147 | 148 | for i in range(self.behavior_typenum): 149 | # [batch, USER_EMBED_DIM] mm [MEMORY_SLOT, USER_EMBED_DIM].T -> [batch, MEMORY_SLOT] 150 | attention = torch.softmax(torch.matmul(b_embedding, torch.transpose(self.KEY_MATs[i], dim0=1, dim1=0)), dim=-1) 151 | # [batch, MEMORY_SLOT, 1] * [batch, 1, USER_EMBED_DIM] -> [batch, MEMORY_SLOT, USER_EMBED_DIM 152 | update = torch.unsqueeze(attention, -1) * torch.unsqueeze(self.update_mlp(b_embedding), 1) 153 | # [batch, MEMORY_SLOT, USER_EMBED_DIM] * [batch, MEMORY_SLOT, 1] 154 | new_memory = memory * (1 - torch.unsqueeze(attention.to(DEVICE), -1)) + update 155 | # [batch] 156 | mask_label = torch.from_numpy(np.array([label==i for label in col_label])).float().to(DEVICE) 157 | mask = mask_label * col_mask 158 | memory = memory * (1-mask.view([len(col_label),1,1])) + new_memory * mask.view([len(col_label),1,1]) 159 | return memory 160 | 161 | # b_profiless: [batch, max_seq_len, sent, word] [1, 3, 20, 50], gpu tensor 162 | # b_seq_lens: [], list 163 | # b_seq_labels: [[], ...], 0,1,2,3 164 | # return: [batch, MEMORY_SLOT, USER_EMBED_DIM] 165 | def process_seq(self, b_seq_profiless, b_seq_lens, b_seq_tlabels, isaexpect=True): 166 | # memory 167 | batch_memory = torch.from_numpy(np.zeros((len(b_seq_lens), MEMORY_SLOT, USER_EMBED_DIM))).float().to(DEVICE) 168 | for i in range(max(b_seq_lens)): 169 | # [1,0,... ] 170 | col_mask = torch.from_numpy((np.array(b_seq_lens)-i>0)+0.).float().to(DEVICE) 171 | col_label = [bstls[i] if len(bstls)>i+1 else 0 for bstls in b_seq_tlabels] 172 | # [batch, USER_EMBED_DIM] 173 | batch_b_embedding = self.get_profile_embeddings(b_seq_profiless[:, i, :, :].contiguous(), not isaexpect) 174 | batch_memory = self.update(batch_memory, batch_b_embedding, col_label, col_mask, isaexpect) 175 | return batch_memory 176 | 177 | # expect_memory: [batch, MEMORY_SLOT, USER_EMBED_DIM] 178 | # job_profiles: [batch, sent, word] 179 | # job_memory: [batch, MEMORY_SLOT, USER_EMBED_DIM] 180 | # expect_profiles: [batch, sent, word] 181 | # label: [], list 182 | def predict(self, expect_memory, job_profiles, job_memory, expect_profiles, tlabel, ise2j): 183 | # read expect 184 | job_embedding = self.get_profile_embeddings(job_profiles, False) 185 | expect_preference = self.read(expect_memory, job_embedding, tlabel) 186 | # read job 187 | expect_embedding = self.get_profile_embeddings(expect_profiles, True) 188 | job_preference = self.read(job_memory, expect_embedding, tlabel) 189 | 190 | # interact 191 | e2j_interact = expect_preference * job_embedding 192 | j2e_interact = job_preference * expect_embedding 193 | 194 | feature = torch.cat([e2j_interact, job_embedding, j2e_interact, expect_embedding], dim=-1) 195 | 196 | if ise2j: 197 | e2j_scores = None 198 | for i in range(self.behavior_typenum): 199 | mask_label = torch.from_numpy(np.array([label == i for label in tlabel])).float().to(DEVICE) 200 | score = torch.squeeze(self.e2j_MTL_MLPs[i](feature), -1) 201 | if i == 0: 202 | e2j_scores = score * mask_label 203 | else: 204 | e2j_scores += score * mask_label 205 | return e2j_scores 206 | else: 207 | j2e_scores = None 208 | for i in range(self.behavior_typenum): 209 | mask_label = torch.from_numpy(np.array([label == i for label in tlabel])).float().to(DEVICE) 210 | score = torch.squeeze(self.j2e_MTL_MLPs[i](feature), -1) 211 | if i==0: 212 | j2e_scores = score * mask_label 213 | else: 214 | j2e_scores += score * mask_label 215 | return j2e_scores 216 | 217 | -------------------------------------------------------------------------------- /DoubleQNet.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn.functional as F 3 | from torch.autograd import Variable 4 | from torch.nn.utils.rnn import pack_padded_sequence 5 | import numpy as np 6 | from copy import deepcopy 7 | 8 | from src.Model.Profile2Vec import Profile2Vec 9 | 10 | DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 11 | 12 | WORD_EMBED_DIM = 32 13 | USER_EMBED_DIM = 16 14 | 15 | STATE_EMBED_DIM = 8 16 | 17 | class StateRep(torch.nn.Module): 18 | def __init__(self, bprofile2vec, a_model_name): 19 | super(StateRep, self).__init__() 20 | self.a_model_name, self.bprofile2vec = a_model_name, bprofile2vec 21 | 22 | self.det_GRU = torch.nn.GRU(input_size=USER_EMBED_DIM, hidden_size=int(USER_EMBED_DIM/2), 23 | num_layers=1, batch_first=True, bidirectional=False) 24 | self.add_GRU = torch.nn.GRU(input_size=USER_EMBED_DIM, hidden_size=int(USER_EMBED_DIM/2), 25 | num_layers=1, batch_first=True, bidirectional=False) 26 | self.add__GRU = torch.nn.GRU(input_size=USER_EMBED_DIM, hidden_size=int(USER_EMBED_DIM/2), 27 | num_layers=1, batch_first=True, bidirectional=False) 28 | self.ac_GRU = torch.nn.GRU(input_size=USER_EMBED_DIM, hidden_size=int(USER_EMBED_DIM/2), 29 | num_layers=1, batch_first=True, bidirectional=False) 30 | 31 | # a_bseq_profiless: [batch, max_seq_len, sent, word] 32 | # a_bseq_lens: [] 33 | def process_seq_full(self, gru, a_bseq_profiless, a_bseq_lens): 34 | # [batch, max_seq_len, sent, word] -> [batch_size, MAX_PROFILELEN, MAX_TERMLEN], [112, 6, 10, 30] 35 | shape = a_bseq_profiless.shape 36 | # print(1, shape): 全部是实的! 37 | a_bseq_profiless_ = a_bseq_profiless.contiguous().view([-1, shape[-2], shape[-1]]) 38 | # [112, 6, 4], word_idx 39 | a_bseq_embeds = self.bprofile2vec(a_bseq_profiless_).view([shape[0], shape[1], -1]) 40 | # sort for pack 41 | lens = a_bseq_lens 42 | lens_sort, ind_sort = lens.sort(dim=0, descending=True) 43 | a_bseq_embeds_ = a_bseq_embeds[ind_sort, :, :] 44 | # pack 45 | seq_pack = pack_padded_sequence(a_bseq_embeds_, lens_sort, batch_first=True) 46 | # gru 47 | seq_output, seq_state = gru(seq_pack) 48 | # rollback sort, [1, 112, 4], [batch, USER_EMBED_DIM] 49 | return seq_state.squeeze(0)[ind_sort, :] 50 | 51 | # a_bseq_profiless: [batch, max_seq_len, sent, word] 52 | # a_bseq_lens: [] 53 | def process_seq(self, gru, a_bseq_profiless, a_bseq_lens): 54 | all_bool = torch.gt(a_bseq_lens, 0) 55 | idx_true = [i for i, x in enumerate(all_bool) if x == True] 56 | if len(idx_true) == all_bool.shape[0]: 57 | return self.process_seq_full(gru, a_bseq_profiless, a_bseq_lens) 58 | elif len(idx_true) == 0: 59 | return torch.from_numpy(np.zeros((all_bool.shape[0], STATE_EMBED_DIM))).float().to(DEVICE) 60 | else: 61 | a_bseq_profiless_ = a_bseq_profiless[idx_true] 62 | a_bseq_lens_ = a_bseq_lens[idx_true] 63 | seq_state_ = self.process_seq_full(gru, a_bseq_profiless_, a_bseq_lens_) 64 | seq_state = torch.from_numpy(np.zeros((all_bool.shape[0], STATE_EMBED_DIM))).float().to(DEVICE) 65 | for ind, idx in enumerate(idx_true): 66 | seq_state[idx, :] = seq_state_[ind, :] 67 | return seq_state 68 | 69 | # input: [(40, 4, 5, 10, 30), (40, 4)] 70 | # output: [batch, USER_EMBED_DIM] 71 | def forward(self, a_bseq_profiless, a_bseq_lenss): 72 | det_state = self.process_seq(self.det_GRU, a_bseq_profiless[:,0,:,:,:], a_bseq_lenss[:,0]) 73 | add_state = self.process_seq(self.add_GRU, a_bseq_profiless[:,1,:,:,:], a_bseq_lenss[:,1]) 74 | add__state = self.process_seq(self.add__GRU, a_bseq_profiless[:,2,:,:,:], a_bseq_lenss[:,2]) 75 | ac_state = self.process_seq(self.ac_GRU, a_bseq_profiless[:,3,:,:,:], a_bseq_lenss[:,3]) 76 | return torch.cat([det_state, add_state, add__state, ac_state], dim=-1) 77 | 78 | def parameters(self): 79 | return list(self.det_GRU.parameters()) + list(self.add_GRU.parameters()) + \ 80 | list(self.add__GRU.parameters()) + list(self.ac_GRU.parameters()) 81 | 82 | class UserAgent(torch.nn.Module): 83 | def __init__(self, a_profile2vec, b_profile2vec, a_staterep, a_model_name): 84 | super(UserAgent, self).__init__() 85 | self.a_model_name = a_model_name 86 | self.a_profile2vec, self.b_profile2vec = a_profile2vec, b_profile2vec 87 | self.a_staterep = a_staterep 88 | 89 | # [(40, 10, 30), (40, 4, 5, 10, 30), (40, 4)] 90 | def forward(self, a_profiles, a_bseq_profiless, a_bseq_lenss): # one 91 | a_embeddings = self.a_profile2vec(a_profiles) 92 | a_state = self.a_staterep(a_bseq_profiless, a_bseq_lenss) 93 | # return torch.cat([a_embeddings, b_embeddings, a_state], dim=-1) 94 | return a_embeddings, a_state 95 | 96 | def fold_dim2(self, data): 97 | shape = [x for x in data.shape] 98 | shape_ = deepcopy(shape) 99 | shape_.pop(1) 100 | shape_[0] = -1 101 | return shape[1], data.view(shape_) 102 | def unfold_dim2(self, data, dim2_len): 103 | shape = [x for x in data.shape] 104 | shape_ = deepcopy(shape) 105 | shape_.insert(1, dim2_len) 106 | shape_[0]=-1 107 | return data.view(shape_) 108 | # [(40, 4-, 10, 30), (40, 4-, 4, 5, 10, 30), (40, 4-, 4)] 109 | def forwards(self, a_action_bprofiles, a_action_state_bprofiles, a_action_state_lens): # many 110 | len1, a_action_bprofiles_ = self.fold_dim2(a_action_bprofiles) 111 | a_embeddings = self.a_profile2vec(a_action_bprofiles_) 112 | a_embeddings_ = self.unfold_dim2(a_embeddings, len1) 113 | 114 | if a_action_state_bprofiles is not None: 115 | len2, a_action_state_bprofiles_ = self.fold_dim2(a_action_state_bprofiles) 116 | _, a_action_state_lens_ = self.fold_dim2(a_action_state_lens) 117 | a_states = self.a_staterep(a_action_state_bprofiles_, a_action_state_lens_) 118 | a_states_ = self.unfold_dim2(a_states, len2) 119 | else: 120 | a_states_ = None 121 | return a_embeddings_, a_states_ 122 | 123 | 124 | class QNet(torch.nn.Module): 125 | def __init__(self, person_profile2vec, job_profile2vec, person_staterep, job_staterep, model_name): 126 | super(QNet, self).__init__() 127 | self.model_name = model_name 128 | self.person_agent = UserAgent(person_profile2vec, job_profile2vec, person_staterep, 'person_agent') 129 | self.job_agent = UserAgent(job_profile2vec, person_profile2vec, job_staterep, 'job_agent') 130 | 131 | # 预测函数:person端和job端共享 132 | 133 | self.match_MLP = torch.nn.Sequential( 134 | torch.nn.Linear(6 * USER_EMBED_DIM, 3 * USER_EMBED_DIM), 135 | torch.nn.Tanh(), 136 | torch.nn.Linear(3 * USER_EMBED_DIM, USER_EMBED_DIM), 137 | torch.nn.Tanh(), 138 | torch.nn.Linear(USER_EMBED_DIM, 1), 139 | # torch.nn.ReLU() 140 | # torch.nn.Sigmoid() 141 | ) 142 | self.person_like_MLP = torch.nn.Sequential( 143 | torch.nn.Linear(4 * USER_EMBED_DIM, 2 * USER_EMBED_DIM), 144 | torch.nn.Tanh(), 145 | torch.nn.Linear(2 * USER_EMBED_DIM, 1), 146 | # torch.nn.ReLU() 147 | # torch.nn.Sigmoid() 148 | ) 149 | self.job_like_MLP = torch.nn.Sequential( 150 | torch.nn.Linear(4 * USER_EMBED_DIM, 2 * USER_EMBED_DIM), 151 | torch.nn.Tanh(), 152 | torch.nn.Linear(2 * USER_EMBED_DIM, 1), 153 | # torch.nn.ReLU() 154 | # torch.nn.Sigmoid() 155 | ) 156 | 157 | def parameters(self): 158 | return list(self.match_MLP.parameters()) + list(self.person_like_MLP.parameters()) + list(self.job_like_MLP.parameters()) 159 | 160 | def predict_like_score(self, a_profiles, b_profiles, a_now_seq_profiless, a_now_seq_lens, is_e2j): 161 | if is_e2j: 162 | e_vector, e_state = self.person_agent.forward(a_profiles, a_now_seq_profiless, a_now_seq_lens) 163 | j_vector = self.person_agent.b_profile2vec(b_profiles) 164 | return self.person_like_MLP(torch.cat([e_vector, e_state, j_vector], dim=-1)).squeeze(-1) 165 | else: 166 | j_vector, j_state = self.job_agent.forward(a_profiles, a_now_seq_profiless, a_now_seq_lens) 167 | e_vector = self.job_agent.b_profile2vec(b_profiles) 168 | return self.job_like_MLP(torch.cat([j_vector, j_state, e_vector], dim=-1)).squeeze(-1) 169 | 170 | def predict_like_max_score(self, a_profiles, a_action_bprofiles, 171 | a_next_state_seq_profiless, a_next_seq_lens, is_e2j): 172 | if is_e2j:#e-to-js 173 | e_vector, e_state = self.person_agent.forward(a_profiles, a_next_state_seq_profiless, a_next_seq_lens) 174 | j_vectors, _ = self.job_agent.forwards(a_action_bprofiles, None, None) 175 | scores = self.person_like_MLP(torch.cat([self._expand_like_(e_vector, j_vectors), self._expand_like_(e_state, j_vectors), 176 | j_vectors], dim=-1)).squeeze(-1) 177 | else:# es-to-j 178 | j_vector, j_state = self.job_agent.forward(a_profiles, a_next_state_seq_profiless, a_next_seq_lens) 179 | e_vectors, _ = self.person_agent.forwards(a_action_bprofiles, None, None) 180 | scores = self.person_like_MLP(torch.cat([self._expand_like_(j_vector, e_vectors), self._expand_like_(j_state, e_vectors), 181 | e_vectors], dim=-1)).squeeze(-1) 182 | indices = torch.max(scores, dim=1)[1] 183 | next_max_b_profiles = torch.cat([torch.unsqueeze(a_action_bprofiles[rind, cind, :, :], 0) for rind, cind in enumerate(indices)], 0) 184 | return next_max_b_profiles 185 | 186 | def predict_match_score(self, e_profiles, j_profiles, 187 | e_now_seq_profiless, e_now_seq_lens, j_now_seq_profiless, j_now_seq_lens): # a2b===e2j 188 | e_vector, e_state = self.person_agent.forward(e_profiles, e_now_seq_profiless, e_now_seq_lens) 189 | j_vector, j_state = self.job_agent.forward(j_profiles, j_now_seq_profiless, j_now_seq_lens) 190 | return self.match_MLP(torch.cat([e_vector, e_state, j_vector, j_state], dim=-1)).squeeze(-1) 191 | 192 | # 1:n 193 | # [(40, 4-, 10, 30), (40, 4-, 4, 5, 10, 30), (40, 4-, 4)] 194 | def predict_match_max_score(self, a_profiles, a_action_bprofiles, 195 | a_next_state_seq_profiless, a_next_seq_lens, 196 | a_action_state_bprofiles, a_action_state_lens, is_e2j): 197 | if is_e2j:# e-to-js 198 | e_vector, e_state = self.person_agent.forward(a_profiles, a_next_state_seq_profiless, a_next_seq_lens) 199 | j_vectors, j_states = self.job_agent.forwards(a_action_bprofiles, a_action_state_bprofiles, a_action_state_lens) 200 | scores = self.match_MLP(torch.cat([self._expand_like_(e_vector, j_vectors), self._expand_like_(e_state, j_states), 201 | j_vectors, j_states], dim=-1)).squeeze(-1) 202 | 203 | else:# es-to-j 204 | j_vector, j_state = self.job_agent.forward(a_profiles, a_next_state_seq_profiless, a_next_seq_lens) 205 | e_vectors, e_states = self.person_agent.forwards(a_action_bprofiles, a_action_state_bprofiles, a_action_state_lens) 206 | scores = self.match_MLP(torch.cat([e_vectors, e_states, self._expand_like_(j_vector, e_vectors), 207 | self._expand_like_(j_state, e_states)], dim=-1)).squeeze(-1) 208 | indices = torch.max(scores, dim=1)[1] 209 | next_max_b_profiles = torch.cat([torch.unsqueeze(a_action_bprofiles[rind,cind,:,:], 0) for rind, cind in enumerate(indices)], 0) 210 | next_max_b_state_seq_profiles = torch.cat([torch.unsqueeze(a_action_state_bprofiles[rind,cind,:,:,:,:], 0) for rind, cind in enumerate(indices)], 0) 211 | next_max_b_state_lens = torch.cat([torch.unsqueeze(a_action_state_lens[rind,cind,:], 0) for rind, cind in enumerate(indices)], 0) 212 | 213 | return next_max_b_profiles, next_max_b_state_seq_profiles, next_max_b_state_lens 214 | 215 | def _expand_like_(self, a, b): 216 | a = torch.unsqueeze(a, 1) 217 | shape = a.shape 218 | return a.expand(shape[0], b.shape[1], shape[2]) 219 | 220 | class DoubleQNetMain(torch.nn.Module): 221 | def __init__(self, word_embeddings): 222 | super(DoubleQNetMain, self).__init__() 223 | self.word_embeddings = torch.nn.Embedding.from_pretrained(word_embeddings, padding_idx=0) 224 | self.word_embeddings.weight.requires_grad = False 225 | 226 | self.person_profile2vec = Profile2Vec(self.word_embeddings, 'person_profile2vec') 227 | self.job_profile2vec = Profile2Vec(self.word_embeddings, 'job_profile2vec') 228 | 229 | self.person_staterep = StateRep(self.job_profile2vec, 'person_staterep') 230 | self.job_staterep = StateRep(self.person_profile2vec, 'job_staterep') 231 | 232 | # person_profile2vec, job_profile2vec, person_staterep, job_staterep, model_name 233 | self.main_policy = QNet(self.person_profile2vec, self.job_profile2vec, self.person_staterep, self.job_staterep, 'main_policy') 234 | self.target_policy = QNet(self.person_profile2vec, self.job_profile2vec, self.person_staterep, self.job_staterep, 'target_policy') 235 | 236 | def like_forward(self, a_profiles, a_now_seq_profiless, a_now_seq_lens, b_profiles, 237 | a_next_state_seq_profiless, a_next_seq_lens, # a=e/j 238 | a_action_bprofiles, is_e2j): 239 | # current 240 | current_reward = self.main_policy.predict_like_score(a_profiles,b_profiles, a_now_seq_profiless, a_now_seq_lens, is_e2j) 241 | # next step 242 | b_profiles_maxscore = self.target_policy.predict_like_max_score(a_profiles, a_action_bprofiles, 243 | a_next_state_seq_profiless, a_next_seq_lens, is_e2j) 244 | next_reward = self.main_policy.predict_like_score(a_profiles,b_profiles_maxscore, a_now_seq_profiless, a_now_seq_lens, is_e2j) 245 | return current_reward, next_reward 246 | 247 | def match_forward(self, e_profiles, e_now_seq_profiless, e_now_seq_lens, 248 | j_profiles, j_now_seq_profiless, j_now_seq_lens, 249 | a_next_state_seq_profiless, a_next_seq_lens, # a=e/j 250 | a_action_bprofiles, a_action_state_bprofiles, a_action_state_lens, 251 | is_e2j): 252 | 253 | # current: 固定e-j match 254 | current_reward = self.main_policy.predict_match_score(e_profiles, j_profiles, 255 | e_now_seq_profiless, e_now_seq_lens, j_now_seq_profiless, j_now_seq_lens) 256 | # next step 257 | if is_e2j: # a=e, b=j 258 | next_max_j_profiles, next_max_j_state_seq_profiles, next_max_j_state_lens = \ 259 | self.target_policy.predict_match_max_score(e_profiles, a_action_bprofiles, 260 | a_next_state_seq_profiless, a_next_seq_lens, 261 | a_action_state_bprofiles, a_action_state_lens, is_e2j) 262 | next_reward = self.predict_match(e_profiles, next_max_j_profiles, 263 | a_next_state_seq_profiless, a_next_seq_lens, 264 | next_max_j_state_seq_profiles, next_max_j_state_lens) 265 | else: # a=j, b=e 266 | next_max_e_profiles, next_max_e_state_seq_profiles, next_max_e_state_lens = \ 267 | self.target_policy.predict_match_max_score(j_profiles, a_action_bprofiles, 268 | a_next_state_seq_profiless, a_next_seq_lens, 269 | a_action_state_bprofiles, a_action_state_lens, is_e2j) 270 | next_reward = self.predict_match(next_max_e_profiles, j_profiles, 271 | next_max_e_state_seq_profiles, next_max_e_state_lens, 272 | a_next_state_seq_profiless, a_next_seq_lens) 273 | return current_reward, next_reward 274 | 275 | def predict_like(self, a_profiles, b_profiles, a_bseq_profiless, a_bseq_lenss, is_e2j): 276 | # current_reward 277 | return self.main_policy.predict_like_score(a_profiles, b_profiles, a_bseq_profiless, a_bseq_lenss, is_e2j) 278 | 279 | def predict_match(self, e_profiles, j_profiles, e_now_seq_profiless, e_now_seq_lens, j_now_seq_profiless, j_now_seq_lens): 280 | # current_reward 281 | return self.main_policy.predict_match_score(e_profiles, j_profiles, e_now_seq_profiless, e_now_seq_lens, j_now_seq_profiless, j_now_seq_lens) 282 | 283 | def parameters(self): 284 | return list(self.person_profile2vec.parameters()) + list(self.job_profile2vec.parameters()) + \ 285 | list(self.person_staterep.parameters()) + list(self.job_staterep.parameters()) + \ 286 | list(self.main_policy.parameters()) 287 | 288 | def update_target_QNet(self): 289 | self.target_policy.load_state_dict(self.main_policy.state_dict()) --------------------------------------------------------------------------------