Skip to content

Commit cf3fec7

Browse files
committed
Add node embeddings visualization using t-SNE
1 parent b4f3b68 commit cf3fec7

File tree

3 files changed

+346
-0
lines changed

3 files changed

+346
-0
lines changed

jupyter/NodeEmbeddings.ipynb

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
{
2+
"cells": [
3+
{
4+
"attachments": {},
5+
"cell_type": "markdown",
6+
"id": "2f0eabc4",
7+
"metadata": {},
8+
"source": [
9+
"# Node Embeddings\n",
10+
"\n",
11+
"Here we will have a look at node embeddings and how to further reduce their dimensionality to be able to visualize them in a 2D plot. \n",
12+
"\n",
13+
"### Note about data dependencies\n",
14+
"\n",
15+
"PageRank centrality and Leiden community are also fetched from the Graph and need to be calculated first.\n",
16+
"This makes it easier to see in the visualization if the embeddings approximate the structural information of the graph.\n",
17+
"If these properties are missing you will only see black dots all of the same size without community coloring.\n",
18+
"In future it might make sense to also run a community detection algorithm co-located in here to not depend on the order of execution.\n",
19+
"\n",
20+
"<br> \n",
21+
"\n",
22+
"### References\n",
23+
"- [jqassistant](https://jqassistant.org)\n",
24+
"- [Neo4j Python Driver](https://neo4j.com/docs/api/python-driver/current)\n",
25+
"- [Tutorial: Applied Graph Embeddings](https://neo4j.com/developer/graph-data-science/applied-graph-embeddings)\n",
26+
"- [Visualizing the embeddings in 2D](https://github.com/openai/openai-cookbook/blob/main/examples/Visualizing_embeddings_in_2D.ipynb)\n",
27+
"- [Fast Random Projection](https://neo4j.com/docs/graph-data-science/current/machine-learning/node-embeddings/fastrp)\n",
28+
"- [scikit-learn TSNE](https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html#sklearn.manifold.TSNE)\n",
29+
"- [AttributeError: 'list' object has no attribute 'shape'](https://bobbyhadz.com/blog/python-attributeerror-list-object-has-no-attribute-shape)"
30+
]
31+
},
32+
{
33+
"cell_type": "code",
34+
"execution_count": null,
35+
"id": "4191f259",
36+
"metadata": {},
37+
"outputs": [],
38+
"source": [
39+
"import os\n",
40+
"import pandas as pd\n",
41+
"import matplotlib.pyplot as plot\n",
42+
"import typing as typ\n",
43+
"import numpy as np\n",
44+
"from sklearn.manifold import TSNE\n",
45+
"from neo4j import GraphDatabase"
46+
]
47+
},
48+
{
49+
"cell_type": "code",
50+
"execution_count": null,
51+
"id": "f8ef41ff",
52+
"metadata": {},
53+
"outputs": [],
54+
"source": [
55+
"import sklearn\n",
56+
"print('The scikit-learn version is {}.'.format(sklearn.__version__))\n",
57+
"print('The pandas version is {}.'.format(pd.__version__))\n"
58+
]
59+
},
60+
{
61+
"cell_type": "code",
62+
"execution_count": null,
63+
"id": "1c5dab37",
64+
"metadata": {},
65+
"outputs": [],
66+
"source": [
67+
"# Please set the environment variable \"NEO4J_INITIAL_PASSWORD\" in your shell \n",
68+
"# before starting jupyter notebook to provide the password for the user \"neo4j\". \n",
69+
"# It is not recommended to hardcode the password into jupyter notebook for security reasons.\n",
70+
"\n",
71+
"driver = GraphDatabase.driver(uri=\"bolt://localhost:7687\", auth=(\"neo4j\", os.environ.get(\"NEO4J_INITIAL_PASSWORD\")))\n",
72+
"driver.verify_connectivity()"
73+
]
74+
},
75+
{
76+
"cell_type": "code",
77+
"execution_count": null,
78+
"id": "c1db254b",
79+
"metadata": {},
80+
"outputs": [],
81+
"source": [
82+
"def get_cypher_query_from_file(filename):\n",
83+
" with open(filename) as file:\n",
84+
" return ' '.join(file.readlines())"
85+
]
86+
},
87+
{
88+
"cell_type": "code",
89+
"execution_count": null,
90+
"id": "59310f6f",
91+
"metadata": {},
92+
"outputs": [],
93+
"source": [
94+
"def query_cypher_to_data_frame(filename, parameters_: typ.Optional[typ.Dict[str, typ.Any]] = None):\n",
95+
" records, summary, keys = driver.execute_query(get_cypher_query_from_file(filename),parameters_=parameters_)\n",
96+
" return pd.DataFrame([r.values() for r in records], columns=keys)"
97+
]
98+
},
99+
{
100+
"cell_type": "code",
101+
"execution_count": null,
102+
"id": "da9e8edb",
103+
"metadata": {},
104+
"outputs": [],
105+
"source": [
106+
"#The following cell uses the build-in %html \"magic\" to override the CSS style for tables to a much smaller size.\n",
107+
"#This is especially needed for PDF export of tables with multiple columns."
108+
]
109+
},
110+
{
111+
"cell_type": "code",
112+
"execution_count": null,
113+
"id": "9deaabce",
114+
"metadata": {},
115+
"outputs": [],
116+
"source": [
117+
"%%html\n",
118+
"<style>\n",
119+
"/* CSS style for smaller dataframe tables. */\n",
120+
".dataframe th {\n",
121+
" font-size: 8px;\n",
122+
"}\n",
123+
".dataframe td {\n",
124+
" font-size: 8px;\n",
125+
"}\n",
126+
"</style>"
127+
]
128+
},
129+
{
130+
"cell_type": "code",
131+
"execution_count": null,
132+
"id": "c2496caf",
133+
"metadata": {},
134+
"outputs": [],
135+
"source": [
136+
"# Main Colormap\n",
137+
"main_color_map = 'nipy_spectral'"
138+
]
139+
},
140+
{
141+
"cell_type": "markdown",
142+
"id": "0c68aa20",
143+
"metadata": {},
144+
"source": [
145+
"## Preparation"
146+
]
147+
},
148+
{
149+
"cell_type": "markdown",
150+
"id": "fcec9b7d",
151+
"metadata": {},
152+
"source": [
153+
"### Create Graph Projection\n",
154+
"\n",
155+
"Create an in-memory undirected graph projection containing Package nodes (vertices) and their dependencies (edges)."
156+
]
157+
},
158+
{
159+
"cell_type": "code",
160+
"execution_count": null,
161+
"id": "20190661",
162+
"metadata": {},
163+
"outputs": [],
164+
"source": [
165+
"package_embeddings_parameters={\n",
166+
" \"dependencies_projection\": \"package-embeddings-notebook\",\n",
167+
" \"dependencies_projection_node\": \"Package\",\n",
168+
" \"dependencies_projection_weight_property\": \"weight25PercentInterfaces\",\n",
169+
" \"dependencies_projection_wright_property\": \"nodeEmbeddingsFastRandomProjection\",\n",
170+
" \"dependencies_projection_embedding_dimension\":\"64\" \n",
171+
"}"
172+
]
173+
},
174+
{
175+
"cell_type": "code",
176+
"execution_count": null,
177+
"id": "82e99db2",
178+
"metadata": {},
179+
"outputs": [],
180+
"source": [
181+
"query_cypher_to_data_frame(\"../cypher/Dependencies_Projection/Dependencies_1_Delete_Projection.cypher\", package_embeddings_parameters)\n",
182+
"query_cypher_to_data_frame(\"../cypher/Dependencies_Projection/Dependencies_2_Delete_Subgraph.cypher\", package_embeddings_parameters)\n",
183+
"query_cypher_to_data_frame(\"../cypher/Dependencies_Projection/Dependencies_4_Create_Undirected_Projection.cypher\", package_embeddings_parameters)\n",
184+
"query_cypher_to_data_frame(\"../cypher/Dependencies_Projection/Dependencies_5_Create_Subgraph.cypher\", package_embeddings_parameters)"
185+
]
186+
},
187+
{
188+
"cell_type": "markdown",
189+
"id": "145dca19",
190+
"metadata": {},
191+
"source": [
192+
"### Generate Node Embeddings using Fast Random Projection (Fast RP)\n",
193+
"\n",
194+
"[Fast Random Projection](https://neo4j.com/docs/graph-data-science/current/machine-learning/node-embeddings/fastrp) calculates an array of floats (length = embedding dimension) for every node in the graph. These numbers approximate the relationship and similarity information of each node and are called node embeddings. Random Projections is used to reduce the dimensionality of the node feature space while preserving pairwise distances.\n",
195+
"\n",
196+
"The result can be used in machine learning as features approximating the graph structure. It can also be used to further reduce the dimensionality to visualize the graph in a 2D plot, as we will be doing here."
197+
]
198+
},
199+
{
200+
"cell_type": "code",
201+
"execution_count": null,
202+
"id": "8efca2cf",
203+
"metadata": {},
204+
"outputs": [],
205+
"source": [
206+
"\n",
207+
"fast_random_projection = query_cypher_to_data_frame(\"../cypher/Node_Embeddings/Node_Embeddings_1d_Fast_Random_Projection_Stream.cypher\", package_embeddings_parameters)\n",
208+
"fast_random_projection.head() # Look at the first entries of the table \n"
209+
]
210+
},
211+
{
212+
"cell_type": "markdown",
213+
"id": "76d8bca1",
214+
"metadata": {},
215+
"source": [
216+
"### Dimensionality reduction with t-distributed stochastic neighbor embedding (t-SNE)\n",
217+
"\n",
218+
"This step takes the original node embeddings with a higher dimensionality (e.g. list of 32 floats) and\n",
219+
"reduces them to a 2 dimensional array for visualization. \n",
220+
"\n",
221+
"> It converts similarities between data points to joint probabilities and tries to minimize the Kullback-Leibler divergence between the joint probabilities of the low-dimensional embedding and the high-dimensional data.\n",
222+
"\n",
223+
"(see https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html#sklearn.manifold.TSNE)"
224+
]
225+
},
226+
{
227+
"cell_type": "code",
228+
"execution_count": null,
229+
"id": "b2de000f",
230+
"metadata": {},
231+
"outputs": [],
232+
"source": [
233+
"# Calling the fit_transform method just with a list doesn't seem to work (anymore?). \n",
234+
"# It leads to an error with the following message: 'list' object has no attribute 'shape'\n",
235+
"# This can be solved by converting the list to a numpy array using np.array(..).\n",
236+
"# See https://bobbyhadz.com/blog/python-attributeerror-list-object-has-no-attribute-shape\n",
237+
"embeddings_as_numpy_array = np.array(fast_random_projection.embedding.to_list())\n",
238+
"\n",
239+
"# Use TSNE to reduce the dimensionality of the previous calculated node embeddings to 2 dimensions for visualization\n",
240+
"t_distributed_stochastic_neighbor_embedding = TSNE(n_components=2, verbose=1, random_state=50)\n",
241+
"two_dimension_node_embeddings = t_distributed_stochastic_neighbor_embedding.fit_transform(embeddings_as_numpy_array)\n",
242+
"two_dimension_node_embeddings.shape"
243+
]
244+
},
245+
{
246+
"cell_type": "code",
247+
"execution_count": null,
248+
"id": "8ce7ea41",
249+
"metadata": {},
250+
"outputs": [],
251+
"source": [
252+
"# Create a new DataFrame with the results of the 2 dimensional node embeddings\n",
253+
"# and the code unit and artifact name of the query above as preparation for the plot\n",
254+
"node_embeddings_for_visualization = pd.DataFrame(data = {\n",
255+
" \"codeUnit\": fast_random_projection.codeUnitName,\n",
256+
" \"artifact\": fast_random_projection.artifactName,\n",
257+
" \"communityId\": fast_random_projection.communityId,\n",
258+
" \"centrality\": fast_random_projection.centrality,\n",
259+
" \"x\": [value[0] for value in two_dimension_node_embeddings],\n",
260+
" \"y\": [value[1] for value in two_dimension_node_embeddings]\n",
261+
"})\n",
262+
"node_embeddings_for_visualization.head()"
263+
]
264+
},
265+
{
266+
"cell_type": "code",
267+
"execution_count": null,
268+
"id": "459a819c",
269+
"metadata": {},
270+
"outputs": [],
271+
"source": [
272+
"plot.scatter(\n",
273+
" x=node_embeddings_for_visualization.x,\n",
274+
" y=node_embeddings_for_visualization.y,\n",
275+
" s=node_embeddings_for_visualization.centrality * 200,\n",
276+
" c=node_embeddings_for_visualization.communityId,\n",
277+
" cmap=main_color_map,\n",
278+
")\n",
279+
"plot.title(\"Package nodes positioned by their dependency relationships using t-SNE\")\n",
280+
"plot.show()"
281+
]
282+
}
283+
],
284+
"metadata": {
285+
"authors": [
286+
{
287+
"name": "JohT"
288+
}
289+
],
290+
"kernelspec": {
291+
"display_name": "Python 3 (ipykernel)",
292+
"language": "python",
293+
"name": "python3"
294+
},
295+
"language_info": {
296+
"codemirror_mode": {
297+
"name": "ipython",
298+
"version": 3
299+
},
300+
"file_extension": ".py",
301+
"mimetype": "text/x-python",
302+
"name": "python",
303+
"nbconvert_exporter": "python",
304+
"pygments_lexer": "ipython3",
305+
"version": "3.11.4"
306+
},
307+
"title": "Object Oriented Design Quality Metrics for Java with Neo4j"
308+
},
309+
"nbformat": 4,
310+
"nbformat_minor": 5
311+
}

