1 数据集和机器学习库说明
1.1 数据集介绍
我们使用的数据集是 capitalbikeshare 包含了几百万条从2010-2020年的旅行记录数,将每一条旅途看做是邻接边列表,权重为两个车站之间旅行路线覆盖的次数。
构造数据的脚本 github jupyter
1.2 使用的node2vec库
我们使用 stellargraph 库(一个python实现的基于图计算的机器学习库) 来实现 node2vec算法。该库包含了诸多神经网络模型、数据集和demo。我们使用用了gensim 作为引擎来产生embedding的 node2vec 实现, stellargraph也包含了keras实现node2vec的实现版本。
1.3 任务说明
node2vec论文作者评估了其在不同数据集上的做链路和节点预测的表现。由于当前数据集没有自然标签,所以当前任务仅限于生成节点的embedding,并对这些embedding聚类。例如,对自行车站基于其在旅行网络中扮演的角色做聚类,评估其是否为一个中转节点。
2 探索步骤
2.1 数据标准化
将数据转化为 stellargraph 库可以识别的格式。该库可以从dataframe中读取边列表,只需要dataframe中的边包含了source 和 target 的边,边的其他属性跟在这两个属性之后。
我们只需要简单的加载 csv文件为dataframe,然后初始化一个带权重的有向图。
虽然node2vec的其他版本存在,但stellargraph库使有向加权网络的有偏向随机漫步的实现成为可能。
import pandas as pd
import networkx as nx
from gensim.models import Word2Vec
import stellargraph as sg
from stellargraph.data import BiasedRandomWalk
import os
import zipfile
import numpy as np
import matplotlib as plt
from sklearn.manifold import TSNE
from sklearn.metrics.pairwise import pairwise_distances
from IPython.display import display, HTML
import matplotlib.pyplot as plt
import igraph as ig
%matplotlib inline
# read csv data
graph_data = pd.read_csv('../data/capital_bikes/graph_data_full.csv')
# instantiate a directed graph with our edge list
graph_bikes = sg.StellarDiGraph(edges = graph_data)
# check that the attributes are correctly loaded
graph_bikes.info()
输出
'StellarDiGraph:
Directed multigraph\n Nodes: 708,
Edges: 150527
Node types:
default: [708]
Features: none
Edge types: default-default->default
Edge types:
default-default->default: [150527]
Weights: range=[1, 42863], mean=175.382, std=688.561\n
Features: none'
图包含了
• 708个节点,一个不再存在的车站也被包含进来了,为了获得更完整的网络视图
• 150000 条边
• 权重为每条旅行路线覆盖了两个车站的次数,所以其实也是个有向带权图
2.2 训练参数
接下来是分析随机游走,并将这些游走列表作为node2vec的输入。使用node2vec时,我们需要决定的是,要获得节点在网络中的同质性(社区)嵌入(embedding) 还是 结构性嵌入(embedding)
由于我们当前决定探索的是识别有相似结构的自行车站,所以我们决定对node2vec使用广度优先搜搜。使用的参数是论文中所使用的,
• 随机游走长度为
• 每个根节点随机游走次数是
• 返回率是
• 离开概率
• 结果embedding维度为
• 窗口尺寸为
• 训练次数
rw = BiasedRandomWalk(graph_bikes, p = 0.25, q = 1, n = 10, length = 80, seed=42, weighted = True)
walks = rw.run(nodes=list(graph_bikes.nodes())) # root nodes
# we pass the random walks to a list
str_walks = [[str(n) for n in walk] for walk in walks]
随机游走结束后,我们将结果解析成二维列表,这是最简单的方法将数据喂入word2vec来生成embedding。我们有708个节点,每个节点游走10次,最终会有7080次游走结果喂入模型;这些游走等同于word2vec中的句子。
2.3 结果解读
模型运行完之后,我们可以找到网络中以余弦距离计算最相似的节点。例如我们想看看在 Downtown DC中与Convention Center 最相似的自行车站。此车站位于镇中心,并且与一个地铁站相邻,并位于人流量较大区域。
# run the model for 20 epochs with a window size of 10.
model = Word2Vec(str_walks, size=128, window=10, min_count=1, sg=1, workers=4, iter=20)
# check for the most similar station in the embeddings space
model.wv.most_similar('Convention Center / 7th & M St NW')
[('5th & K St NW', 0.6973601579666138),
('8th & O St NW', 0.6972965002059937),
('8th & H St NW', 0.6935445070266724),
('Metro Center / 12th & G St NW', 0.6875154376029968),
('11th & M St NW', 0.6858331561088562),
('7th & F St NW / National Portrait Gallery', 0.684096097946167),
('Largo Town Center Metro', 0.6574602127075195),
('1301 McCormick Dr / Wayne K. Curry Admin Bldg', 0.6564565896987915),
('15th & K St NW', 0.6558513045310974),
('Thomas Circle', 0.6542125344276428)]
结果显示,大部分相似的节点都是在DC区,并且都与一个地铁站相邻,结果看起来不错。
为了进一步挖掘我们发现的是同质性还是结构性角色,我们需要对所有的节点的embedding做聚类,然后查看最终的聚类结果里面同一个簇内节点是否存在结构等同性。
# Retrieve node embeddings and corresponding subjects
node_ids = model.wv.index2word # list of node IDs
node_embeddings = (model.wv.vectors)
2.4 用kmean来判定节点结构相似性
2.4.1 kmean聚类判定节点
Kmean需要预定义最佳的聚类簇数量,但是HDBSCAN不需要。
from sklearn.cluster import KMeans
from sklearn import metrics
from scipy.spatial.distance import cdist
import numpy as np
import matplotlib.pyplot as plt
distortions = []
K = range(3,16)
for k in K:
k_cluster = KMeans(n_clusters=k, max_iter=500, random_state=3425).fit(node_embeddings)
k_cluster.fit(node_embeddings)
distortions.append(k_cluster.inertia_)
# Plot the elbow
plt.plot(K, distortions, 'bx-')
plt.xlabel('k')
plt.ylabel('Distortion')
plt.title('The Elbow Method showing the optimal k')
plt.show()
上面对kmean使用了多个聚类簇数量来寻找合适的k,发现k=10时效果可能是最好的。
获得10个聚类簇的kmean结果
kmeans_cluster = KMeans(n_clusters=10, init='k-means++', n_init=300, random_state=3425).fit(node_embeddings)
kmeans_labels = kmeans_cluster.labels_
2.4.2 HDBSCAN判定节点结构相似性
HDBSCAN采用基于密度的方法,寻找数据中比周围空间更密集的区域,并根据密集的区域定义集群。
该算法根据参数 min_samples在数据中的每个点周围创建一个圆,直到它包含了该参数定义的点的数量,在实践中它被设置为与min_cluster_size相同的值。这个圆圈的半径将等于与上一步定义的点在邻域中最远的距离;这被称为核心距离。
算法评估的是从一个圆圈到另外一个圆圈的难易程度,即相互可达距离(mutual reachability distance). 算法使用一种最小生成树来构建层次聚类。
最后,HDBSCAN游走最小生成树,并在每个划分中检查由该划分形成的新聚类节点数是否小于参数指定的最小聚类,如果是,则将这些节点当做噪音数据丢掉,但是保留父聚类中的节点。如果聚类划分的节点数满足参数定义的最小聚类节点数,则认为划分是有效的(创建新的聚类簇),当整个最小生成树遍历完算法结束。
import hdbscan
hdbs_model = hdbscan.HDBSCAN(min_cluster_size = 5)
hdbs_model.fit(node_embeddings)
hbds_scan_labels = hdbs_model.labels_
# create a dataframe of the nodes with their cluster labels
nodes_labels = pd.DataFrame(zip(node_ids, kmeans_labels, hbds_scan_labels), columns = ['node_ids','kmeans','hdbscan'])
2.5 使用T-SNE对聚类结果探索
对于上面有node2vec embedding特征后,使用聚类得到的节点标签,我们使用T-SNE来进一步探索。
T-SNE将高纬度的欧式距离转换为条件概率并尝试在高斯分布最大化相邻节点的概率密度,再使用梯度下降将高维数据降维到2-3维。
2.5.1 T-SEN对kmean的结果探索
# fit our embeddings with t-SNE
from sklearn.manifold import TSNE
trans = TSNE(n_components = 2, early_exaggeration = 10,
perplexity = 35, n_iter = 1000, n_iter_without_progress = 500,
learning_rate = 600.0, random_state = 42)
node_embeddings_2d = trans.fit_transform(node_embeddings)
# create the dataframe that has information about the nodes and their x and y coordinates
data_tsne = pd.DataFrame(zip(node_ids, list(node_embeddings_2d[:,0]),list(node_embeddings_2d[:,1])),columns = ['node_ids','x','y'])
data_tsne = pd.merge(data_tsne, nodes_labels, left_on='node_ids', right_on='node_ids', how = 'left')
# plot using seaborn.
import seaborn as sns
plt.figure(figsize=(10, 10))
sns.scatterplot(data=data_tsne, x='x', y='y',hue='kmeans', palette="bright",alpha=0.55, s=200).set_title('Node2vec clusters with k-means')
plt.savefig('images/kmeans_node2vec.svg')
plt.show
可以看到kmean产生的结果在T-SNE中被划分得很清晰,只有极少量样本有重叠。
2.5.2 T-SNE对HDBSCAN的结果探索
再对HDBSCAN的聚类结果使用T-SNE可视化
plt.figure(figsize=(10, 10))
sns.scatterplot(data=data_tsne, x='x', y='y',hue='hdbscan', palette="bright",alpha=0.75, s=200).set_title('Node2vec clusters with hdbscan')
plt.savefig('images/hdbscan_node2vec.svg')
plt.show
HDBSCAN找到了8个聚类簇,比kmeans少2个,包括一个大的中心簇和几个小一点但是稠密聚集的笑簇。需要注意的是,HDBSCAN给某些节点标签为-1,即离群点。
2.6 将聚类结果映射会站点地理位置
2.6.1 映射kmean的聚类结果
为了更好地了解它们是否是具有类似结构作用的节点集群,我将在地图上按集群成员的颜色编码绘制自行车站
import plotly.express as px
import plotly.offline as py_offline
stations = pd.read_csv('../data/capital_bikes/bike_locations.csv')
stations = pd.merge(stations, nodes_labels, left_on='ADDRESS', right_on='node_ids',
how = 'inner')
stations['kmeans'] = stations['kmeans'].astype(str)
stations['size'] = 5
fig = px.scatter_mapbox(stations, lat="LATITUDE", lon="LONGITUDE", hover_name="node_ids",
hover_data=['node_ids'],
size = 'size',
size_max = 10,
zoom=10,
width=700, height=600,
color_discrete_sequence=px.colors.qualitative.Plotly,
color = 'kmeans',
)
fig.update_layout(mapbox_style="carto-positron")
fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
fig.show()
可以看到结果中,每个聚类簇几乎与华盛顿大都会区的区域一一对应。例如簇2对应了DC中央区,并包含了人口最多的区域。橙色的簇8对应的是Arlington。
聚类结果基本挖掘到了节点的同质性,因为这些节点是地理位置决定的,其中相似的节点在地理上也是临近的站点。
2.6.2 HDBSCAN的结果映射回站点地理位置
检查HDBSCAN是否也能挖掘到车站节点的同质性。
import plotly.express as px
stations = pd.read_csv('../data/capital_bikes/bike_locations.csv')
stations = pd.merge(stations, nodes_labels, left_on='ADDRESS', right_on='node_ids',how = 'inner')
stations['hdbscan'] = stations['hdbscan'].astype(str)
stations['size'] = 5
fig = px.scatter_mapbox(stations, lat="LATITUDE", lon="LONGITUDE", hover_name="node_ids",
hover_data=['node_ids'],
size = 'size',
size_max = 10,
zoom=10,
width=700, height=600,
color_discrete_sequence=px.colors.qualitative.Plotly,
color = 'hdbscan',
)
fig.update_layout(mapbox_style="carto-positron")
fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
fig.show()
HDBSCAN同样也能挖掘到临近节点在地理位置上的相似性。
3 node2vec的不足
node2vec和Deepwalk算法的一个缺点是,你从这些算法中得到的结果是由网络的结构特征决定的,因此它们可能无法真正捕捉到结构的等价性。汉诺威大学和斯塔万格大学的研究人员表明,对于具有低聚类、低跨度和低互惠性的有向图,有偏向的随机漫步不会提供额外的信息,这些信息可以在创建嵌入时用来推断结构角色
我们来看看如果聚类系数很低时的结果
# check the structural characteristics of the graph
import networkx as nx
graph_bikes_nx = nx.from_pandas_edgelist(graph_data, 'source', 'target', ['weight'], create_using=nx.DiGraph)
graph_bikes_clustering_coefficient = nx.average_clustering(graph_bikes_nx, weight='weight')
graph_bikes_transitivity_coefficient = nx.transitivity(graph_bikes_nx)
graph_bikes_reciprocity = nx.reciprocity(graph_bikes_nx)
graph_bike_properties = {'clustering coefficient':graph_bikes_clustering_coefficient,
'transitivity_coefficient': graph_bikes_transitivity_coefficient,
'reciprocity': graph_bikes_reciprocity}
print(graph_bike_properties)