rdf2vecgpu 0.1.0__py3-none-any.whl
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.
- rdf2vecgpu/__init__.py +5 -0
- rdf2vecgpu/corpus/__init__.py +0 -0
- rdf2vecgpu/corpus/walk_corpus.py +459 -0
- rdf2vecgpu/embedders/__init__.py +0 -0
- rdf2vecgpu/embedders/word2vec.py +106 -0
- rdf2vecgpu/embedders/word2vec_loader.py +125 -0
- rdf2vecgpu/gpu_rdf2vec.py +447 -0
- rdf2vecgpu/helper/__init__.py +0 -0
- rdf2vecgpu/helper/functions.py +121 -0
- rdf2vecgpu/logger/__init__.py +0 -0
- rdf2vecgpu/logger/mlflow_logger.py +0 -0
- rdf2vecgpu/logger/wandb_logger.py +0 -0
- rdf2vecgpu/reader/__init__.py +0 -0
- rdf2vecgpu/reader/kg_file_reader.py +66 -0
- rdf2vecgpu/reader/kg_reader.py +64 -0
- rdf2vecgpu-0.1.0.dist-info/METADATA +222 -0
- rdf2vecgpu-0.1.0.dist-info/RECORD +19 -0
- rdf2vecgpu-0.1.0.dist-info/WHEEL +4 -0
- rdf2vecgpu-0.1.0.dist-info/licenses/LICENSE +21 -0
rdf2vecgpu/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import cupy as cp
|
|
2
|
+
import cudf
|
|
3
|
+
import dask_cudf as dcudf
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from cugraph import uniform_random_walks, filter_unreachable, bfs
|
|
6
|
+
import cugraph
|
|
7
|
+
from cugraph import Graph # single-GPU Graph
|
|
8
|
+
from cugraph.dask import uniform_random_walks as dask_uniform_random_walks
|
|
9
|
+
from cugraph.dask.traversal.bfs import bfs as dask_bfs
|
|
10
|
+
|
|
11
|
+
class single_gpu_walk_corpus:
|
|
12
|
+
def __init__(self, graph: Graph):
|
|
13
|
+
self.G = graph
|
|
14
|
+
|
|
15
|
+
def _replace_entities_with_tokens(self, tokeninzation: cudf.Series, word: cudf.Series, edge_df: cudf.DataFrame) -> tuple[cudf.DataFrame, cudf.DataFrame]:
|
|
16
|
+
"""_summary_
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
tokeninzation (cudf.Series): _description_
|
|
20
|
+
word (cudf.Series): _description_
|
|
21
|
+
edge_df (cudf.DataFrame): _description_
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
tuple[cudf.DataFrame, cudf.DataFrame]: _description_
|
|
25
|
+
"""
|
|
26
|
+
word2idx = cudf.concat([cudf.Series(tokeninzation), cudf.Series(word)], axis=1)
|
|
27
|
+
word2idx.columns = ["token", "word"]
|
|
28
|
+
edge_df["subject"] = edge_df.merge(word2idx, left_on="subject", right_on="word", how="left")["token"]
|
|
29
|
+
edge_df["predicate"] = edge_df.merge(word2idx, left_on="predicate", right_on="word", how="left")["token"]
|
|
30
|
+
edge_df["object"] = edge_df.merge(word2idx, left_on="object", right_on="word", how="left")["token"]
|
|
31
|
+
edge_df = edge_df.astype("int32")
|
|
32
|
+
return edge_df, word2idx
|
|
33
|
+
|
|
34
|
+
def _triples_to_tokens(self, df: cudf.DataFrame, min_count) -> cudf.DataFrame:
|
|
35
|
+
"""_summary_
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
df (cudf.DataFrame): _description_
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
cudf.DataFrame: _description_
|
|
42
|
+
"""
|
|
43
|
+
df = df.sort_values(['walk_id', 'step'])
|
|
44
|
+
|
|
45
|
+
# predicate tokens --------------------------------------------------------
|
|
46
|
+
pred_tok = cudf.DataFrame({
|
|
47
|
+
'walk_id': df.walk_id,
|
|
48
|
+
'pos': df.step * 2 + 1,
|
|
49
|
+
'token': df.predicate
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
# dst tokens --------------------------------------------------------------
|
|
53
|
+
dst_tok = cudf.DataFrame({
|
|
54
|
+
'walk_id': df.walk_id,
|
|
55
|
+
'pos': df.step * 2 + 2,
|
|
56
|
+
'token': df.dst
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
# src tokens (only for the first row in each walk) ------------------------
|
|
60
|
+
first_rows = df[df.step == 0]
|
|
61
|
+
src_tok = cudf.DataFrame({
|
|
62
|
+
'walk_id': first_rows.walk_id,
|
|
63
|
+
'pos': [0] * len(first_rows),
|
|
64
|
+
'token': first_rows.src
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
# concat & sort ----------------------------------------------------------
|
|
68
|
+
tokens = cudf.concat([src_tok, pred_tok, dst_tok])
|
|
69
|
+
token_counts = tokens.groupby('token').size()
|
|
70
|
+
token_counts = token_counts[token_counts >= min_count]
|
|
71
|
+
token_counts = token_counts.reset_index()
|
|
72
|
+
tokens = tokens.loc[tokens["token"].isin(token_counts["token"])]
|
|
73
|
+
return tokens.sort_values(['walk_id', 'pos'])
|
|
74
|
+
|
|
75
|
+
def _skipgram_pairs(self, tokens: cudf.DataFrame, window: int):
|
|
76
|
+
pairs = []
|
|
77
|
+
|
|
78
|
+
# one inner merge per offset (all happen on-GPU)
|
|
79
|
+
for d in range(-window, window + 1):
|
|
80
|
+
if d == 0:
|
|
81
|
+
continue
|
|
82
|
+
left = tokens.rename(columns={'token': 'center'})
|
|
83
|
+
right = tokens.rename(columns={'token': 'context'})
|
|
84
|
+
right = right.assign(pos=right.pos - d) # shift
|
|
85
|
+
|
|
86
|
+
pairs.append(
|
|
87
|
+
left.merge(right, on=['walk_id', 'pos'],
|
|
88
|
+
how='inner')[['center', 'context']]
|
|
89
|
+
)
|
|
90
|
+
return cudf.concat(pairs, ignore_index=True)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _cbow_pairs(self, tokens: cudf.DataFrame, window: int):
|
|
94
|
+
"""
|
|
95
|
+
For each position in each walk, collect all context tokens within the given window
|
|
96
|
+
into a list, paired with the centre token.
|
|
97
|
+
|
|
98
|
+
Returns a DataFrame with columns:
|
|
99
|
+
- 'context' : list of context tokens
|
|
100
|
+
- 'center' : the centre token
|
|
101
|
+
"""
|
|
102
|
+
pairs = []
|
|
103
|
+
|
|
104
|
+
# for each offset, shift tokens to become a 'context' column
|
|
105
|
+
for d in range(-window, window + 1):
|
|
106
|
+
if d == 0:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
# rename token → context, and shift its pos so it lines up with the centre at pos+d
|
|
110
|
+
ctx = (
|
|
111
|
+
tokens
|
|
112
|
+
.rename(columns={'token': 'context'})
|
|
113
|
+
.assign(pos=tokens.pos - d)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# join centre and this slice of context
|
|
117
|
+
centre = tokens.rename(columns={'token': 'center'})
|
|
118
|
+
merged = centre.merge(
|
|
119
|
+
ctx,
|
|
120
|
+
on=['walk_id', 'pos'],
|
|
121
|
+
how='inner'
|
|
122
|
+
)[['walk_id', 'pos', 'center', 'context']]
|
|
123
|
+
|
|
124
|
+
pairs.append(merged)
|
|
125
|
+
|
|
126
|
+
# stack all (centre, single-context) rows
|
|
127
|
+
all_pairs = cudf.concat(pairs, ignore_index=True)
|
|
128
|
+
|
|
129
|
+
# aggregate each centre into a list of contexts
|
|
130
|
+
cbow = (
|
|
131
|
+
all_pairs
|
|
132
|
+
.groupby(['walk_id', 'pos', 'center'])
|
|
133
|
+
.agg_list('context') # now each row has a 'context' list
|
|
134
|
+
.reset_index()
|
|
135
|
+
)
|
|
136
|
+
# return just the two columns we need for training
|
|
137
|
+
return cbow[['context', 'center']]
|
|
138
|
+
|
|
139
|
+
def bfs_walk(self, edge_df: cudf.DataFrame, walk_vertices: cudf.Series, walk_depth: int, random_state: int, word2vec_model: str, min_count: int) -> cudf.DataFrame:
|
|
140
|
+
out = []
|
|
141
|
+
max_walk_id = 0
|
|
142
|
+
for v in walk_vertices.to_cupy():
|
|
143
|
+
bfs_extraction = bfs(self.G, start=v, depth_limit=walk_depth)
|
|
144
|
+
bfs_extraction_filtered = filter_unreachable(bfs_extraction)
|
|
145
|
+
bfs_edges = (bfs_extraction_filtered[bfs_extraction_filtered.predecessor != -1].rename(columns={"predecessor": "subject", "vertex": "object"}).reset_index(drop=True))
|
|
146
|
+
outdeg = bfs_edges.groupby("subject").size().rename("out_deg")
|
|
147
|
+
leave_intermediate = bfs_extraction_filtered[["vertex"]].merge(outdeg, left_on="vertex", right_index=True, how="left")
|
|
148
|
+
leaves = leave_intermediate[leave_intermediate.out_deg.isnull()].rename(columns={"vertex": "object"}).reset_index(drop=True)
|
|
149
|
+
walk_id_list = list(range(max_walk_id, len(leaves) + max_walk_id))
|
|
150
|
+
leaves["walk_id"] = walk_id_list
|
|
151
|
+
max_walk_id = max(walk_id_list) + 1
|
|
152
|
+
walk_edges = cudf.DataFrame(columns=["subject", "object", "walk_id"])
|
|
153
|
+
frontier = leaves[["object", "walk_id"]]
|
|
154
|
+
while len(frontier):
|
|
155
|
+
# join to find each frontier vertex’s parent
|
|
156
|
+
step = frontier.merge(bfs_edges, on="object", how="left") # adds `source`
|
|
157
|
+
step = step.dropna(subset=["subject"]) # reached the root?
|
|
158
|
+
|
|
159
|
+
# collect the edges that belong to these walks
|
|
160
|
+
walk_edges = cudf.concat([walk_edges, step[["subject", "object", "walk_id"]]],
|
|
161
|
+
ignore_index=True)
|
|
162
|
+
|
|
163
|
+
# next frontier is this layer’s parents
|
|
164
|
+
frontier = step[["subject", "walk_id"]].rename(columns={"subject": "object"})
|
|
165
|
+
walk_edges = (
|
|
166
|
+
walk_edges.astype({"subject": "int32", "object": "int32", "walk_id": "int32"})
|
|
167
|
+
.sort_values(["walk_id", "subject"])
|
|
168
|
+
.reset_index(drop=True)
|
|
169
|
+
)
|
|
170
|
+
out.append(walk_edges)
|
|
171
|
+
bfs_all = cudf.concat(out, ignore_index=True)
|
|
172
|
+
bfs_all["step"] = bfs_all.groupby("walk_id").cumcount()
|
|
173
|
+
bfs_all = bfs_all.merge(edge_df, left_on=["subject", "object"], right_on=["subject", "object"], how="left")[["subject", "predicate", "object", "walk_id", "step"]]
|
|
174
|
+
triple_to_token_df = self._triples_to_tokens(bfs_all, min_count)
|
|
175
|
+
if word2vec_model == "skipgram":
|
|
176
|
+
skipgram_df = self._skipgram_pairs(triple_to_token_df, window=5)
|
|
177
|
+
return skipgram_df
|
|
178
|
+
elif word2vec_model == "cbow":
|
|
179
|
+
cbow_df = self._cbow_pairs(triple_to_token_df, window=5)
|
|
180
|
+
return cbow_df
|
|
181
|
+
else:
|
|
182
|
+
raise ValueError("word2vec_model should be either 'skipgram' or 'cbow'")
|
|
183
|
+
|
|
184
|
+
def random_walk(self, edge_df: cudf.DataFrame, walk_vertices: cudf.Series, walk_depth: int, random_state: int, word2vec_model: str, min_count: int) -> cudf.DataFrame:
|
|
185
|
+
"""_summary_
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
edge_df (cudf.DataFrame): _description_
|
|
189
|
+
walk_vertices (cudf.Series): _description_
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
NotImplementedError: _description_
|
|
193
|
+
ValueError: _description_
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
cudf.DataFrame: _description_
|
|
197
|
+
"""
|
|
198
|
+
random_walks, _, max_length = uniform_random_walks(self.G, start_vertices=walk_vertices, max_depth=walk_depth,random_state=random_state)
|
|
199
|
+
group_keys = cudf.Series(range(len(random_walks))) // max_length
|
|
200
|
+
transformed_random_walk = random_walks.to_frame(name="src")
|
|
201
|
+
transformed_random_walk["walk_id"] = group_keys
|
|
202
|
+
transformed_random_walk["dst"] = transformed_random_walk['src'].shift(-1)
|
|
203
|
+
transformed_random_walk = transformed_random_walk.mask(transformed_random_walk == -1, [None, None, None]).dropna()
|
|
204
|
+
|
|
205
|
+
transformed_random_walk["step"] = transformed_random_walk.groupby("walk_id").cumcount()
|
|
206
|
+
merged_walks = transformed_random_walk.merge(edge_df, left_on=["src", "dst"], right_on= ["subject", "object"], how="left")[["src", "predicate", "dst", "walk_id", "step"]]
|
|
207
|
+
merged_walks = merged_walks.dropna()
|
|
208
|
+
merged_walks.sort_values(["walk_id", "step"])
|
|
209
|
+
triple_to_token_df = self._triples_to_tokens(merged_walks, min_count)
|
|
210
|
+
if word2vec_model == "skipgram":
|
|
211
|
+
skipgram_df = self._skipgram_pairs(triple_to_token_df, window=5)
|
|
212
|
+
return skipgram_df
|
|
213
|
+
elif word2vec_model == "cbow":
|
|
214
|
+
cbow_df = self._cbow_pairs(triple_to_token_df, window=5)
|
|
215
|
+
return cbow_df
|
|
216
|
+
else:
|
|
217
|
+
raise ValueError("word2vec_model should be either 'skipgram' or 'cbow'")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _ensure_dask_frame(df, nparts=None):
|
|
221
|
+
"""Return a dask_cudf.DataFrame (zero-copy if already dask)."""
|
|
222
|
+
if isinstance(df, dcudf.DataFrame):
|
|
223
|
+
return df
|
|
224
|
+
return dcudf.from_cudf(df, npartitions=nparts or 1)
|
|
225
|
+
|
|
226
|
+
def _dst_per_partition(df: cudf.DataFrame):
|
|
227
|
+
"""Partition-safe dst = src.shift(-1) and drop last row."""
|
|
228
|
+
df["dst"] = df["src"].shift(-1)
|
|
229
|
+
return df.iloc[:-1]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class multi_gpu_walk_corpus:
|
|
233
|
+
"""
|
|
234
|
+
A fully Dask/cuDF/cuGraph implementation of random-walk and BFS
|
|
235
|
+
corpora that scales to many GPUs without triggering the 32-bit
|
|
236
|
+
`size_type` overflow.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def __init__(self, graph: Graph):
|
|
240
|
+
# Convert single-GPU Graphs to distributed Graphs on demand
|
|
241
|
+
self.G = graph
|
|
242
|
+
|
|
243
|
+
# --------------- Low-level helpers (unchanged API) ------------------- #
|
|
244
|
+
|
|
245
|
+
def _triples_to_tokens(self, df: dcudf.DataFrame, min_count: int) -> dcudf.DataFrame:
|
|
246
|
+
df = df.sort_values(['walk_id', 'step'])
|
|
247
|
+
|
|
248
|
+
# predicate tokens --------------------------------------------------
|
|
249
|
+
pred_tok = dcudf.DataFrame({
|
|
250
|
+
'walk_id': df.walk_id,
|
|
251
|
+
'pos': df.step * 2 + 1,
|
|
252
|
+
'token': df.predicate
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
# dst tokens --------------------------------------------------------
|
|
256
|
+
dst_tok = dcudf.DataFrame({
|
|
257
|
+
'walk_id': df.walk_id,
|
|
258
|
+
'pos': df.step * 2 + 2,
|
|
259
|
+
'token': df.dst
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
# src tokens (first row of each walk) ------------------------------
|
|
263
|
+
first_rows = df[df.step == 0]
|
|
264
|
+
src_tok = dcudf.DataFrame({
|
|
265
|
+
'walk_id': first_rows.walk_id,
|
|
266
|
+
'pos': 0,
|
|
267
|
+
'token': first_rows.src
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
tokens = dcudf.concat([src_tok, pred_tok, dst_tok])
|
|
271
|
+
|
|
272
|
+
# frequency filter
|
|
273
|
+
tok_counts = tokens.groupby('token').size()
|
|
274
|
+
tok_counts = tok_counts[tok_counts >= min_count].reset_index()
|
|
275
|
+
tokens = tokens.merge(tok_counts[['token']], on='token')
|
|
276
|
+
|
|
277
|
+
return tokens.sort_values(['walk_id', 'pos'])
|
|
278
|
+
|
|
279
|
+
def _skipgram_pairs(self, tokens: dcudf.DataFrame, window: int):
|
|
280
|
+
pairs = []
|
|
281
|
+
for d in range(-window, window + 1):
|
|
282
|
+
if d == 0:
|
|
283
|
+
continue
|
|
284
|
+
left = tokens.rename(columns={'token': 'center'})
|
|
285
|
+
right = tokens.rename(columns={'token': 'context'})
|
|
286
|
+
right = right.assign(pos=right.pos - d)
|
|
287
|
+
|
|
288
|
+
pairs.append(
|
|
289
|
+
left.merge(right, on=['walk_id', 'pos'],
|
|
290
|
+
how='inner')[['center', 'context']]
|
|
291
|
+
)
|
|
292
|
+
return dcudf.concat(pairs, ignore_index=True)
|
|
293
|
+
|
|
294
|
+
def _cbow_pairs(self, tokens: dcudf.DataFrame, window: int):
|
|
295
|
+
"""
|
|
296
|
+
For CBOW we emit one pair per (target, context) exactly like Skip-Gram
|
|
297
|
+
but invert the columns so downstream code can treat the left column as
|
|
298
|
+
the *label* (word to predict) and the right column as a single context
|
|
299
|
+
word. If your trainer expects aggregated contexts, groupby/aggregate
|
|
300
|
+
after `.compute()`.
|
|
301
|
+
"""
|
|
302
|
+
pairs = []
|
|
303
|
+
for d in range(-window, window + 1):
|
|
304
|
+
if d == 0:
|
|
305
|
+
continue
|
|
306
|
+
left = tokens.rename(columns={'token': 'target'}) # word to predict
|
|
307
|
+
right = tokens.rename(columns={'token': 'context'})
|
|
308
|
+
right = right.assign(pos=right.pos - d)
|
|
309
|
+
|
|
310
|
+
pairs.append(
|
|
311
|
+
left.merge(right, on=['walk_id', 'pos'],
|
|
312
|
+
how='inner')[['target', 'context']]
|
|
313
|
+
)
|
|
314
|
+
return dcudf.concat(pairs, ignore_index=True)
|
|
315
|
+
|
|
316
|
+
# ---------------------- Random-walk corpus -------------------------- #
|
|
317
|
+
|
|
318
|
+
def random_walk(
|
|
319
|
+
self,
|
|
320
|
+
edge_df: cudf.DataFrame | dcudf.DataFrame,
|
|
321
|
+
walk_vertices: cudf.Series | dcudf.Series,
|
|
322
|
+
walk_depth: int,
|
|
323
|
+
random_state: int,
|
|
324
|
+
word2vec_model: str,
|
|
325
|
+
min_count: int,
|
|
326
|
+
):
|
|
327
|
+
"""Multi-GPU random-walk corpus builder (Skip-Gram / CBOW)."""
|
|
328
|
+
|
|
329
|
+
# ---- Step 0 – inputs must be distributed ----
|
|
330
|
+
edge_ddf = _ensure_dask_frame(edge_df)
|
|
331
|
+
start_vertices = _ensure_dask_frame(
|
|
332
|
+
cudf.DataFrame({'v': walk_vertices})
|
|
333
|
+
)['v']
|
|
334
|
+
|
|
335
|
+
# ---- Step 1 – distributed random walks ----
|
|
336
|
+
logger.info("Running uniform_random_walks on {} GPUs …", dcudf.get_device_count())
|
|
337
|
+
walks_s, _, max_len = dask_uniform_random_walks(
|
|
338
|
+
self.G,
|
|
339
|
+
start_vertices=start_vertices,
|
|
340
|
+
max_depth=walk_depth,
|
|
341
|
+
random_state=random_state,
|
|
342
|
+
) # walks_s: dask_cudf.Series
|
|
343
|
+
|
|
344
|
+
walks = walks_s.to_frame(name="src").reset_index(drop=False)
|
|
345
|
+
walks = walks.rename(columns={'index': 'global_pos'})
|
|
346
|
+
|
|
347
|
+
# walk_id derived from the GLOBAL index – no overflow, no giant range
|
|
348
|
+
walks["walk_id"] = (walks["global_pos"].astype("int64") // max_len)
|
|
349
|
+
|
|
350
|
+
# ---- Step 2 – dst, step columns (partition-safe) ----
|
|
351
|
+
walks = walks.map_partitions(_dst_per_partition)
|
|
352
|
+
walks["step"] = walks.groupby("walk_id").cumcount()
|
|
353
|
+
|
|
354
|
+
# ---- Step 3 – join predicates ------------------------
|
|
355
|
+
merged = walks.merge(
|
|
356
|
+
edge_ddf,
|
|
357
|
+
left_on=["src", "dst"],
|
|
358
|
+
right_on=["subject", "object"],
|
|
359
|
+
how="left",
|
|
360
|
+
).dropna(subset=["predicate"])
|
|
361
|
+
|
|
362
|
+
# keep needed columns only
|
|
363
|
+
merged = merged[["src", "predicate", "dst", "walk_id", "step"]]
|
|
364
|
+
|
|
365
|
+
# ---- Step 4 – tokens & (skip-gram | CBOW) pairs ----
|
|
366
|
+
tokens = self._triples_to_tokens(merged, min_count)
|
|
367
|
+
|
|
368
|
+
if word2vec_model == "skipgram":
|
|
369
|
+
pairs = self._skipgram_pairs(tokens, window=5)
|
|
370
|
+
elif word2vec_model == "cbow":
|
|
371
|
+
pairs = self._cbow_pairs(tokens, window=5)
|
|
372
|
+
else:
|
|
373
|
+
raise ValueError("word2vec_model must be 'skipgram' or 'cbow'")
|
|
374
|
+
|
|
375
|
+
return pairs # still a dask_cudf.DataFrame – call .compute() when needed
|
|
376
|
+
|
|
377
|
+
# -------------------------- BFS corpus ----------------------------- #
|
|
378
|
+
# (Algorithmically unchanged – wrapped for Dask simplicity) #
|
|
379
|
+
# ------------------------------------------------------------------ #
|
|
380
|
+
|
|
381
|
+
def bfs_walk(
|
|
382
|
+
self,
|
|
383
|
+
edge_df: cudf.DataFrame | dcudf.DataFrame,
|
|
384
|
+
walk_vertices: cudf.Series | dcudf.Series,
|
|
385
|
+
walk_depth: int,
|
|
386
|
+
random_state: int,
|
|
387
|
+
word2vec_model: str,
|
|
388
|
+
min_count: int,
|
|
389
|
+
):
|
|
390
|
+
"""
|
|
391
|
+
Distributed BFS-to-leaf walks. The inner loop is identical to the
|
|
392
|
+
single-GPU version but runs once **per worker** in parallel because
|
|
393
|
+
each start vertex is scheduled on the GPU that owns it.
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
edge_ddf = _ensure_dask_frame(edge_df)
|
|
397
|
+
start_vertices = _ensure_dask_frame(
|
|
398
|
+
cudf.DataFrame({'v': walk_vertices})
|
|
399
|
+
)['v']
|
|
400
|
+
|
|
401
|
+
# Helper that runs on a single GPU – we wrap the old single-GPU BFS
|
|
402
|
+
def _bfs_from_vertex(v):
|
|
403
|
+
bfs_extraction = cugraph.bfs(self.G.to_delayed(), start_vertices=v, max_depth=walk_depth)
|
|
404
|
+
bfs_extraction_filtered = cugraph.filter_unreachable(bfs_extraction)
|
|
405
|
+
|
|
406
|
+
bfs_edges = (
|
|
407
|
+
bfs_extraction_filtered[bfs_extraction_filtered.predecessor != -1]
|
|
408
|
+
.rename(columns={"predecessor": "subject", "vertex": "object"})
|
|
409
|
+
.reset_index(drop=True)
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
outdeg = bfs_edges.groupby("subject").size().rename("out_deg")
|
|
413
|
+
leaves = (
|
|
414
|
+
bfs_extraction_filtered[["vertex"]]
|
|
415
|
+
.merge(outdeg, left_on="vertex", right_index=True, how="left")
|
|
416
|
+
)
|
|
417
|
+
leaves = leaves[leaves.out_deg.isnull()].rename(columns={"vertex": "object"}).reset_index(drop=True)
|
|
418
|
+
|
|
419
|
+
# build walks root→leaf (same as before) ------------------
|
|
420
|
+
walk_edges = cudf.DataFrame(columns=["subject", "object", "walk_id"])
|
|
421
|
+
frontier = leaves.assign(walk_id=cp.arange(len(leaves), dtype="int32"))
|
|
422
|
+
|
|
423
|
+
while len(frontier):
|
|
424
|
+
step = frontier.merge(bfs_edges, on="object", how="left")
|
|
425
|
+
step = step.dropna(subset=["subject"])
|
|
426
|
+
|
|
427
|
+
walk_edges = cudf.concat(
|
|
428
|
+
[walk_edges, step[["subject", "object", "walk_id"]]],
|
|
429
|
+
ignore_index=True
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
frontier = step[["subject", "walk_id"]].rename(columns={"subject": "object"})
|
|
433
|
+
|
|
434
|
+
walk_edges["step"] = walk_edges.groupby("walk_id").cumcount()
|
|
435
|
+
return walk_edges
|
|
436
|
+
|
|
437
|
+
# Apply the helper across the Dask Series of start vertices
|
|
438
|
+
walks = start_vertices.map_partitions(
|
|
439
|
+
lambda s: cudf.concat([_bfs_from_vertex(v) for v in s.to_cupy()]),
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
merged = walks.merge(
|
|
443
|
+
edge_ddf,
|
|
444
|
+
left_on=["subject", "object"],
|
|
445
|
+
right_on=["subject", "object"],
|
|
446
|
+
how="left",
|
|
447
|
+
).dropna(subset=["predicate"])
|
|
448
|
+
|
|
449
|
+
merged = merged.rename(columns={'subject': 'src', 'object': 'dst'})
|
|
450
|
+
tokens = self._triples_to_tokens(merged[["src", "predicate", "dst", "walk_id", "step"]], min_count)
|
|
451
|
+
|
|
452
|
+
if word2vec_model == "skipgram":
|
|
453
|
+
pairs = self._skipgram_pairs(tokens, window=5)
|
|
454
|
+
elif word2vec_model == "cbow":
|
|
455
|
+
pairs = self._cbow_pairs(tokens, window=5)
|
|
456
|
+
else:
|
|
457
|
+
raise ValueError("word2vec_model must be 'skipgram' or 'cbow'")
|
|
458
|
+
|
|
459
|
+
return pairs
|
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
from torch.optim import Adam
|
|
4
|
+
import lightning as L
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
torch.cuda.set_per_process_memory_fraction(0.9)
|
|
8
|
+
|
|
9
|
+
class SkipGram(L.LightningModule):
|
|
10
|
+
def __init__(self, vocab_size, embedding_dim, neg_samples, learning_rate=0.003, use_sparse=True):
|
|
11
|
+
super().__init__()
|
|
12
|
+
self.vocab_size = vocab_size
|
|
13
|
+
self.learning_rate = learning_rate
|
|
14
|
+
|
|
15
|
+
self.in_embeddings = nn.Embedding(vocab_size, embedding_dim, sparse=use_sparse)
|
|
16
|
+
self.output_embeddings = nn.Embedding(vocab_size, embedding_dim, sparse=use_sparse)
|
|
17
|
+
|
|
18
|
+
bound = 1.0 / embedding_dim
|
|
19
|
+
|
|
20
|
+
nn.init.uniform_(self.in_embeddings.weight, -bound, bound)
|
|
21
|
+
nn.init.uniform_(self.output_embeddings.weight, -bound, bound)
|
|
22
|
+
|
|
23
|
+
self.neg_samples = neg_samples
|
|
24
|
+
self.loss_fn = nn.BCEWithLogitsLoss(reduction='none')
|
|
25
|
+
self.use_sparse = use_sparse
|
|
26
|
+
|
|
27
|
+
def forward(self, center, context, negative):
|
|
28
|
+
center_embed = self.in_embeddings(center) # (batch, embed)
|
|
29
|
+
context_embed = self.output_embeddings(context) # (batch, embed)
|
|
30
|
+
negative_embed = self.output_embeddings(negative) # (batch, neg, embed)
|
|
31
|
+
|
|
32
|
+
# Positive score
|
|
33
|
+
pos_logits = (center_embed * context_embed).sum(dim=1)
|
|
34
|
+
pos_targets = torch.ones_like(pos_logits)
|
|
35
|
+
# pos_score = torch.sum(torch.mul(center_embed, context_embed), dim=1)
|
|
36
|
+
pos_loss = self.loss_fn(pos_logits, pos_targets)
|
|
37
|
+
|
|
38
|
+
# Negative score
|
|
39
|
+
neg_score = torch.bmm(negative_embed, center_embed.unsqueeze(-1)).squeeze(-1)
|
|
40
|
+
neg_targets = torch.zeros_like(neg_score)
|
|
41
|
+
neg_loss = self.loss_fn(neg_score, neg_targets).sum(dim=1)
|
|
42
|
+
|
|
43
|
+
return torch.mean(pos_loss + neg_loss)
|
|
44
|
+
|
|
45
|
+
def training_step(self, batch, batch_idx):
|
|
46
|
+
center, context = batch
|
|
47
|
+
negative = torch.randint(0, self.vocab_size, (center.size(0), self.neg_samples), device=self.device)
|
|
48
|
+
loss = self(center, context, negative)
|
|
49
|
+
self.log("train_loss", loss, prog_bar=True)
|
|
50
|
+
return loss
|
|
51
|
+
|
|
52
|
+
def configure_optimizers(self):
|
|
53
|
+
opt_function = torch.optim.SparseAdam if self.use_sparse else torch.optim.Adam
|
|
54
|
+
return opt_function(self.parameters(), lr=self.learning_rate)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CBOW(L.LightningModule):
|
|
58
|
+
def __init__(self, vocab_size, embedding_dim, learning_rate, use_sparse=True):
|
|
59
|
+
super().__init__()
|
|
60
|
+
self.save_hyperparameters()
|
|
61
|
+
self.vocab_size = vocab_size
|
|
62
|
+
self.learning_rate = learning_rate
|
|
63
|
+
self.use_sparse = use_sparse
|
|
64
|
+
|
|
65
|
+
self.in_embeddings = nn.Embedding(vocab_size, embedding_dim)
|
|
66
|
+
self.output_embeddings = nn.Embedding(vocab_size, embedding_dim)
|
|
67
|
+
|
|
68
|
+
bound = 1.0 / embedding_dim
|
|
69
|
+
nn.init.uniform_(self.in_embeddings.weight, -bound, bound)
|
|
70
|
+
nn.init.uniform_(self.output_embeddings.weight, -bound, bound)
|
|
71
|
+
self.loss_fn = nn.BCEWithLogitsLoss(reduction='none')
|
|
72
|
+
|
|
73
|
+
def forward(self, center, context, negative):
|
|
74
|
+
center_embed = self.in_embeddings(center) # (batch, embed)
|
|
75
|
+
context_embed = self.output_embeddings(context) # (batch, embed)
|
|
76
|
+
negative_embed = self.output_embeddings(negative)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
pos_logits = torch.sum(torch.mul(center_embed, context_embed), dim=-1)
|
|
80
|
+
pos_targets = torch.ones_like(pos_logits)
|
|
81
|
+
pos_loss = self.loss_fn(pos_logits, pos_targets)
|
|
82
|
+
|
|
83
|
+
neg_logits = torch.bmm(center_embed, context_embed.unsqueeze(-1)).squeeze(-1)
|
|
84
|
+
neg_targets = torch.zeros_like(neg_logits)
|
|
85
|
+
neg_loss = self.loss_fn(neg_logits, neg_targets)
|
|
86
|
+
|
|
87
|
+
return torch.mean(pos_loss + neg_loss)
|
|
88
|
+
|
|
89
|
+
def training_step(self, batch, batch_idx):
|
|
90
|
+
center, context = batch
|
|
91
|
+
batch_size, device = center.size(0), center.device
|
|
92
|
+
|
|
93
|
+
negative = torch.randint(0, self.vocab_size, (batch_size, self.window_size), device=device)
|
|
94
|
+
loss = self(center, context, negative)
|
|
95
|
+
self.log("train_loss", loss, prog_bar=True)
|
|
96
|
+
return loss
|
|
97
|
+
|
|
98
|
+
def configure_optimizers(self):
|
|
99
|
+
opt_function = torch.optim.SparseAdam if self.use_sparse else torch.optim.Adam
|
|
100
|
+
return opt_function(self.parameters(), lr=self.learning_rate)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class OrderAwareSkipgram:
|
|
104
|
+
def __init__(self):
|
|
105
|
+
super().__init__()
|
|
106
|
+
pass
|