基于双视角图表示算法的双向人职匹配偏好建模推荐系统构建

文章目录

  • 基于双视角图表示算法的双向人职匹配偏好建模推荐系统构建
    • 1. 传统推荐系统模型
    • 2. 协同过滤算法
    • 3. 基于双视角图表示学习算法的模型构建
      • 3.1 数据输入
      • 3.2 双视角交互图的构建
      • 3.3 混合偏好传播策略
      • 3.4 对于双向意图预测的评价
      • 3.5 自监督增强的双视角排序优化
    • 4. 代码工程实践
    • 参考论文

1. 传统推荐系统模型

基于内容的推荐算法(Content-Based Recommendations,CB)是一种经典推荐算法,一般只依赖于用户及物品自身的内容属性和行为属性,而不涉及其他用户的行为,在冷启动的情况下(即新用户或者新物品)依然可以做出推荐。在本课题的题设条件下,我们难以找到显式反馈,在这种情况下,可以直接使用求职者的特征信息以及岗位信息的特征信息进行相似度计算,按照相似度度量降序排列后,对评分高的几项进行选取推荐。
基于内容的推荐算法使用最广泛的是K近邻算法(K-Nearest Neighbor,KNN),算法假设在同一个特征空间中,如果与目标样本最相似的K个样本(近邻)大多属于同一种类别,则目标样本属于这个类别的可能性也会很高。设 S m , n S_{m,n} Sm,n为求职者m与招聘信息n的相似度,一般相似度的度量有皮尔逊相似度、余弦相似度、杰卡德相似度和欧氏距离等。

基于内容的推荐算法是一个简单有效的推荐算法模型,原理质朴且易于复现,但是其有一定的局限性,该算法模型仅仅考虑了文本内容的相似度,对于信息的挖掘局限于文本信息本身,所以基于内容的推荐算法的思想并不能很好的解决本课题的重点与难点。

2. 协同过滤算法

在传统的推荐算法领域中,为了充分挖掘相似人群的潜在物料的偏好信息,绝大部分情况下都采用基于用户的协同过滤算法(user -based collaborative filtering),协同是指通过用户的持续协同作用,使得物料对用户整个群体的推荐作用越来越明显,所谓过滤,指的是从可行的决策方案中将用户喜欢的方案挑出,在实际问题表现为从推荐方案中将用户匹配的物料从物料的集合中找出。

基于用户的协同过滤算法在对用户建模时通常使用一个用户对物料的评分矩阵。

Item1 Item2 Item3 Item4
User1 4 0 2 2
User2 0 5 5 6
User3 6 2 1 2
User4 1 7 0 0

User-CF中,核心的考量是物料群体以及用户群体之间的相似度问题,一般取用cosine余弦相似度对行为向量衡量,设 v 1 v_1 v1为用户1在n维项目空间中的评分向量,设 v 2 v_2 v2为用户2在n维向量空间中的评分,余弦相似度如下式所示:
s i m ( v 1 , v 2 ) = v 1 ⋅ v 2 ∣ ∣ v 1 ∣ ∣ × ∣ ∣ v 2 ∣ ∣ sim\left(v_1,v_2\right)=\frac{v_1\cdot v_2}{\left|\left|v_1\right|\right|\times\left|\left|v_2\right|\right|} sim(v1,v2)=v1×v2v1v2
产生推荐项目的过程也是推荐系统中的排序系统的任务,上述任务是召回任务,设用户i对物料d的评分为 R i , d R_{i,d} Ri,d,设用户j对所有物料的评分均值为 R j ˉ \bar{R_j} Rjˉ,设用户i对所有物料的评分均值为 R i ˉ \bar{R_i} RiˉS i S_i Si为用户i的邻用户集,那么加权平均后的评分值如下式所示,对于推荐项目的生成过程应用下式对未进行评分的用户进行评分预测,评分降序排列后取评分高的N个物料推荐给目标用户。
P i , d = R i ˉ + ∑ j ∈ S i s i m ( i , j ) ∗ ( R j , d − R j ˉ ) ∑ j ∈ S i ( ∣ s i m ( i , j ) ∣ ) P_{i,d}=\bar{R_i}+\frac{\sum_{j\in S_i}{sim\left(i,j\right)\ast\left(R_{j,d}-\bar{R_j}\right)}}{\sum_{j\in S_i}\left(\left|sim\left(i,j\right)\right|\right)} Pi,d=Riˉ+jSi(sim(i,j))jSisim(i,j)(Rj,dRjˉ)
在本题中可以建模为用户与招聘职位之间的协同算法,但是由于我们的爬取数据缺少用户与招聘信息之间的反馈交互信息,所以需要我们自行在用户关于岗位的描述信息以及岗位对于求职者的期望信息中挖掘关联度,进而间接进行评分以适应基于用户的协同过滤算法。

但是协同过滤算法具有一些局限性,其聚焦于相似群体的发现,不能关注到个性化的泛化结果,同时无论是基于用户的协同过滤算法还是基于物料(在本课题中反映为招聘信息)的协同过滤算法,都有其侧重的群体,这样的方式使得求职者与招聘岗位之间的推荐算法呈现了匹配分布的不对等性,即对于招聘岗位的抽样服从于求职者的随机变量取值为评分机制结果条件概率模型下的分布,而非具有两种约束的联合分布。对于这个问题在本课题中的考量只能靠再次使用基于物品的协同过滤算法,也即同时用基于用户和基于物品的协同过滤算法两个算法模型组合推荐评分,基于招聘信息的协同过滤算法偏好分析如下:

在评分的分布上寻找最近点以确定协同过滤算法的推荐项,保证对于求职者和岗位信息都有相似群体偏好的发现。
但是这种方法没有从根本上解决同时考虑求职者和招聘方的双向推荐的问题,只是对两个协同过滤算法的推荐方案进行了折中抽取,设k为协同过滤算法推荐项数,也即推荐匹配项生成后取排序靠前的k个项作为算法推荐方案,不能确定一个统一的k值保证重合匹配项的出现。

3. 基于双视角图表示学习算法的模型构建

