风控图算法Graph Embedding(DeepWalk&Node2Vec)代码实现

风控图算法Graph Embedding(DeepWalk&Node2Vec)代码实现

在上一篇中我们简单介绍了常用的Graph Embedding算法,今天来对其中较为常用的两种算法——DeepWalk和Node2Vec进行python代码实现。



一、Karate Club算法包

Karate Club 是一个 Python 算法包,专门用于图形分析和图形挖掘。它提供了一系列经典和先进的图形聚类和图形嵌入算法,旨在帮助研究人员和数据科学家处理和分析各种类型的图形数据。该算法包的名字取自 Zachary’s Karate Club,这是一个关于社交网络的著名案例,用于研究社区结构和社交网络动态性。Karate Club 旨在为用户提供一种简单而灵活的方式来探索和分析图形数据,同时提供了一些高效的算法和工具,以解决图形分析中的常见问题。

import networkx as nx

# 创建或加载一个图
G = nx.karate_club_graph()

二、DeepWalk算法实现

  • 原始版本的DeepWalk针对无向无权图设计,即不考虑边的方向也不考虑边的权重。
  • 应用DeepWalk于有向图时,随机游走会遵循边的方向,这可能会影响游走的覆盖性和最终的嵌入结果。
  • 有些变体或类似的方法可能会扩展DeepWalk以考虑边权重,通过调整随机游走的过程来偏好权重较高的边。

DeepWalk参数

  • walk_number:游走次数
    1.游走次数指的是对每个节点执行随机游走的次数。增加游走次数可以提高模型对节点之间关系的把握程度,但也会增加计算成本。
    2.通常情况下,较多的游走次数可以帮助算法更好地探索图形的结构,尤其是对于大型图形数据集
    3.可以选择 10 到 80之间的游走次数,但具体的选择取决于数据集的规模和复杂度。
  • walk_length:游走长度
    1.游走长度指的是在每次随机游走中经过的节点数量。较长的游走长度能够更好地捕捉到节点之间的全局结构信息,但也会增加计算成本。
    2.对于大型图形数据集或者密集连接的图形,通常选择较长的游走长度,以便更全面地探索图形的结构
    3.对于稀疏连接的图形或者想要更注重局部结构信息的情况,可以选择较短的游走长度。
    4.常见的游走长度通常在 10 到 80 之间,具体选择取决于数据集的特点和算法的目标。
  • dimensions:嵌入维度
    1.嵌入维度指的是学习到的节点表示的维度大小。较高的嵌入维度可以提供更丰富的节点表示信息,但也可能增加计算和存储成本。
    2.对于大型图形数据集或者需要较高精度的任务,可以选择较高的嵌入维度。
    3.对于资源受限或者对计算效率要求较高的情况,可以选择较低的嵌入维度

Karate Club实现

from karateclub import DeepWalk
from karateclub import Estimator

# 初始化DeepWalk模型
deepwalk = DeepWalk(walk_number=10, walk_length=80, dimensions=64, epochs=10)

# 训练模型
deepwalk.fit(G)

# 获取嵌入
embedding = deepwalk.get_embedding()

模型参数

  • walk_number(int):随机行走的次数。默认值为10。
  • walk_length(int):随机行走的长度。默认值为80。
  • dimensions(int):嵌入的维数。默认值为128。
  • workers(int):核心数。默认值为4。
  • window_size(int):矩阵幂序。默认值为5。
  • epochs(int):迭代次数。默认值为1。
  • use_hierarchical_softmax(bool):是使用分层softmax还是负采样来训练模型。默认值为True。
  • number_of_onegative_samples(int):要采样的负节点数(通常在5-20之间)。如果设置为0,则不使用负采样。默认值为5。
  • learning_rate(float):学习率。默认值为0.05。
  • min_count(int):节点出现次数的最小值。默认值为1。
  • seed(int):随机种子值。默认值为42

DeepWalk手动代码实现

