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

news2024/7/30 5:20:19

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

文章目录

  • 基于双视角图表示算法的双向人职匹配偏好建模推荐系统构建
    • 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),协同是指通过用户的持续协同作用,使得物料对用户整个群体的推荐作用越来越明显,所谓过滤,指的是从可行的决策方案中将用户喜欢的方案挑出,在实际问题表现为从推荐方案中将用户匹配的物料从物料的集合中找出。
在这里插入图片描述
基于用户的协同过滤算法在对用户建模时通常使用一个用户对物料的评分矩阵。

Item1Item2Item3Item4
User14022
User20556
User36212
User41700

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 ja c p c^p cp之间加边。
(3)当求职者与招聘方达成协议,这反映了双向的主动偏好意向,增加 c a c^a ca j p j^p jp j a j^a ja c 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 1zv(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.2

ADD_BERT: True
pretrained_bert_model: ~
BERT_embedding_size: 768
BERT_output_size: 32

数据预处理转换

# .udoc

import 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]  # 分词,返回分词后的 list
    return wds


def 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:
            continue
        sent = clean_text(sent)
        if len(sent) < 1:
            continue
        wds = cut_sent(sent)
        sent_wds.extend(wds)
        sent_lens.append(len(wds))
    if len(sent_wds) < 1:
        return None, None, None
    assert 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 == '':
                continue
            s_new_line = lines[0] + '\t' + s_word_line + '\n'
            f_udoc.write(s_new_line)
    except:
        if sents == '':
            continue
        f_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 re

i_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:
                    continue
                s_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 timeStamp


f_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 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]  # 分词,返回分词后的 list
    return wds


def 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:
            continue
        sent = clean_text(sent)
        if len(sent) < 1:
            continue
        wds = cut_sent(sent)
        sent_wds.extend(wds)
        sent_lens.append(len(wds))
    if len(sent_wds) < 1:
        return None, None, None
    assert 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 == '':
                continue
            s_new_line = lines[0] + '\t' + s_word_line + '\n'
            f_udoc.write(s_new_line)
    except:
        if sents == '':
            continue
        f_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 re

i_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:
                        continue
                    s_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 InputType


class DPGNN(GeneralRecommender):
    input_type = InputType.PAIRWISE

    def __init__(self, config, dataset):
        super(DPGNN, self).__init__(config, dataset)

        from recbole_pjf.model.layer import GCNConv

        self.NEG_USER_ID = config['NEG_PREFIX'] + self.USER_ID
        self.NEG_ITEM_ID = config['NEG_PREFIX'] + self.ITEM_ID
        # load parameters info
        self.embedding_size = config['embedding_size']

        # load dataset info
        self.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 info 
        self.latent_dim = config['embedding_size']  # int type:the embedding size of lightGCN
        self.n_layers = config['n_layers']  # int type:the layer num of lightGCN
        self.reg_weight = config['reg_weight']  # float32 type: the weight decay for l2 normalization
        self.mul_weight = config['mutual_weight']
        self.temperature = config['temperature']

        # layers
        self.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 part
        self.ADD_BERT = config['ADD_BERT']
        self.BERT_e_size = config['BERT_output_size'] or 1
        self.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 data
        self.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 it
        using 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 degree
        n_all = self.n_users + self.n_items

        # success edge
        row = torch.LongTensor(self.interaction_matrix.row)
        col = torch.LongTensor(self.interaction_matrix.col) + self.n_users
        edge_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 edge
        row = torch.LongTensor(self.user_add_matrix.row)
        col = torch.LongTensor(self.user_add_matrix.col) + self.n_users
        edge_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 edge
        row = torch.LongTensor(self.job_add_matrix.row)
        col = torch.LongTensor(self.job_add_matrix.col) + self.n_users
        edge_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 edge
        geek = 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 edge
        edge_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_weight

    def 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.weight
        item_embeddings_a = self.item_embedding_a.weight
        user_embeddings_p = self.user_embedding_p.weight
        item_embeddings_p = self.item_embedding_p.weight
        ego_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_embeddings

    def 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.temperature

        return logits, labels

    def 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_a

    def 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 active
        u_e_a = user_e_a[user]
        n_u_e_a = user_e_a[neg_user]
        # item negative
        i_e_p = item_e_p[item]
        n_i_e_p = item_e_p[neg_item]

        # user negative
        u_e_p = user_e_p[user]
        n_u_e_p = user_e_p[neg_user]
        # item active
        i_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 Loss
        u_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_loss

        logits_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 loss

    def 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 activate
        u_e_a = user_e_a[user]
        # item negative
        i_e_p = item_e_p[item]

        # user negative
        u_e_p = user_e_p[user]
        # item negative
        i_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 Loss
        scores = I_geek + I_job

        return 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.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/617324.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