推荐系统一般分为召回系统和排序系统,向量化的神经网络参与的架构一般有两种形式,第一种是通过处理分别将求职者和招聘岗位的信息输入神经网络,输出求职者和招聘岗位的表征向量,经过混合运算,得到匹配分数,此种方式的架构如下图:

另外一种是将求职者信息和岗位招聘信息整体输入神经网络处理,直接预测出匹配得分结果。

双视角图表示学习算法属于第一种算法模型的范畴,由于算法需要一定的交互信息,所以在数据输入算法前需要对信息进行假设处理,我们在问题三中已经计算了岗位匹配度,岗位匹配度可以作为求职者与招聘信息发生互动的参考指标,即岗位匹配度能够间接反映用户与招聘信息发生互动的个人意愿,而用户满意度更多的是体现求职者对于互动意愿的期望。

双视角图表示学习算法对于求职者和岗位招聘信息的匹配使用双视角图神经网络(Duel-perspective Graph Neural Network),设 C = c 1 , c 2 , … , c n C=c_1,c_2,\ldots,c_n C=c1,c2,,cn为求职者的集合, J = j 1 , j 2 , … , j m J=j_1,j_2,\ldots,j_m J=j1,j2,,jm为招聘信息的集合, n n n表示求职者的数量, m m m表示招聘信息的数量,设匹配集 M = ( c i , j k ) ∣ c k ∈ C , j k ∈ J M=\left(c_i,j_k\right)|c_k\in C,j_k\in J M=(ci,jk)ckC,jkJ表示求职者与招聘信息条目的双向匹配成功的集合,在算法中求职者和招聘信息都与其相关职位或者技能等相关描述信息关联,同时其与一组定向交互记录关联(即浏览、投递、满意三个指标),设交互集 A c i = c i → j ′ ∣ c i ∈ C , j ′ ∈ J A_{c_i}={c_i\rightarrow j^\prime|c_i\in C,j^\prime\in J} Aci=cijciC,jJ与分别为表示求职者i与招聘信息以及招聘信息k与求职者的直接交互行为,在交互行为的指标中,浏览可以转换交互行为建模语言为 c i → j k c_i\rightarrow j_k cijk,投递可以转换交互行为建模语言为 j k → c i j_k\rightarrow c_i jkci,而指标满意可以转换交互行为建模语言为 ( j k → c i ) ∪ ( c i → j k ) \left(j_k\rightarrow c_i\right)\cup\left(c_i\rightarrow j_k\right) (jkci)(cijk),设 为求职者 c i c_i ci对招聘岗位 j k j_k jk的选择偏好建模, s k → i s_{k\rightarrow i} ski为招聘岗位 j k j_k jk对求职者 c i c_i ci的选择偏好建模,算法的整体处理流程如图

3.1 数据输入

根据算法流程以及结构,数据集要以求职者和招聘方相独立的形式逐条输入BERT进行编码工作,BERT的本质是一种文本表征(context representation),BERT的直观作用是将文本表达为矩阵或者向量,word2vec等工具也能够达到同样的效果,但是word2vec是静态的,而BERT是动态的,因为BERT是将输入考虑语序后经过transformer输出的,能够挖掘文本信息数据中的更多语义信息,对于每条的文本描述信息进行原有序列信息的保留的同时对文本信息进行信息首部 [ C L S ] t o k e n \left[CLS\right]token [CLS]token的添加,BERT结构的embedding过程如下图所示,每条求职者或者招聘方的信息都会经过BERT处理生成文本信息向量 (我们设BERT的输出向量维度为 d T d_T dT),为充分提取文本特征,达到反向传播机器学习的目的。对文本信息向量加入线性层得到输出维度为 d T d_T dT向量,同时对偏好ID(首次运行随机生成)使用embedding_lookup的方式生成编码向量 (我们设lookup方式生成的embedding维度为 ),将偏好ID的编码向量以及BERT处理经线性映射的向量进行连接操作从而得到神经网络处理的向量,由于输入BERT之前的数据文本是经过预处理过的,形式相对来说都是短文本的(低于512字),所以对于BERT的配置使用默认配置,embedding的大小使用768维,隐单元的个数配置为128个,可以满足本课题的模型效果要求。

3.2 双视角交互图的构建

双视角图表示学习算法的核心运作模块是图神经网络的模块,而图的数据结构必然有节点和边以及权重值的表达[31],对于节点,每个求职者或者招聘方都由两个节点组成,对于求职者c,其偏好有主动选择偏好(active selection preference)和被动选择偏好(active selection preference),记ca为求职者c的主动选择偏好、cp为求职者c的被动选择偏好,同样地,对于招聘方j也按照选择偏好分为两个节点ja和jp,两种节点代表了求职者(招聘方)在主动视角(对意向岗位或者意向求职者的主动选择)和被动视角(来自意向岗位或者意向求职者的被动选择),因此,在主动视角和被动视角的意愿同样强烈的情况下,能够生成最终的匹配。
数据输入章节所述的对于文本和偏好ID的向量化,是节点初始化工作的必要准备输入,对于节点n的初始化,需要将偏好ID编码向量与文本描述编码向量混合计算,节点n的初始化表征如下式:
z ( 0 ) n = [ e n ; W ⋅ t n ] ∈ R d {z^{\left(0\right)}}_n=\left[e_n;W\cdot t_n\right]\in\mathbb{R}^d z(0)n=[en;Wtn]Rd
其中 W ∈ R d T × d O W\in\mathbb{R}^{d_T\times d_O} WRdT×dO为可传播学习的变换矩阵,运算符 [ ; ] [;] [;]表示连接操作(concatenation), d = d E + d T d=d_{E}+d_{T} d=dE+dT 为节点的初始维度。
节点之间的边,是由不同节点的交互作用和同一求职者(招聘信息)节点的关联产生的,其特征是对称的,因此边的生成由以下三种情况组成:
(1)求职者投递简历但是未被录用,这反映了求职者的主动偏好,在 c a c^a ca和j p ^p p之间添加边。
(2)招聘岗位主动与求职者联系,但是求职者拒绝,这反映了招聘方的主动偏好,这种情况下在 j a j^a jac p c^p cp之间加边。
(3)当求职者与招聘方达成协议,这反映了双向的主动偏好意向,增加 c a c^a caj p j^p jpj a j^a jac p c^p cp两条边。

