图谱特征工程基础:节点与边的定义、属性特征、结构特征、邻接矩阵与稀疏表示
做反欺诈图谱,第一步不是撸代码,而是想清楚你的图里装了什么。
说白了,图谱特征工程就是把现实世界的欺诈关系,翻译成机器能看懂的数字语言。我刚开始做这块的时候,觉得这步很简单——不就是把用户、设备、IP画个圈连个线吗?后来被坑了几次才发现,特征定义的好坏,直接决定模型的上限。
1. 节点与边的定义
图由两部分组成:节点(Vertex/Node)和边(Edge)。
节点:图中的基本实体。反欺诈场景下,常见的节点类型有:
- 用户节点:账号、手机号、身份证
- 设备节点:设备指纹、IMEI、MAC地址
- 环境节点:IP地址、WiFi BSSID、GPS位置
- 行为节点:订单、登录记录、支付交易
边:节点之间的关系。边可以有方向(有向图)或无方向(无向图)。
关键概念:同构与异构
如果所有节点是同一类(比如全是用户),叫同构图。如果节点类型超过一种(比如用户+设备+IP),叫异构图。反欺诈图谱99%都是异构的。
我在一个黑产团伙挖掘项目中,把「设备ID」也作为节点。结果发现一个设备绑定了上百个账号——这个特征直接揪出了一个机刷集群。
2. 属性特征
节点和边都可以携带属性信息。属性特征是图谱特征中最直观、最容易提取的一类。
| 特征类型 | 含义 | 反欺诈示例 |
|---|---|---|
| 节点属性 | 节点本身的描述信息 | 用户注册时间、设备型号、IP地理位置 |
| 边属性 | 节点之间关系的描述信息 | 交易金额、操作时间戳、关系强度(如通话次数) |
| 环境属性 | 图谱上下文信息 | 图谱创建时间、数据源标记 |
实战建议:不要把所有属性都塞进去。我只选跟欺诈强相关的:比如设备首次活跃时间、同一IP下用户数量。属性噪声太多反而影响下游计算效率。
我记得第一次做图谱特征时,把用户注册时间直接作为一个属性传到GNN里。结果发现模型根本不收敛...后来才意识到,时间戳应该转换成「注册距今天数」或者「近7天活跃次数」这类归一化特征。
3. 结构特征
结构特征反映的是节点在图中的「位置」和「重要性」。这部分我当初花了最多功夫理解。
- 度:节点的邻居数量。一个节点有100个邻居,说明它连接了很多其他节点——可能是团伙核心。
- 聚类系数:衡量节点的邻居之间是否也相互连接。团伙内部通常呈现高聚类特征。
- PageRank:节点的重要性排名。用于发现图中最「有影响力」的节点。
- K-Core:节点所在的核层次。越内层的节点连接越稠密。
- 最短路径长度:两个节点之间的距离。欺诈节点往往以短路径聚集。
举个例子:一个正常用户通常有3-5个好友,聚类系数在0.2左右。但一个养号团伙的节点,可能同时连接几百个账号,聚类系数接近0.8——因为都是机器批量互关。
避坑指南:结构特征不能独立使用。我曾经单纯用「度」来判断节点是否异常,结果把正常的大V用户误判成了欺诈节点。大V的度也高,但聚类系数低,组合使用才靠谱。
4. 邻接矩阵与稀疏表示
邻接矩阵是图的标准数学表示。它是一张 N × N 的矩阵(N 为节点数),矩阵元素 Aij 表示节点 i 和节点 j 之间是否有边。
有边则 Aij = 1(有权重时存权重),无边则 Aij = 0。
# 一个简单的邻接矩阵示例(3个节点)
# 节点A连接节点B和C,节点B只连接节点A
# 无向图
import numpy as np
A = np.array([
[0, 1, 1], # 节点A
[1, 0, 0], # 节点B
[1, 0, 0] # 节点C
])
print(A)
# 输出:
# [[0 1 1]
# [1 0 0]
# [1 0 0]]
但问题来了——真实图谱节点数动辄千万甚至上亿。邻接矩阵是 N × N 的,存储空间根本扛不住。一个1000万节点的邻接矩阵,需要 1014 个浮点数,约400TB内存——疯了吧?
所以实际工程中都用稀疏表示,只存非零元素的位置和值。
稀疏矩阵的三种常用格式
- COO格式:用三个列表存(行索引、列索引、值)。简单直观,适合构建图。
- CSR格式:行压缩存储。查邻居快,适合图算法。
- CSC格式:列压缩存储。按列遍历快,用途相对少。
from scipy.sparse import coo_matrix, csr_matrix
# 用COO格式构建上面那个图的稀疏表示
rows = [0, 0, 1, 2]
cols = [1, 2, 0, 0]
data = [1, 1, 1, 1]
sparse_coo = coo_matrix((data, (rows, cols)), shape=(3, 3))
print(sparse_coo)
# 输出坐标和值,不存多余的0
# 转换成CSR格式查邻居
sparse_csr = sparse_coo.tocsr()
# 查节点0的邻居
neighbors = sparse_csr.getrow(0).indices
print(neighbors) # [1, 2]
工程习惯:我建议在特征衍生阶段用COO格式,因为添加新边方便。模型训练阶段转CSR,查询效率高。别来回转——大量重复转换很耗内存。
5. 为何稀疏表示在反欺诈中如此重要?
反欺诈图谱有个特点:极度稀疏。一个用户在千万级用户的图中,通常只跟几十个节点有连接。稀疏度是 99.999% 级别的。
如果用稠密矩阵存储,大部分算力和内存都在处理0——浪费。稀疏表示只算有连接的部分,计算量和存储都是O(边数),这才是可行方案。
我曾经在处理一个5亿节点、200亿边的社交网络图时,用CSR格式存储,内存只用了30GB左右。换成稠密矩阵?想都不敢想。
说到这里,你应该能感觉到图谱特征工程的套路了:先确定节点和边是什么,再提取属性和结构特征,最后用稀疏表示把图存进计算机。这三步走扎实了,后面的特征衍生和模型训练才能吃得开。