from gensim.models.word2vec import Word2Vec
import random
from functools import partial
from typing import List, Callable
import numpy as np
# 随机游走
class RandomWalker:
    """
    Class to do fast first-order random walks.

    Args:
        walk_length (int): Number of random walks.
        walk_number (int): Number of nodes in truncated walk.
    """

    def __init__(self, walk_length: int, walk_number: int):
        self.walk_length = walk_length
        self.walk_number = walk_number

    def do_walk(self, node: int) -> List[str]:
        """
        Doing a single truncated random walk from a source node.

        Arg types:
            * **node** *(int)* - The source node of the random walk.

        Return types:
            * **walk** *(list of strings)* - A single truncated random walk.
        """
        walk: List[int] = [node]
        for _ in range(self.walk_length - 1):
            nebs = [node for node in self.graph.neighbors(walk[-1])]
            if len(nebs) > 0:
                walk = walk + random.sample(nebs, 1)
        walk = [str(w) for w in walk]
        return walk

    def do_walks(self, graph):
        """
        Doing a fixed number of truncated random walk from every node in the graph.

        Arg types:
            * **graph** *(NetworkX graph)* - The graph to run the random walks on.
        """
        self.walks: List[List[str]] = []
        self.graph = graph
        for node in self.graph.nodes():
            for _ in range(self.walk_number):
                walk_from_node = self.do_walk(node)
                self.walks.append(walk_from_node)
# DeepWalk
class CustomDeepWalk(Estimator):
    def __init__(
        self,
        walk_number: int = 10,
        walk_length: int = 80,
        dimensions: int = 128,
        workers: int = 4,
        window_size: int = 5,
        epochs: int = 1,
        use_hierarchical_softmax: bool = True,
        number_of_negative_samples: int = 5,
        learning_rate: float = 0.05,
        min_count: int = 1,
        seed: int = 42,
    ):
        self.walk_number = walk_number
        self.walk_length = walk_length
        self.dimensions = dimensions
        self.workers = workers
        self.window_size = window_size
        self.epochs = epochs
        self.use_hierarchical_softmax = use_hierarchical_softmax
        self.number_of_negative_samples = number_of_negative_samples
        self.learning_rate = learning_rate
        self.min_count = min_count
        self.seed = seed

    def fit(self, graph: nx.classes.graph.Graph):
        """
        Fitting a DeepWalk model.

        Arg types:
            * **graph** *(NetworkX graph)* - The graph to be embedded.
        """
        self._set_seed()
        graph = self._check_graph(graph)
        # 随机游走生成序列
        walker = RandomWalker(self.walk_length, self.walk_number)
        walker.do_walks(graph)

        # Word2Vec
        model = Word2Vec(
            walker.walks,
            hs=1 if self.use_hierarchical_softmax else 0,
            negative=self.number_of_negative_samples,
            alpha=self.learning_rate,
            epochs=self.epochs,
            vector_size=self.dimensions,
            window=self.window_size,
            min_count=self.min_count,
            workers=self.workers,
            seed=self.seed,
        )

        num_of_nodes = graph.number_of_nodes()
        self._embedding = [model.wv[str(n)] for n in range(num_of_nodes)]

    def get_embedding(self) -> np.array:
        r"""Getting the node embedding.

        Return types:
            * **embedding** *(Numpy array)* - The embedding of nodes.
        """
        return np.array(self._embedding)

# 初始化自定义DeepWalk模型
customdeepwalk = CustomDeepWalk(walk_number=10, walk_length=80, dimensions=64, epochs=10)

# 训练模型
customdeepwalk.fit(G)

# 获取嵌入
embedding = customdeepwalk.get_embedding()

三、Node2Vec算法实现

  • 原始版本的DeepWalk针对无向无权图设计,即不考虑边的方向也不考虑边的权重。
  • 相比DeepWalk中的随机游走过程,Node2Vec通过引入两个参数 p 和 q来扩展随机游走过程,从而在游走过程中平衡探索(breadth-first)和开发(depth-first)策略。参数 p控制了回到先前节点的可能性,而参数 q 控制了跳转到邻居节点的可能性。这样,Node2Vec 可以在随机游走中更加灵活地探索图的局部结构。
  • Node2Vec 算法相对于 DeepWalk 更复杂一些,因为它引入了额外的参数 p 和 q,以及更复杂的随机游走策略。这可能使得Node2Vec 在一些情况下更耗时,尤其是在参数调优和图规模较大时。

Karate Club实现

from karateclub import Node2Vec

# 初始化 Node2Vec 模型
model = Node2Vec()

# 训练 Node2Vec 模型
model.fit(G)

# 获取节点的嵌入向量
embeddings = model.get_embedding()

模型参数

  • walk_number(int):随机行走的次数。默认值为10。
  • walk_length(int):随机行走的长度。默认值为80。
  • p(float):返回上一个节点的概率(1/p转换概率)。
  • q(float):输入-输出参数(1/q转换概率),用于远离前一个节点。
  • dimensions(int):嵌入的维数。默认值为128。
  • workers(int):核心数。默认值为4。
  • window_size(int):矩阵幂序。默认值为5。
  • epochs(int):迭代次数。默认值为1。
  • use_hierarchical_softmax(bool):是使用分层softmax还是负采样来训练模型。默认值为True。
  • number_of_onegative_samples(int):要采样的负节点数(通常在5-20之间)。如果设置为0,则不使用负采样。默认值为5。
  • learning_rate(float):学习率。默认值为0.05。
  • min_count(int):节点出现次数的最小值。

