graph-dictlearn 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graph_dictlearn-0.1.0/LICENSE +21 -0
- graph_dictlearn-0.1.0/PKG-INFO +34 -0
- graph_dictlearn-0.1.0/README.md +4 -0
- graph_dictlearn-0.1.0/pyproject.toml +43 -0
- graph_dictlearn-0.1.0/setup.cfg +4 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn/__init__.py +8 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn/dict_learners/__init__.py +5 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn/dict_learners/aksvd.py +30 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn/dict_learners/base.py +13 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn/dict_learners/ksvd.py +84 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn/encoders/__init__.py +5 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn/encoders/base.py +54 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn/encoders/wl.py +147 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn/encoders/wl_hashing.py +97 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn/loader/__init__.py +5 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn/loader/loader.py +86 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn.egg-info/PKG-INFO +34 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn.egg-info/SOURCES.txt +19 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn.egg-info/dependency_links.txt +1 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn.egg-info/requires.txt +10 -0
- graph_dictlearn-0.1.0/src/graph_dictlearn.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nimendra Gunawardana
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: graph_dictlearn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A set of representation learning and dictionary learning algorithms for graph data.
|
|
5
|
+
Author-email: Nimendra Gnawardana <nimendraamiththa1@gmail.com>, Siyathma Wedamulla <wedamullasiyathma@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nimendra-ag/graph_dictlearn
|
|
8
|
+
Project-URL: Repository, https://github.com/nimendra-ag/graph_dictlearn
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: gensim==4.4.0
|
|
20
|
+
Requires-Dist: joblib==1.5.3
|
|
21
|
+
Requires-Dist: matplotlib==3.11.0
|
|
22
|
+
Requires-Dist: networkx==3.6.1
|
|
23
|
+
Requires-Dist: numpy==2.5.0
|
|
24
|
+
Requires-Dist: pandas==3.0.3
|
|
25
|
+
Requires-Dist: rdkit==2026.3.3
|
|
26
|
+
Requires-Dist: scikit-learn==1.9.0
|
|
27
|
+
Requires-Dist: scipy==1.18.0
|
|
28
|
+
Requires-Dist: seaborn==0.13.2
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# graph_dictlearn
|
|
32
|
+
A set of representation learning and dictionary learning algorithms for graph data.
|
|
33
|
+
|
|
34
|
+
You can use this pip package to apply dictionary learning algorithms for graph data.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "graph_dictlearn"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A set of representation learning and dictionary learning algorithms for graph data."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Nimendra Gnawardana", email = "nimendraamiththa1@gmail.com"},
|
|
14
|
+
{name = "Siyathma Wedamulla", email = "wedamullasiyathma@gmail.com"}
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"gensim==4.4.0",
|
|
27
|
+
"joblib==1.5.3",
|
|
28
|
+
"matplotlib==3.11.0",
|
|
29
|
+
"networkx==3.6.1",
|
|
30
|
+
"numpy==2.5.0",
|
|
31
|
+
"pandas==3.0.3",
|
|
32
|
+
"rdkit==2026.3.3",
|
|
33
|
+
"scikit-learn==1.9.0",
|
|
34
|
+
"scipy==1.18.0",
|
|
35
|
+
"seaborn==0.13.2",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/nimendra-ag/graph_dictlearn"
|
|
40
|
+
Repository = "https://github.com/nimendra-ag/graph_dictlearn"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["src"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""A set of representation learning and dictionary learning algorithms for graph data."""
|
|
2
|
+
|
|
3
|
+
from graph_dictlearn.loader.loader import SDFLoader
|
|
4
|
+
from graph_dictlearn.encoders.wl import WL
|
|
5
|
+
from graph_dictlearn.dict_learners.aksvd import AKSVD
|
|
6
|
+
|
|
7
|
+
__all__ = ["SDFLoader", "WL", "AKSVD"]
|
|
8
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from graph_dictlearn.dict_learners.base import DictLearner
|
|
2
|
+
from graph_dictlearn.dict_learners.ksvd import ApproximateKSVD
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AKSVD(DictLearner):
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
dimensions: int = 16,
|
|
9
|
+
max_iter: int = 10,
|
|
10
|
+
tol: float = 1e-6,
|
|
11
|
+
n_non_zero_coefs: int = 10,
|
|
12
|
+
):
|
|
13
|
+
super().__init__(name="AKSVD")
|
|
14
|
+
self._dictionary = None
|
|
15
|
+
self.dimensions = dimensions
|
|
16
|
+
self.max_iter = max_iter
|
|
17
|
+
self.tol = tol
|
|
18
|
+
self.n_non_zero_coefs = n_non_zero_coefs
|
|
19
|
+
self.aksvd = ApproximateKSVD(n_components=self.dimensions, max_iter=self.max_iter, tol=self.tol,
|
|
20
|
+
transform_n_nonzero_coefs=self.n_non_zero_coefs)
|
|
21
|
+
|
|
22
|
+
def fit(self, training_graph_embeddings):
|
|
23
|
+
self._dictionary = self.aksvd.fit(training_graph_embeddings).components_
|
|
24
|
+
|
|
25
|
+
# self._embedding = self.aksvd.transform(training_graph_embeddings)
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def infer(self, infer_graph_embeddings):
|
|
29
|
+
sparse_embeddings = self.aksvd.transform(infer_graph_embeddings)
|
|
30
|
+
return sparse_embeddings
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
class DictLearner(ABC):
|
|
4
|
+
def __init__(self, name: str):
|
|
5
|
+
self.name = name
|
|
6
|
+
|
|
7
|
+
@abstractmethod
|
|
8
|
+
def fit(self, training_graph_embeddings):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
@abstractmethod # ← add this decorator
|
|
12
|
+
def infer(self, infer_graph_embeddings):
|
|
13
|
+
pass
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy as sp
|
|
3
|
+
from sklearn.linear_model import orthogonal_mp_gram
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ApproximateKSVD():
|
|
7
|
+
def __init__(self, n_components, max_iter=10, tol=1e-6,
|
|
8
|
+
transform_n_nonzero_coefs=None):
|
|
9
|
+
"""
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
n_components:
|
|
13
|
+
Number of dictionary elements
|
|
14
|
+
|
|
15
|
+
max_iter:
|
|
16
|
+
Maximum number of iterations
|
|
17
|
+
|
|
18
|
+
tol:
|
|
19
|
+
tolerance for error
|
|
20
|
+
|
|
21
|
+
transform_n_nonzero_coefs:
|
|
22
|
+
Number of nonzero coefficients to target
|
|
23
|
+
"""
|
|
24
|
+
self.components_ = None
|
|
25
|
+
self.max_iter = max_iter
|
|
26
|
+
self.tol = tol
|
|
27
|
+
self.n_components = n_components
|
|
28
|
+
self.transform_n_nonzero_coefs = transform_n_nonzero_coefs
|
|
29
|
+
|
|
30
|
+
def _update_dict(self, X, D, gamma):
|
|
31
|
+
for j in range(self.n_components):
|
|
32
|
+
I = gamma[:, j] > 0
|
|
33
|
+
if np.sum(I) == 0:
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
D[j, :] = 0
|
|
37
|
+
g = gamma[I, j].T
|
|
38
|
+
r = X[I, :] - gamma[I, :].dot(D)
|
|
39
|
+
d = r.T.dot(g)
|
|
40
|
+
d /= np.linalg.norm(d)
|
|
41
|
+
g = r.dot(d)
|
|
42
|
+
D[j, :] = d
|
|
43
|
+
gamma[I, j] = g.T
|
|
44
|
+
return D, gamma
|
|
45
|
+
|
|
46
|
+
def _initialize(self, X):
|
|
47
|
+
if min(X.shape) < self.n_components:
|
|
48
|
+
D = np.random.randn(self.n_components, X.shape[1])
|
|
49
|
+
else:
|
|
50
|
+
u, s, vt = sp.sparse.linalg.svds(X, k=self.n_components)
|
|
51
|
+
D = np.dot(np.diag(s), vt)
|
|
52
|
+
D /= np.linalg.norm(D, axis=1)[:, np.newaxis]
|
|
53
|
+
return D
|
|
54
|
+
|
|
55
|
+
def _transform(self, D, X):
|
|
56
|
+
gram = D.dot(D.T)
|
|
57
|
+
Xy = D.dot(X.T)
|
|
58
|
+
|
|
59
|
+
n_nonzero_coefs = self.transform_n_nonzero_coefs
|
|
60
|
+
if n_nonzero_coefs is None:
|
|
61
|
+
n_nonzero_coefs = int(0.1 * X.shape[1])
|
|
62
|
+
|
|
63
|
+
return orthogonal_mp_gram(
|
|
64
|
+
gram, Xy, n_nonzero_coefs=n_nonzero_coefs).T
|
|
65
|
+
|
|
66
|
+
def fit(self, X):
|
|
67
|
+
"""
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
X: shape = [n_samples, n_features]
|
|
71
|
+
"""
|
|
72
|
+
D = self._initialize(X)
|
|
73
|
+
for i in range(self.max_iter):
|
|
74
|
+
gamma = self._transform(D, X)
|
|
75
|
+
e = np.linalg.norm(X - gamma.dot(D))
|
|
76
|
+
if e < self.tol:
|
|
77
|
+
break
|
|
78
|
+
D, gamma = self._update_dict(X, D, gamma)
|
|
79
|
+
|
|
80
|
+
self.components_ = D
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def transform(self, X):
|
|
84
|
+
return self._transform(self.components_, X)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import random
|
|
3
|
+
import numpy as np
|
|
4
|
+
import networkx as nx
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
# abstract base class
|
|
8
|
+
class GraphEncoder(ABC):
|
|
9
|
+
def __init__(self, name: str):
|
|
10
|
+
super().__init__()
|
|
11
|
+
self.name = name
|
|
12
|
+
self.embeddings = None
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def generate_training_embeddings(self, graphs):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def generate_inferencing_embeddings(self, graphs):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def _set_seed(self):
|
|
23
|
+
"""Creating the initial random seed."""
|
|
24
|
+
random.seed(self.seed)
|
|
25
|
+
np.random.seed(self.seed)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def _ensure_integrity(graph: nx.classes.graph.Graph) -> nx.classes.graph.Graph:
|
|
29
|
+
"""Ensure walk traversal conditions."""
|
|
30
|
+
edge_list = [(index, index) for index in range(graph.number_of_nodes())]
|
|
31
|
+
graph.add_edges_from(edge_list)
|
|
32
|
+
|
|
33
|
+
return graph
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _check_indexing(graph: nx.classes.graph.Graph):
|
|
37
|
+
"""Checking the consecutive numeric indexing."""
|
|
38
|
+
numeric_indices = [index for index in range(graph.number_of_nodes())]
|
|
39
|
+
node_indices = sorted([node for node in graph.nodes()])
|
|
40
|
+
|
|
41
|
+
assert numeric_indices == node_indices, "The node indexing is wrong."
|
|
42
|
+
|
|
43
|
+
def _check_graph(self, graph: nx.classes.graph.Graph) -> nx.classes.graph.Graph:
|
|
44
|
+
"""Check the Karate Club assumptions about the graph."""
|
|
45
|
+
self._check_indexing(graph)
|
|
46
|
+
graph = self._ensure_integrity(graph)
|
|
47
|
+
|
|
48
|
+
return graph
|
|
49
|
+
|
|
50
|
+
def _check_graphs(self, graphs: List[nx.classes.graph.Graph]):
|
|
51
|
+
"""Check the Karate Club assumptions for a list of graphs."""
|
|
52
|
+
graphs = [self._check_graph(graph) for graph in graphs]
|
|
53
|
+
|
|
54
|
+
return graphs
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from gensim.models.doc2vec import TaggedDocument
|
|
3
|
+
from graph_dictlearn.encoders.base import GraphEncoder
|
|
4
|
+
from graph_dictlearn.encoders.wl_hashing import WeisfeilerLehmanHashing
|
|
5
|
+
from collections import Counter
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WL(GraphEncoder):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
wl_iterations: int = 2,
|
|
15
|
+
attributed: bool = True,
|
|
16
|
+
erase_base_features: bool = True,
|
|
17
|
+
n_vocab: int = 1000,
|
|
18
|
+
min_features: int = 50,
|
|
19
|
+
seed: int = 42
|
|
20
|
+
):
|
|
21
|
+
|
|
22
|
+
super().__init__(name="ImbalanceAwareWL")
|
|
23
|
+
|
|
24
|
+
self.seed = seed
|
|
25
|
+
self.vocab = None
|
|
26
|
+
self.graph_embeddings = None
|
|
27
|
+
self.wl_iterations = wl_iterations
|
|
28
|
+
self.attributed = attributed
|
|
29
|
+
self.erase_base_features = erase_base_features
|
|
30
|
+
self.n_vocab = n_vocab
|
|
31
|
+
self.min_features = min_features
|
|
32
|
+
|
|
33
|
+
def create_wl_hash(self, graph_list):
|
|
34
|
+
|
|
35
|
+
documents = []
|
|
36
|
+
|
|
37
|
+
for graph in graph_list:
|
|
38
|
+
g = self._check_graph(graph)
|
|
39
|
+
|
|
40
|
+
document = WeisfeilerLehmanHashing(
|
|
41
|
+
g, self.wl_iterations, self.attributed, self.erase_base_features)
|
|
42
|
+
|
|
43
|
+
documents.append(document)
|
|
44
|
+
|
|
45
|
+
documents = [
|
|
46
|
+
TaggedDocument(words=doc.get_graph_features(), tags=[str(i)])
|
|
47
|
+
for i, doc in enumerate(documents)
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
return documents
|
|
51
|
+
|
|
52
|
+
def create_vocab(self, corpus, labels):
|
|
53
|
+
majority_df = Counter()
|
|
54
|
+
minority_df = Counter()
|
|
55
|
+
|
|
56
|
+
majority_graphs = 0
|
|
57
|
+
minority_graphs = 0
|
|
58
|
+
|
|
59
|
+
for doc, label in zip(corpus, labels):
|
|
60
|
+
|
|
61
|
+
# unique subtree hashes in this graph
|
|
62
|
+
# document frequency instead of raw counts
|
|
63
|
+
unique_words = Counter(doc.words)
|
|
64
|
+
if label == -1:
|
|
65
|
+
majority_graphs += 1
|
|
66
|
+
for word in unique_words:
|
|
67
|
+
majority_df[word] += 1
|
|
68
|
+
else:
|
|
69
|
+
minority_graphs += 1
|
|
70
|
+
for word in unique_words:
|
|
71
|
+
minority_df[word] += 1
|
|
72
|
+
|
|
73
|
+
all_words = set(list(majority_df.keys()) + list(minority_df.keys()))
|
|
74
|
+
|
|
75
|
+
scored_vocab = []
|
|
76
|
+
|
|
77
|
+
for word in all_words:
|
|
78
|
+
p_majority = majority_df[word] / majority_graphs
|
|
79
|
+
|
|
80
|
+
p_minority = (minority_df[word] / minority_graphs)
|
|
81
|
+
|
|
82
|
+
discriminative_score = abs(np.sqrt(p_majority) - np.sqrt(p_minority))
|
|
83
|
+
|
|
84
|
+
total_presence = p_majority + p_minority
|
|
85
|
+
|
|
86
|
+
# Final score
|
|
87
|
+
|
|
88
|
+
score = total_presence * discriminative_score
|
|
89
|
+
scored_vocab.append((word, score))
|
|
90
|
+
|
|
91
|
+
# Sort features by discriminative importance
|
|
92
|
+
scored_vocab = sorted(
|
|
93
|
+
scored_vocab,
|
|
94
|
+
key=lambda x: x[1],
|
|
95
|
+
reverse=True
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# selection
|
|
99
|
+
scores = np.array([x[1] for x in scored_vocab])
|
|
100
|
+
|
|
101
|
+
threshold = scores.mean() - scores.std()
|
|
102
|
+
trimmed_vocab = [item for item in scored_vocab if item[1] >= threshold]
|
|
103
|
+
|
|
104
|
+
# fallback if too few selected
|
|
105
|
+
logger.info("Selected %d features from adaptive selection", len(trimmed_vocab))
|
|
106
|
+
if len(trimmed_vocab) < 50:
|
|
107
|
+
trimmed_vocab = scored_vocab[:self.n_vocab]
|
|
108
|
+
|
|
109
|
+
self.n_vocab = len(trimmed_vocab)
|
|
110
|
+
return trimmed_vocab
|
|
111
|
+
|
|
112
|
+
def calc_coefficients(self, corpus):
|
|
113
|
+
|
|
114
|
+
sparse_vector = np.zeros([len(corpus), self.n_vocab])
|
|
115
|
+
|
|
116
|
+
i = 0
|
|
117
|
+
for corpus in corpus:
|
|
118
|
+
words = corpus.words
|
|
119
|
+
|
|
120
|
+
words_count = Counter(corpus.words)
|
|
121
|
+
j = 0
|
|
122
|
+
for atom, _ in self.vocab:
|
|
123
|
+
sparse_vector[i][j] = words_count[atom]
|
|
124
|
+
j = j + 1
|
|
125
|
+
|
|
126
|
+
i = i + 1
|
|
127
|
+
|
|
128
|
+
norms = np.linalg.norm(sparse_vector, axis=1, keepdims=True)
|
|
129
|
+
norms[norms == 0] = 1.0
|
|
130
|
+
sparse_vector = sparse_vector / norms
|
|
131
|
+
|
|
132
|
+
return sparse_vector
|
|
133
|
+
|
|
134
|
+
def generate_training_embeddings(self, graphs, labels):
|
|
135
|
+
self._set_seed()
|
|
136
|
+
documents = self.create_wl_hash(graphs)
|
|
137
|
+
self.vocab = self.create_vocab(documents, labels)
|
|
138
|
+
train_graph_embeddings = self.calc_coefficients(documents)
|
|
139
|
+
return train_graph_embeddings
|
|
140
|
+
|
|
141
|
+
def generate_inferencing_embeddings(self, graphs):
|
|
142
|
+
self._set_seed()
|
|
143
|
+
documents = self.create_wl_hash(graphs)
|
|
144
|
+
infer_graph_embeddings = self.calc_coefficients(
|
|
145
|
+
documents
|
|
146
|
+
)
|
|
147
|
+
return infer_graph_embeddings
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import networkx as nx
|
|
3
|
+
from typing import List, Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WeisfeilerLehmanHashing(object):
|
|
7
|
+
"""
|
|
8
|
+
Weisfeiler-Lehman feature extractor class.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
graph (NetworkX graph): NetworkX graph for which we do WL hashing.
|
|
12
|
+
wl_iterations (int): Number of WL iterations.
|
|
13
|
+
attributed (bool): Presence of attributes.
|
|
14
|
+
erase_base_feature (bool): Deleting the base features.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
graph: nx.classes.graph.Graph,
|
|
20
|
+
wl_iterations: int,
|
|
21
|
+
attributed: bool,
|
|
22
|
+
erase_base_features: bool,
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Initialization method which also executes feature extraction.
|
|
26
|
+
"""
|
|
27
|
+
self.wl_iterations = wl_iterations
|
|
28
|
+
self.graph = graph
|
|
29
|
+
self.attributed = attributed
|
|
30
|
+
self.erase_base_features = erase_base_features
|
|
31
|
+
self._set_features()
|
|
32
|
+
self._do_recursions()
|
|
33
|
+
|
|
34
|
+
def _set_features(self):
|
|
35
|
+
"""
|
|
36
|
+
Creating the features.
|
|
37
|
+
"""
|
|
38
|
+
if self.attributed:
|
|
39
|
+
self.features = nx.get_node_attributes(self.graph, "feature")
|
|
40
|
+
else:
|
|
41
|
+
self.features = {
|
|
42
|
+
node: self.graph.degree(node) for node in self.graph.nodes()
|
|
43
|
+
}
|
|
44
|
+
self.extracted_features = {k: [str(v)] for k, v in self.features.items()}
|
|
45
|
+
|
|
46
|
+
def _erase_base_features(self):
|
|
47
|
+
"""
|
|
48
|
+
Erasing the base features
|
|
49
|
+
"""
|
|
50
|
+
for k, v in self.extracted_features.items():
|
|
51
|
+
del self.extracted_features[k][0]
|
|
52
|
+
|
|
53
|
+
def _do_a_recursion(self):
|
|
54
|
+
"""
|
|
55
|
+
The method does a single WL recursion.
|
|
56
|
+
|
|
57
|
+
Return types:
|
|
58
|
+
* **new_features** *(dict of strings)* - The hash table with extracted WL features.
|
|
59
|
+
"""
|
|
60
|
+
new_features = {}
|
|
61
|
+
for node in self.graph.nodes():
|
|
62
|
+
nebs = self.graph.neighbors(node)
|
|
63
|
+
degs = [self.features[neb] for neb in nebs]
|
|
64
|
+
features = [str(self.features[node])] + sorted([str(deg) for deg in degs])
|
|
65
|
+
features = "_".join(features)
|
|
66
|
+
hash_object = hashlib.md5(features.encode())
|
|
67
|
+
hashing = hash_object.hexdigest()
|
|
68
|
+
new_features[node] = hashing
|
|
69
|
+
self.extracted_features = {
|
|
70
|
+
k: self.extracted_features[k] + [v] for k, v in new_features.items()
|
|
71
|
+
}
|
|
72
|
+
return new_features
|
|
73
|
+
|
|
74
|
+
def _do_recursions(self):
|
|
75
|
+
"""
|
|
76
|
+
The method does a series of WL recursions.
|
|
77
|
+
"""
|
|
78
|
+
for _ in range(self.wl_iterations):
|
|
79
|
+
self.features = self._do_a_recursion()
|
|
80
|
+
if self.erase_base_features:
|
|
81
|
+
self._erase_base_features()
|
|
82
|
+
|
|
83
|
+
def get_node_features(self) -> Dict[int, List[str]]:
|
|
84
|
+
"""
|
|
85
|
+
Return the node level features.
|
|
86
|
+
"""
|
|
87
|
+
return self.extracted_features
|
|
88
|
+
|
|
89
|
+
def get_graph_features(self) -> List[str]:
|
|
90
|
+
"""
|
|
91
|
+
Return the graph level features.
|
|
92
|
+
"""
|
|
93
|
+
return [
|
|
94
|
+
feature
|
|
95
|
+
for node, features in self.extracted_features.items()
|
|
96
|
+
for feature in features
|
|
97
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Generic graph data loader for SDF molecular files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Tuple
|
|
8
|
+
|
|
9
|
+
import networkx as nx
|
|
10
|
+
from rdkit import Chem
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SDFLoader:
|
|
16
|
+
"""Load molecular graphs and labels from an SDF file.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
filepath : str or Path
|
|
21
|
+
Path to the .sdf file.
|
|
22
|
+
label_property : str
|
|
23
|
+
SDF molecule property that holds the class label. Defaults to "value".
|
|
24
|
+
remove_hydrogens : bool
|
|
25
|
+
Whether to strip explicit hydrogens. Defaults to False.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
filepath: str | Path,
|
|
31
|
+
label_property: str = "value",
|
|
32
|
+
remove_hydrogens: bool = False,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.filepath = Path(filepath)
|
|
35
|
+
self.label_property = label_property
|
|
36
|
+
self.remove_hydrogens = remove_hydrogens
|
|
37
|
+
|
|
38
|
+
if not self.filepath.exists():
|
|
39
|
+
raise FileNotFoundError(f"SDF file not found: {self.filepath}")
|
|
40
|
+
if self.filepath.suffix.lower() != ".sdf":
|
|
41
|
+
raise ValueError(f"Expected an .sdf file, got '{self.filepath.suffix}'")
|
|
42
|
+
|
|
43
|
+
def load(self) -> Tuple[List[nx.Graph], List[int]]:
|
|
44
|
+
"""Read the SDF file and return (graphs, labels)."""
|
|
45
|
+
supplier = Chem.SDMolSupplier(
|
|
46
|
+
str(self.filepath),
|
|
47
|
+
removeHs=self.remove_hydrogens,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
graphs: List[nx.Graph] = []
|
|
51
|
+
labels: List[int] = []
|
|
52
|
+
|
|
53
|
+
for mol in supplier:
|
|
54
|
+
if mol is None:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
graph = self._mol_to_graph(mol)
|
|
58
|
+
label = int(float(mol.GetProp(self.label_property)))
|
|
59
|
+
|
|
60
|
+
graphs.append(graph)
|
|
61
|
+
labels.append(label)
|
|
62
|
+
|
|
63
|
+
logger.info("Loaded %d graphs from %s", len(graphs), self.filepath.name)
|
|
64
|
+
return graphs, labels
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _mol_to_graph(mol) -> nx.Graph:
|
|
68
|
+
"""Convert an RDKit Mol to a NetworkX graph."""
|
|
69
|
+
graph = nx.Graph()
|
|
70
|
+
|
|
71
|
+
for atom in mol.GetAtoms():
|
|
72
|
+
graph.add_node(atom.GetIdx(), feature=atom.GetSymbol())
|
|
73
|
+
|
|
74
|
+
for bond in mol.GetBonds():
|
|
75
|
+
graph.add_edge(
|
|
76
|
+
bond.GetBeginAtomIdx(),
|
|
77
|
+
bond.GetEndAtomIdx(),
|
|
78
|
+
bond_type=str(bond.GetBondType()),
|
|
79
|
+
bond_order=bond.GetBondTypeAsDouble(),
|
|
80
|
+
aromatic=bond.GetIsAromatic(),
|
|
81
|
+
in_ring=bond.IsInRing(),
|
|
82
|
+
conjugated=bond.GetIsConjugated(),
|
|
83
|
+
stereo=str(bond.GetStereo()),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return graph
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: graph_dictlearn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A set of representation learning and dictionary learning algorithms for graph data.
|
|
5
|
+
Author-email: Nimendra Gnawardana <nimendraamiththa1@gmail.com>, Siyathma Wedamulla <wedamullasiyathma@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nimendra-ag/graph_dictlearn
|
|
8
|
+
Project-URL: Repository, https://github.com/nimendra-ag/graph_dictlearn
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: gensim==4.4.0
|
|
20
|
+
Requires-Dist: joblib==1.5.3
|
|
21
|
+
Requires-Dist: matplotlib==3.11.0
|
|
22
|
+
Requires-Dist: networkx==3.6.1
|
|
23
|
+
Requires-Dist: numpy==2.5.0
|
|
24
|
+
Requires-Dist: pandas==3.0.3
|
|
25
|
+
Requires-Dist: rdkit==2026.3.3
|
|
26
|
+
Requires-Dist: scikit-learn==1.9.0
|
|
27
|
+
Requires-Dist: scipy==1.18.0
|
|
28
|
+
Requires-Dist: seaborn==0.13.2
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# graph_dictlearn
|
|
32
|
+
A set of representation learning and dictionary learning algorithms for graph data.
|
|
33
|
+
|
|
34
|
+
You can use this pip package to apply dictionary learning algorithms for graph data.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/graph_dictlearn/__init__.py
|
|
5
|
+
src/graph_dictlearn.egg-info/PKG-INFO
|
|
6
|
+
src/graph_dictlearn.egg-info/SOURCES.txt
|
|
7
|
+
src/graph_dictlearn.egg-info/dependency_links.txt
|
|
8
|
+
src/graph_dictlearn.egg-info/requires.txt
|
|
9
|
+
src/graph_dictlearn.egg-info/top_level.txt
|
|
10
|
+
src/graph_dictlearn/dict_learners/__init__.py
|
|
11
|
+
src/graph_dictlearn/dict_learners/aksvd.py
|
|
12
|
+
src/graph_dictlearn/dict_learners/base.py
|
|
13
|
+
src/graph_dictlearn/dict_learners/ksvd.py
|
|
14
|
+
src/graph_dictlearn/encoders/__init__.py
|
|
15
|
+
src/graph_dictlearn/encoders/base.py
|
|
16
|
+
src/graph_dictlearn/encoders/wl.py
|
|
17
|
+
src/graph_dictlearn/encoders/wl_hashing.py
|
|
18
|
+
src/graph_dictlearn/loader/__init__.py
|
|
19
|
+
src/graph_dictlearn/loader/loader.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
graph_dictlearn
|