3.3 混合偏好传播策略

基于双视角交互图的构建,我们采用图卷积网络对图结构数据建模进行处理,由于双视角交互图的求职者和招聘信息节点都有主动选择偏好视角节点和被动选择偏好视角节点两个节点,所以与通常的图卷积网络的构建方式有差异性[32]。而这种差异性来自于边集,与节点类型无关,所以在建模时可以忽略差异性进行统一建模,混合偏好传播的偏好只考虑匹配集M以及交互集A,对于节点n,由于统一建模的假设成立,所以忽略节点n的类型是求职者节点还是招聘信息节点、是主动选择偏好还是被动选择偏好,只考虑匹配集M_n以及交互集A_n,混合偏好传播的更新策略定义如下式:
z n ( l ) = ∑ u ∈ M n 1 ∣ N n ∣ ∣ N n ∣ z u ( l − 1 ) + ω ∑ v ∈ A n 1 ∣ N n ∣ ∣ N n ∣ z v ( l − 1 ) z_n^{\left(l\right)}=\sum_{u\in M_n}\frac{1}{\sqrt{\left|N_n\right|\left|N_n\right|}}z_u^{\left(l-1\right)}+\omega\sum_{v\in A_n}{\frac{1}{\sqrt{\left|N_n\right|\left|N_n\right|}}z_v^{\left(l-1\right)}} zn(l)=uMnNnNn

1zu(l1)+ωvAnNnNn

1
zv(l1)

其中, 为节点n在匹配集 M n M_n Mn的邻节点, 为节点 n n n在交互集 A n A_n An中的邻节点,超参数 ω \omega ω为传达不同级别偏好的作用参数,用以平衡来自匹配集和交互集的传播更新影响力。
在图卷积网络中,第L+1层的节点n的最终更新策略结果如下式:
z n = ∑ l = 0 L 1 L + 1 z n l z_n=\sum_{l=0}^{L}{\frac{1}{L+1}z_n^l} zn=l=0LL+11znl

3.4 对于双向意图预测的评价

基于交互图的构建和混合偏好传播策略的确定,我们提出一种对于双向意图预测的评价得分指标,这个指标也是问题三中的求职者满意度的指标。
对于求职者 c i c_i ci选择招聘信息 j k j_k jk的偏好意向得分 定义为内积形式 ,对于招聘信息 j k j_k jk选择求职者 c i c_i ci的偏好意向得分 同样定义为内积形式 ,出于对求职者和招聘方的意图同等重要的看待,双向意图预测的评价得分的定义如下式:
y ^ = 1 2 r i → k + 1 2 s k → i \hat{y}=\frac{1}{2}r_{i\rightarrow k}+\frac{1}{2}s_{k\rightarrow i} y^=21rik+21ski

3.5 自监督增强的双视角排序优化

为了优化求职者与招聘信息的匹配预测结果过拟合和发散问题,采用基于排序优化的方法优化整个模型的过程,通过定义四元损失函数的方式优化正样本和负样本距离的偏阶;通过采用双视角对比学习的方式,为求职者和招聘信息的两种表示设计了双视角对比学习的优化函数,通过最大化一致性和最小化差异性的方式建立了 I n f o N C E InfoNCE InfoNCE损失的定义,结合求职者和招聘信息的 I n f o N C E L o s s InfoNCE Loss InfoNCELoss,最终实现了自监督的效果。
在给定一个匹配记录 < c i , j k > <c_i,j_k> <ci,jk>和不匹配记录 < c i , j k ′ > <c_i,j_{k^\prime}> <ci,jk>< c i ′ , j k > <c_{i^\prime},j_k> <ci,jk>的情况下,设匹配分数的生成算法为f,匹配分数应该满足下式:
f ( c i , j k ) > f ( c i , j k ′ ) f ( c i , j k ) > f ( c i ′ , j k ) f\left(c_i,j_k\right)>f\left(c_i,j_{k^\prime}\right)\\ f\left(c_i,j_k\right)>f\left(c_{i^\prime},j_k\right) f(ci,jk)>f(ci,jk)f(ci,jk)>f(ci,jk)
基于上述思想,使用BRP损失扩展,对四元组 < i , k , i ′ , k ′ > <i,k,i^\prime,k^\prime> <i,k,i,k>建模损失如下式:
L m a i n = − 1 ∣ D ∣ ∑ ( i , k , i ′ , k ′ ) ∈ D l o g ( σ ( y ^ i , k − 1 2 y ^ i , k ′ − 1 2 y ^ i ′ , k ) ) L_{main}=-\frac{1}{\left|D\right|}\sum_{\left(i,k,i^\prime,k^\prime\right)\in D} l o g\left(\sigma\left({\hat{y}}_{i,k}-\frac{1}{2}{\hat{y}}_{i,k^\prime}-\frac{1}{2}{\hat{y}}_{i^\prime,k}\right)\right) Lmain=D1(i,k,i,k)Dlog(σ(y^i,k21y^i,k21y^i,k))
其中 为 函数,设 M − M^- M为不匹配集, D D D为定义在匹配集 M M M和交互集 A A A上的四元组 , s i g m o i d sigmoid sigmoid函数在处理来自匹配集 M M M中的分数 与来自不匹配集 的分数 y ^ i , k ′ {\hat{y}}_{i,k^\prime} y^i,k、 的均值的偏差,通过反向传播使得损失减小从而达到优化生成算法 f f f不等式的偏阶。
在考虑求职者与招聘信息的两种表示时,应当在性质上相似的自然约束条件下,通过上述思路设计了一个双视角对比学习优化函数,考虑将 与 、 与 作为正对(positive pairs),考虑将 与 、 与 作为负对(negative pairs),正对促进了不同视角的一致性,而负对扩大了不同视角的差异性,应用 I n f o N C E InfoNCE InfoNCE损失来最大化一致性、最小化差异,求职者的 I n f o N C E L o s s InfoNCE\quad Loss InfoNCELoss定义如下式:
L s s l C = − ∑ c i ∈ C l o g ( e x p ( ( c i a ⋅ c i p ) / τ ) ∑ c i ∈ C ( e x p ( ( c i a ⋅ c i ′ p ) / τ ) + e x p ( ( c i ′ a ⋅ c i p ) / τ ) ) ) L_{ssl}^C=-\sum_{c_i\in C} l o g\left(\frac{exp\left({(c}_i^a\cdot c_i^p)/\tau\right)}{\sum_{c_i\in C}\left(exp\left(\left(c_i^a\cdot c_{i\prime}^p\right)/\tau\right)+exp\left(\left(c_{i\prime}^a\cdot c_i^p\right)/\tau\right)\right)}\right) LsslC=ciClog(ciC(exp((ciacip)/τ)+exp((ciacip)/τ))exp((ciacip)/τ))
其中, τ \tau τ为温度超参数,式是求职者的 I n f o N C E InfoNCE InfoNCE损失定义,那么同样可以得到招聘信息的 损失,将两者结合,就得到了最终的自监督任务的 I n f o N C E InfoNCE InfoNCE损失,如下式:
L s s l = L s s l C + L s s l J L_{ssl}=L_{ssl}^C+L_{ssl}^J Lssl=LsslC+LsslJ
在图神经网络的训练阶段,设\lambda为个体层面的对比学习任务强度超参数,同时优化上述所有损失,训练阶段的损失如下式所示:
L = L m a i n + λ L s s l L=L_{main}+\lambda L_{ssl} L=Lmain+λLssl