jupyter/environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ dependencies:
1010
- numpy=1.23.*
1111
- pandas=1.5.*
1212
- pip=22.3.*
13+
- scikit-learn=1.3.* # NodeEmbeddings.ipynb uses sklearn.manifold.TSNE
1314
- pip:
1415
- monotonic==1.*
1516
- wordcloud==1.9.*
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env bash
2+
3+
# Creates the "node-embeddings" report (ipynb, md, pdf) based on the Jupyter Notebook "NodeEmbeddings.ipynb".
4+
# It shows how to create node embeddings for package dependencies using "Fast Random Projection" and
5+
# how these embeddings can be further reduced in their dimensionality down to two dimensions for visualization.
6+
# The plot also shows the community as color and the PageRank as size to have a visual feedback on how well they are clustered.
7+
8+
# Requires executeJupyterNotebook.sh
9+
10+
# Overrideable Constants (defaults also defined in sub scripts)
11+
REPORTS_DIRECTORY=${REPORTS_DIRECTORY:-"reports"}
12+
13+
## Get this "scripts/reports" directory if not already set
14+
# Even if $BASH_SOURCE is made for Bourne-like shells it is also supported by others and therefore here the preferred solution.
15+
# CDPATH reduces the scope of the cd command to potentially prevent unintended directory changes.
16+
# This way non-standard tools like readlink aren't needed.
17+
REPORTS_SCRIPT_DIR=${REPORTS_SCRIPT_DIR:-$( CDPATH=. cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P )}
18+
echo "NodeEmbeddingsJupyter: REPORTS_SCRIPT_DIR=${REPORTS_SCRIPT_DIR}"
19+
20+
# Get the "scripts" directory by taking the path of this script and going one directory up.
21+
SCRIPTS_DIR=${SCRIPTS_DIR:-"${REPORTS_SCRIPT_DIR}/.."} # Repository directory containing the shell scripts
22+
echo "NodeEmbeddingsJupyter: SCRIPTS_DIR=${SCRIPTS_DIR}"
23+
24+
# Get the "jupyter" directory by taking the path of this script and going two directory up and then to "jupyter".
25+
JUPYTER_NOTEBOOK_DIRECTORY=${JUPYTER_NOTEBOOK_DIRECTORY:-"${SCRIPTS_DIR}/../jupyter"} # Repository directory containing the Jupyter Notebooks
26+
echo "NodeEmbeddingsJupyter: JUPYTER_NOTEBOOK_DIRECTORY=$JUPYTER_NOTEBOOK_DIRECTORY"
27+
28+
# Create report directory
29+
REPORT_NAME="node-embeddings"
30+
FULL_REPORT_DIRECTORY="${REPORTS_DIRECTORY}/${REPORT_NAME}"
31+
mkdir -p "${FULL_REPORT_DIRECTORY}"
32+
33+
# Execute and convert the Jupyter Notebook "InternalDependencies.ipynb" within the given reports directory
34+
(cd "${FULL_REPORT_DIRECTORY}" && exec ${SCRIPTS_DIR}/executeJupyterNotebook.sh ${JUPYTER_NOTEBOOK_DIRECTORY}/NodeEmbeddings.ipynb) || exit 1

0 commit comments

Comments
 (0)