H5项目怎么打包成APP

文章目录 前言一、新建5APP项目二、删除不需要的文件三、将H5打包的文件拷贝到当前目录下四、配置APP五、发行-云打包六、安装apk总结 前言 开发uni-app的编辑器HBuilderX可以将H5项目打包成APP&#xff0c;相信很多小伙伴还不知道这个功能&#xff0c;下面将介绍下如何将H5打…

linux MMU内存管理单元

本篇文章简要阐述MMU的概念&#xff0c;以及以段地址的转换过程为例&#xff0c;简单说明MMU将虚拟地址转换成物理地址的过程。更多详细内容请查看《ARM-MMU(中文手册).pdf》。 1、MMU概述 在ARM存储系统中&#xff0c;使用MMU实现虚拟地址到实际物理地址的映射。为何要实现这…

人工智能学习07--pytorch21--目标检测:YOLO系列理论合集(YOLOv1~v3)

如果直接看yolov3论文的话&#xff0c;会发现有好多知识点没见过&#xff0c;所以跟着视频从头学一下。 学习up主霹雳吧啦Wz大佬的学习方法&#xff1a; 想学某个网络的代码时&#xff1a;到网上搜这个网络的讲解 → 对这个网络大概有了印象 → 读论文原文&#xff08; 很多细…

Django实现接口自动化平台(五)httprunner(4.x)介绍【持续更新中】

上一章&#xff1a; Django实现接口自动化平台&#xff08;四&#xff09;解决跨域问题【持续更新中】_做测试的喵酱的博客-CSDN博客 下一章&#xff1a; 一、httpruner介绍 1.1 背景&#xff1a; 之所以学习httpruner的用法&#xff0c;是要把httpruner嵌入我们的自动化平…

全网最全、最新MyBatis框架核心知识

MyBatis框架 1. 软件开发常用结构 MyBatis是操作数据库的&#xff0c;相当于是一个增强的JDBC 1.1 三层架构 三层架构包括&#xff1a; 界面层&#xff08;User Interface layer&#xff09;业务逻辑层&#xff08;Business Logic Layer&#xff09;数据访问层&#xff08;Dat…

Window搭建IOS App自动化测试环境

平台搭建&#xff1a;tidevice&#xff08;Windows逆向通信iOS工具&#xff09;WebDriverAgent&#xff08;iOS通信服务&#xff09;facebook-wda&#xff08;iOS测试框架&#xff09; macOSXcode&#xff1a;在手机上安装WebDriverAgent的时候需要用到&#xff0c;必须要Xcod…

HDFS概述及其优缺点

什么是HDFS&#xff1f; HDFS的全称是hadoop distributed file system&#xff0c;即hadoop的分布式文件系统。 见名知意&#xff0c;它就是用来进行文件存储的。毕竟它是大数据的一个组件&#xff0c;用来存储这种海量的数据。 它是基于03年10月份&#xff0c;谷歌发表的GFS…

Hadoop 怎么委任和解除节点?

前言 本文隶属于专栏《大数据技术体系》&#xff0c;该专栏为笔者原创&#xff0c;引用请注明来源&#xff0c;不足和错误之处请在评论区帮忙指出&#xff0c;谢谢&#xff01; 本专栏目录结构和参考文献请见大数据技术体系 正文 Hadoop 集群的管理员经常需要向集群中添加节点…

Android Studio引用第三方库的方式

title: 大小端详解 date: 2023-06-06 21:01:24 comments: true #是否可评论 toc: true #是否显示文章目录 categories: #分类 - gradle - android studio tags: #标签 - gradle - android studio summary: android stduio 导入第三方库方式 Android Studio引用第三方库的方式 …

NCUTer 我的创作纪念日(2023-06-06)