4. 代码工程实践

作者开源项目地址https://github.com/RUCAIBox/DPGNN
DPGNN参数配置

# Model
embedding_size: 128
n_layers: 2
reg_weight: 1e-05
mutual_weight: 0.05
temperature: 0.2ADD_BERT: True
pretrained_bert_model: ~
BERT_embedding_size: 768
BERT_output_size: 32

数据预处理转换

# .udocimport re
def split_sent(text):text = re.split('(?:[0-9][.;。:.•)\)])', text)  # 按照数字分割包括  1.  1;  1。  1:  1) 等ans = []for t in text:for tt in re.split('(?:[\ ][0-9][、,])', t):  #for ttt in re.split('(?:^1[、,])', tt):   # 1、for tttt in re.split('(?:\([0-9]\))', ttt):   # (1)ans += re.split('(?:[。;…●])', tttt)return [_.strip() for _ in ans if len(_.strip()) > 0]def cut_sent(text):wds = [_.strip() for _ in text.split(' ') if len(_.strip()) > 0]  # 分词,返回分词后的 listreturn wdsdef clean_text(text):illegal_set = ',.;?!~[]\'"@#$%^&*()-_=+{}`~·!¥()—「」【】|、“”《<》>?,。…:'   # 定义非法字符for c in illegal_set:text = text.replace(c, ' ')     # 非法字符 替换为 空格text = ' '.join([_ for _ in text.split(' ') if len(_) > 0])return text    # 空格间隔def raw2token_seq(s):sents = split_sent(s)sent_wds = []sent_lens = []for sent in sents:if len(sent) < 2:continuesent = clean_text(sent)if len(sent) < 1:continuewds = cut_sent(sent)sent_wds.extend(wds)sent_lens.append(len(wds))if len(sent_wds) < 1:return None, None, Noneassert sum(sent_lens) == len(sent_wds)# 返回3个值,第一个是用空格连接的词,第二个是各句子长度,第三个是总词数return ' '.join(sent_wds), ' '.join(map(str, sent_lens)), len(sent_wds)u_doc_index = [3, 4, 5, 7, 8]
f = open('users.tsv', 'r', encoding='utf-8')
f.readline()
f_his = open('user_history.tsv', 'r', encoding='utf-8')
f_his.readline()f_udoc = open('jobrec.udoc', 'w', encoding='utf-8')
head = ['user_id:token', 'user_doc:token_seq']
f_udoc.write('\t'.join(head) + '\n')for line in f_his:lines = line[:-1].split('\t')sents = lines[4]try:sent_wds, sent_lens, _ = raw2token_seq(sents)sent_wds = sent_wds.split(' ')sent_lens = sent_lens.split(' ')a = -(int)(sent_lens[-1])for j in range(len(sent_lens)):a += (int)(sent_lens[j - 1])s_word_line = ' '.join(sent_wds[a:a + (int)(sent_lens[j])])if s_word_line == '':continues_new_line = lines[0] + '\t' + s_word_line + '\n'f_udoc.write(s_new_line)except:if sents == '':continuef_udoc.write(lines[0] + '\t' + sents + '\n')for line in f:lines = line[:-1].split('\t')for i in u_doc_index:if lines[i] and lines[i] != '-':sents = lines[i]try:sent_wds, sent_lens, _ = raw2token_seq(sents)sent_wds = sent_wds.split(' ')sent_lens = sent_lens.split(' ')a = -(int)(sent_lens[-1])for j in range(len(sent_lens)):a += (int)(sent_lens[j - 1])s_word_line = ' '.join(sent_wds[a:a + (int)(sent_lens[j])])s_new_line = lines[0] + '\t' + s_word_line + '\n'f_udoc.write(s_new_line)except:f_udoc.write(lines[0] + '\t' + sents + '\n')f.close()
f_his.close()
f_udoc.close()# .idoc
import rei_doc_index = [2, 3, 4]
f = open('jobs.tsv', 'r', encoding='utf-8')
f.readline()f_idoc = open('jobrec.idoc', 'w', encoding='utf-8')
head = ['item_id:token', 'item_doc:token_seq']
f_idoc.write('\t'.join(head) + '\n')for line in f:lines = line[:-1].split('\t')for i in i_doc_index:if lines[i] and lines[i] != '-':sents = lines[i]sents = re.sub('\\\\r', ' ', sents)sents = re.sub('<[^>]+>', ' ', sents)sents = re.sub('&nbsp;', ' ', sents)sents = sents.split('   ')for sent in sents:sent = sent.strip()if len(sent) < 20:continues_new_line = lines[0] + '\t' + sent + '\n'f_idoc.write(s_new_line)f.close()
f_idoc.close()from collections import defaultdict
f = open('jobrec.udoc', encoding='utf-8')
f.readline()
u_doc_num = defaultdict(int)
for l in f:lines = l.split('\t')u_doc_num[lines[0]] += 1
u_doc = sorted(u_doc_num.items(), key=lambda x: x[1])
count = 1
for i in u_doc:if i[1] < 10:count += 1
print(count)from collections import defaultdict
f = open('jobrec.idoc', encoding='utf-8')
f.readline()
i_doc_num = defaultdict(int)
for l in f:lines = l.split('\t')i_doc_num[lines[0]] += 1
i_doc = sorted(i_doc_num.items(), key=lambda x: x[1])
count = 1
for i in i_doc:if i[1] < 10:count += 1
print(count)USER_THRESHOLD = 10
JOB_THRESHOLD = 20# .inter
import time
def get_timestamp(timeStr):timeArray = time.strptime(timeStr, '%Y-%m-%d %H:%M:%S')timeStamp = int(time.mktime(timeArray))return timeStampf_inter = open('apps.tsv', 'r', encoding='utf-8')
f_inter_target = open('jobrec.inter', 'w', encoding='utf-8')
f_inter_target.write('user_id:token\titem_id:token\ttimestamp:float\n')
f_inter.readline()
user = set()
job = set()
l = f_inter.readline()
while l:lines = l.split('\t')uid = lines[0]iid = lines[-1][:-1]new_line = lines[0] + '\t' + lines[-1][:-1] + '\t' + str(get_timestamp(lines[-2][:19])) + '\n'if u_doc_num[lines[0]] > USER_THRESHOLD and i_doc_num[lines[0]] > JOB_THRESHOLD:user.add(uid)job.add(iid)f_inter_target.write(new_line)l = f_inter.readline()f_inter.close()
f_inter_target.close()# .user
u_index = [0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
f_user = open('users.tsv', 'r', encoding='utf-8')
f_user_target = open('jobrec.user', 'w', encoding='utf-8')
head = f_user.readline()[:-1].split('\t')
head = [head[i] for i in u_index]
head = [i + ':token' for i in head]
head[0] = 'user_id:token'
f_user_target.write('\t'.join(head) + '\n')
l = f_user.readline()while l:lines = l[:-1].split('\t')lines = [lines[i] for i in u_index]new_line = '\t'.join(lines) + '\n'if lines[0] in user and u_doc_num[lines[0]] > USER_THRESHOLD:f_user_target.write(new_line)l = f_user.readline()f_user.close()
f_user_target.close()# .item
i_index = [0, 5, 6, 7, 8]
f_item = open('jobs.tsv', 'r', encoding='utf-8')
f_item_target = open('jobrec.item', 'w', encoding='utf-8')head = f_item.readline()[:-1].split('\t')
head = [head[i] for i in i_index]
head = [i + ':token' for i in head]
head[0] = 'item_id:token'
f_item_target.write('\t'.join(head) + '\n')
l = f_item.readline()while l:lines = l.split('\t')lines = [lines[i] for i in i_index]new_line = '\t'.join(lines) + '\n'if lines[0] in job and i_doc_num[lines[0]] > JOB_THRESHOLD:f_item_target.write(new_line)l = f_item.readline()f_item.close()
f_item_target.close()# .udoc
import redef split_sent(text):text = re.split('(?:[0-9][.;。:.•)\)])', text)  # 按照数字分割包括  1.  1;  1。  1:  1) 等ans = []for t in text:for tt in re.split('(?:[\ ][0-9][、,])', t):  #for ttt in re.split('(?:^1[、,])', tt):   # 1、for tttt in re.split('(?:\([0-9]\))', ttt):   # (1)ans += re.split('(?:[。;…●])', tttt)return [_.strip() for _ in ans if len(_.strip()) > 0]def cut_sent(text):wds = [_.strip() for _ in text.split(' ') if len(_.strip()) > 0]  # 分词,返回分词后的 listreturn wdsdef clean_text(text):illegal_set = ',.;?!~[]\'"@#$%^&*()-_=+{}`~·!¥()—「」【】|、“”《<》>?,。…:'   # 定义非法字符for c in illegal_set:text = text.replace(c, ' ')     # 非法字符 替换为 空格text = ' '.join([_ for _ in text.split(' ') if len(_) > 0])return text    # 空格间隔def raw2token_seq(s):sents = split_sent(s)sent_wds = []sent_lens = []for sent in sents:if len(sent) < 2:continuesent = clean_text(sent)if len(sent) < 1:continuewds = cut_sent(sent)sent_wds.extend(wds)sent_lens.append(len(wds))if len(sent_wds) < 1:return None, None, Noneassert sum(sent_lens) == len(sent_wds)# 返回3个值,第一个是用空格连接的词,第二个是各句子长度,第三个是总词数return ' '.join(sent_wds), ' '.join(map(str, sent_lens)), len(sent_wds)u_doc_index = [3, 4, 5, 7, 8]
f = open('users.tsv', 'r', encoding='utf-8')
f.readline()
f_his = open('user_history.tsv', 'r', encoding='utf-8')
f_his.readline()f_udoc = open('jobrec.udoc', 'w', encoding='utf-8')
head = ['user_id:token', 'user_doc:token_seq']
f_udoc.write('\t'.join(head) + '\n')for line in f_his:lines = line[:-1].split('\t')sents = lines[4]try:sent_wds, sent_lens, _ = raw2token_seq(sents)sent_wds = sent_wds.split(' ')sent_lens = sent_lens.split(' ')a = -(int)(sent_lens[-1])for j in range(len(sent_lens)):a += (int)(sent_lens[j - 1])s_word_line = ' '.join(sent_wds[a:a + (int)(sent_lens[j])])if s_word_line == '':continues_new_line = lines[0] + '\t' + s_word_line + '\n'f_udoc.write(s_new_line)except:if sents == '':continuef_udoc.write(lines[0] + '\t' + sents + '\n')for line in f:lines = line[:-1].split('\t')if u_doc_num[lines[0]] > USER_THRESHOLD:for i in u_doc_index:if lines[i] and lines[i] != '-':sents = lines[i]try:sent_wds, sent_lens, _ = raw2token_seq(sents)sent_wds = sent_wds.split(' ')sent_lens = sent_lens.split(' ')a = -(int)(sent_lens[-1])for j in range(len(sent_lens)):a += (int)(sent_lens[j - 1])s_word_line = ' '.join(sent_wds[a:a + (int)(sent_lens[j])])s_new_line = lines[0] + '\t' + s_word_line + '\n'f_udoc.write(s_new_line)except:f_udoc.write(lines[0] + '\t' + sents + '\n')f.close()
f_his.close()
f_udoc.close()# .idoc
import rei_doc_index = [2, 3, 4]
f = open('jobs.tsv', 'r', encoding='utf-8')
f.readline()f_idoc = open('jobrec.idoc', 'w', encoding='utf-8')
head = ['item_id:token', 'item_doc:token_seq']
f_idoc.write('\t'.join(head) + '\n')for line in f:lines = line[:-1].split('\t')if i_doc_num[lines[0]] > JOB_THRESHOLD:for i in i_doc_index:if lines[i] and lines[i] != '-':sents = lines[i]sents = re.sub('\"', '', sents)sents = re.sub('\\\\r', ' ', sents)sents = re.sub('<[^>]+>', ' ', sents)sents = re.sub('&nbsp;', ' ', sents)sents = sents.split('   ')for sent in sents:sent = sent.strip()if len(sent) < 20:continues_new_line = lines[0] + '\t' + sent + '\n'f_idoc.write(s_new_line)f.close()
f_idoc.close()

DPGNN网络结构定义、网络传播、损失定义:

# @Time   : 2022/3/21
# @Author : Chen Yang
# @Email  : flust@ruc.edu.cn"""
pjfbole
"""import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.init import xavier_normal_
from recbole.model.abstract_recommender import GeneralRecommender
from recbole.model.loss import BPRLoss, EmbLoss
from recbole.utils import InputTypeclass DPGNN(GeneralRecommender):input_type = InputType.PAIRWISEdef __init__(self, config, dataset):super(DPGNN, self).__init__(config, dataset)from recbole_pjf.model.layer import GCNConvself.NEG_USER_ID = config['NEG_PREFIX'] + self.USER_IDself.NEG_ITEM_ID = config['NEG_PREFIX'] + self.ITEM_ID# load parameters infoself.embedding_size = config['embedding_size']# load dataset infoself.interaction_matrix = dataset.inter_matrix(form='coo').astype(np.float32)self.user_add_matrix = dataset.user_single_inter_matrix(form='coo').astype(np.float32)self.job_add_matrix = dataset.item_single_inter_matrix(form='coo').astype(np.float32)# load parameters infoself.latent_dim = config['embedding_size']  # int type:the embedding size of lightGCNself.n_layers = config['n_layers']  # int type:the layer num of lightGCNself.reg_weight = config['reg_weight']  # float32 type: the weight decay for l2 normalizationself.mul_weight = config['mutual_weight']self.temperature = config['temperature']# layersself.user_embedding_a = nn.Embedding(self.n_users, self.latent_dim)self.item_embedding_a = nn.Embedding(self.n_items, self.latent_dim)self.user_embedding_p = nn.Embedding(self.n_users, self.latent_dim)self.item_embedding_p = nn.Embedding(self.n_items, self.latent_dim)self.gcn_conv = GCNConv(dim=self.latent_dim)self.mf_loss = BPRLoss()self.reg_loss = EmbLoss()self.mutual_loss = nn.CrossEntropyLoss()self.loss = 0# bert partself.ADD_BERT = config['ADD_BERT']self.BERT_e_size = config['BERT_output_size'] or 1self.bert_lr = nn.Linear(config['BERT_embedding_size'], self.BERT_e_size)if self.ADD_BERT:self.bert_user = dataset.bert_user.to(config['device'])self.bert_item = dataset.bert_item.to(config['device'])# generate intermediate dataself.edge_index, self.edge_weight = self.get_norm_adj_mat()self.edge_index = self.edge_index.to(config['device'])self.edge_weight = self.edge_weight.to(config['device'])self.apply(self._init_weights)def _init_weights(self, module):if isinstance(module, nn.Embedding):xavier_normal_(module.weight.data)def get_norm_adj_mat(self):r"""Get the normalized interaction matrix of users and items.Construct the square matrix from the training data and normalize itusing the laplace matrix... math::A_{hat} = D^{-0.5} \times A \times D^{-0.5}Returns:The normalized interaction matrix in Tensor."""#   user a node: [0 ~~~ n_users] (len: n_users)#   item p node: [n_users + 1 ~~~ n_users + 1 + n_items] (len: n_users)#   user p node: [~] (len: n_users)#   item a node: [~] (len: n_items)from torch_geometric.utils import degreen_all = self.n_users + self.n_items# success edgerow = torch.LongTensor(self.interaction_matrix.row)col = torch.LongTensor(self.interaction_matrix.col) + self.n_usersedge_index1 = torch.stack([row, col])edge_index2 = torch.stack([col, row])edge_index3 = torch.stack([row + n_all, col + n_all])edge_index4 = torch.stack([col + n_all, row + n_all])edge_index_suc = torch.cat([edge_index1, edge_index2, edge_index3, edge_index4], dim=1)# user_add edgerow = torch.LongTensor(self.user_add_matrix.row)col = torch.LongTensor(self.user_add_matrix.col) + self.n_usersedge_index1 = torch.stack([row, col])edge_index2 = torch.stack([col, row])edge_index_user_add = torch.cat([edge_index1, edge_index2], dim=1)# job_add edgerow = torch.LongTensor(self.job_add_matrix.row)col = torch.LongTensor(self.job_add_matrix.col) + self.n_usersedge_index1 = torch.stack([row + n_all, col + n_all])edge_index2 = torch.stack([col + n_all, row + n_all])edge_index_job_add = torch.cat([edge_index1, edge_index2], dim=1)# self edgegeek = torch.LongTensor(torch.arange(0, self.n_users))job = torch.LongTensor(torch.arange(0, self.n_items) + self.n_users)edge_index_geek_1 = torch.stack([geek, geek + n_all])edge_index_geek_2 = torch.stack([geek + n_all, geek])edge_index_job_1 = torch.stack([job, job + n_all])edge_index_job_2 = torch.stack([job + n_all, job])edge_index_self = torch.cat([edge_index_geek_1, edge_index_geek_2, edge_index_job_1, edge_index_job_2], dim=1)# all edgeedge_index = torch.cat([edge_index_suc, edge_index_user_add, edge_index_job_add, edge_index_self], dim=1)deg = degree(edge_index[0], (self.n_users + self.n_items) * 2)norm_deg = 1. / torch.sqrt(torch.where(deg == 0, torch.ones([1]), deg))edge_weight = norm_deg[edge_index[0]] * norm_deg[edge_index[1]]return edge_index, edge_weightdef get_ego_embeddings(self):r"""Get the embedding of users and items and combine to an embedding matrix.Returns:Tensor of the embedding matrix. Shape of [n_items+n_users, embedding_dim]"""user_embeddings_a = self.user_embedding_a.weightitem_embeddings_a = self.item_embedding_a.weightuser_embeddings_p = self.user_embedding_p.weightitem_embeddings_p = self.item_embedding_p.weightego_embeddings = torch.cat([user_embeddings_a,item_embeddings_p,user_embeddings_p,item_embeddings_a], dim=0)if self.ADD_BERT:self.bert_u = self.bert_lr(self.bert_user)self.bert_i = self.bert_lr(self.bert_item)bert_e = torch.cat([self.bert_u,self.bert_i,self.bert_u,self.bert_i], dim=0)return torch.cat([ego_embeddings, bert_e], dim=1)return ego_embeddingsdef info_nce_loss(self, index, is_user):all_embeddings = self.get_ego_embeddings()user_e_a, item_e_p, user_e_p, item_e_a = torch.split(all_embeddings,[self.n_users, self.n_items, self.n_users, self.n_items])if is_user:u_e_a = F.normalize(user_e_a[index], dim=1)u_e_p = F.normalize(user_e_p[index], dim=1)similarity_matrix = torch.matmul(u_e_a, u_e_p.T)else:i_e_a = F.normalize(item_e_a[index], dim=1)i_e_p = F.normalize(item_e_p[index], dim=1)similarity_matrix = torch.matmul(i_e_a, i_e_p.T)mask = torch.eye(index.shape[0], dtype=torch.bool).to(self.device)positives = similarity_matrix[mask].view(index.shape[0], -1)negatives = similarity_matrix[~mask].view(index.shape[0], -1)logits = torch.cat([positives, negatives], dim=1)labels = torch.zeros(logits.shape[0], dtype=torch.long).to(self.device)logits = logits / self.temperaturereturn logits, labelsdef forward(self):all_embeddings = self.get_ego_embeddings()embeddings_list = [all_embeddings]for layer_idx in range(self.n_layers):all_embeddings = self.gcn_conv(all_embeddings, self.edge_index, self.edge_weight)embeddings_list.append(all_embeddings)lightgcn_all_embeddings = torch.stack(embeddings_list, dim=1)lightgcn_all_embeddings = torch.mean(lightgcn_all_embeddings, dim=1)user_e_a, item_e_p, user_e_p, item_e_a = torch.split(lightgcn_all_embeddings,[self.n_users, self.n_items, self.n_users, self.n_items])return user_e_a, item_e_p, user_e_p, item_e_adef calculate_loss(self, interaction):user = interaction[self.USER_ID]item = interaction[self.ITEM_ID]neg_user = interaction[self.NEG_USER_ID]neg_item = interaction[self.NEG_ITEM_ID]user_e_a, item_e_p, user_e_p, item_e_a = self.forward()# user activeu_e_a = user_e_a[user]n_u_e_a = user_e_a[neg_user]# item negativei_e_p = item_e_p[item]n_i_e_p = item_e_p[neg_item]# user negativeu_e_p = user_e_p[user]n_u_e_p = user_e_p[neg_user]# item activei_e_a = item_e_a[item]n_i_e_a = item_e_a[neg_item]r_pos = torch.mul(u_e_a, i_e_p).sum(dim=1)s_pos = torch.mul(u_e_p, i_e_a).sum(dim=1)r_neg1 = torch.mul(u_e_a, n_i_e_p).sum(dim=1)s_neg1 = torch.mul(u_e_p, n_i_e_a).sum(dim=1)r_neg2 = torch.mul(n_u_e_a, i_e_p).sum(dim=1)s_neg2 = torch.mul(n_u_e_p, i_e_a).sum(dim=1)mf_loss_u = self.mf_loss(2 * r_pos + 2 * s_pos, r_neg1 + s_neg1 + r_neg2 + s_neg2)# calculate Emb Lossu_ego_embeddings_a = self.user_embedding_a(user)u_ego_embeddings_p = self.user_embedding_p(user)pos_ego_embeddings_a = self.item_embedding_a(item)pos_ego_embeddings_p = self.item_embedding_p(item)neg_ego_embeddings_a = self.item_embedding_a(neg_item)neg_ego_embeddings_p = self.item_embedding_p(neg_item)neg_u_ego_embeddings_a = self.user_embedding_a(neg_user)neg_u_ego_embeddings_p = self.user_embedding_p(neg_user)reg_loss = self.reg_loss(u_ego_embeddings_a, u_ego_embeddings_p,pos_ego_embeddings_a, pos_ego_embeddings_p,neg_ego_embeddings_a, neg_ego_embeddings_p,neg_u_ego_embeddings_a, neg_u_ego_embeddings_p)loss = mf_loss_u + self.reg_weight * reg_losslogits_user, labels = self.info_nce_loss(user, is_user=True)loss += self.mul_weight * self.mutual_loss(logits_user, labels)logits_job, labels = self.info_nce_loss(item, is_user=False)loss += self.mul_weight * self.mutual_loss(logits_job, labels)return lossdef predict(self, interaction):user = interaction[self.USER_ID]item = interaction[self.ITEM_ID]user_e_a, item_e_p, user_e_p, item_e_a = self.forward()# user activateu_e_a = user_e_a[user]# item negativei_e_p = item_e_p[item]# user negativeu_e_p = user_e_p[user]# item negativei_e_a = item_e_a[item]I_geek = torch.mul(u_e_a, i_e_p).sum(dim=1)I_job = torch.mul(u_e_p, i_e_a).sum(dim=1)# calculate BPR Lossscores = I_geek + I_jobreturn scores

参考论文

Yang C, Hou Y, Song Y, et al. Modeling Two-Way Selection Preference for Person-Job Fit[C]//Proceedings of the 16th ACM Conference on Recommender Systems. 2022: 102-112.

基于双视角图表示算法的双向人职匹配偏好建模推荐系统构建相关推荐

  1. java计算机毕业设计基于springboot人职匹配推荐系统

    项目介绍 随着科学技术的飞速发展,各行各业都在努力与现代先进技术接轨,通过科技手段提高自身的优势:对于人职匹配推荐系统当然也不能排除在外,随着网络技术的不断成熟,带动了人职匹配推荐系统,它彻底改变了过 ...

  2. 【推荐系统】{1} —— 基于用户的协同过滤算法

    协同过滤(英语:Collaborative Filtering,简称CF),简单来说是利用某兴趣相投.拥有共同经验之群体的喜好来推荐用户感兴趣的信息,个人透过合作的机制给予信息相当程度的回应(如评分) ...

  3. 推荐算法——基于用户的协同过滤算法(User-base CF)的java实现

    推荐算法--基于用户的协同过滤算法(User-base CF)的java实现 推荐系统 什么是推荐系统 为什么要有推荐系统 推荐算法 基于用户的协同过滤算法(User-base CF) 算法介绍 代码 ...

  4. ​RecSys 2022 | 面向人岗匹配的双向选择偏好建模

    本文为 BOSS 直聘联合中国人民大学提出的建模双边选择偏好的人岗匹配模型.目前,该论文已被推荐系统国际会议 RecSys 2022 接收. 论文标题: Modeling Two-Way Select ...

  5. 百奥赛图与LiberoThera共同开发全人GPCR抗体药物取得里程碑式进展

    2021年2月1日,百奥赛图(北京)医药科技股份有限公司("百奥赛图")与日本LiberoThera有限公司("LiberoThera")共同宣布,基于百奥赛图 ...

  6. 使用Go基于国密算法实现双向认证

    国内做2B(to Biz)或2G(to Gov)产品和解决方案的企业都绕不过国密算法,越来越多的国内甲方在采购需求中包含了基于国密算法的认证.签名.加密等需求.对于国内的车联网平台来说,支持基于国密的 ...

  7. C语言LMS双麦克风消噪算法,基于两个时域LMS算法双麦克风系统分析.doc

    基于两个时域LMS算法双麦克风系统分析 基于两个时域LMS算法双麦克风系统分析 [摘要] 本文介绍了基于两个时域LMS算法的双麦克风系统结构以及性能分析.本文提出一种采用两个时域LMS的算法,并将此算 ...

  8. 计算机树表查找算法的适用场景,利用基于R-树连续最近邻查询算法来渲染雨滴,形成了逼真的下雨天场景图...

    摘要:自动驾驶领域最近的发展表明,这些车辆在不久的将来会出现在每个城市的街道上,而这些城市不会容忍交通事故的发生.为了保证安全需求,自动驾驶汽车和被使用的软件必须在尽可能多的条件下进行详尽的测试.通常 ...

  9. 读《基于深度学习的跨视角步态识别算法研究》

    2020 背景: 作为一种新兴的识别技术,步态识别具有在非受控.远距离.低分辨率的场景下进行身份识别的优点,并且步态不易改变和伪装,所以近年来得到的关注逐渐增多. 步态识别作为一种新兴的身份识别技术, ...

最新文章

  1. shell中和||的使用方法
  2. power bi 中计算_Power BI中的期间比较
  3. Java Web整合开发(13) -- XML
  4. 【PAT乙】1044 火星数字 (20分)
  5. Atitit.java jna 调用c++ dll 的总结
  6. 32个参数累加_「机械设计教程」滚珠丝杠选型过程中考虑的9个参数
  7. 同济大学计算机直博生条件,同济大学攻读博士学位研究生培养工作规定(2016年修订).doc...
  8. C语言射击类打飞机小游戏
  9. 学计算机学体育生闺女,适合女孩学的体育项目
  10. 【NDN实验】ndnSIM 2.0: A new version of the NDN simulator for NS-3 全文翻译
  11. QQ开始对每日添加好友人数作出限制(转)
  12. python写api接口实战
  13. C++ Win32程序编写入门
  14. 深入浅出Pytorch函数——torch.arange
  15. 大学文科生vs大学理科生
  16. matlab中selector用法,MATLAB SIMULINK Bus Selector 总线选择
  17. android电池系统
  18. CCleaner 下载使用
  19. win10能用计算机考试么,Win10计算器在哪里?三种可以打开Win10计算器的方法图文介绍...
  20. 2023年最新版初级商业数字营销师直通车题库

热门文章

  1. 谁才是小米今年的真旗舰手机?红米Note 8 Pro性能强,小米MIX4让人心动
  2. 惠普的服务器改win7系统,惠普笔记本win8改win7系统设置bios的详细教程
  3. 9.已知两个链表A和B分别表示两个集合,其元素递增排列,设计一个算法,求A和B的交集,并存放于A链表中(A中只有A与B相同的部分)
  4. SSH2项目引入百度富文本编辑器Ueditor编辑器
  5. InputStreamReader读数据的2种方式
  6. C#下查找并杀死子进程(进程树)
  7. 铁路公安局:去年12月至今抓获票贩子2558人
  8. c语言-位运算符-详解
  9. python最适合做什么生意-个人利用Python爬虫技术怎么挣钱-10万被动收入
  10. 2022-2028年全球洞察即服务市场收入年复合增长率CAGR为 12.4%