Node2Vec手动代码实现

def _undirected(node, graph) -> List[tuple]:
    # 对于无向图,返回所有的边
    edges = graph.edges(node)

    return edges


def _directed(node, graph) -> List[tuple]:
    # 对于有向图,只返回出边
    edges = graph.out_edges(node, data=True)

    return edges


def _get_edge_fn(graph) -> Callable:
    # 判断图的类型,采取不同的获取边的方案
    fn = _directed if nx.classes.function.is_directed(graph) else _undirected

    fn = partial(fn, graph=graph)
    return fn

def _unweighted(edges: List[tuple]) -> np.ndarray:
    # 没有权重都设置为1
    return np.ones(len(edges))


def _weighted(edges: List[tuple]) -> np.ndarray:
    # 有权重用自身权重
    weights = map(lambda edge: edge[-1]["weight"], edges)

    return np.array([*weights])


def _get_weight_fn(graph) -> Callable:
    # 判断图形是否存在权重
    fn = _weighted if nx.classes.function.is_weighted(graph) else _unweighted

    return fn

# 扩展的随机游走过程(引入p、q)
class BiasedRandomWalker:
    """
    Args:
        walk_length (int): Number of random walks.
        walk_number (int): Number of nodes in truncated walk.
        p (float): Return parameter (1/p transition probability) to move towards previous node.
        q (float): In-out parameter (1/q transition probability) to move away from previous node.
    """
    walks: list
    graph: nx.classes.graph.Graph
    
    # 表示可调用对象
    edge_fn: Callable
    weight_fn: Callable

    def __init__(self, walk_length: int, walk_number: int, p: float, q: float):
        self.walk_length = walk_length
        self.walk_number = walk_number
        self.p = p
        self.q = q

    def do_walk(self, node: int) -> List[str]:
        """
        从源节点执行单个截断的二阶随机遍历

        Arg types:
            * **node** *(int)* - The source node of the random walk.

        Return types:
            * **walk** *(list of strings)* - A single truncated random walk.
        """
        walk = [node]
        previous_node = None
        previous_node_neighbors = []
        for _ in range(self.walk_length - 1):
            current_node = walk[-1]
            edges = self.edge_fn(current_node)
            # 获取当前节点的邻居节点
            current_node_neighbors = np.array([edge[1] for edge in edges])

            # 获取到邻居节点的边的权重
            weights = self.weight_fn(edges)
            # 根据权重生成概率
            probability = np.piecewise(
                weights,
                [
                    current_node_neighbors == previous_node,
                    # 判断当前节点的邻居节点是否在前一个节点的邻居节点中
                    np.isin(current_node_neighbors, previous_node_neighbors),
                ],
                # 返回前一个节点的概率  
                [lambda w: w / self.p, lambda w: w / 1, lambda w: w / self.q],
            )

            # 标准化概率值
            norm_probability = probability / sum(probability)
            # 从 current_node_neighbors中以norm_probability概率抽取一个值
            selected = np.random.choice(current_node_neighbors, 1, p=norm_probability)[
                0
            ]
            walk.append(selected)

            previous_node_neighbors = current_node_neighbors
            previous_node = current_node

        walk = [str(w) for w in walk]
        return walk

    def do_walks(self, graph) -> None:
        """
        Doing a fixed number of truncated random walk from every node in the graph.

        Arg types:
            * **graph** *(NetworkX graph)* - The graph to run the random walks on.
        """
        self.walks = []
        self.graph = graph

        self.edge_fn = _get_edge_fn(graph)
        self.weight_fn = _get_weight_fn(graph)

        for node in self.graph.nodes():
            for _ in range(self.walk_number):
                walk_from_node = self.do_walk(node)
                self.walks.append(walk_from_node)