前言 大家好&#xff0c;我是NCUTer&#xff0c;今天收到私信&#xff0c;突然发现&#xff0c;我来到CSDN已经2年多了&#xff0c;距离发布第一篇博客已经2年整了。正式规划性的写博客&#xff0c;是在2021年6月底开始的&#xff0c;当时啥也不懂&#xff0c;不知道该怎么去做…

10万字XX市开发区智慧综治中心平台建设一期工程招标文件word

&#xff08;一&#xff09;智慧综治中心信息化平台 需基于“一个平台&#xff0c;多级用户”的原则&#xff0c;利用移动互联网、物联网、大数据、人工智能和地理信息等新一代信息技术&#xff0c;整合辖区多方社会治理数据&#xff0c;建立一个覆盖全区三级综治中心用户的统一…

2个原因解答:为什么网络安全缺口大,招聘却很少?

2023年我国网络空间安全人才数量缺口超过了140万&#xff0c;就业人数却只有10多万&#xff0c;缺口高达了93%。这里就有人会问了&#xff1a; 1、网络安全行业为什么这么缺人&#xff1f; 2、明明人才那么稀缺&#xff0c;为什么招聘时招安全的人员却没有那么多呢&#xff1f;…

【博客639】Life of a label in prometheus

prometheus中label的生命周期 前言 Prometheus labels allow you to model your application deployment in the manner best suited to your organisation. As directly supporting every potential configurations would be impossible, we offer relabelling to give you t…

Paxos算法

组成 Paxos算法有proposer, accepter, leaner三种角色节点&#xff0c;其中proposer有点像客户端&#xff0c;而accepter是存储节点。 持久化需要 basic-Paxos是二阶段进行的 第一阶段 propose发起prepare请求&#xff0c;带上rnd accpeter如果发现rnd&#xff0c;比如自己…

图论在数学建模中的应用及MATLAB实现

2023年9月数学建模国赛期间提供ABCDE题思路加Matlab代码,专栏链接(赛前一个月恢复源码199,欢迎大家订阅):http://t.csdn.cn/Um9Zd 目录 图论基本概念 图论原理 1. 最短路径问题 2. 最小生成树问题 MATLAB实现 1. 创建图 2. 最短路径算法 3. 最小生成树算法 数学建模案…

第一章 小程序入门

文章目录 前言一、❎ 环境搭建1、AppID2、设置外观和代理3、小程序项目构成小程序的基本结构小程序的页面组成部分 4、JSON 配置文件JSON 配置文件的作用app.json 配置文件project.config.json 配置文件sitemap.json 配置文件页面 .json 配置文件 5、WXML 模板什么是 wxmlwxml …

Java --- springboot3之web静态资源配置

目录 一、静态资源规则 二、欢迎页规则 三、favicon.ioc规则 四、HTTP缓存机制 五、自定义静态资源规则 5.1、配置方式 5.2、代码方式 一、静态资源规则 Override public void addResourceHandlers(ResourceHandlerRegistry registry) {if (!this.resourceProperties.is…

UA-ModelCompiler 的编译和使用

UA-ModelCompiler 是OPCFoundation的开源程序&#xff0c;它实现将NodeSet 的xml 文件编译成C# 的类&#xff0c;以便结合到UA Server 中。同时&#xff0c;它也能够将一种简便的模型xml 文档(ModelDesgin.xml) 转换成为NodeSet2.xml 。 最近的项目中要使用UA-ModelCompiler &…

【C++】C++ 右值 相关常见问题

【C】C 右值 相关常见问题 文章目录 【C】C 右值 相关常见问题1.介绍一下左值引用和右值引用1.1左值和左值引用1.2右值和右值引用 2.左值引用与右值引用比较3.左值引用的使用场景4. move语义5.完美转发 C 11 关于右值相关概念&#xff1a; 在 C 中&#xff0c;右值是指仅作为表…

【3DsMAX】从零开始建房(3)

目录 步骤 1. 统一材质 2. 制作椅子 3. 制作货物盒 步骤 1. 统一材质 选中所有的模型&#xff0c;按下M键打开材质编辑器 选择精简材质编辑器 选择64示例窗 可以随便选中一个材质球&#xff0c;然后将材质指定给选定对象 然后可以修改线框颜色为黑色 2. 制作椅子 激活…