# 扩展的随机游走过程(引入p、q)
class BiasedRandomWalker:
    """
    Args:
        walk_length (int): Number of random walks.
        walk_number (int): Number of nodes in truncated walk.
        p (float): Return parameter (1/p transition probability) to move towards previous node.
        q (float): In-out parameter (1/q transition probability) to move away from previous node.
    """
    walks: list
    graph: nx.classes.graph.Graph
    
    # 表示可调用对象
    edge_fn: Callable
    weight_fn: Callable

    def __init__(self, walk_length: int, walk_number: int, p: float, q: float):
        self.walk_length = walk_length
        self.walk_number = walk_number
        self.p = p
        self.q = q

    def do_walk(self, node: int) -> List[str]:
        """
        从源节点执行单个截断的二阶随机遍历

        Arg types:
            * **node** *(int)* - The source node of the random walk.

        Return types:
            * **walk** *(list of strings)* - A single truncated random walk.
        """
        walk = [node]
        previous_node = None
        previous_node_neighbors = []
        for _ in range(self.walk_length - 1):
            current_node = walk[-1]
            edges = self.edge_fn(current_node)
            # 获取当前节点的邻居节点
            current_node_neighbors = np.array([edge[1] for edge in edges])

            # 获取到邻居节点的边的权重
            weights = self.weight_fn(edges)
            # 根据权重生成概率
            probability = np.piecewise(
                weights,
                [
                    current_node_neighbors == previous_node,
                    # 判断当前节点的邻居节点是否在前一个节点的邻居节点中
                    np.isin(current_node_neighbors, previous_node_neighbors),
                ],
                # 返回前一个节点的概率  
                [lambda w: w / self.p, lambda w: w / 1, lambda w: w / self.q],
            )

            # 标准化概率值
            norm_probability = probability / sum(probability)
            # 从 current_node_neighbors中以norm_probability概率抽取一个值
            selected = np.random.choice(current_node_neighbors, 1, p=norm_probability)[
                0
            ]
            walk.append(selected)

            previous_node_neighbors = current_node_neighbors
            previous_node = current_node

        walk = [str(w) for w in walk]
        return walk

    def do_walks(self, graph) -> None:
        """
        Doing a fixed number of truncated random walk from every node in the graph.

        Arg types:
            * **graph** *(NetworkX graph)* - The graph to run the random walks on.
        """
        self.walks = []
        self.graph = graph

        self.edge_fn = _get_edge_fn(graph)
        self.weight_fn = _get_weight_fn(graph)

        for node in self.graph.nodes():
            for _ in range(self.walk_number):
                walk_from_node = self.do_walk(node)
                self.walks.append(walk_from_node)

# 初始化自定义Node2Vec模型
customnode2vec = CustomNode2Vec(walk_number=10, walk_length=80, dimensions=128, epochs=10)

# 训练模型
customnode2vec.fit(G)

# 获取嵌入
embedding = customnode2vec.get_embedding()

总结

欢迎关注我的公众号~

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
拍贷“魔镜风控系统”从平均 拍贷“魔镜风控系统”从平均 拍贷“魔镜风控系统”从平均 拍贷“魔镜风控系统”从平均 拍贷“魔镜风控系统”从平均 拍贷“魔镜风控系统”从平均 拍贷“魔镜风控系统”从平均 拍贷“魔镜风控系统”从平均 400 个数据维度评估用户当前的信状态,给每借款 个数据维度评估用户当前的信状态,给每借款 个数据维度评估用户当前的信状态,给每借款 个数据维度评估用户当前的信状态,给每借款 个数据维度评估用户当前的信状态,给每借款 个数据维度评估用户当前的信状态,给每借款 个数据维度评估用户当前的信状态,给每借款 个数据维度评估用户当前的信状态,给每借款 个数据维度评估用户当前的信状态,给每借款 个数据维度评估用户当前的信状态,给每借款 人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个人打出当前状态的 信用分,在此基础上再结合新发标息对于每个6个月内逾 个月内逾 期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来期率的预测 ,为投资人提供关键决策依据。本次竞赛目标是根用户历史行数来用户在未来 用户在未来 用户在未来 6个月内是否会逾期还款的概率。 个月内是否会逾期还款的概率。 个月内是否会逾期还款的概率。 个月内是否会逾期还款的概率。 个月内是否会逾期还款的概率。 个月内是否会逾期还款的概率。 个月内是否会逾期还款的概率。 问题转换成 问题转换成 问题转换成 2分类问题,评估指标为 分类问题,评估指标为 分类问题,评估指标为 分类问题,评估指标为 分类问题,评估指标为 分类问题,评估指标为 分类问题,评估指标为 AUC ,从 Master Master Master,LogInfoLogInfo LogInfo ,UpdateInfo UpdateInfo UpdateInfo 表中构建 表中构建 特征,考虑评估指标为 特征,考虑评估指标为 特征,考虑评估指标为 特征,考虑评估指标为 特征,考虑评估指标为 AUC AUC,其本质是排序优化问题,所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 ,其本质是排序优化问题所以我们在模型顶层融合也使用基于 排序优化的 排序优化的 排序优化的 RANK_AVG RANK_AVG RANK_AVG融合方法。 融合方法。 融

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值